bài trước ta thấy một network namespace mới bị bịt kín hoàn toàn — không ra được đâu cho tới khi có một veth. Bài này đo cái veth đó, và người anh em của nó là cầu nối (bridge) — hai mảnh ghép cuối cùng để hiểu mạng container hoạt động ra sao. veth là sợi cáp; cầu nối là cái switch. Tôi dựng cả hai trong container để đo, và trên đường đi vấp đúng một cái bẫy khiến dự đoán của tôi sai — không phải vì tôi hiểu sai lý thuyết, mà vì cái môi trường tôi đo trên đó đã âm thầm đổi luật chơi.

Cầu nối và veth

veth là cáp, cầu nối là switch

Một cặp veth là hai giao diện mạng ảo luôn sinh ra thành đôi, nối với nhau như hai đầu của một sợi cáp: gói đi vào đầu này lập tức ra ở đầu kia. Đặt mỗi đầu vào một namespace là bạn có một liên kết trực tiếp giữa hai vùng. Nhưng nó là điểm-tới-điểm: một sợi cáp nối đúng hai đầu, không hơn.

Một cầu nối (bridge) là một switch Ethernet ảo trong nhân. Bạn tạo nó, rồi cắm nhiều đầu veth vào — mỗi vùng một sợi cáp nối tới cầu. Giờ tất cả các vùng nằm trên cùng một đoạn mạng L2, và cầu chuyển khung giữa chúng đúng như một switch thật: học địa chỉ MAC nào nằm ở cổng nào, rồi forward theo đó. Đây chính xác là cách mạng mặc định của Docker hoạt động — một cầu tên docker0 mà mọi container cắm vào.

Đo: cáp, chuỗi cáp, và cầu nối

Một — veth như một sợi cáp. Tôi nối hai vùng c1c2 bằng một cặp veth, gán IP cùng subnet. c1 ping c2: 0,064 ms, thông ngay. Một sợi cáp, đúng hai đầu, không cần gì thêm.

Hai — thử nối ba vùng bằng chuỗi cáp. Nếu veth chỉ nối được hai đầu, làm sao nối ba vùng? Ý tưởng ngây thơ: xâu chuỗi — c1—c2—c3, với c2 ở giữa có hai sợi cáp. Tôi gán IP hai đoạn khác subnet, thêm tuyến cho c1c3 trỏ qua c2, rồi ping c1 → c3.

Ba — cầu nối như một switch. Tôi tạo cầu br0, cắm ba vùng b1, b2, b3 vào (mỗi vùng một veth tới cầu), tất cả cùng subnet 10.9.0.0/24. Kết quả:

b1 <-> b2 : thông
b1 <-> b3 : thông
b2 <-> b3 : thông        (tất cả, KHÔNG cần một tuyến định tuyến nào)
bridge fdb: p1, p2, p3   (cầu đã học MAC của từng cổng)

Ba vùng thấy nhau trực tiếp, không cần định tuyến, không cần cấu hình tuyến thủ công — chỉ cắm vào cùng một cầu. Và bridge fdb cho thấy cầu đã học địa chỉ MAC của từng cổng, đúng như một switch. Chi phí thêm của cầu so với cáp trực tiếp: ping qua cầu 0,096 ms so với veth thẳng 0,058 ms — thêm ~0,04 ms cho một chặng switch, gần như không đáng kể.

Một lần tôi đo hớ: chuỗi cáp "chạy được" vì môi trường

Quay lại phép hai. Tôi dựng cái chuỗi c1—c2—c3 với một mục đích rõ ràng trong đầu: chứng minh rằng xâu cáp không tạo thành mạng. Lý lẽ của tôi: c2 ở giữa chỉ là một host bình thường, mà host thì không tự chuyển tiếp gói giữa hai giao diện của nó (tính năng ip_forward mặc định tắt). Nên c1 sẽ không thể với tới c3 — gói tới c2 rồi tắc ở đó. Tôi chắc chắn về điều này.

Chạy lần đầu: c1 ping c3 thông ngay, 0% mất gói. Dự đoán của tôi sai thẳng thừng.

Tôi không viết bừa "à chắc nó tự route" mà truy cho ra. Kiểm net.ipv4.ip_forward trong c2: nó là 1, không phải 0. Vì sao? Vì tôi đang chạy tất cả bên trong một container Docker, mà Docker bật sẵn ip_forward=1 ở ngăn xếp mạng gốc của nó (nó cần thế để định tuyến cho các container). Và một network namespace mới tạo bên trong đó thừa kế giá trị ip_forward=1 ấy, chứ không khởi tạo về 0 như trên một máy Linux trần. Nên c2 chuyển tiếp gói — cái chuỗi cáp thành một mạng định tuyến thật, ngoài ý tôi.

