Phần 15 kết thúc ở chỗ tắt swap nghĩa là bị giết ngay khi vượt giới hạn. Bài này đo xem ai bị giết.
Bố trí
Container giới hạn 700 MB, không swap. Ba tiến trình lần lượt cấp và chạm vào bộ nhớ:
A-nho 100 MB oom_score 670
B-vua 200 MB oom_score 673
C-lon 250 MB oom_score 674
Tổng 550 MB, còn dư. Rồi tiến trình thứ tư xin thêm 300 MB — tổng lên 850 MB, vượt giới hạn.
Kẻ gây tràn không phải kẻ bị giết
còn sống sau khi OOM:
300 D-giot-nuoc-tran rss 308432 KB
100 A-nho rss 103632 KB
200 B-vua rss 206032 KB
C-lớn chết. D — tiến trình vừa làm tràn giới hạn — sống, và giữ trọn 300 MB nó xin.
Chạy lại lần thứ hai, kết quả giống hệt.
Đây là điều quan trọng nhất cần nhớ về OOM killer: nó không trừng phạt kẻ gây ra vấn đề. Nó tìm cách giải phóng nhiều bộ nhớ nhất với ít lần giết nhất, và tiêu chí đó dẫn tới việc chọn tiến trình lớn nhất tại thời điểm đó.
Hệ quả thực tế rất khó chịu: một tác vụ nền chạy sai — một lệnh import, một script dọn dữ liệu — có thể làm chết chính cơ sở dữ liệu, rồi tự nó chạy tiếp bình thường. Nhật ký sẽ ghi cơ sở dữ liệu bị giết, và không có gì trỏ về thủ phạm.
oom_score gần như vô dụng trong container
Bốn tiến trình chênh nhau ba lần về bộ nhớ. Điểm của chúng:
| Tiến trình | Bộ nhớ | oom_score |
|---|---|---|
| A-nho | 100 MB | 670 |
| B-vua | 200 MB | 673 |
| C-lon | 250 MB | 674 |
| D | 300 MB | 676 |
6 điểm chênh lệch cho 3 lần chênh bộ nhớ.
Lý do: oom_score trong /proc được tính theo phần trăm tổng RAM của máy — ở đây 16 GB — chứ không theo giới hạn của cgroup. 100 MB và 300 MB đều là những phần nhỏ xíu của 16 GB, nên chúng gần như bằng nhau. Con số 670 là phần nền chung.
Nghĩa là đọc oom_score để đoán ai sẽ chết là vô ích khi tiến trình chạy trong container. Muốn đoán, hãy nhìn thẳng vào memory.current của từng cgroup con, hoặc VmRSS của từng tiến trình trong cùng cgroup.
oom_score_adj có tác dụng, nhưng không cứu được ai
Đặt oom_score_adj = 1000 cho A-nho — nghĩa là "giết cái này trước":
100 A-nho-adj-1000 adj 1000 score 1336
250 C-lon adj 0 score 674
A chết trước, đúng như mong đợi. Nhưng:
oom_kill = 2
Giải phóng 100 MB không đủ, nên OOM killer quay lại và giết luôn C-lớn. Kết cục: mất hai tiến trình thay vì một.
Đưa một tiến trình nhỏ lên đầu danh sách hy sinh không cứu được tiến trình lớn. Nó chỉ thêm một lần giết nữa trước khi chuyện cũ vẫn xảy ra.
Chiều ngược lại — bảo vệ tiến trình quan trọng — mới là cách dùng đúng:
echo -900 > /proc/$(pidof postgres)/oom_score_adj
Giá trị âm cần CAP_SYS_RESOURCE; trong container thường không có, và lệnh sẽ báo I/O error — tôi vấp đúng chuyện đó khi thử. Với systemd thì khai trong unit:
[Service]
OOMScoreAdjust=-900
Dải giá trị là −1000 đến 1000. −1000 nghĩa là miễn nhiễm hoàn toàn, và đó là thứ nên dùng rất dè dặt: nếu tiến trình miễn nhiễm chính là cái đang rò rỉ bộ nhớ, nhân sẽ giết hết mọi thứ khác rồi bó tay, và máy treo cứng.
Ba con số trong memory.events
cat /sys/fs/cgroup/memory.events
max 86
oom 4
oom_kill 1
| Trường | Nghĩa |
|---|---|
max |
Số lần chạm trần giới hạn. Lớn nhưng oom bằng 0 nghĩa là nhân thu hồi kịp — sát nút nhưng chưa nguy. |
oom |
Số lần không còn gì để thu hồi. |
oom_kill |
Số tiến trình thật sự bị giết. |
max cao mà oom_kill bằng 0 là tín hiệu cảnh báo sớm tốt nhất bạn có được: giới hạn đang quá chặt, và chỉ cần một cú tăng tải là có người chết.
Với Kubernetes, max cao tương ứng với việc pod liên tục sát trần limits.memory, và điều đó không hiện ra trong sự kiện của pod cho tới khi có kill thật.
Tìm dấu vết sau khi đã xảy ra
dmesg -T | grep -iE 'killed process|out of memory|oom-kill'
journalctl -k --since "1 hour ago" | grep -i oom
Dòng nhật ký của nhân liệt kê đầy đủ mọi tiến trình trong cgroup kèm RSS của từng cái tại thời điểm đó — đây là ảnh chụp duy nhất bạn có về hiện trường, và nó thường trả lời được câu hỏi "cái gì đã phình ra".
Với container, thêm một chỗ nữa:
docker inspect <ten> --format '{{.State.OOMKilled}} {{.State.ExitCode}}'
kubectl get pod <ten> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}'
Mã thoát 137 kèm OOMKilled: true là chữ ký không thể nhầm.
Thử ba mươi giây
Xem tiến trình nào đang đứng đầu danh sách hy sinh trên máy bạn:
for p in $(ls /proc | grep -E '^[0-9]+$'); do
sc=$(cat /proc/$p/oom_score 2>/dev/null) || continue
adj=$(cat /proc/$p/oom_score_adj 2>/dev/null)
rss=$(awk '/^VmRSS:/{print $2}' /proc/$p/status 2>/dev/null)
[ -n "$rss" ] && printf "%6s %6s %9s %s\n" "$sc" "$adj" "$rss" \
"$(tr -d '\0' < /proc/$p/cmdline | cut -c1-45)"
done 2>/dev/null | sort -rn | head -10 | \
awk 'BEGIN{print " score adj rss(KB) lenh"} {print}'
echo "--- lich su ---"
dmesg -T 2>/dev/null | grep -i "killed process" | tail -5
Dòng đầu tiên là tiến trình sẽ chết trước nếu máy hết bộ nhớ ngay bây giờ. Nếu đó là cơ sở dữ liệu hoặc dịch vụ chính của bạn, hãy hạ oom_score_adj của nó xuống — và quan trọng hơn, hãy tìm xem cái gì đang tranh bộ nhớ với nó.
Phần sau: cgroup v2 — cách nhân thực sự giới hạn CPU, bộ nhớ và I/O.