Hãy hình dung một người đọc sách giỏi sắp xếp bàn làm việc: những cuốn hay giở tới nhất, họ để ngay trên mặt bàn trong tầm với; phần còn lại nằm dưới thư viện, muốn lấy phải đi xuống. Nếu bạn nhìn vào bàn và thấy nó chất đầy sách, đừng vội nghĩ "hết chỗ rồi" — đó chính là dấu hiệu người này làm việc thông minh, vì bất cứ cuốn nào trên bàn cũng có bản gốc dưới thư viện, gạt đi lúc nào cũng được để lấy chỗ cho cuốn cần hơn. Cái mặt bàn ấy là bộ đệm trang của Linux, và bài này đo xem để sách trên bàn nhanh hơn xuống thư viện tới mức nào — cùng lý do vì sao "RAM trống gần bằng 0" là tin tốt.
Phần 11 kết thúc ở chỗ available mới là con số đáng nhìn, không phải free. Bài này đo cái làm nên khác biệt đó: bộ đệm trang.
Đo cái giá thật
Dùng fio, 4 KB đọc ngẫu nhiên trong một tệp 512 MB, một luồng, không xếp hàng lệnh:
| IOPS | Trễ trung bình | p99 | |
|---|---|---|---|
| Qua bộ đệm trang | 425.874 | 2,2 µs | 38,1 µs |
| Qua bộ đệm (lặp lại) | 431.325 | 2,2 µs | 38,1 µs |
O_DIRECT |
28.655 | 34,6 µs | 140,3 µs |
O_DIRECT (lặp lại) |
28.308 | 35,0 µs | 140,3 µs |
15 lần về số thao tác, 16 lần về độ trễ.
Chú thích quan trọng: con số này đo trên NVMe. Đề bài của phần này ghi "nhanh hơn nghìn lần" — đó là tỷ lệ của thời đĩa quay, khi một lần tìm kiếm tốn 5–10 ms. Với NVMe, khoảng cách thu hẹp còn một bậc độ lớn. Vẫn rất đáng, nhưng nói "nghìn lần" trên phần cứng hôm nay là sai.
RAM "hết" là chuyện bình thường
Xoá sạch bộ đệm rồi đọc một tệp 512 MB:
buff/cache |
available |
|
|---|---|---|
| Sau khi xoá bộ đệm | 2.776 MB | 7.407 MB |
| Sau khi đọc 512 MB | 3.290 MB | 7.401 MB |
| Chênh lệch | +514 MB | −6 MB |
Bộ đệm ăn thêm nửa gigabyte, còn bộ nhớ khả dụng gần như không đổi. Đó là toàn bộ ý nghĩa của cột available: nhân đếm cả phần bộ đệm trang có thể thu hồi vào đó. Khi một tiến trình xin RAM và không còn trang trống, nhân chỉ việc vứt bỏ trang bộ đệm sạch — chúng còn bản gốc trên đĩa, vứt đi không mất gì. Gạt cuốn sách trên bàn xuống, bản gốc vẫn ở thư viện.
Nên RAM trống là RAM lãng phí. Một máy chủ chạy lâu ngày luôn có free gần bằng 0 và điều đó là đúng đắn, không phải triệu chứng. Ba con số, ba ý nghĩa:
free -m
# total used free shared buff/cache available
| Cột | Nghĩa |
|---|---|
free |
RAM chưa được dùng vào việc gì. Gần 0 là bình thường. |
buff/cache |
RAM đang giữ dữ liệu tệp. Sẵn sàng trả lại. |
available |
Ước lượng RAM cấp được ngay mà không phải tráo đổi. Đây là con số cần theo dõi. |
Ngưỡng cảnh báo nên đặt trên available, không phải free. Cảnh báo dựa trên free sẽ kêu mọi lúc trên mọi máy chủ khoẻ mạnh, và người trực sẽ học cách bỏ qua nó — rồi bỏ qua luôn lần nó kêu đúng.
Sai lầm khi đo: drop_caches không chạm tới máy chủ bên dưới
Phép đo đầu tiên tôi làm là đọc tuần tự, xoá bộ đệm giữa mỗi lần:
sync; echo 3 > /proc/sys/vm/drop_caches
dd if=/data/big of=/dev/null bs=1M
lạnh 1: 0.287 s, 1.9 GB/s
lạnh 2: 0.103 s, 5.2 GB/s
lạnh 3: 0.129 s, 4.2 GB/s
nóng : 0.079 s, 6.8 GB/s
Ba lần "lạnh" lẽ ra phải giống nhau. Lần 2 nhanh gấp gần ba lần lần 1. Nguyên nhân: container này chạy trong một máy ảo. drop_caches xoá bộ đệm trang của nhân trong máy ảo; hệ điều hành máy chủ bên dưới vẫn giữ nguyên dữ liệu trong bộ đệm của nó. Lần "lạnh" thứ hai vẫn đọc từ RAM của máy chủ.
Đó là lý do tôi chuyển sang O_DIRECT: nó bỏ qua bộ đệm ở mọi tầng nó đi qua và cho con số ổn định — 28.655 rồi 28.308 IOPS, chênh nhau 1,2%. Bài học chung: khi ba lần đo cùng điều kiện cho ba con số khác nhau theo một chiều, điều kiện chưa thật sự giống nhau.
Điều khiển bộ đệm cho một tệp
Xoá bộ đệm toàn hệ thống bằng drop_caches là công cụ quá thô — nó vứt luôn mọi thứ đang nóng của tiến trình khác. Với một tệp cụ thể:
posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED); /* bo tep nay khoi bo dem */
posix_fadvise(fd, 0, 0, POSIX_FADV_WILLNEED); /* keo tep nay vao bo dem */
posix_fadvise(fd, 0, 0, POSIX_FADV_RANDOM); /* tat doc truoc */
Trên dòng lệnh, dd ... oflag=nocache và công cụ nocache làm việc tương tự.
Chỗ dùng thật: một tác vụ sao lưu đọc hết vài trăm gigabyte sẽ đẩy toàn bộ dữ liệu nóng của cơ sở dữ liệu ra khỏi bộ đệm, và sau đó dịch vụ chậm hẳn trong nhiều phút. Gọi POSIX_FADV_DONTNEED sau mỗi đoạn đọc giữ được bộ đệm cho người khác — dọn cuốn sách chỉ giở một lần khỏi bàn thay vì để nó chiếm chỗ của sách người khác hay dùng.
Xem tệp nào đang nằm trong bộ đệm
# tong quan
grep -E '^(Cached|Buffers|Dirty|Writeback):' /proc/meminfo
# theo tung tep, can cai vmtouch
vmtouch -v /var/lib/postgresql/data/base
Dirty là phần đã ghi nhưng chưa xuống đĩa. Con số này lớn và không giảm nghĩa là hệ thống đang ghi nhanh hơn ổ đĩa chịu được — và một lệnh sync lúc đó sẽ treo rất lâu.
Mẫu số chung: cache đầy là dấu khoẻ, và "trống" là tài nguyên chưa sinh lời
Cái làm người ta hoảng nhầm ở đây — nhìn thấy bộ nhớ "hết" rồi báo động — không riêng gì bộ đệm trang: gần như mọi lớp cache đều được thiết kế để chiếm hết phần tài nguyên được giao và giữ đầy, vì phần trống là phần chưa sinh lời; nên "đầy" là trạng thái vận hành đúng, không phải cảnh báo. Cùng chuyện ở mọi tầng.
Trong thế giới Java, đây đúng là hiểu lầm kinh điển về heap: người ta thấy RSS hay heap-used leo sát -Xmx rồi tưởng sắp OOM — nhưng GC theo thiết kế để rác tích lại rồi mới dọn một thể cho hiệu quả; heap gần đầy ngay trước một chu kỳ GC là bình thường. Con số đáng lo không phải "đã dùng bao nhiêu" mà là "sau GC còn giữ lại bao nhiêu" (live set) — đúng như available chứ không phải buff/cache.
Trong Go, cùng chuyện: RSS của tiến trình Go bao gồm cả bộ nhớ runtime giữ lại để tái dùng, và nó không tụt ngay sau khi giải phóng — nhìn RSS mà báo động là báo nhầm; phải nhìn HeapInuse/HeapReleased mới ra bức tranh thật. Cache và pool được thiết kế để giữ đầy.
Còn ở cơ sở dữ liệu, đây là shared_buffers của Postgres và buffer pool của InnoDB: chúng được cấu hình để chiếm phần lớn RAM và giữ đầy dữ liệu nóng — buffer pool 100% "đầy" với tỷ lệ trúng cao là mục tiêu, không phải sự cố; chỉ số cần theo dõi là cache hit ratio và eviction rate, không phải "còn trống bao nhiêu".
Mẫu số chung: một lớp cache tốt luôn tiến tới trạng thái đầy — phần trống là phần chưa sinh lời — nên chỉ số 'còn trống bao nhiêu' gần như luôn báo động sai; con số thật sự đáng theo dõi là phần không thu hồi được (available, live set, working set) và hiệu quả của cache (tỷ lệ trúng), không phải mức lấp đầy. Nên khi thấy bộ nhớ một hệ thống "gần hết", câu hỏi đúng không phải "sao nó ăn nhiều thế" mà là "phần này có thu hồi được ngay khi cần không, và cái tôi đang báo động là mức lấp đầy hay là phần thật sự bị giữ cứng" — vì với cache, đầy là khoẻ, và trống mới là lãng phí.
Muốn tự chứng minh bộ đệm trang có thật, trên máy bạn, không cần root:
f=/tmp/thu-cache.bin
dd if=/dev/urandom of=$f bs=1M count=512 2>/dev/null
echo "lan 1 (co the lanh):"
dd if=$f of=/dev/null bs=1M 2>&1 | tail -1
echo "lan 2 (chac chan nong):"
dd if=$f of=/dev/null bs=1M 2>&1 | tail -1
echo "lan 3 (chac chan nong):"
dd if=$f of=/dev/null bs=1M 2>&1 | tail -1
free -m | awk '/Mem:/{print "buff/cache:", $6, "MB | available:", $7, "MB"}'
rm -f $f
Nếu lần 1 và lần 2 chênh nhau ít, đừng vội kết luận bộ đệm không hoạt động — nhiều khả năng tệp vừa được ghi nên nó đã nằm sẵn trong bộ đệm ngay từ lần đọc đầu tiên. Đó cũng chính là bộ đệm trang đang làm việc.
Phần sau: swap và OOM killer — đo lúc bộ nhớ thật sự hết thì chuyện gì xảy ra.