Hãy nghĩ về container như việc chia một căn nhà lớn thành nhiều phòng riêng cho khách trọ. Có những vách ngăn dựng trong nháy mắt: treo một tấm biển tên cửa khác, đánh số lại các phòng — gần như không tốn gì. Nhưng dựng một phòng có hệ thống ống nước và điện riêng biệt thì lại là chuyện khác hẳn: phải kéo cả đường ống mới, lắp đồng hồ riêng, đấu nối lại toàn bộ. Và điều kỳ lạ mà ít ai để ý: khi khách trả phòng, tháo dỡ cái hệ thống ống nước riêng ấy còn mất công hơn cả lúc lắp. Không gian tên mạng trong Linux đúng là cái phòng có ống nước riêng đó, và bài này đo xem nó đắt hơn các vách ngăn khác tới mức nào.
Container là cgroup (phần 34) cộng với không gian tên. Phần 34 đo phần giới hạn tài nguyên; bài này đo phần cách ly.
Chi phí tạo tiến trình theo từng loại
clone() với các cờ khác nhau, đo thời gian tạo rồi thu hồi một tiến trình con:
| Tạo kèm không gian tên | µs mỗi lần | Phần thêm |
|---|---|---|
| không tạo gì | 100,5 | mốc so sánh |
+ uts (tên máy) |
96,5 | miễn phí |
+ pid (bảng tiến trình) |
99,7 | miễn phí |
+ ipc |
109,1 | +9 µs |
+ mount (hệ thống tệp) |
151,7 | +51 µs |
+ net (mạng) |
640,7 | +540 µs |
+ mount, uts, ipc, pid |
193,6 | +93 µs |
+ cả năm |
644,0 | +544 µs |
Lặp lại ba lần cho hai dòng quan trọng: không có không gian tên 97,0 / 91,3 / 80,4 µs; đủ cả năm 668,5 / 632,2 / 655,5 µs.
Con số nói gọn một câu: bốn loại kia cộng lại tốn 93 µs. Riêng mạng tốn 540 µs — gấp gần sáu lần. Một mình nó đắt hơn tất cả những cái còn lại gộp lại.
Vì sao mạng đắt đến thế
uts và pid gần như chỉ là cấp phát một cấu trúc và tăng bộ đếm tham chiếu — treo tấm biển tên, đánh số phòng. mount phải sao chép cây gắn kết, nên tốn hơn — 51 µs cho một cây nhỏ, và sẽ tốn hơn nữa nếu máy chủ có hàng trăm điểm gắn.
Không gian tên mạng thì phải dựng cả một ngăn xếp mạng mới: bảng định tuyến, bảng netfilter, giao diện lo, các bộ đếm thống kê, cấu trúc theo dõi kết nối. Đó là kéo cả hệ thống ống nước riêng. Và khi huỷ, tất cả phải được dọn — quá trình dọn này chạy qua synchronize_rcu() và là phần đắt nhất. Đây chính là lý do trong nhiều báo cáo, việc xoá hàng loạt container còn chậm hơn việc tạo chúng — tháo ống nước lâu hơn lắp.
Chuyện này nghĩa là gì trong thực tế
Hệ thống tạo container liên tục phải trả 540 µs mỗi lần chỉ riêng cho phần mạng. Với CI chạy hàng nghìn job, hoặc nền tảng hàm serverless khởi động container theo yêu cầu, con số đó cộng dồn thành thứ đáng nhìn. Nửa mili giây không lớn với một lần, nhưng nó là chi phí cố định không giảm được bằng cách tối ưu ứng dụng — bạn tối ưu code cỡ nào cũng không bớt được cái giá dựng ngăn xếp mạng.
Nên nếu tác vụ không cần cách ly mạng, đừng tạo không gian tên mạng làm gì:
docker run --network host ...
spec:
hostNetwork: true # Kubernetes
Đổi lại là container dùng chung ngăn xếp mạng với máy chủ: nó thấy mọi giao diện, và nó phải tự tránh xung đột cổng. Với dịch vụ hệ thống (thu thập số liệu, proxy, agent) thì đó thường là đánh đổi đúng.
Chi phí lúc chạy, không chỉ lúc tạo
Không gian tên mạng còn tốn thêm ở mỗi gói tin, vì gói phải đi qua cặp veth và cầu nối:
| Đường đi | Thông lượng |
|---|---|
--network host (không có không gian tên mạng riêng) |
14.286 / 11.612 / 15.156 MB/s |
Mạng bridge (qua veth và cầu nối) |
7.906 / 7.006 / 9.099 MB/s |
Khoảng 1,7 lần.
Cần nói rõ giới hạn của phép so sánh này: bên trái là loopback trong cùng một không gian tên, bên phải đi qua veth rồi cầu nối. Hai đường khác nhau về bản chất, nên 1,7 lần là mức chênh của cả đường đi chứ không phải chi phí riêng của veth. Tôi không tách được hai phần đó với công cụ có sẵn — nói thẳng ra để bạn đừng trích con số này như thể nó là "chi phí veth".
Điều chắc chắn: đường qua cầu nối làm nhiều việc hơn, và với dịch vụ cần thông lượng tối đa giữa hai container trên cùng máy, --network host là lựa chọn đáng cân nhắc.
Bảy loại không gian tên
ls -l /proc/self/ns/
lsns # liet ke moi khong gian ten dang co
| Loại | Cách ly cái gì | Đo được |
|---|---|---|
uts |
tên máy, tên miền | miễn phí |
pid |
số hiệu tiến trình | miễn phí |
ipc |
hàng đợi thông điệp, bộ nhớ chung | +9 µs |
mnt |
cây gắn kết hệ thống tệp | +51 µs |
net |
ngăn xếp mạng | +540 µs |
user |
ánh xạ UID/GID | không đo |
cgroup |
gốc của cây cgroup | không đo |
time |
đồng hồ khởi động (nhân 5.6+) | không đo |
Ba loại cuối tôi không đo. user đặc biệt đáng quan tâm vì nó là nền tảng của container không đặc quyền, và nó thêm chi phí ở mỗi lần kiểm tra quyền chứ không chỉ lúc tạo — một dạng chi phí khác hẳn, và cần cách đo khác.
Chia sẻ không gian tên
Không gian tên tồn tại độc lập với tiến trình. Một tiến trình vào được không gian tên có sẵn:
nsenter -t <pid> -n ip addr # vao khong gian ten MANG cua tien trinh do
nsenter -t <pid> -m -p -n /bin/sh # vao mount, pid, net
Đây là công cụ gỡ lỗi tốt nhất cho container không có shell hay không có công cụ mạng: bạn chạy tcpdump của máy chủ bên trong không gian tên mạng của container.
Kubernetes dùng chính cơ chế này: mọi container trong một pod dùng chung không gian tên mạng, nên chúng thấy nhau qua localhost — và cũng vì thế mà chúng không được nghe cùng một cổng.
# xem container nao dung chung khong gian ten mang voi container khac
docker run --network container:<ten-container-khac> ...
Mẫu số chung: cách ly rẻ ở tầng tên, đắt ở tầng phải dựng lại cả một hệ thống
Cái phân tầng chi phí ở đây — vách ngăn "đổi tên/đánh số" thì gần miễn phí, còn vách ngăn "dựng lại cả một hệ thống con" thì đắt cả lúc tạo lẫn lúc dọn — lặp lại y hệt ở mọi tầng phần mềm mà bạn cần sự cô lập.
Trong thế giới Java, đây đúng là khác biệt giữa cô lập bằng ClassLoader/ThreadLocal (nhẹ, chỉ là một tham chiếu và một bảng tra) và dựng hẳn một ClassLoader con với toàn bộ đồ thị class riêng cho mỗi plugin/tenant — cái sau nạp lại cả cây class, giữ bộ nhớ, và khi gỡ tenant thì phải chờ GC dọn hết reference mới giải phóng được, đúng cái "dọn lâu hơn tạo".
Trong Go, tạo một goroutine với context riêng gần như miễn phí (như uts/pid), nhưng cô lập thật sự — chạy tác vụ trong một tiến trình con hoặc một sandbox riêng — thì phải dựng lại cả môi trường thực thi, và cái giá nằm ở cả lúc khởi động lẫn lúc teardown, không phải lúc chạy.
Còn ở cơ sở dữ liệu, cùng phân tầng: cô lập bằng schema hay bằng search_path trong cùng một instance là gần như miễn phí (đổi tên không gian), còn cấp cho mỗi tenant một database riêng — nói gì tới một instance riêng — thì mỗi cái mang theo connection pool, bộ đệm, tiến trình nền riêng, và dọn dẹp khi xoá tenant tốn kém hẳn. Người ta chọn schema-per-tenant thay vì db-per-tenant chính vì phân tầng chi phí này.
Mẫu số chung: cô lập ở tầng "đặt tên khác" gần như miễn phí, còn cô lập ở tầng "dựng lại một hệ thống con hoàn chỉnh" (ngăn xếp mạng, đồ thị class, môi trường thực thi, instance CSDL) đắt cả lúc tạo lẫn lúc dọn — và phần dọn thường mới là phần đắt nhất, vì nó chạy nền và giữ tài nguyên. Nên câu hỏi đúng khi thiết kế sự cô lập không phải "cô lập càng nhiều càng tốt" mà là "tác vụ này thật sự cần cách ly tới tầng nào" — vì mỗi tầng sâu thêm là một hệ thống con phải dựng lên rồi tháo xuống, và bạn trả giá đó ở cả hai đầu.
Muốn tự thấy mình đang giữ bao nhiêu vách ngăn, xem tiến trình đang ở những không gian tên nào và ai dùng chung:
echo "--- khong gian ten cua shell nay ---"
ls -l /proc/self/ns/ | awk '{print " " $9, $10, $11}'
echo "--- tong hop toan may ---"
lsns -o NS,TYPE,NPROCS,PID,COMMAND 2>/dev/null | head -15
echo "--- so khong gian ten mang dang ton tai ---"
lsns -t net 2>/dev/null | wc -l
ip netns list 2>/dev/null | wc -l
Số ở dòng cuối xấp xỉ số container đang chạy. Nếu nó lớn hơn nhiều so với số container bạn biết, có những không gian tên mạng mồ côi — thường do container đã chết mà quá trình dọn chưa xong, và chúng vẫn âm thầm giữ tài nguyên.
Phần sau: điểm chuẩn cho đúng — những sai lầm tôi đã mắc trong ba mươi sáu phần trước.