Giới hạn tài nguyên của Docker hoạt động chính xác — nhưng container không biết mình bị giới hạn. Khoảng cách giữa hai điều đó là nguồn của một lớp sự cố rất khó chẩn đoán.
--cpus giới hạn đúng như khai
Bốn luồng tính toán, đo từ ngoài container:
| Thời gian | |
|---|---|
--cpus 0.5 |
19,67 s |
--cpus 1 |
9,77 s |
--cpus 2 |
4,58 s |
--cpus 4 |
2,33 s |
--cpus 8 |
2,40 s |
| không giới hạn | 2,40 s |
Tỉ lệ nghịch gần như hoàn hảo: mỗi lần gấp đôi hạn mức thì thời gian giảm một nửa, cho tới khi chạm số luồng công việc (4). Từ đó trở đi thêm CPU không giúp gì — đúng như đường cong đã thấy ở phần 23.
Cơ chế bên dưới là quota theo chu kỳ:
--cpus 0.5 -> cgroup cpu.max = 50000 100000
--cpus 2 -> cgroup cpu.max = 200000 100000
Nghĩa là "được dùng 50 000 micro giây CPU trong mỗi chu kỳ 100 000 micro giây". Với --cpus 2 thì quota lớn hơn chu kỳ — hai nhân chạy song song.
Nhưng container nhìn thấy cả máy
--cpus 0.5 : nproc bao 16
--cpus 1 : nproc bao 16
--cpus 4 : nproc bao 16
-m 64m : /proc/meminfo bao MemTotal: 16354680 kB
-m 256m : /proc/meminfo bao MemTotal: 16354680 kB
nproc báo 16 nhân dù hạn mức là nửa nhân. /proc/meminfo báo 16 GB dù hạn mức là 64 MB.
Đây không phải lỗi — /proc là của nhân, và nhân thì thật sự có 16 nhân với 16 GB. Không gian tên không ảo hoá /proc/cpuinfo hay /proc/meminfo.
Hậu quả thì rất thật: mọi thư viện tự cấu hình theo số nhân đều đọc sai. Số luồng worker, kích thước pool kết nối, kích thước heap — tất cả tính theo 16 nhân và 16 GB trong khi container chỉ có nửa nhân và 64 MB. Đó là chủ đề của phần sau.
Con số đúng nằm ở cgroup:
cat /sys/fs/cgroup/cpu.max # "50000 100000"
cat /sys/fs/cgroup/memory.max # so byte
Vượt hạn mức bộ nhớ
Một chương trình cấp phát dần 500 MB:
| Mã thoát | OOMKilled |
Dừng ở | |
|---|---|---|---|
-m 128m |
137 | true | 200 MB |
-m 256m |
137 | true | 350 MB |
-m 1g |
0 | false | xong 500 MB |
Không có cảnh báo, không có lỗi từ ứng dụng — tiến trình bị SIGKILL giữa chừng.
Chú ý cột cuối: với hạn mức 128 MB, chương trình cấp phát được tới 200 MB rồi mới chết. Nhân đẩy trang cũ ra và thu hồi bộ nhớ đệm trước khi giết, nên ngưỡng chết không trùng khít với hạn mức. Đừng đặt hạn mức sát mức dùng thật rồi tin rằng nó sẽ chết đúng ở đó.
Mã 137 có hai nghĩa
Phần 3 đã gặp mã 137 khi docker stop hết thời gian ân hạn. Giờ nó lại xuất hiện khi OOM. Cách phân biệt:
het gio dung : ExitCode=137 OOMKilled=false
bi OOM : ExitCode=137 OOMKilled=true
docker inspect <ten> --format 'ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}}'
Hai nguyên nhân hoàn toàn khác nhau và cách chữa cũng khác: một bên là ứng dụng không xử lý SIGTERM, một bên là thiếu bộ nhớ. Nhìn mã thoát thôi thì không phân biệt được.
Trường hợp nguy hiểm nhất: thoát 0 mà vẫn bị OOM
Nếu container chạy nhiều tiến trình, OOM killer chọn tiến trình ngốn nhất, không nhất thiết là PID 1. Tôi cho PID 1 là một script nhỏ, và tiến trình con là chương trình ăn RAM:
PID1=1 con=7
Killed
con thoat voi ma 137
PID 1 VAN SONG
ket qua: ExitCode=0 OOMKilled=true
Tiến trình con bị giết. PID 1 sống sót, chạy nốt script, và container thoát với mã 0.
Từ bên ngoài, đây là một container chạy thành công. Mọi cảnh báo dựa trên mã thoát đều im lặng. Chỉ có OOMKilled=true nói lên sự thật, và trường đó không xuất hiện trong docker ps, không xuất hiện trong log, không xuất hiện ở bất cứ đâu bạn hay nhìn.
Đây là biến thể container của đúng luật đã đo suốt sê-ri Vert.x: thứ giết dịch vụ của bạn thường không sinh ra lỗi nào.
Kịch bản thật hay gặp: container chạy một entrypoint script gọi ứng dụng làm tiến trình con — đúng cái mẫu mà phần 8 đã chỉ ra là thiếu exec. Ứng dụng bị OOM giết, script chạy tiếp và thoát bình thường, và bạn có một container "khoẻ mạnh" không phục vụ ai.
Một lý do nữa để dòng cuối entrypoint script luôn bắt đầu bằng exec.
--cpu-shares khác --cpus
Hai cờ dễ nhầm:
| Chạy một mình | Thời gian |
|---|---|
--cpu-shares 512 |
2,30 s |
--cpu-shares 2048 |
2,33 s |
--cpus 1 |
4,54 s |
--cpu-shares không giới hạn gì cả khi container chạy một mình — hai giá trị cách nhau bốn lần cho ra cùng thời gian. Nó chỉ là trọng số cho bộ lập lịch khi có tranh chấp.
Cho hai container tranh nhau hai nhân:
it-uu-tien (512) : 8,67 s
nhieu-uu-tien(2048): 8,12 s
Chênh khoảng 7%, không phải bốn lần như tỉ lệ trọng số gợi ý. Trọng số ảnh hưởng tới việc chia thời gian CPU nhưng không phải một đảm bảo tỉ lệ, và kết quả phụ thuộc nhiều vào hình dạng tải.
Luật thực dụng: dùng --cpus khi cần một giới hạn chắc chắn, dùng --cpu-shares khi chỉ muốn nói "cái này quan trọng hơn cái kia" trên một máy chạy nhiều thứ.
Nên đặt gì
docker run \
--cpus 2 \
--memory 512m \
--memory-swap 512m \
--pids-limit 200 \
...
--memory-swapbằng--memoryđể tắt swap. Không đặt thì container được dùng swap gấp đôi hạn mức, và ứng dụng chậm thảm hại thay vì chết dứt khoát — chậm thì khó chẩn đoán hơn nhiều.--pids-limitchặn fork bomb, kể cả vô tình. Không tốn gì để đặt.- Luôn đặt
--memorycho mọi container chạy thật. Không đặt thì một rò rỉ bộ nhớ sẽ ăn hết RAM máy chủ và OOM killer của nhân sẽ chọn nạn nhân theo tiêu chí của nó — có thể là một dịch vụ khác hoàn toàn.
Thử ba mươi giây
Xem container nào trên máy bạn không có hạn mức:
docker ps -q | while read c; do
printf "%-28s cpu=%s mem=%s\n" \
"$(docker inspect $c --format '{{.Name}}')" \
"$(docker inspect $c --format '{{.HostConfig.NanoCpus}}')" \
"$(docker inspect $c --format '{{.HostConfig.Memory}}')"
done
Giá trị 0 nghĩa là không giới hạn. Và kiểm xem có container nào từng bị OOM mà bạn không biết:
docker ps -aq | while read c; do
docker inspect $c --format '{{.Name}} {{.State.ExitCode}} {{.State.OOMKilled}}'
done | grep true
Phần sau đo chuyện JVM và Node đọc sai số nhân với RAM trong container, và cấu hình sai gây ra gì.