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.

OOM killer chọn nạn nhân như thế nào

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.