Phần 12 đo bộ đệm trang ở chiều đọc. Chiều ghi có một tính chất mà chiều đọc không có: lời hứa của nhân là lời hứa suông cho tới khi dữ liệu thật sự xuống đĩa.

Dòng thời gian của một lần ghi 512 MB

dd nói dối, và nó có lý do

Ghi 512 MB rồi theo dõi Dirty trong /proc/meminfo:

dd: 536870912 bytes (537 MB) copied, 0.148729 s, 3.6 GB/s
dd trả về sau 0.150 s

giây  1: Dirty  524440 KB
giây  5: Dirty  524612 KB
giây 10: Dirty     264 KB
giây 15: Dirty     684 KB

dd báo xong sau 0,15 giây. Ở giây thứ năm, toàn bộ 512 MB vẫn còn nguyên trong RAM và chưa có byte nào xuống đĩa. Tới đâu đó giữa giây 5 và giây 10 thì luồng nền mới ghi.

Trong khoảng đó, cúp điện là mất sạch — và ứng dụng đã được báo là ghi thành công.

Nhân làm vậy vì hai lý do đúng đắn: gom nhiều lần ghi nhỏ thành lần ghi lớn, và bỏ qua hẳn dữ liệu bị ghi đè trước khi kịp xuống đĩa. Với tệp tạm thời, đó là món hời. Với sổ giao dịch của cơ sở dữ liệu, đó là thảm hoạ — nên mọi CSDL nghiêm túc đều gọi fsync, và phần 14 sẽ đo cái giá của nó.

Ba con số cho cùng một việc

Đo ba lần mỗi cách, cùng 512 MB:

Cách ghi Lần 1 Lần 2 Lần 3
Không đồng bộ 3,9 GB/s 4,0 GB/s 4,2 GB/s
conv=fsync 2,0 GB/s 1,6 GB/s 2,0 GB/s

Ghi liền 6 GB — vượt xa ngưỡng chặn — cho 1,4 GB/s.

Ba con số đo ba thứ khác nhau:

Con số Thực chất là tốc độ của
3,9–4,2 GB/s chép dữ liệu vào RAM
1,6–2,0 GB/s ghi thật, có chờ xuống đĩa
1,4 GB/s ghi bền vững, khi bộ đệm đã hết chỗ

Đo tốc độ ghi bằng một lần ghi ngắn và không fsync là đo tốc độ của RAM. Đây là lỗi phổ biến nhất khi so sánh ổ đĩa hoặc so sánh hệ thống tệp, và nó luôn cho kết quả đẹp đến khó tin.

Muốn con số đúng: ghi lượng dữ liệu lớn hơn RAM, hoặc thêm fsync, hoặc dùng O_DIRECT.

Bốn nút điều khiển

grep . /proc/sys/vm/dirty_{ratio,background_ratio,expire_centisecs,writeback_centisecs}

Trên máy đo, 15.971 MB RAM:

Nút Giá trị Nghĩa
dirty_background_ratio 10 Vượt 1.597 MB bẩn thì luồng nền bắt đầu ghi. Ứng dụng không bị chậm.
dirty_ratio 20 Vượt 3.194 MB thì tiến trình đang ghi bị chặn lại cho tới khi ghi kịp.
dirty_expire_centisecs 3000 Trang bẩn quá 30 giây thì phải ghi.
dirty_writeback_centisecs 1500 Cứ 15 giây luồng nền thức dậy kiểm.

Hai ngưỡng đầu giải thích một triệu chứng quen thuộc: hệ thống chạy mượt rồi đột ngột đứng vài giây khi đang ghi nhiều. Đó là lúc Dirty chạm dirty_ratio và nhân chặn mọi lời gọi write lại.

Chữa bằng cách hạ ngưỡng xuống — nghe ngược đời nhưng đúng:

sysctl -w vm.dirty_background_ratio=5
sysctl -w vm.dirty_ratio=10

Ngưỡng thấp nghĩa là ghi xuống đĩa sớm và đều hơn, nên không bao giờ tích tụ đủ để phải chặn. Đánh đổi là thông lượng đỉnh giảm.

Với máy nhiều RAM, dùng dạng byte thay vì phần trăm — 10% của 512 GB là 51 GB trang bẩn, một con số vô nghĩa:

sysctl -w vm.dirty_background_bytes=$((256*1024*1024))
sysctl -w vm.dirty_bytes=$((1024*1024*1024))

Đặt dạng _bytes sẽ tự làm _ratio thành 0 và ngược lại — chúng loại trừ nhau, không cộng dồn.

Theo dõi

Hai dòng cần nhìn trong /proc/meminfo:

watch -n1 "grep -E '^(Dirty|Writeback):' /proc/meminfo"

Dirty là dữ liệu chờ ghi. Writeback là dữ liệu đang được ghi.

Trong phép đo của tôi, Writeback luôn hiện 0 KB ở mọi lần lấy mẫu cách nhau 200 ms. Không phải vì không có gì được ghi — mà vì trên NVMe, 512 MB đi qua trạng thái đó nhanh hơn khoảng lấy mẫu. Trên hệ thống chậm hoặc quá tải, Writeback sẽ đứng ở con số lớn hàng chục giây, và đó chính là lúc sync treo.

Dirty lớn và không giảm nghĩa là đang ghi nhanh hơn ổ đĩa chịu được. Lúc đó mọi thứ vẫn có vẻ nhanh — cho tới khi chạm dirty_ratio.

Hai lời gọi ép ghi

fsync(fd);       /* du lieu VA sieu du lieu cua tep nay */
fdatasync(fd);   /* chi du lieu, bo qua thoi gian sua doi... */
sync();          /* toan he thong, khong doi lay ai */

fdatasync rẻ hơn fsync vì bỏ qua phần cập nhật siêu dữ liệu không ảnh hưởng tới việc đọc lại. Phần 14 sẽ đo khoảng cách đó.

Còn sync() trên dòng lệnh trả về khi mọi thứ đã xuống — nên time sync sau một lần ghi lớn là cách nhanh nhất để thấy bạn đang nợ đĩa bao nhiêu.

Thử ba mươi giây

Bắt quả tang lời hứa suông:

sync; sleep 1
grep '^Dirty:' /proc/meminfo

dd if=/dev/zero of=/tmp/thu.bin bs=1M count=512 2>&1 | tail -1
grep '^Dirty:' /proc/meminfo          # ngay sau khi dd bao xong

echo "cho 2 giay..."; sleep 2
grep '^Dirty:' /proc/meminfo

echo -n "time sync: "; time sync
grep '^Dirty:' /proc/meminfo          # gio moi that su bang 0

rm -f /tmp/thu.bin

Dòng Dirty ngay sau dd sẽ hiện vài trăm nghìn KB. Đó là lượng dữ liệu mà ứng dụng của bạn tin là đã an toàn nhưng chưa hề rời khỏi RAM.

Nếu time sync mất hơn một giây, hệ thống của bạn đang tích tụ nợ ghi — và ở đâu đó có một tiến trình sắp bị chặn mà không hiểu vì sao.

Phần sau: fsync tốn bao nhiêu — đo trên bốn loại thiết bị.