Xem network interface
Xem thửTình huống: health check của load balancer báo gdev-api chết, nhưng SSH vào thì thấy nó sống
22:15, load balancer của GDev đánh dấu VM gdev-api là unhealthy. Bạn SSH vào máy, chạy curl http://localhost:8080/healthz và nhận ngay ok. Ứng dụng chạy tốt. Một đồng đội mở terminal ở máy khác trong cùng mạng, chạy curl http://10.20.1.10:8080/healthz và nhận Connection refused.
Cùng một ứng dụng, cùng một cổng, hai kết quả trái ngược. Để giải thích, bạn cần trả lời hai câu hỏi mà chưa ai hỏi: máy này đang có những địa chỉ nào trên những cổng mạng nào, và ứng dụng đang nghe ở địa chỉ nào trong số đó.
Sau bài này bạn làm được các việc sau:
- Đọc
ip -br a,ip -br linkvàip addr showđể biết interface nào đang UP, có địa chỉ gì, thuộc mạng nào. - Tính tay một subnet CIDR (ví dụ
/24và/26): bao nhiêu địa chỉ, dải nào, một địa chỉ có nằm trong subnet không. - Dùng
ss -tlnpđể biết ứng dụng bind ở đâu và suy ra ai kết nối được. - Tự tái hiện lỗi
Connection refuseddo bind127.0.0.1và sửa nó.
Bạn cần có: Docker cài sẵn trên máy (Linux, macOS hoặc WSL2), đã quen gõ lệnh Linux cơ bản như ở bài Lệnh Linux cho DevOps.
Ba mô hình cần nhớ
- Interface là một cái tên, một trạng thái và một hoặc nhiều địa chỉ kèm độ dài prefix:
eth0,UP,10.20.1.10/24. Không có địa chỉ thì host không biết mình ở mạng nào; không UP thì không có gì đi qua. - Prefix
/Nchia địa chỉ IP thành hai phần: N bit đầu là mạng (subnet), phần còn lại là host. Hai máy cùng subnet nói chuyện trực tiếp với nhau; khác subnet thì phải nhờ gateway. - Ứng dụng nghe trên một cặp địa chỉ và cổng cụ thể: có địa chỉ trên máy chưa có nghĩa là ứng dụng nhận kết nối từ địa chỉ đó.
127.0.0.1:8080và0.0.0.0:8080là hai thứ khác nhau.
Chuẩn bị môi trường lab
Cả chương dùng một lab nhỏ bằng Docker. Mỗi container có network namespace riêng (một bộ interface, bảng route và bảng cổng riêng), nên với các lệnh mạng nó cư xử như một VM nhỏ. Lab không cần quyền root trên máy chủ thật, chỉ cần quyền chạy Docker.
Tạo thư mục netlab với file Dockerfile sau. Image nền là Alpine 3.20 (khoảng 8 MB), cài thêm các công cụ mạng đầy đủ vì lệnh ip và ping của BusyBox mặc định thiếu nhiều tính năng.
FROM alpine:3.20
RUN apk add --no-cache iproute2 iputils traceroute tcpdump iptables curl busybox-extras
docker build -t lab-tools netlab/
docker network create --subnet 10.20.1.0/24 lab-app
docker run -d --name lab-api --network lab-app --ip 10.20.1.10 \
--cap-add NET_ADMIN lab-tools sleep infinity
docker exec lab-api ip -br a
Giải thích ba tùy chọn:
--subnet 10.20.1.0/24tạo một mạng ảo riêng tênlab-appvới dải địa chỉ ta chọn.--ip 10.20.1.10gán cố định địa chỉ cholab-api, đóng vai VMgdev-api.--cap-add NET_ADMINcho phép container đổi cấu hình mạng của chính nó (thêm route, hạ interface). Quyền này chỉ áp dụng trong namespace của container, không động tới mạng máy bạn.
Kết quả mong đợi ở lệnh cuối:
lo UNKNOWN 127.0.0.1/8
eth0@if437 UP 10.20.1.10/24
Hai interface xuất hiện. Số sau @if trên máy bạn sẽ khác. Nếu dải 10.20.1.0/24 đã trùng với một mạng khác trên máy, hãy đổi sang dải khác và đổi theo trong mọi lệnh.
Toàn bộ output trong bài được chạy thật trong lab này (Alpine 3.20, iproute2 6.9.0, iputils 20240117). Chỗ nào giá trị phụ thuộc máy bạn (số interface, bộ đếm) sẽ ghi "kết quả minh họa".
Đọc interface: ba lệnh, ba mức chi tiết
Trước khi chạy, thử đoán: container vừa có hai dòng interface. Dòng nào là "cửa" để máy khác gọi vào gdev-api?
docker exec lab-api ip -br a
docker exec lab-api ip -br link
docker exec lab-api ip addr show eth0
Kết quả minh họa (số interface và MAC khác nhau theo máy):
lo UNKNOWN 127.0.0.1/8
eth0@if437 UP 10.20.1.10/24
lo UNKNOWN 00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>
eth0@if437 UP 02:42:0a:14:01:0a <BROADCAST,MULTICAST,UP,LOWER_UP>
436: eth0@if437: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:0a:14:01:0a brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.20.1.10/24 brd 10.20.1.255 scope global eth0
valid_lft forever preferred_lft forever
Lệnh -br (brief) cho một dòng mỗi interface, dùng để quét nhanh. Lệnh đầy đủ cho từng trường, đọc như sau:
eth0là interface nối ra mạng.lolà loopback, interface ảo chỉ nối máy với chính nó, nên câu trả lời làeth0.@if437nghĩa làeth0này là một đầu của cặp cáp ảo (veth pair); đầu kia là interface số 437 nằm ngoài container. Trên VM thật bạn sẽ không thấy phần này, tên thường làens5hayenp0s3do systemd đặt theo vị trí phần cứng.state UPlà trạng thái quản trị (administrative state): người dùng hoặc hệ thống đã bật interface. Trong dấu ngoặc nhọn, cờLOWER_UPmới cho biết tầng vật lý có tín hiệu (cáp cắm, hoặc cổng ảo nối). Một interface có thểUPmà không cóLOWER_UPkhi dây bị rút.link/ether 02:42:0a:14:01:0alà địa chỉ MAC, danh tính ở tầng liên kết dữ liệu (data link). Docker sinh MAC từ IP:0a 14 01 0alà 10, 20, 1, 10 viết ở hệ 16.inet 10.20.1.10/24là địa chỉ IPv4 cộng độ dài prefix.brd 10.20.1.255là địa chỉ broadcast của subnet.scope globalnghĩa là địa chỉ dùng được ra ngoài máy;scope hostcủa127.0.0.1thì chỉ dùng trong máy.mtu 1500là kích thước tối đa của một gói ở tầng này, tính bằng byte. Nếu hai đầu cấu hình MTU lệch nhau (hay gặp với VPN, overlay), gói to bị rơi còn gói nhỏ vẫn đi.
Một điều đáng nhớ: ip thay ifconfig và netstat, những lệnh thuộc gói net-tools đã lỗi thời. Lệnh ip và ss nằm trong gói iproute2, nên có sẵn trên hầu hết bản server hiện đại. Theo man page ip(8), ip quản lý cả interface, địa chỉ, route và bảng neighbor.
Bộ đếm lỗi của interface
docker exec lab-api ip -s link show eth0
Kết quả minh họa:
436: eth0@if437: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
link/ether 02:42:0a:14:01:0a brd ff:ff:ff:ff:ff:ff link-netnsid 0
RX: bytes packets errors dropped missed mcast
817 8 0 0 0 0
TX: bytes packets errors dropped carrier collsns
0 0 0 0 0 0
Bộ đếm tính từ lúc interface được tạo. Giá trị tuyệt đối ít ý nghĩa; thứ cần nhìn là errors, dropped, carrier có tăng theo thời gian hay không. Nếu tăng, vấn đề nằm ở card, driver, cáp hoặc hàng đợi quá tải, và việc sửa ứng dụng sẽ không giúp gì.
Mô hình 2: prefix /N và phép tính subnet
Mỗi địa chỉ IPv4 là 32 bit. Prefix /24 nói rằng 24 bit đầu là phần mạng, 8 bit còn lại là phần host. Từ đó suy ra mọi con số:
- Số bit host bằng 32 trừ N.
- Tổng số địa chỉ là 2 mũ số bit host.
- Số host dùng được theo quy ước cổ điển bằng tổng trừ 2, vì địa chỉ đầu là địa chỉ mạng và địa chỉ cuối là broadcast.
| Thông số | 10.20.1.0/24 | 10.20.1.0/26 |
|---|---|---|
| Netmask | 255.255.255.0 | 255.255.255.192 |
| Bit host | 8 | 6 |
| Tổng địa chỉ | 256 | 64 |
| Host dùng được | 254 | 62 |
| Dải host | 10.20.1.1 tới 10.20.1.254 | 10.20.1.1 tới 10.20.1.62 |
| Broadcast | 10.20.1.255 | 10.20.1.63 |
Vì sao netmask của /26 là .192? Octet cuối có 2 bit mạng đứng đầu: 11000000 bằng 128 cộng 64, bằng 192. Chia một /24 thành /26 cho ra bốn khối, mỗi khối 64 địa chỉ:
10.20.1.0/26 địa chỉ 0 tới 63
10.20.1.64/26 địa chỉ 64 tới 127
10.20.1.128/26 địa chỉ 128 tới 191
10.20.1.192/26 địa chỉ 192 tới 255
Câu hỏi hay gặp khi đọc sơ đồ VPC: 10.20.1.70 nằm trong 10.20.1.0/26 không? Lấy 70 chia 64 được 1, nghĩa là nó nằm ở khối thứ hai, bắt đầu từ 64. Vậy không, nó thuộc 10.20.1.64/26. Nếu cùng địa chỉ đó đặt trong /24 thì nó cùng mạng với 10.20.1.10.
Bạn có thể kiểm chứng bằng Python có sẵn trên máy (mọi con số ở trên đều từ lệnh này):
python3 -c "import ipaddress as i; n=i.ip_network('10.20.1.0/26'); print(n.netmask, n.num_addresses, n.broadcast_address, i.ip_address('10.20.1.70') in n)"
Kết quả:
255.255.255.192 64 10.20.1.63 False
Hệ quả trực tiếp cho vận hành: hai máy chỉ nói chuyện trực tiếp được khi cùng subnet. Nếu gdev-api ở 10.20.1.10/26 và máy kia ở 10.20.1.70, hai bên thuộc hai mạng khác nhau dù ba octet đầu giống hệt. Gói tin giữa chúng phải đi qua gateway. Bài Routing sẽ mổ xẻ chuyện này.
Cloud có thêm quy tắc riêng. Theo tài liệu AWS về kích thước subnet, AWS giữ lại 5 địa chỉ trong mỗi subnet IPv4 (bốn địa chỉ đầu và địa chỉ cuối), nên một subnet /28 chỉ còn 16 trừ 5, bằng 11 địa chỉ cho máy của bạn. Khi chọn cỡ subnet cho cụm có autoscaling, hãy trừ phần này trước.
Mô hình 3: ứng dụng nghe ở đâu quyết định ai gọi được
Quay lại sự cố mở đầu. Hãy đoán trước khi chạy lab: ứng dụng gdev-api đang bind (gắn socket nghe) vào 127.0.0.1:8080. Một máy khác gọi tới 10.20.1.10:8080 sẽ nhận kết quả gì: timeout, hay bị từ chối ngay?
Sơ đồ dưới đây không vẽ lại topology, mà cho thấy cách kernel của gdev-api đưa một kết nối đến tới đúng socket.
Đang tải sơ đồ…
Cách đọc sơ đồ:
- Nhánh đầu tiên kiểm tra xem địa chỉ đích có thuộc về máy này không.
10.20.1.10là địa chỉ củaeth0, nên kernel đi tiếp. Nếu địa chỉ đích không phải của máy, và máy không bật chuyển tiếp, gói bị bỏ; đây là phần việc của router sẽ học ở bài sau. - Nhánh thứ hai là chỗ lỗi mở đầu xảy ra. Kernel tìm socket đang LISTEN khớp với địa chỉ đích và cổng đích. Socket
0.0.0.0:8080khớp với mọi địa chỉ của máy. Socket127.0.0.1:8080chỉ khớp với đích127.0.0.1, nên gói gửi tới10.20.1.10không có socket nào nhận. - Khi không có socket, kernel trả gói RST ngay lập tức. Máy gọi thấy
Connection refusedtrong vài mili giây, chứ không phải chờ hết thời gian. Câu trả lời cho câu hỏi dự đoán là: bị từ chối ngay.
Điều này cho một quy tắc chẩn đoán quan trọng. Connection refused nghĩa là gói đã tới đúng máy, đúng kernel, nhưng không có ai nghe ở cặp địa chỉ và cổng đó. Mạng không có lỗi. Timeout thì ngược lại: không có hồi âm nào, và bạn phải tìm xem gói bị mất ở đâu (route, firewall, máy tắt).
Các cách bind phổ biến và ai kết nối được:
Local Address trong ss | Ý nghĩa | Ai kết nối được |
|---|---|---|
127.0.0.1:8080 | Chỉ nghe trên loopback | Chỉ tiến trình trong cùng máy |
10.20.1.10:8080 | Chỉ nghe trên một địa chỉ cụ thể | Máy nào tới được địa chỉ đó |
0.0.0.0:8080 | Nghe trên mọi địa chỉ IPv4 của máy | Bất kỳ máy nào tới được một địa chỉ của máy này |
[::]:8080 hoặc *:8080 | Nghe trên mọi địa chỉ IPv6, thường gồm cả IPv4 | Như dòng trên |
Bind vào 127.0.0.1 không phải lỗi. Nó là lựa chọn đúng cho database cục bộ hoặc ứng dụng chỉ sau một reverse proxy cùng máy. Nó chỉ sai khi load balancer ở máy khác phải gọi vào.
Hãy xem lại sự cố ở cấp độ từng bước. Trước khi bấm, dự đoán: ở bước nào máy gdev-api quyết định từ chối, và gói nào được gửi trả về?
Đang tải Animation…
Failure lab: tái hiện Connection refused do bind sai địa chỉ
Bước 1. Chạy một web server BusyBox (httpd) trong lab-api, bind vào 127.0.0.1:8080. Đây đóng vai ứng dụng cấu hình sai:
docker exec lab-api sh -c 'mkdir -p /www && echo "gdev-api ok" > /www/index.html'
docker exec -d lab-api httpd -f -p 127.0.0.1:8080 -h /www
docker exec lab-api ss -tlnp
Kết quả:
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 9 127.0.0.1:8080 0.0.0.0:* users:(("httpd",pid=14,fd=3))
LISTEN 0 4096 127.0.0.11:34315 0.0.0.0:*
Hai dòng nghe được. Dòng 127.0.0.11 là DNS nội bộ của Docker cho mạng tự tạo, không phải ứng dụng của ta. Dòng httpd là thứ ta quan tâm: Local Address:Port là 127.0.0.1:8080. Cột Send-Q của socket LISTEN là kích thước hàng đợi chờ kết nối (backlog). Cờ -t lấy TCP, -l lấy socket đang nghe, -n không đổi số thành tên, -p hiện tiến trình.
Bước 2. Gọi từ trong máy, rồi từ một máy khác trong cùng mạng lab-app:
docker exec lab-api curl -sS http://127.0.0.1:8080/
docker run --rm --network lab-app lab-tools curl -sS -m 3 http://10.20.1.10:8080/
Kết quả:
gdev-api ok
curl: (7) Failed to connect to 10.20.1.10 port 8080 after 0 ms: Could not connect to server
Triệu chứng đúng như đầu bài: gọi nội bộ thành công, gọi từ máy khác báo lỗi mã 7. Muốn thấy chữ Connection refused rõ ràng, thêm cờ -v cho curl; dòng connect to 10.20.1.10 port 8080 failed: Connection refused sẽ xuất hiện. Con số after 0 ms xác nhận điều ở phần trên: bị từ chối gần như tức thì.
Bước 3. Xác minh nguyên nhân bằng ss (đã thấy 127.0.0.1:8080 ở trên) rồi sửa: dừng httpd cũ và bind vào mọi địa chỉ.
docker exec lab-api pkill httpd
docker exec -d lab-api httpd -f -p 0.0.0.0:8080 -h /www
docker exec lab-api ss -tlnp | grep 8080
docker run --rm --network lab-app lab-tools curl -sS -m 3 http://10.20.1.10:8080/
Kết quả:
LISTEN 0 9 0.0.0.0:8080 0.0.0.0:* users:(("httpd",pid=25,fd=3))
gdev-api ok
Ngoài 0.0.0.0, bạn có thể bind vào chính 10.20.1.10 để thu hẹp bề mặt. Với ứng dụng thật, đây là một tham số cấu hình (thường là host, bind hoặc listen), và nhiều framework mặc định là 127.0.0.1 để an toàn.
Lỗi thứ hai: interface bị hạ rồi bật lại
docker exec lab-api ip route
docker exec lab-api ip link set eth0 down
docker exec lab-api ip -br a
docker exec lab-api ip route
docker exec lab-api ip link set eth0 up
docker exec lab-api ip route
Kết quả:
default via 10.20.1.1 dev eth0
10.20.1.0/24 dev eth0 proto kernel scope link src 10.20.1.10
lo UNKNOWN 127.0.0.1/8
eth0@if456 DOWN 10.20.1.10/24
10.20.1.0/24 dev eth0 proto kernel scope link src 10.20.1.10
Ba điều rút ra. Khi eth0 DOWN, địa chỉ vẫn còn nhưng bảng route trống rỗng: lệnh ip route thứ hai không in gì. Khi bật lại, kernel tự thêm route của subnet, nhưng route default via 10.20.1.1 không quay lại. Máy vẫn gọi được hàng xóm cùng subnet nhưng mất đường ra ngoài, và lỗi chỉ lộ ra khi gọi địa chỉ ở subnet khác. Cuối cùng, đây là lý do sau mỗi lần thao tác với interface, bạn cần chạy lại ip route. Khôi phục lab:
docker exec lab-api ip route add default via 10.20.1.1
Dọn dẹp khi kết thúc bài (các bài sau của chương dùng lại lab này, nên chỉ dọn khi học xong cả chương):
docker rm -f lab-api
docker network rm lab-app
Lỗi thường gặp
| Triệu chứng | Nguyên nhân hay gặp | Cách kiểm chứng |
|---|---|---|
curl localhost chạy, máy khác Connection refused | Ứng dụng bind 127.0.0.1 | ss -tlnp, xem cột Local Address |
Connection refused ngay lập tức | Tới đúng máy nhưng không ai nghe ở cổng đó | ss -tlnp trên máy đích |
ip -br a có IP nhưng mất đường ra ngoài | Interface bị hạ rồi bật lại, mất route default | ip route |
Interface UP nhưng không có LOWER_UP | Tầng vật lý không có tín hiệu (cáp, cổng ảo) | ip -br link, ip -s link |
Gọi được máy .5 nhưng không gọi được .70 | Prefix /26 chia hai máy vào hai subnet khác nhau | Tính tay hoặc dùng ipaddress của Python |
| Nhầm interface public và private trên cloud VM | Máy có nhiều interface hoặc nhiều địa chỉ | ip -br a rồi so với console cloud |
ifconfig: command not found | Image tối giản không cài net-tools | Dùng ip và ss thay thế |
Lỗi thứ ba trong bảng nguy hiểm vì nó chỉ lộ ra với địa chỉ ở subnet khác, lúc bạn đã quên mình từng đụng vào interface.
Tự kiểm tra
Hãy trả lời trước khi đọc đáp án ngay bên dưới mỗi câu.
Câu 1. ss -tlnp hiện 127.0.0.1:5432 cho PostgreSQL. Ứng dụng gdev-api chạy ở một VM khác muốn kết nối vào. Bạn thấy lỗi gì và cần đổi gì?
Đáp án: lỗi Connection refused. PostgreSQL phải được cấu hình nghe ở địa chỉ mạng của VM (ví dụ tham số listen_addresses), vì nghe ở loopback thì chỉ tiến trình trong cùng máy kết nối được. Phía firewall và mạng không phải nguyên nhân.
Câu 2. VM có 10.20.1.10/26. Máy 10.20.1.100 có gọi trực tiếp được không, và tại sao?
Đáp án: không trực tiếp. /26 chia 10.20.1.0/24 thành các khối 64 địa chỉ, .10 thuộc khối 0 tới 63 còn .100 thuộc khối 64 tới 127. Hai máy khác subnet nên gói phải đi qua gateway.
Câu 3. Một người báo "interface eth0 có 10.20.1.10/24 nên mạng ổn". Còn ba thứ nào bạn cần kiểm tra thêm trước khi tin?
Đáp án: trạng thái UP và cờ LOWER_UP, bảng route (đặc biệt route default), và bộ đếm lỗi từ ip -s link. Có địa chỉ chỉ là điều kiện cần.
Nếu chỉ nhớ 3 điều
ip -br acho biết interface, trạng thái và địa chỉ/prefix;ip routesau đó cho biết gói đi đâu. Luôn kiểm tra cả hai.- Prefix
/Nquyết định ai cùng subnet:/24có 256 địa chỉ,/26có 64, và các khối/26bắt đầu ở 0, 64, 128, 192. Connection refusedngay lập tức nghĩa là gói tới đúng máy nhưng không socket nào khớp. Dùngss -tlnpđể xem ứng dụng bind ở đâu.
Bài tiếp theo là Thiết bị mạng: khi hai máy không cùng subnet, gói tin đi qua những thiết bị nào.
Tài liệu chính thức: ip(8), ss(8) và kích thước subnet VPC của AWS.