Mọi container đều chạy trong một cgroup. Bài này đo xem giới hạn CPU và bộ nhớ thật sự làm gì — và hai chỗ chúng khác hẳn với điều bạn thấy từ bên trong.
Giới hạn CPU thực thi rất chính xác
docker --cpus |
cpu.max |
nproc báo |
Thực tế dùng được | Bị ghim / 6 giây |
|---|---|---|---|---|
| 1 | 100000 100000 |
16 | 1,02 nhân | 61 |
| 2 | 200000 100000 |
16 | 2,01 nhân | 60 |
| 4 | 400000 100000 |
16 | 4,03 nhân | 60 |
Cột "thực tế" khớp gần như hoàn hảo. cpu.max đọc là "được dùng x micro giây trong mỗi chu kỳ y micro giây", và nhân thực thi đúng con số đó.
Cột "bị ghim" cũng đáng chú ý: 60 lần trong 6 giây — nghĩa là chu kỳ 100 ms nào cũng chạm trần. Với tải tính toán liên tục thì đó là bình thường, không phải triệu chứng.
Rồi nproc báo 16
Ba dòng đều báo 16, bất kể quota. Container thấy toàn bộ nhân của máy chủ; giới hạn nằm ở lượng thời gian chứ không ở số nhân.
Đây là gốc rễ của một lớp lỗi cấu hình rất phổ biến, vì rất nhiều thứ tự chọn kích thước theo số nhân:
| Thứ | Đọc gì |
|---|---|
nginx worker_processes auto |
số nhân |
Go GOMAXPROCS mặc định |
số nhân |
Node os.cpus().length |
số nhân |
| Java trước JDK 10 | số nhân |
| Bể luồng của phần lớn thư viện | số nhân |
Với quota 1 nhân, tất cả những thứ trên tạo 16 worker.
Và 32 luồng cho quota 1 nhân làm chậm 44%
Cùng một khối lượng công việc, chia cho N luồng, quota 1 CPU:
| Số luồng | Tổng thời gian | Luồng nhanh nhất / chậm nhất | Lệch |
|---|---|---|---|
| 1 | 5,243 s | 5,242 / 5,242 s | 1,0× |
| 2 | 5,220 s | 5,208 / 5,220 s | 1,0× |
| 4 | 5,386 s | 5,377 / 5,386 s | 1,0× |
| 8 | 5,581 s | 5,470 / 5,581 s | 1,0× |
| 16 | 6,038 s | 5,417 / 6,038 s | 1,1× |
| 32 | 7,542 s | 3,718 / 7,539 s | 2,0× |
Hai cái giá, và cái thứ hai đắt hơn:
Thông lượng giảm 44% — cùng công việc, thêm 2,3 giây, chỉ vì chia nhỏ ra nhiều luồng hơn.
Đuôi phân bố giãn gấp đôi — luồng nhanh nhất xong ở 3,718 s, luồng chậm nhất ở 7,539 s. Trong một máy chủ web, "luồng" là "yêu cầu", và con số đó là p99 của bạn.
Cách chữa là bảo runtime đọc quota thay vì đọc nproc:
GOMAXPROCS=1 ./dich-vu # Go, hoac dung uber-go/automaxprocs
java -XX:ActiveProcessorCount=1 ... # JVM, hoac de JDK 10+ tu doc cgroup
worker_processes 1; # nginx
UV_THREADPOOL_SIZE=2 node app.js # Node
JDK từ phiên bản 10 tự đọc cgroup và availableProcessors() trả về đúng quota — một trong những cải tiến ít được nhắc mà đáng giá nhất cho Java trong container.
Đọc quota cho đúng
cat /sys/fs/cgroup/cpu.max # "100000 100000" hoac "max 100000"
cat /sys/fs/cgroup/cpu.stat # usage_usec, nr_throttled, throttled_usec
nr_throttled và throttled_usec là hai con số chẩn đoán tốt nhất mà phần lớn hệ thống giám sát không thu thập. Chúng trả lời câu hỏi "dịch vụ này chậm vì quota hay vì lý do khác" một cách dứt khoát.
Với cgroup v1 (còn gặp trên hệ thống cũ) thì đường dẫn khác hẳn:
cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us
cat /sys/fs/cgroup/cpu/cpu.cfs_period_us
Bộ nhớ: bộ đệm trang tính vào giới hạn
Container giới hạn 512 MB. Ghi một tệp 400 MB rồi sync:
memory.current 413 MB (81% giới hạn)
file (bộ đệm) 400 MB
anon (thật) 0 MB
Chương trình không giữ byte nào, và bảng giám sát báo 81%.
Đây là nguồn của rất nhiều cảnh báo giả. Con số đáng nhìn không phải memory.current mà là dòng anon trong memory.stat:
awk '/^anon |^file |^slab /{print}' /sys/fs/cgroup/memory.stat
Cấp thêm 300 MB bộ nhớ thật vào đúng container đó:
memory.current 511 MB
file 203 MB <- bi thu hoi
anon 300 MB
OOM: 0 lần. Chạm trần: 789 lần.
Nhân thu hồi bộ đệm trang để nhường chỗ. Không ai chết. memory.events ghi 789 lần chạm trần — và đó cũng là bình thường, không phải sự cố.
Nhưng bộ đệm bẩn thì không thu hồi kịp
Lần đo đầu tiên tôi quên sync sau khi ghi tệp. Kết quả:
memory.current 511 MB | file 465 MB | anon 0 MB
OOM: 1 lần
Tiến trình xin 300 MB bị giết, trong khi lần chạy có sync thì không.
Trang bộ đệm sạch vứt đi được ngay — bản gốc còn trên đĩa. Trang bẩn thì phải ghi xuống trước, và việc đó mất thời gian mà bộ cấp phát không có.
Nối thẳng với phần 13: một container ghi nhiều và nhanh có thể tích tụ hàng trăm megabyte trang bẩn, và khi có ai đó xin bộ nhớ đúng lúc ấy thì OOM killer ra tay — dù anon gần bằng 0 và không ai rò rỉ gì cả.
Đây là dạng OOM khó chẩn đoán nhất, vì mọi số liệu sau sự cố đều trông bình thường.
Ba mức áp lực bộ nhớ của cgroup v2
cgroup v2 tách rõ ba mức, và cgroup v1 không có mức giữa:
| Nút | Nghĩa |
|---|---|
memory.low |
Dưới mức này thì nhân ưu tiên không thu hồi. Bảo vệ mềm. |
memory.high |
Vượt thì tiến trình bị làm chậm lại để nhân kịp thu hồi. Không giết. |
memory.max |
Vượt thì OOM. |
memory.high là công cụ tốt nhất mà ít người dùng: nó biến một cú chết đột ngột thành một đợt chậm dần, đủ thời gian để hệ thống giám sát kịp báo. Docker không phơi nó ra (-m chỉ đặt memory.max), nhưng systemd có MemoryHigh= và Kubernetes đang dần hỗ trợ.
Thử ba mươi giây
Xem container hoặc dịch vụ của bạn đang bị ghim bao nhiêu:
c=/sys/fs/cgroup # ben trong container
echo "cpu.max : $(cat $c/cpu.max 2>/dev/null)"
echo "nproc : $(nproc)"
grep -E 'nr_throttled|throttled_usec|usage_usec' $c/cpu.stat 2>/dev/null
echo "--- bo nho ---"
echo "max : $(cat $c/memory.max 2>/dev/null)"
echo "current : $(cat $c/memory.current 2>/dev/null)"
awk '/^anon |^file /{printf " %-6s %d MB\n", $1, $2/1048576}' $c/memory.stat 2>/dev/null
grep . $c/memory.events 2>/dev/null | tr '\n' ' '; echo
Đọc hai lần cách nhau một phút. nr_throttled tăng đều với một dịch vụ đang rảnh là dấu hiệu quota quá chặt. Và nếu current cao mà anon thấp, cảnh báo bộ nhớ của bạn đang đo bộ đệm trang chứ không đo ứng dụng.
Phần sau: theo dõi trong sản phẩm — chọn chỉ số nào và đọc chúng ra sao.