Hai câu hỏi hay bị gộp làm một: nên đọc mỗi lần bao nhiêu byte, và có cần căn chỉnh không. Đo ra thì chúng là hai câu chuyện hoàn toàn khác nhau.
Giá nằm ở mỗi thao tác, không nằm ở mỗi byte
Cùng phép đọc tuần tự, O_DIRECT, chỉ đổi kích thước khối:
| Khối | IOPS | Thông lượng | Độ trễ |
|---|---|---|---|
| 512 B | 27.778 | 13,6 MB/s | 35,7 µs |
| 1 KB | 26.942 | 26,3 MB/s | 36,8 µs |
| 4 KB | 28.053 | 109,6 MB/s | 35,3 µs |
| 16 KB | 28.914 | 451,8 MB/s | 34,3 µs |
| 64 KB | 24.645 | 1.540,3 MB/s | 40,3 µs |
| 256 KB | 17.168 | 4.292,1 MB/s | 57,9 µs |
| 1 MB | 7.983 | 7.983,3 MB/s | 124,9 µs |
| 4 MB | 3.312 | 13.247,8 MB/s | 301,5 µs |
Từ 512 byte tới 16 KB, số thao tác mỗi giây gần như không đổi — 27.778 rồi 28.914 — trong khi thông lượng tăng 33 lần.
Đọc 512 byte tốn đúng bằng đọc 16 KB. Ba mươi hai lần dữ liệu, cùng một cái giá.
Lý do: mỗi thao tác I/O có chi phí cố định — lời gọi hệ thống, dựng yêu cầu, gửi xuống thiết bị, ngắt báo hoàn thành. Chi phí đó khoảng 35 µs ở đây, và nó áp đảo thời gian chuyển dữ liệu cho tới khi khối đủ lớn.
Từ 64 KB trở đi, độ trễ mới bắt đầu tăng (40,3 → 301,5 µs) và giá chuyển sang tính theo byte.
Nghĩa là
Khối nhỏ hơn 16 KB là tiền vứt đi. Một chương trình đọc từng bản ghi 200 byte bằng một lời gọi read đang trả giá của 16 KB cho mỗi bản ghi.
Cách chữa không phải đổi kích thước bản ghi mà là gom lại: đọc một khối lớn vào bộ đệm rồi cắt trong bộ nhớ. Đó chính là việc fread của thư viện chuẩn C làm sẵn cho bạn — và là lý do dùng read trực tiếp cho từng bản ghi nhỏ luôn chậm hơn.
Khối lớn hơn 64 KB đánh đổi độ trễ lấy thông lượng. Với sao lưu hay quét dữ liệu thì đúng. Với dịch vụ phải trả lời trong vài mili giây thì 301 µs cho một khối 4 MB là chi phí không cần thiết.
Vùng 16–64 KB là chỗ hầu hết các hệ thống nên bắt đầu, và cũng là kích thước trang mặc định của phần lớn cơ sở dữ liệu — PostgreSQL 8 KB, InnoDB 16 KB, SQL Server 8 KB. Không phải ngẫu nhiên.
Căn chỉnh qua bộ đệm trang: không đo được khác biệt nào
Đọc 4 KB, lệch dần so với biên trang:
| Lệch | IOPS |
|---|---|
| 0 | 947.741 |
| 512 B | 913.974 |
| 1 KB | 955.104 |
| 2 KB | 943.671 |
| 4 KB | 955.410 |
Toàn bộ dải nằm trong 5%, và thứ hạng không theo mức lệch.
Lý do đơn giản: nhân luôn làm việc theo trang 4 KB. Xin 4 KB lệch 512 byte thì nhân đọc hai trang rồi cắt ra phần bạn cần. Việc đọc thêm một trang đã nằm sẵn trong bộ đệm không tốn gì đo được.
Vậy nên với I/O thông thường qua bộ đệm trang, căn chỉnh không phải chuyện đáng lo về hiệu năng.
Với O_DIRECT thì nó không phải chuyện hiệu năng — nó là chuyện đúng sai
bộ đệm căn 4096, đọc 4096 byte -> OK
bộ đệm LỆCH 512, đọc 4096 byte -> OK
offset lệch 512 -> OK
đọc 512 byte -> OK
đọc 100 byte -> Invalid argument
O_DIRECT không có bộ đệm trang để che đi. Nhân chuyển thẳng dữ liệu giữa ổ đĩa và bộ nhớ của bạn, nên cả ba thứ — địa chỉ bộ đệm, vị trí trong tệp, và độ dài — phải là bội số của kích thước khối logic.
Sai thì không chậm. Sai thì EINVAL.
Điều đáng chú ý: ngưỡng ở đây là 512 byte, không phải 4096. Đó là logical_block_size của thiết bị:
cat /sys/block/nvme0n1/queue/logical_block_size # 512 o may toi do
cat /sys/block/nvme0n1/queue/physical_block_size
Nghĩa là cùng một đoạn mã chạy tốt trên ổ 512e và trả về EINVAL trên ổ 4Kn. Đây là một lớp lỗi chỉ lộ ra khi đổi phần cứng, và thông báo lỗi (Invalid argument) không hề gợi ý nguyên nhân.
Cách viết đúng là hỏi hệ thống thay vì đoán:
size_t align = 4096; /* an toan tren moi thiet bi hien nay */
void *buf;
posix_memalign(&buf, align, len); /* dung malloc roi tu can */
posix_memalign với 4096 an toàn ở mọi nơi, vì mọi kích thước khối logic thực tế đều là ước của 4096.
Bốn con số của thiết bị
d=nvme0n1
for k in logical_block_size physical_block_size minimum_io_size optimal_io_size; do
printf "%-22s %s\n" "$k" "$(cat /sys/block/$d/queue/$k)"
done
| Trường | Nghĩa |
|---|---|
logical_block_size |
Đơn vị nhỏ nhất địa chỉ hoá được. Đây là ngưỡng của O_DIRECT. |
physical_block_size |
Đơn vị ghi thật của thiết bị. Ghi nhỏ hơn nó gây đọc-sửa-ghi. |
minimum_io_size |
Kích thước nên dùng tối thiểu |
optimal_io_size |
Kích thước tối ưu — với RAID là kích thước sọc |
Trên thiết bị tôi đo, optimal_io_size là 0 (không khai báo) với loop và 131.072 với nbd. Con số này là thứ đáng đọc khi tạo hệ thống tệp trên RAID: mkfs.xfs và mkfs.ext4 tự đọc nó để căn chỉnh siêu dữ liệu, nhưng chỉ khi tầng dưới khai báo đúng.
Chỗ căn chỉnh thật sự cắn: ghi
Phép đo trên là đọc. Với ghi, lệch căn chỉnh gây ra đọc-sửa-ghi: thiết bị phải đọc cả khối vật lý về, sửa phần bạn đổi, rồi ghi lại. Trên SSD, nó còn kéo theo khuếch đại ghi và hao mòn.
Tôi thử đo phần này qua bộ đệm trang và không tách được tín hiệu ra khỏi nhiễu — cùng phép đo cho 45.350, 56.326 và 70.914 IOPS ở ba mức lệch, với mức lệch nhiều nhất lại nhanh nhất. Con số đó rõ ràng phản ánh trạng thái bộ đệm chứ không phản ánh căn chỉnh. Muốn đo đúng phải dùng O_DIRECT trên thiết bị 4Kn thật.
Ghi lại đây để không ai lấy ba con số đó làm bằng chứng cho bất cứ điều gì.
Thử ba mươi giây
Tìm kích thước khối tốt nhất cho ổ đĩa của bạn:
f=/duong/dan/thu.bin
dd if=/dev/zero of=$f bs=1M count=1024 2>/dev/null
for bs in 4k 16k 64k 256k 1m; do
fio --name=t --filename=$f --rw=read --bs=$bs --size=1G --runtime=5 \
--time_based --ioengine=psync --direct=1 --output-format=terse 2>/dev/null | \
awk -F';' -v b=$bs '{printf "bs=%-6s %8s IOPS %8.1f MB/s tre tb %7.1f us\n", b, $8, $7/1024, $16/1000}'
done
rm -f $f
echo "--- thiet bi ---"
d=$(df --output=source /duong/dan | tail -1 | sed 's|/dev/||;s|[0-9]*$||')
grep . /sys/block/$d/queue/{logical,physical}_block_size 2>/dev/null
Cột IOPS phẳng tới đâu thì tới đó bạn đang trả tiền cho dữ liệu mình không dùng. Đọc ở kích thước ngay dưới điểm gãy là cách lấy gần hết thông lượng mà chưa phải trả thêm độ trễ.
Phần sau: hệ tệp ext4, xfs, btrfs — đo cùng một tải.