Để chắc, tôi ép ip_forward=0 trên c2 rồi ping lại: c1 → c3 mất 100% gói. Đúng như tôi tưởng ban đầu. Bật lại thành 1: thông trở lại. Vậy cơ chế đã rõ ràng, và dự đoán gốc của tôi đúng về lý thuyết — chỉ sai vì cái mặc định mà tôi giả định (ip_forward=0) không phải là cái mặc định của môi trường tôi đang đo.

Bài học đo lường, đúng như tinh thần cả sê-ri: cái máy bạn đo trên đó là một biến số, và nó mang theo những mặc định vô hình. Tôi tưởng mình đang đo một tính chất của Linux nói chung, hóa ra đang đo một Linux đã bị Docker chỉnh sẵn một sysctl mấu chốt. Một phép đo cho kết quả trái dự đoán không phải lúc nào cũng nghĩa là lý thuyết sai — đôi khi nghĩa là môi trường đã đổi một hằng số mà bạn quên kiểm. Luôn kiểm sysctl thừa kế trước khi kết luận.

Vì sao điều này quan trọng khi lập trình

Hệ quả đầu tiên là hiểu mạng Docker không còn là hộp đen. Khi bạn docker run, mỗi container nhận một namespace (không gian mạng riêng, bài trước), một đầu veth nối ra một cầu docker0 trên host, và host chạy NAT để container ra Internet (bài NAT). Ba thứ này — namespace, veth, bridge — là chính xác những gì bài này và hai bài trước đo tay. "Container cùng một mạng Docker thấy nhau" chỉ có nghĩa là chúng cắm chung một cầu; "container khác mạng không thấy nhau" nghĩa là khác cầu. Không có phép màu, chỉ có switch ảo.

Hệ quả thứ hai là chọn đúng công cụ nối theo số nút. Nối hai thứ thì một veth là đủ và rẻ nhất (điểm-tới-điểm, không có chi phí switch). Nối ba thứ trở lên, đừng xâu chuỗi cáp qua các host trung gian (phải bật forwarding, phải khai tuyến từng chặng, dễ sai và khó mở rộng) — hãy dùng một cầu, để mọi nút trên cùng một đoạn L2, thấy nhau không cần định tuyến. Đây là lý do mọi hệ thống container thật đều dựng trên bridge (hoặc các mạng overlay phức tạp hơn), không phải trên lưới veth.

Hệ quả thứ ba là một nguyên tắc gỡ lỗi: khi hành vi mạng bất ngờ, kiểm các sysctl và mặc định của môi trường trước khi nghi ngờ logic của mình. ip_forward, rp_filter (lọc đường về), các chính sách iptables FORWARD — tất cả đều là những công tắc vô hình mà một môi trường (Docker, một image cơ sở, một nhà cung cấp cloud) có thể đã bật hay tắt sẵn, và chúng đổi kết quả mà không để lại dấu vết trong mã của bạn. Con số mang theo: veth là cáp điểm-tới-điểm nối đúng hai vùng; cầu nối là switch ảo nối nhiều vùng trên một đoạn L2 không cần định tuyến (thêm ~0,04 ms mỗi chặng) — và đó là bộ khung của mạng Docker; còn một chuỗi cáp chỉ thành mạng khi nút giữa chịu chuyển tiếp, điều phụ thuộc vào một sysctl mà môi trường có thể đã đổi sẵn. Đo tay ba mảnh đó một lần, và mạng container không còn là bí ẩn.

Thử ba mươi giây

Trên một máy có Docker, chạy ip link show type bridge — bạn sẽ thấy docker0, cái cầu mặc định. Rồi tạo hai container (docker run -d --name a alpine sleep 999 và tương tự b), và chạy ip link trên host: bạn sẽ thấy các giao diện veth...@if... mọc lên — chính là những đầu veth phía host, đầu kia nằm trong container, cắm vào docker0. Muốn thấy cầu học MAC, chạy bridge fdb show br docker0. Còn nếu có quyền sudo và thích tự tay: sudo ip netns add x; sudo ip netns exec x cat /proc/sys/net/ipv4/ip_forward để xem chính cái sysctl đã làm tôi đo hớ — trên máy trần nó thường là 0, và đó là lý do một chuỗi veth ở đó sẽ không tự thành mạng.