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ì.
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.
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.
Thử ba mươi giây
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.