Câu giải thích quen thuộc là "container nhẹ hơn máy ảo vì dùng chung nhân". Đúng, nhưng nó không cho bạn con số nào để ra quyết định. Bài này đo cái giá thật của một container: bao nhiêu mili giây, bao nhiêu megabyte, và bao nhiêu cái thì máy chịu được.

Môi trường đo, ghi lại để bạn đối chiếu:

Docker 29.3.1 | storage-driver overlayfs | cgroup v2
16 nhân | 15,6 GB RAM cấp cho máy ảo Linux
nhân trong container: 6.12.76-linuxkit

Khởi động một container mất bao lâu

Hai mươi lần, lấy trung vị:

Thời gian
docker run --rm alpine true 260,1 ms (thấp nhất 231,7 — cao nhất 366,1)
docker create 74,4 ms
docker start trên container đã tạo 193,1 ms
docker exec vào container có sẵn 54,1 ms
docker version (chỉ đi về daemon) 19,6 ms

Đọc bảng này theo chiều từ dưới lên thì thấy tiền đi đâu: 19,6 ms là chi phí CLI gọi sang daemon và về, tức là cái sàn bạn phải trả cho mọi lệnh docker. 54,1 ms là chạy một tiến trình trong không gian tên đã có sẵn. Và 260,1 ms là dựng mới toàn bộ: tạo lớp ghi, dựng không gian tên, gắn mạng, khởi động tiến trình, rồi dọn.

Nhưng con số 260 ms đó gây hiểu nhầm nếu bạn suy ra "máy này dựng được 4 container mỗi giây". Tôi dựng 100 container song song:

100 container trong 4,3 giay = 43 ms moi cai

Nhanh gấp sáu lần so với chạy tuần tự. Nghĩa là phần lớn trong 260 ms kia là thời gian chờ — chờ daemon trả lời, chờ tiến trình khởi động — chứ không phải công sức. Máy làm được nhiều container cùng lúc mà không tốn thêm gì đáng kể.

Đây là khuôn hình sẽ lặp lại suốt sê-ri này, nên đáng nhớ ngay từ bài đầu: một phép đo tuần tự đo độ trễ, không đo năng lực. Muốn biết máy chịu được bao nhiêu thì phải đo song song.

Một container tốn bao nhiêu bộ nhớ

Câu hỏi này khó đo hơn tôi tưởng. Lần đầu tôi đọc MemAvailable của máy ảo Linux trước và sau khi dựng container, rồi lấy hiệu. Kết quả:

20 container : -22,8 MB
=> gia cach ly: -5,5 MB

Bộ nhớ âm. Máy này còn 35 container khác đang chạy, và dao động của chúng lớn hơn hẳn thứ tôi đang cố đo. Một con số âm cho thứ chỉ có thể dương là dấu hiệu phép đo hỏng, không phải phát hiện.

Cách đúng là đọc cgroup của từng container, thứ không bị hoạt động của máy làm nhiễu:

docker stats --no-stream --format '{{.MemUsage}}' <ten-container>
Tổng Mỗi tiến trình
20 container, mỗi cái 1 tiến trình sleep 10,57 MB 0,53 MB
1 container chạy 20 tiến trình sleep 2,93 MB 0,15 MB

Chênh lệch 7,65 MB cho 20 container, tức mỗi container tốn thêm khoảng 0,38 MB so với việc chỉ chạy tiến trình đó trần trụi. Đó là toàn bộ cái giá của cách ly: một bộ không gian tên, một cgroup, một lớp filesystem ghi được, một giao diện mạng ảo.

Để có cảm giác về tỉ lệ: 100 container chạy đồng thời chiếm 52,8 MB. Một máy ảo Linux tối giản, chưa chạy gì, thường cần vài trăm megabyte. Đó là khác biệt thật giữa hai công nghệ, và giờ nó có con số.

Vì sao rẻ đến vậy: cùng một nhân

Bằng chứng trực tiếp, không cần tin ai:

$ docker run --rm alpine:3.20 uname -r
6.12.76-linuxkit

$ docker run --rm --privileged --pid=host alpine:3.20 uname -r
6.12.76-linuxkit

