Bạn mở top hoặc ps, thấy app của mình có cột bộ nhớ ghi 2GB, và tim đập nhanh — "sao nó ngốn 2GB RAM?". Nhưng rất có thể app đó chỉ đang dùng vài trăm MB RAM thật. Vấn đề là bạn đang nhìn nhầm cột. Linux báo cáo hai con số bộ nhớ rất khác nhau: VSZ (bộ nhớ ảo) và RSS (bộ nhớ thật), và nhầm lẫn giữa chúng dẫn tới hoảng loạn vô cớ hoặc bỏ sót rò rỉ thật. Bài này (phần 5 loạt Debug) chạy thật để thấy sự khác biệt và biết nhìn con số nào.

Hai con số bộ nhớ

  • VSZ (Virtual Size): toàn bộ không gian địa chỉ ảo mà tiến trình đã "đặt chỗ" — gồm cả phần đã xin nhưng chưa dùng, các thư viện được map, file được map vào bộ nhớ. Đây là "hạn mức tối đa có thể", không phải mức đang dùng.
  • RSS (Resident Set Size): lượng RAM vật lý thật tiến trình đang chiếm ngay lúc này. Đây là con số bạn quan tâm khi lo về bộ nhớ.

Nhầm VSZ với RAM thật là lỗi phổ biến. Một app có VSZ khổng lồ (vì map nhiều file hoặc xin trước bộ nhớ) mà RSS nhỏ thì hoàn toàn lành mạnh.

Vì sao VSZ > RSS: cấp phát lười

Điểm mấu chốt: khi chương trình xin bộ nhớ (malloc/mmap), kernel không giao RAM thật ngay — nó chỉ ghi sổ rằng "vùng địa chỉ này thuộc về bạn". Chỉ khi bạn chạm vào một trang (đọc/ghi), kernel mới xử lý một page fault và giao một trang RAM thật cho trang đó. Đây là cấp phát lười (lazy allocation).

m = mmap(500MB)   # VSZ tăng 500MB, nhưng RSS gần như không đổi (chưa chạm)
m[offset] = 1     # chạm một trang -> kernel giao RAM thật -> RSS tăng

Ảnh chụp đoạn mã nền tối minh hoạ RSS vs VSZ app dùng 2GB bộ nhớ ảo hay thật, hai con số bộ nhớ khác nhau VSZ Virtual Size toàn bộ không gian địa chỉ ảo tiến trình đặt chỗ gồm cả phần chưa dùng thư viện map file map RSS Resident Set Size RAM vật lý thật đang chiếm nhầm VSZ với RAM thật hoảng loạn vô cớ RSS mới đáng lo, vì sao VSZ lớn hơn RSS cấp phát lười lazy allocation xin 500MB malloc mmap kernel chỉ ghi sổ chưa giao RAM chỉ khi bạn chạm đọc ghi một trang kernel mới giao trang RAM thật page fault VSZ tăng ngay RSS tăng dần m bằng mmap 500MB VSZ cộng 500 RSS gần như 0 m offset bằng 1 chạm RSS mới tăng, đọc đúng cột ps -o pid vsz rss comm cột rss bằng RAM thật KB grep VmRSS proc pid status cùng con số RSS theo dõi RSS tăng dần theo thời gian bằng rò rỉ bộ nhớ thật, overcommit tổng VSZ có thể lớn hơn RAM vật lý vì phần lớn VSZ không được chạm Linux cho tổng VSZ vượt RAM nhưng nếu mọi tiến trình chạm hết hết RAM thật OOM killer

Hình 1: VSZ là không gian ảo đặt chỗ (gồm phần chưa dùng), RSS là RAM thật; cấp phát lười khiến VSZ tăng ngay khi xin nhưng RSS chỉ tăng khi chạm; đọc cột rss trong ps; overcommit cho tổng VSZ vượt RAM.

Đo thật: VSZ nhảy 500MB, RSS vẫn 7MB

Mình mmap 500MB rồi chạm dần, theo dõi VSZ và RSS qua /proc/<pid>/status:

Ảnh chụp bảng kết quả chạy thật mmap 500MB rồi chạm dần output thật, theo dõi VSZ và RSS qua từng bước một ban đầu VSZ 13MB RSS 7MB, hai mmap 500MB chưa chạm VSZ 513MB RSS 7MB VSZ cộng 500 nhưng RSS không đổi chưa tốn RAM thật, ba sau khi chạm 200MB VSZ 513MB RSS 207MB RSS tăng khoảng 200 vì đã ghi thật kernel giao trang, bốn sau khi chạm hết 500MB VSZ 513MB RSS 507MB giờ RSS xấp xỉ VSZ đã dùng thật gần hết, ps -o pid vsz rss giữ 300MB đã chạm PID 91329 VSZ 320528 RSS 314360 python3 RSS khoảng 314MB bằng RAM thật, kết VSZ bằng không gian ảo đặt chỗ gồm cả chưa dùng RSS bằng RAM thật khi lo về bộ nhớ nhìn RSS RSS tăng mãi bằng rò rỉ bộ nhớ thật

