"Đọc tuần tự nhanh hơn đọc ngẫu nhiên" là một trong những quy tắc được nhắc nhiều nhất khi thiết kế hệ thống lưu trữ. Bài này đo lại nó.
Kết quả: không có khác biệt
fio, khối 4 KB, O_DIRECT để bỏ qua mọi bộ đệm, ba lần đo mỗi bên:
| Lần 1 | Lần 2 | Lần 3 | |
|---|---|---|---|
| Tuần tự | 27.767 | 26.521 | 27.973 IOPS |
| Ngẫu nhiên | 28.325 | 27.921 | 28.034 IOPS |
Ngẫu nhiên thậm chí nhỉnh hơn một chút — nằm trong sai số.
Với khối lớn hơn cũng vậy:
| Khối | Tuần tự | Ngẫu nhiên |
|---|---|---|
| 4 KB | 108,3 MB/s | 109,6 MB/s |
| 16 KB | 437,6 MB/s | 423,8 MB/s |
| 64 KB | 1.557,5 MB/s | 1.552,6 MB/s |
| 1 MB | 8.955,4 MB/s | 8.001,9 MB/s |
Vì sao quy tắc cũ không còn đúng
Quy tắc "tuần tự nhanh hơn" là quy tắc của đĩa quay. Ở đó, đọc một khối ở vị trí ngẫu nhiên đòi hỏi đầu đọc di chuyển sang rãnh khác (5–10 ms) rồi chờ đĩa quay tới đúng chỗ (2–4 ms). Đọc khối kế tiếp thì không cần gì cả. Tỷ lệ chênh lệch lên tới hàng trăm lần, và mọi thiết kế cơ sở dữ liệu của ba thập kỷ trước đều xoay quanh việc tránh cái giá đó.
Ổ thể rắn không có bộ phận nào chuyển động. Đọc ô nhớ nào cũng như nhau. Quy tắc mất nền tảng vật lý.
Nói lại lần thứ hai vì nó vẫn còn ảnh hưởng tới nhiều quyết định kiến trúc: nếu hệ thống của bạn chạy trên SSD hoặc NVMe, tối ưu để đọc tuần tự thay vì ngẫu nhiên gần như không mang lại gì. Công sức đó nên dồn vào chỗ khác.
Chỗ khác đó là độ sâu hàng đợi
Cùng khối 4 KB, cùng đọc ngẫu nhiên, chỉ đổi số yêu cầu gửi đi cùng lúc:
iodepth |
IOPS | Thông lượng | Độ trễ trung bình |
|---|---|---|---|
| 1 | 27.128 | 106,0 MB/s | 36,3 µs |
| 4 | 67.846 | 265,0 MB/s | 58,6 µs |
| 16 | 174.239 | 680,6 MB/s | 91,6 µs |
| 64 | 233.687 | 912,8 MB/s | 273,2 µs |
| 128 | 233.973 | 914,0 MB/s | 546,3 µs |
8,6 lần thông lượng.
Lý do: NVMe có nhiều kênh song song bên trong và chỉ đạt công suất khi có nhiều yêu cầu đang bay cùng lúc. Một chương trình đọc từng khối một, chờ xong rồi mới đọc tiếp, dùng được chưa tới 12% năng lực ổ đĩa.
Đây là cùng một hiện tượng mà phần 9 đã đo ở tầng bộ nhớ đệm CPU: truy cập phụ thuộc nhau thì chậm, truy cập độc lập thì song song hoá được. Ở tầng RAM, tỷ lệ là 28 lần; ở tầng ổ đĩa, 8,6 lần.
Bảng trên cũng cho thấy điểm dừng
Từ iodepth 64 lên 128, thông lượng đứng yên (233.687 → 233.973 IOPS) trong khi độ trễ tăng gấp đôi (273,2 → 546,3 µs).
Đó là chữ ký kinh điển của bão hoà: hàng đợi đã đầy, yêu cầu thêm chỉ xếp hàng chờ chứ không được phục vụ sớm hơn.
Hệ quả thực dụng: tăng số kết nối hoặc số luồng khi ổ đĩa đã bão hoà chỉ làm mọi yêu cầu chậm đi, không làm hệ thống nhanh hơn. Đây chính là lý do các bể kết nối cơ sở dữ liệu có kích thước tối ưu, và nó thường nhỏ hơn nhiều so với trực giác.
Điểm bão hoà tìm bằng cách quét đúng như bảng trên: tăng độ sâu tới khi thông lượng ngừng tăng, rồi lùi lại một bậc.
Ba cách để có nhiều yêu cầu đang bay
Nhiều luồng hoặc nhiều tiến trình. Cách đơn giản nhất, và là cách máy chủ web vốn đã làm.
I/O bất đồng bộ. io_uring (nhân 5.1 trở lên) hoặc libaio. Một luồng phát hàng trăm yêu cầu rồi thu kết quả về sau.
Đọc trước. Với truy cập tuần tự, nhân tự làm việc này. Bảo nó làm mạnh hơn:
posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL); /* tang cua so doc truoc */
readahead(fd, offset, len); /* keo truoc mot doan cu the */
blockdev --getra /dev/nvme0n1 # cua so doc truoc, don vi 512 byte
blockdev --setra 512 /dev/nvme0n1 # 256 KB
Đây là chỗ duy nhất mà tuần tự thật sự còn lợi thế trên SSD: nhân nhận ra mẫu tuần tự và tự tạo ra độ sâu hàng đợi giúp bạn. Với truy cập ngẫu nhiên, bạn phải tự tạo.
Giới hạn của phép đo này
Tôi đo trong một container chạy trên máy ảo. O_DIRECT bỏ qua bộ đệm của nhân trong máy ảo, nhưng không bỏ qua được bộ đệm của hệ điều hành máy chủ bên dưới — đây là cùng vấn đề đã gặp ở phần 12 với drop_caches.
Nghĩa là con số 233.973 IOPS là giới hạn của lớp lưu trữ ảo, không phải của chip NVMe. Hình dạng đường cong — bão hoà ở đâu, tuần tự bằng ngẫu nhiên — vẫn đúng và vẫn là điều đáng nhớ. Con số tuyệt đối thì phải đo lại trên chính máy chủ của bạn.
Vì lý do tương tự, tôi không đo được đĩa mạng như đề bài của phần này mong đợi. Điều chắc chắn về đĩa mạng: độ trễ mỗi thao tác cao hơn hẳn (hàng trăm µs tới vài ms), nên tỷ lệ lợi ích của việc tăng độ sâu hàng đợi còn lớn hơn nhiều so với bảng trên.
Thử ba mươi giây
Đo điểm bão hoà của chính ổ đĩa bạn:
f=/duong/dan/thu.bin
fallocate -l 2G $f || dd if=/dev/zero of=$f bs=1M count=2048
for d in 1 4 16 64; do
fio --name=t --filename=$f --rw=randread --bs=4k --size=2G \
--runtime=10 --time_based --ioengine=libaio --direct=1 \
--iodepth=$d --output-format=terse 2>/dev/null | \
awk -F';' -v d=$d '{printf "iodepth=%-4s %8s IOPS tre tb %8s us\n", d, $8, $16/1000}'
done
rm -f $f
Độ sâu mà thông lượng ngừng tăng chính là số yêu cầu song song tối đa có ích. Cấu hình bể kết nối, số worker, hay innodb_io_capacity vượt quá con số đó chỉ làm độ trễ xấu đi.
Phần sau: bộ lập lịch I/O — đo bốn lựa chọn của nhân.