Cùng một chuỗi. Container không có nhân riêng — nó chỉ là tiến trình chạy trên nhân của máy chủ, được nhân đó giấu đi những thứ không cho thấy.

Cái "giấu đi" ấy cũng đo được. Cho một container chạy sleep, rồi nhìn từ hai chỗ:

# tu ben trong chinh no
$ docker exec nhin-thay ps -o pid,comm
PID   COMMAND
    1 sleep
    7 ps

# tu mot container khac, nhung xin nhin bang khong gian ten cua may chu
$ docker run --rm --pid=host alpine:3.20 ps -o pid,comm | grep -c sleep
1

Bên trong, tiến trình sleepPID 1 và không thấy gì khác. Từ bên ngoài với --pid=host, nó chỉ là một tiến trình bình thường trong danh sách. Cùng một tiến trình, hai cách nhìn — đó chính là toàn bộ ý tưởng của không gian tên, và là lý do container rẻ hơn máy ảo hai bậc độ lớn.

Nhưng trên macOS thì bạn vẫn đang chạy máy ảo

Đây là chi tiết mà rất nhiều hướng dẫn bỏ qua, và nó giải thích khá nhiều chuyện khó hiểu về sau.

Nhìn lại kết quả uname ở trên: container báo Linux 6.12.76-linuxkit. Còn máy của tôi:

$ uname -a
Darwin ... 25.5.0 ... arm64

Darwin không phải Linux. Container không thể chạy trực tiếp trên nhân macOS, nên Docker Desktop dựng sẵn một máy ảo Linux và mọi container chạy bên trong đó:

RAM cap cho may ao Linux : 15,6 GB
com.docker.backend chiem : 819 MB tren macOS

Nghĩa là trên macOS bạn trả tiền máy ảo một lần — cho cả hệ thống, không phải cho mỗi container. Điều đó vẫn giữ nguyên lợi thế về mật độ, nhưng nó có ba hệ quả thực tế mà sê-ri này sẽ gặp lại:

  • localhost trong container không phải máy Mac của bạn — nó là máy ảo Linux.
  • Bind mount phải đi qua ranh giới máy ảo, và đó là lý do I/O trên macOS chậm hơn Linux rất nhiều. Chúng ta sẽ đo con số này ở phần về lưu trữ.
  • Mọi phép đo hiệu năng trên macOS đều mang thêm một tầng, nên đừng đem con số của máy Mac đi so với máy chủ Linux.

Trên Linux thì không có tầng này: Docker chạy thẳng trên nhân của máy, và các con số ở trên còn đẹp hơn nữa.

Vậy Docker giải quyết vấn đề gì

Sau khi có số, câu trả lời gọn hơn nhiều so với lời quảng cáo thường nghe. Docker không làm ứng dụng của bạn chạy nhanh hơn — sê-ri này sẽ đo và cho thấy nó gần như không đổi. Nó cho bạn ba thứ:

  • Đóng gói kèm phụ thuộc. Ứng dụng cộng thư viện cộng cấu hình thành một thứ chuyển đi được.
  • Cách ly với giá 0,38 MB. Đủ rẻ để mỗi dịch vụ một container thay vì nhồi chung một máy.
  • Cùng một thứ chạy ở mọi nơi — với dấu sao ở phần macOS phía trên.

Và đổi lại, bạn nhận thêm một tầng nữa để hiểu, để gỡ lỗi, và để đo. Toàn bộ sê-ri này là về tầng đó.

Thử ba mươi giây

Đo cái giá của một container trên chính máy bạn:

# thoi gian dung mot container
time docker run --rm alpine:3.20 true

# bo nho that su cua mot container dang chay
docker run -d --name thu alpine:3.20 sleep 60
docker stats --no-stream --format '{{.Name}}: {{.MemUsage}}' thu
docker rm -f thu

Nếu con số thời gian của bạn lớn hơn 260 ms nhiều, hãy chạy lại — lần đầu luôn gồm cả thời gian tải image về.

Phần sau mổ xẻ image: layer là gì, chồng lên nhau ra sao, và vì sao mười image có thể chỉ tốn dung lượng của một.