Phần 4 đo cách cgroup giới hạn bộ nhớ và OOM killer ra tay khi vượt trần. Phần này quay lại cùng cơ chế nhưng ở mức chi tiết hơn, để trả lời một câu hỏi mà nhiều người vận hành hiểu sai: khi memory.current của container leo gần memory.max, có phải nó sắp bị OOM không? Câu trả lời là không nhất thiết, và lý do nằm ở chỗ memory.current gộp hai loại byte có số phận hoàn toàn khác nhau trước OOM killer. Một loại kernel có thể thu hồi và nhường chỗ khi cần, loại kia thì không. Bài này đo trực tiếp sự khác biệt đó bằng cách đọc một file lớn gấp nhiều lần giới hạn mà không bị giết, so với cấp phát bộ nhớ ẩn danh cùng cỡ thì bị giết ngay, và giới thiệu memory.pressure như một tín hiệu cảnh báo sớm.
Không phải byte nào cũng như nhau trước OOM killer

Hình 1: memory.current gồm anon (RSS, không thu hồi) và file (page cache, thu hồi được), cùng memory.pressure và cách đo trong cgroup 128 MB.
cgroup v2 giới hạn bộ nhớ của một nhóm tiến trình bằng file memory.max, và lượng đang dùng đọc ở memory.current. Điểm mấu chốt mà bài này đo là memory.current không phải một khối đồng nhất; nó gộp hai loại bộ nhớ rất khác nhau, phân rã ra trong memory.stat. Loại thứ nhất là anon, bộ nhớ ẩn danh, còn gọi là RSS theo nghĩa phần thường trú: đây là bộ nhớ cấp phát qua malloc, các biến, stack, mọi thứ không có bản sao trên đĩa. Kernel không thu hồi được anon, vì không có chỗ nào để đẩy nó ra khi swap bị tắt; nó phải nằm trong RAM. Loại thứ hai là file, page cache: nội dung các file đã đọc được giữ trong bộ nhớ để lần sau đọc nhanh hơn. Page cache thu hồi được, vì nếu cần chỗ, kernel chỉ việc bỏ nó đi và đọc lại từ đĩa khi cần.
Sự khác biệt này quyết định hành vi khi cgroup chạm trần. Khi memory.current tiến tới memory.max, kernel không giết tiến trình ngay; trước tiên nó thử thu hồi page cache để lấy lại chỗ. Chỉ khi đã thu hồi hết cache mà vẫn còn quá nhiều anon không nén được, OOM killer mới ra tay và giết tiến trình tốn RAM nhất bằng SIGKILL. Hệ quả trực tiếp và hơi phản trực giác: một tiến trình đọc một file lớn gấp nhiều lần giới hạn vẫn sống khỏe, vì cache cứ được đẩy ra để nhường chỗ cho phần mới đọc; nhưng một tiến trình cấp phát bộ nhớ ẩn danh vượt giới hạn thì chết, vì không có gì để đẩy. Cùng một con số byte, hai số phận khác nhau tùy loại.
Để quan sát áp lực bộ nhớ trước khi mọi chuyện thành OOM, cgroup v2 có memory.pressure, một phần của cơ chế PSI. Dòng "some" của nó cho tổng thời gian, tính bằng micro giây, mà các tiến trình trong cgroup bị kẹt chờ bộ nhớ; con số này tăng lên nghĩa là kernel đang phải vất vả thu hồi cache để nhường chỗ, một tín hiệu cảnh báo sớm hữu ích. Bên cạnh đó, memory.events có trường oom_kill đếm số lần OOM killer đã ra tay trong cgroup này, và memory.peak cho biết đỉnh memory.current từng chạm, để biết container có áp sát trần không. Cách đo gồm hai thí nghiệm trong một cgroup 128 MB tắt swap: đọc một file 400 MB chỉ đọc, ba lượt thành 1200 MB, rồi xem memory.current, phân rã anon so với file, oom_kill, và pressure; và fork một tiến trình con cấp phát RSS bằng bytearray chạm từng trang cho tới khi hết, rồi xem tín hiệu giết con, memory.peak, và oom_kill tăng.
Đo thật: đọc 1200 MB cache không OOM, cấp RSS thì bị giết