Hình 2: Chạy thật — ban đầu VSZ=13MB/RSS=7MB; sau mmap 500MB VSZ nhảy lên 513MB nhưng RSS vẫn 7MB (chưa chạm, chưa tốn RAM); chạm 200MB → RSS lên 207MB; chạm hết → RSS 507MB ≈ VSZ; ps cho RSS 314360KB cho tiến trình giữ 300MB đã chạm.

  • VSZ tăng, RSS không: xin 500MB làm VSZ nhảy từ 13MB lên 513MB, nhưng RSS vẫn 7MB — chưa một byte RAM thật nào bị dùng, vì mình chưa chạm vào vùng đó. Đây chính là điều khiến một app có VSZ khổng lồ mà RAM thật nhỏ.
  • RSS tăng khi chạm: ghi vào 200MB đầu → RSS lên ~207MB (kernel giao trang thật cho phần đã ghi). Ghi hết 500MB → RSS ~507MB ≈ VSZ. RSS chỉ phản ánh phần thực sự được dùng.
  • Đọc đúng cột: ps -o pid,vsz,rss,comm cho cả hai; cột rss (314360KB ≈ 314MB cho tiến trình giữ 300MB đã chạm) là RAM thật. top cũng có cột RES = RSS. Đây là con số cần theo dõi.

Đánh đổi cần cân nhắc

Theo dõi RSS tăng dần để bắt rò rỉ bộ nhớ thật. Như rò rỉ fd ở bài trước, rò rỉ bộ nhớ hiện ra là RSS tăng không ngừng theo thời gian (cấp phát mà không giải phóng). Một lần chụp RSS không nói lên gì; theo dõi xu hướng (watch grep VmRSS /proc/$pid/status) mới thấy. RSS ổn định ở một mức = lành mạnh; RSS bò lên mãi = rò rỉ.

RSS gồm cả bộ nhớ chia sẻ — có thể đếm trùng. RSS của một tiến trình bao gồm các trang chia sẻ (thư viện dùng chung, file map chung). Nếu cộng RSS của nhiều tiến trình dùng chung một thư viện, bạn đếm trùng phần chia sẻ đó. Để đo bộ nhớ riêng thực sự của một tiến trình, xem PSS (Proportional Set Size, trong /proc/<pid>/smaps_rollup) — nó chia đều phần chia sẻ. Với ước lượng nhanh RSS đủ dùng; với kế toán bộ nhớ chính xác dùng PSS.

Overcommit: tổng VSZ có thể vượt RAM vật lý — nhưng có giới hạn. Vì phần lớn VSZ thường không được chạm, Linux cho phép tổng VSZ của mọi tiến trình vượt RAM vật lý (overcommit). Điều này thường an toàn, nhưng nếu các tiến trình bỗng chạm hết phần đã xin, RAM thật cạn và OOM killer ra tay giết một tiến trình (như bài cgroups). Đây là lý do đặt --memory cho container theo RSS đỉnh thật, không theo VSZ.

Ba ý mang về

  1. VSZ là ảo (đặt chỗ), RSS là RAM thật: đo thật mmap 500MB làm VSZ nhảy lên 513MB nhưng RSS vẫn 7MB — không tốn RAM cho tới khi chạm. Đừng hoảng vì VSZ lớn; nhìn RSS.
  2. Cấp phát lười giải thích khoảng cách: đo thật RSS chỉ tăng khi ghi thật (200MB→RSS 207MB, 500MB→RSS 507MB) vì kernel giao trang RAM theo page fault, không giao ngay lúc xin.
  3. Đọc đúng cột và theo dõi xu hướng: cột rss trong ps (hay RES trong top, VmRSS trong /proc) là RAM thật; RSS tăng mãi = rò rỉ bộ nhớ; và nhớ RSS đếm trùng bộ nhớ chia sẻ (dùng PSS để chính xác), overcommit có thể dẫn tới OOM.

Nguồn

Phần sau ta phân tích một chương trình chậm: time cho real/user/sys — ba con số này khác nhau thế nào cho biết chương trình bị chặn ở CPU hay ở I/O, và cách đọc chúng để chẩn đoán đúng hướng.