Hình dung bạn đăng ký một lô đất. Bạn có thể xí chỗ một mảnh 3 héc-ta trên giấy tờ gần như tức thì và miễn phí — nhưng bạn chỉ thật sự tốn tiền xây dựng cho phần đất bạn thực sự xây nhà lên. Và khi cả khu phố muốn tính "tổng diện tích sử dụng", con đường chung trước nhà không nên đếm một lần cho mỗi hộ. Ba con số này — mảnh đất đã xí, phần đã xây, và phần chia của tiện ích chung — chính là VSZ, RSS và PSS của một tiến trình. Lẫn lộn chúng là nguồn của rất nhiều báo động bộ nhớ sai.

Bạn mở top hay ps, thấy một tiến trình có VSZ 3 GB và RSS 50 MB, và tự hỏi: nó dùng bao nhiêu bộ nhớ? Hai con số này thường bị lẫn lộn, và chọn nhầm con số dẫn tới hoặc hoảng loạn vô cớ (tưởng dùng 3 GB) hoặc đếm sai tổng RAM của cả hệ. Chúng đo hai thứ khác nhau: VSZ là bộ nhớ đã đặt chỗ (ảo), RSS là bộ nhớ thật đang chiếm (vật lý). Và còn một con số thứ ba ít người biết — PSS — mới là thứ đúng khi cộng nhiều tiến trình. Tôi đo cả ba từ /proc trong container gcc:13.

Ba con số, ba nghĩa khác nhau

VSZ (VmSize) — tổng không gian địa chỉ ảo mà tiến trình đã đặt chỗ (mapped): mọi vùng mmap, mọi malloc, thư viện chia sẻ, stack, guard page. Quan trọng: nó gồm cả những vùng bạn chưa hề chạm tới — malloc một vùng lớn làm VSZ tăng ngay dù chưa một byte nào được cấp RAM thật. Đây là mảnh đất đã xí trên giấy.

RSS (VmRSS) — bộ nhớ vật lý thật tiến trình đang chiếm: chỉ những trang đã thực sự được cấp RAM (đã "backing"). RSS chia nhỏ: RssAnon (bộ nhớ ẩn danh riêng — heap, stack), RssFile (trang từ file/thư viện được map — thường chia sẻ với tiến trình khác), RssShmem (shared memory phần 26). Đây là phần đất đã thật sự xây nhà.

PSS (Pss) — như RSS, nhưng các trang chia sẻ được chia theo tỉ lệ: nếu một trang thư viện dùng chung bởi 5 tiến trình, mỗi tiến trình chỉ tính 1/5 trang đó. Đây là con số đúng khi bạn muốn cộng bộ nhớ của nhiều tiến trình mà không đếm trùng — phần chia của con đường chung.

Đo: VSZ nhảy 3GB, RSS đứng yên

Tôi đọc /proc/self/status và smaps_rollup qua bốn mốc:

                            VSZ           RSS      (Anon / File-lib)   PSS
1) lúc khởi động        :    2.204 KB    1.016 KB  (84 / 932)          453 KB
2) sau malloc 3GB (chưa chạm): 3.147.936 KB  1.216 KB  (92 / 1124)     461 KB
3) sau chạm 200 MB      : 3.147.936 KB  206.208 KB (205.084 / 1124)  205.453 KB
4) sau chạm 500 MB      : 3.147.936 KB  513.408 KB (512.284 / 1124)  512.653 KB

Nhìn dòng 2: sau malloc(3 GB) mà chưa chạm, VSZ nhảy vọt lên ~3,15 GB — nhưng RSS gần như không đổi (1,0 → 1,2 MB). Vùng 3 GB kia hoàn toàn trên giấy: không gian địa chỉ đã đặt chỗ, nhưng chưa một trang vật lý nào được cấp. Nếu bạn theo dõi VSZ để đo RAM, bạn sẽ báo động "tiến trình dùng 3 GB!" trong khi nó thực sự dùng ~1 MB. RSS mới là con số thật, và nó chỉ tăng khi bạn chạm: dòng 3 (chạm 200 MB) RSS lên ~201 MB, dòng 4 (chạm 500 MB) RSS lên ~501 MB — đúng bằng lượng đã chạm, trong khi VSZ giữ nguyên 3 GB suốt.