Hình 2: Đọc 1200 MB page cache trong cgroup 128 MB không gây OOM, còn cấp phát RSS vượt trần thì OOM killer SIGKILL ngay.
| phép đo | kết quả |
|---|---|
| đọc 1200 MB file cache (trần 128 MB) | không OOM |
| memory.current / memory.peak (test cache) | 120,1 MB / 128,0 MB |
| memory.stat anon / file (test cache) | 3,9 MB / 114,4 MB |
| oom_kill (test cache) | 0 |
| memory.pressure some total (test cache) | 0 → 15 505 µs |
| cấp phát RSS tới khi hết | con bị SIGKILL (tín hiệu 9) |
| memory.peak (test RSS) | 128,0 MB (chạm đúng trần) |
| oom_kill (test RSS) | 0 → 1 |
| anon còn lại sau khi con chết | 3,8 MB |
Đọc 1200 MB page cache trong cgroup 128 MB mà không hề bị OOM. Thí nghiệm đầu đọc một file 400 MB ba lượt, tổng 1200 MB, gấp khoảng chín lần giới hạn 128 MB của cgroup. Kết quả: không có OOM nào. memory.current dừng ở 120,1 MB, sát trần nhưng dưới, và memory.peak chạm đúng 128,0 MB. Phân rã memory.stat cho thấy phần lớn bộ nhớ đang dùng là page cache: file là 114,4 MB, trong khi anon chỉ 3,9 MB. oom_kill là 0. Đây là bằng chứng trực tiếp rằng page cache thu hồi được: khi đọc tới phần mới của file, kernel đẩy các trang cache cũ ra để lấy chỗ, nên lượng cache đang giữ không bao giờ vượt trần, và tiến trình đọc trọn 1200 MB mà không gặp vấn đề gì. Dấu hiệu duy nhất của áp lực là memory.pressure dòng "some" tăng từ 0 lên 15 505 micro giây, phản ánh công kernel bỏ ra để liên tục thu hồi cache.
Cấp phát bộ nhớ ẩn danh cùng cỡ thì bị OOM killer giết ngay. Thí nghiệm thứ hai cho một tiến trình con cấp phát RSS bằng cách tạo bytearray và chạm vào từng trang để chúng thực sự thường trú trong RAM, không chỉ là cấp phát ảo. Khi tổng anon tiến tới trần, OOM killer ra tay: con bị tín hiệu số 9, tức SIGKILL, đúng cách OOM killer kết liễu tiến trình tốn RAM nhất. memory.peak chạm 128,0 MB, đúng trần. memory.events cho thấy oom_kill tăng từ 0 lên 1, xác nhận chính OOM killer của cgroup này đã hành động. Và ngay sau khi con chết, anon tụt về 3,8 MB, vì toàn bộ RSS của con được giải phóng khi nó bị giết. So sánh hai thí nghiệm làm rõ nguyên lý: cùng một cgroup 128 MB, 114 MB page cache thì sống và chỉ đẩy pressure lên, còn hơn 128 MB anon RSS thì chết với một lần oom_kill.
Kết luận vận hành: đặt --memory theo đỉnh RSS, không theo memory.current. Phép đo này giải một hiểu nhầm phổ biến. Khi thấy memory.current hay thống kê RAM của container cao gần trần, nhiều người kết luận container sắp hết bộ nhớ và vội nâng --memory. Nhưng phần lớn con số cao đó thường là page cache, thứ vô hại và tự nhường chỗ khi cần. Chỉ phần anon (RSS) vượt trần mới thực sự gây OOM. Vì vậy, muốn đặt --memory đúng, nên nhìn đỉnh anon trong memory.stat hoặc đỉnh RSS của ứng dụng, không nhìn memory.current lúc chạy. Đặt giới hạn quá sát một con số bị page cache thổi phồng sẽ cắt mất chỗ cache hữu ích mà không giải quyết rủi ro OOM thật.
Đánh đổi cần cân nhắc
Theo dõi memory.pressure để cảnh báo sớm, thay vì chỉ chờ OOM. oom_kill là tín hiệu quá muộn: khi nó tăng, một tiến trình đã chết rồi. memory.pressure dòng "some" và "full" tăng dần trước đó, cho biết cgroup đang chật vật thu hồi bộ nhớ ngay cả khi chưa ai bị giết. Một hệ thống giám sát đọc memory.pressure của từng cgroup có thể cảnh báo khi áp lực leo cao và cho người vận hành thời gian nâng giới hạn hoặc giảm tải trước khi OOM thật xảy ra. Đây là lợi thế lớn của PSI so với chỉ theo dõi memory.current, vì một memory.current cao do cache không có nghĩa là nguy hiểm, còn pressure cao thì luôn có nghĩa.
Tắt swap trong container cho hành vi OOM rõ ràng, nhưng hiểu cái giá. Phép đo này đặt --memory-swap bằng --memory để tắt swap, nhờ đó anon không có chỗ tràn ra và OOM xảy ra đúng tại trần, dễ đo và dễ đoán. Trong vận hành thật, cho phép một ít swap có thể giúp container chịu được đỉnh anon ngắn mà không bị giết, đổi lại hiệu năng tụt khi phải swap. Lựa chọn tùy workload: dịch vụ nhạy độ trễ thường tắt swap để tránh những khựng khó đoán, còn batch job có thể chấp nhận swap để không chết giữa chừng. Điều quan trọng là biết mình đang chọn gì, vì mặc định của từng nền tảng khác nhau.
Phạm vi đo. Bài đo anon so với file, OOM trên anon, và PSI, không đi vào các loại bộ nhớ khác trong memory.stat như shmem, kernel, hay pagetables, vốn cũng tính vào memory.current. Trang cache bẩn, tức đã sửa nhưng chưa ghi xuống đĩa, là trường hợp trung gian: nó phải được ghi back trước khi thu hồi, nên dưới áp lực nặng có thể gây khựng hoặc, trong cgroup chật, góp phần vào OOM; bài này cố ý đọc file read-only để tách riêng cache sạch. Chưa đo memory.high như ngưỡng mềm bóp tiến trình trước khi chạm memory.max, và chưa đo hành vi khi bật swap, những biến thể không đổi kết luận cốt lõi về anon so với cache.
Ba ý mang về
- memory.current gộp hai loại byte khác số phận.
anon(RSS) không thu hồi được và gây OOM khi vượt trần;file(page cache) thu hồi được nên đọc gấp chín lần giới hạn vẫn sống. Nhìnmemory.statđể phân biệt. - Đặt --memory theo đỉnh RSS, không theo memory.current lúc chạy. Con số đó thường phồng vì page cache vô hại; đặt giới hạn quá sát nó cắt mất cache hữu ích mà không giảm rủi ro OOM.
- memory.pressure cảnh báo sớm hơn oom_kill. PSI tăng khi kernel chật vật thu hồi cache, trước khi có ai bị giết; theo dõi nó cho thời gian phản ứng mà
oom_killkhông có.
Nguồn
- Linux — Control Group v2, tài liệu chính thức về
memory.max,memory.current,memory.stat,memory.events: https://docs.kernel.org/admin-guide/cgroup-v2.html - Linux — PSI: Pressure Stall Information,
memory.pressurevà các chỉ số some/full: https://docs.kernel.org/accounting/psi.html - Linux — quản lý page cache và thu hồi (reclaim), anon so với file: https://docs.kernel.org/admin-guide/mm/concepts.html
- Docker — tùy chọn
--memory,--memory-swapvà hành vi OOM: https://docs.docker.com/engine/containers/resource_constraints/#memory - Linux
proc(5)— RSS và các chỉ số bộ nhớ của tiến trình: https://man7.org/linux/man-pages/man5/proc.5.html
Phần sau là bài tổng kết cả sê-ri: một checklist thực chiến để chạy ứng dụng backend trong container cho đúng, gom lại mọi phép đo từ namespace, cgroup, OverlayFS, capabilities, seccomp, file descriptor, PID 1 tới bộ nhớ thành những quyết định cấu hình cụ thể mà bạn áp dụng được ngay cho Dockerfile và lệnh chạy của mình.