Một người có hạn mức thấu chi 4 tỉ trong tài khoản không phải là người "đang có 4 tỉ" — con số đó chỉ nói ngân hàng cho phép rút tới đó, còn số dư thật có thể chỉ vài triệu. Nhìn hạn mức mà tưởng là tiền mặt là nhầm lẫn tai hại; nhìn VSZ mà tưởng là RAM đang dùng cũng vậy. Tôi suýt báo cáo một chương trình dùng 1 MB thành con quái vật 4 GB chỉ vì đọc nhầm cột. Chương trình của bạn tưởng như sở hữu cả một dải bộ nhớ liền mạch khổng lồ, nhưng đó là bộ nhớ ảo — một ảo giác do kernel dựng nên. Mỗi tiến trình có một không gian địa chỉ ảo riêng, và kernel ánh xạ (lười biếng) các phần được dùng sang RAM vật lý thật. mmap là cách trực tiếp nhất để yêu cầu một vùng không gian ảo đó. Bài này đo mmap để tách bạch hai thứ người ta hay nhầm là một: bộ nhớ ảo đã đặt chỗ, và RAM thật đang dùng.
mmap đặt chỗ ảo, RAM trả sau
mmap không cấp RAM. Khi bạn gọi mmap một vùng, kernel chỉ đặt chỗ một khoảng địa chỉ ảo và ghi vào bảng ánh xạ của tiến trình rằng "khoảng này hợp lệ" — nhưng chưa có một byte RAM vật lý nào được gắn vào. Chỉ khi chương trình chạm vào một trang trong vùng đó, phần cứng gây một lỗi trang, và lúc ấy kernel mới cấp một trang RAM thật (như bài lỗi trang đã đo). Đây chính là "cấp phát lười" đứng sau malloc cho vùng lớn.
Vì cấp lười như vậy, hệ điều hành theo dõi hai con số bộ nhớ hoàn toàn khác nhau cho mỗi tiến trình, đọc được trong /proc/self/status:
- VmSize (VSZ) — tổng địa chỉ ảo đã đặt chỗ, gồm cả phần chưa hề chạm tới.
- VmRSS (RSS) — lượng RAM vật lý tiến trình đang thật sự dùng (resident set size).
Đo: mmap 4 GB, RAM không nhúc nhích
Máy trong container có 7934 MB RAM. Tôi đọc VSZ/RSS ở ba thời điểm quanh một lần mmap 4 GB, ghim lõi 0:
| Bước | VSZ (ảo) | RSS (thật) |
|---|---|---|
| Khởi đầu | 2 MB | 1 MB |
Sau mmap 4 GB (chưa chạm) |
4098 MB | 1 MB |
| Sau khi chạm 512 MB | 4098 MB | 513 MB |
Nhìn dòng giữa: mmap 4 GB làm VSZ nhảy vọt thêm 4096 MB, nhưng RSS không đổi — vẫn 1 MB. Bốn gigabyte không gian ảo được đặt chỗ mà không tốn một byte RAM nào, vì tôi chưa chạm tới. Khi tôi chạm 512 MB đầu, RSS tăng đúng 512 MB — chỉ bằng phần đã chạm — còn VSZ giữ nguyên. RSS lớn dần theo cái bạn dùng; VSZ đứng yên vì nó chỉ là ranh giới địa chỉ.
Và vì mmap chỉ đặt chỗ ảo, nó có thể đặt chỗ vùng lớn hơn cả RAM thật: mmap 64 GB trên máy 8 GB thành công — kernel vui vẻ cấp 64 GB địa chỉ ảo, biết rằng phần lớn sẽ không bao giờ được chạm (cơ chế overcommit, sẽ đo kỹ ở bài riêng). Điều tương tự xảy ra khi một chương trình nạp nhiều thư viện dùng chung: mỗi thư viện được ánh xạ vào không gian ảo, cộng dồn vào VSZ, nhưng các trang mã của chúng thường được chia sẻ giữa mọi tiến trình và chỉ nạp phần thật sự chạy — nên VSZ phồng lên trong khi RAM riêng của tiến trình vẫn khiêm tốn.
Nhìn nhầm cột, phóng đại RAM bốn nghìn lần
Câu hỏi tôi đặt ra rất đời thường: "chương trình này dùng bao nhiêu bộ nhớ sau khi mmap 4 GB?". Để trả lời, tôi nhìn con số bộ nhớ đập vào mắt trước nhất — và đó là VSZ, cột mà lệnh ps thường khoe to nhất. VSZ ghi 4098 MB. Tôi suýt kết luận ngay: "chương trình này ngốn 4 GB RAM, một con quái vật cần phải lo".
May là tôi đọc luôn dòng bên cạnh, VmRSS: chỉ 1 MB. Chương trình "4 GB" của tôi thật ra đang dùng vỏn vẹn một megabyte RAM. VSZ không phải bộ nhớ đang dùng — nó là địa chỉ ảo đã đặt chỗ, mà phần lớn chưa hề chạm và chẳng có RAM nào chống lưng. Nếu tôi tin VSZ, tôi đã phóng đại mức dùng RAM lên bốn nghìn lần.
Điều rút ra: "bộ nhớ" không phải một con số — VSZ và RSS là hai đại lượng khác hẳn cùng đội lốt chữ "memory", và đo nhầm cột phóng đại tới hàng nghìn lần. Khi hỏi "tiến trình này dùng bao nhiêu RAM", câu trả lời là RSS (cột RES trong top, VmRSS trong /proc), không phải VSZ. Đây là một lỗi vận hành kinh điển: người ta hoảng vì một tiến trình có VSZ khổng lồ (thường do mmap vùng lớn, hoặc ánh xạ nhiều thư viện) trong khi RAM thật nó chiếm rất nhỏ. Phải hỏi rõ "cột nào" trước khi tin một con số bộ nhớ — đúng tinh thần "đo nhầm đại lượng" mà cả sê-ri lặp lại.
Về đến code hằng ngày
Điều đầu tiên: theo dõi RSS, không phải VSZ, khi lo về mức dùng RAM. Một tiến trình mmap một file lớn, hay đặt chỗ một arena lớn để cấp phát dần, sẽ có VSZ to mà RSS nhỏ — hoàn toàn khỏe mạnh. Cảnh báo giám sát nên đặt ngưỡng trên RSS (hoặc bộ nhớ của cgroup, sẽ đo sau), vì đó mới là RAM thật đang bị chiếm. Đặt cảnh báo trên VSZ sẽ báo động giả liên tục với bất kỳ chương trình nào dùng mmap hào phóng.
Điều thứ hai: mmap là công cụ mạnh nhờ đúng tính chất lười này. Bạn có thể mmap một file cực lớn và chỉ đọc phần cần (RSS chỉ tăng theo phần chạm), đặt chỗ một vùng địa chỉ lớn rồi lấp dần (cấp phát arena, sparse array), hay dành sẵn không gian mà không trả tiền RAM cho tới khi dùng. Nhiều cấu trúc dữ liệu và cơ sở dữ liệu dựa vào chính điều này: đặt chỗ ảo hào phóng, để kernel biến nó thành RAM thật đúng theo nhu cầu. Hiểu VSZ ≠ RSS giúp bạn khai thác mmap mà không sợ "tốn 4 GB".
Và cái bẫy "một khái niệm đội hai con số, tin nhầm cái là hoảng vô cớ" này lặp lại ở mọi runtime cấp cao — đây là nơi rất nhiều báo động giả về bộ nhớ ra đời. Một JVM đặt -Xmx8g sẽ có VSZ khổng lồ (đặt chỗ ảo cho heap tối đa + metaspace + stack + mã JIT) trong khi RSS thật nhỏ hơn nhiều cho tới khi object sống lấp đầy — đó là lý do người mới hay hoảng khi thấy VIRT hàng chục GB trong top; con số cần nhìn là RSS, và trong container là bộ nhớ cgroup. Go runtime cũng đặt chỗ vùng ảo lớn cho arena, VSZ to mà RSS theo dữ liệu thật. Ngay cả CSDL: shared_buffers của PostgreSQL là bộ nhớ đặt chỗ, và một cột "memory" trong dashboard có thể là committed, reserved, working-set hay resident — mỗi cái một nghĩa. Mẫu số chung: "bộ nhớ" gần như luôn là nhiều đại lượng khác nhau (ảo đã đặt chỗ / RAM thật đang dùng / đã cam kết), và trước khi hoảng hay tối ưu theo một con số, phải hỏi nó đo cột nào — nhìn nhầm cột là báo động giả hoặc bỏ sót vấn đề thật. Gói lại bằng số: mmap 4 GB làm VSZ (ảo đã đặt chỗ) nhảy lên 4098 MB nhưng RSS (RAM vật lý thật) giữ ở 1 MB; RSS chỉ tăng theo phần được chạm (chạm 512 MB → RSS 513 MB); và mmap 64 GB trên máy 8 GB vẫn thành công vì chỉ là đặt chỗ ảo — nên khi hỏi "dùng bao nhiêu RAM", nhìn RSS chứ đừng nhìn VSZ.
Muốn tự thấy thì chạy ps aux hoặc top và nhìn hai cột VSZ (hay VIRT) và RSS (hay RES) của vài tiến trình — bạn sẽ thấy nhiều tiến trình có VSZ lớn hơn RSS rất nhiều, đặc biệt là các chương trình lớn dùng nhiều thư viện dùng chung hoặc mmap nhiều. Rồi đọc /proc/<pid>/status và tìm VmSize với VmRSS cho con số chính xác tới kilobyte. Muốn thấy tận mắt, viết một chương trình mmap vài GB rồi in /proc/self/status: VSZ nhảy lên, RSS đứng yên. Chạm một phần vùng đó, in lại: RSS tăng đúng phần bạn chạm, VSZ không đổi. Đó là toàn bộ khác biệt giữa "đặt chỗ" và "dùng thật", hiện ra trong hai con số.