Giờ nhìn cột PSS ở dòng 1: RSS là 1.016 KB nhưng PSS chỉ 453 KB — thấp hơn hẳn. Vì sao? Vì RSS gồm RssFile = 932 KB, là các trang thư viện chia sẻ (libc, loader…) được map vào tiến trình. Những trang đó dùng chung với các tiến trình khác trong hệ, nên RSS đếm chúng đầy đủ trong mỗi tiến trình, còn PSS chỉ tính phần chia của tiến trình này. Ở dòng 3–4, sau khi chạm bộ nhớ ẩn danh riêng (RssAnon), PSS ≈ RSS — vì bộ nhớ riêng thì không chia sẻ, đếm đủ ở cả hai. Khác biệt PSS < RSS chính là phần trang chia sẻ bị RSS đếm trùng.

Hai niềm tin đối nghịch, cùng sai

Tôi vào đo với hai niềm tin. Thứ nhất: "VSZ là bộ nhớ tiến trình dùng; theo dõi VSZ để biết RAM". Con số bác thẳng — malloc 3 GB chưa chạm cho VSZ ~3 GB mà RSS chỉ ~1 MB. VSZ là không gian ảo đặt chỗ, phần lớn chưa được cấp RAM (đúng cái overcommit của phần 29); RSS mới là RAM thật.

Thứ hai, ngược chiều: "muốn biết tổng RAM nhiều tiến trình dùng, cộng RSS của chúng lại". Cũng sai — RSS đếm trang chia sẻ (thư viện, shm) đầy đủ trong mỗi tiến trình; cộng RSS của 10 tiến trình cùng dùng libc sẽ đếm libc 10 lần. Đo cho thấy ngay trong một tiến trình: RSS 1016 KB nhưng PSS chỉ 453 KB — chênh lệch chính là trang thư viện bị đếm trùng. Nếu cộng RSS để tính tổng, con số có thể vượt cả RAM vật lý của máy — dấu hiệu rõ ràng rằng cách cộng sai.

Về đến code hằng ngày

Điều đầu tiên: theo dõi RAM bằng RSS (một tiến trình), không bằng VSZ. Khi giám sát bộ nhớ của một dịch vụ, RSS là con số phản ánh áp lực RAM thật; VSZ cao là bình thường và không có nghĩa gì (nhất là với chương trình mmap nhiều vùng lớn, JVM, Go runtime đặt chỗ heap ảo lớn). Đừng đặt cảnh báo trên VSZ.

Điều thứ hai: khi cộng bộ nhớ nhiều tiến trình (một nhóm, một container), dùng PSS. Cộng RSS đếm trùng thư viện chia sẻ và shared memory; smaps_rollup cho Pss là cách đúng để chia phần chia sẻ. Đây là lý do các công cụ như smem báo PSS, và vì sao "tổng RSS" của nhiều worker chia sẻ thư viện luôn phóng đại mức dùng thật. Container/cgroup phần 35 đo bộ nhớ theo cách kế toán riêng, gần với thực tế hơn cộng RSS.

Chỗ này đúng là nơi các runtime cao cấp hay bị hiểu lầm nhất, nên biết trước rất đỡ. JVM đặt chỗ (reserve) cả -Xmx heap tối đa dưới dạng bộ nhớ ảo ngay khi khởi động, nên VSZ của một app Java trông khổng lồ dù nó mới dùng vài chục MB — báo động trên VSZ ở đây là báo động giả kinh điển. Go runtime cũng đặt chỗ vùng địa chỉ ảo lớn cho heap arena vì lý do tương tự. Còn khi bạn chạy nhiều tiến trình con fork từ một master (như kiểu prefork của nhiều web server, hay các worker Python gunicorn), chúng dùng chung phần lớn trang code nhờ copy-on-write — cộng RSS sẽ phóng đại kinh khủng, và PSS (hoặc con số cgroup) mới nói đúng máy đang thật sự tốn bao nhiêu. Nhận ra "RSS đếm trùng phần chia sẻ" là lúc bạn ngừng hoảng vì những con số phồng.

Muốn tự thấy thì viết một chương trình malloc một vùng lớn (vài GB) không chạm, in /proc/self/status | grep -E "VmSize|VmRSS", rồi chạm dần và in lại. Bạn sẽ thấy VmSize (VSZ) nhảy ngay khi malloc và giữ nguyên, còn VmRSS chỉ bò lên khi bạn chạm. Rồi so VmRSS với dòng Pss trong /proc/self/smaps_rollup: với một chương trình dùng thư viện chia sẻ, PSS sẽ thấp hơn RSS — phần chênh là trang thư viện dùng chung. Vài phút đó cho bạn thấy điều mà top không nói rõ: VSZ là lời hứa đặt chỗ, RSS là RAM thật đang giữ, và khi bạn muốn cộng nhiều tiến trình, chỉ PSS mới không đếm trùng — chọn nhầm con số là hiểu sai hoàn toàn về bộ nhớ của chương trình.