"Đọ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ó.

Tuần tự so với ngẫu nhiên, và ảnh hưởng của độ sâu hàng đợi

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.