Có một câu chuyện cũ: một người đàn ông tìm chìa khoá dưới cột đèn đường lúc nửa đêm. Người qua đường hỏi có chắc đánh rơi ở đây không, ông đáp "không, tôi đánh rơi đằng kia — nhưng chỗ này mới có ánh sáng để tìm." Chỉnh hiệu năng hay mắc đúng cái bẫy đó: người ta đổ công vào cái nút dễ chỉnh và hay được nhắc, không phải cái nút thật sự quyết định. Bài này đo đúng hai cái nút đó cạnh nhau — bộ lập lịch I/O mà ai cũng khuyên đổi, và một nút gần như không ai đụng — và con số nói rõ chỗ nào là cột đèn, chỗ nào là chìa khoá.

Đổi bộ lập lịch I/O là một trong những lời khuyên tinh chỉnh phổ biến nhất. Ta đo xem nó cho bao nhiêu.

Bài kiểm tra

Đây đúng là kịch bản mà bộ lập lịch sinh ra để xử lý: một tiến trình ghi tuần tự liên tục (tác vụ nền), một tiến trình khác đọc khối nhỏ 4 KB ngẫu nhiên (dịch vụ nhạy cảm với độ trễ). Bộ lập lịch tốt phải bảo vệ được kẻ đọc. Ba lần đo mỗi bộ lập lịch:

Lần none mq-deadline kyber
1 15.726 IOPS, p99 171 µs 15.702 IOPS, p99 169 µs 15.190 IOPS, p99 183 µs
2 15.430 IOPS, p99 167 µs 15.424 IOPS, p99 165 µs 15.900 IOPS, p99 169 µs
3 15.310 IOPS, p99 183 µs 15.591 IOPS, p99 171 µs 15.879 IOPS, p99 173 µs

Toàn bộ dải: 15.190 – 15.900 IOPS, p99 165 – 183 µs. Không bộ lập lịch nào tách ra khỏi nhiễu. Thứ hạng còn đổi giữa các lần chạy.

Vì sao chúng bằng nhau

Bộ lập lịch I/O sinh ra để giải hai bài toán của đĩa quay:

  • Sắp xếp lại yêu cầu theo vị trí trên đĩa để đầu đọc đi ít nhất — thuật toán "thang máy".
  • Chống bỏ đói: giữ một yêu cầu đọc nhỏ khỏi bị chôn dưới hàng nghìn yêu cầu ghi tuần tự.

Bài toán thứ nhất biến mất trên ổ thể rắn: phần 19 đã đo và tuần tự với ngẫu nhiên cho cùng một con số, nên sắp xếp lại chẳng để làm gì. Bài toán thứ hai gần như biến mất theo: NVMe có hàng nghìn hàng đợi phần cứng và phục vụ chúng song song, nên yêu cầu nhỏ không phải xếp sau yêu cầu lớn.

Đó là lý do none là mặc định cho NVMe trên mọi bản phân phối hiện đại. Nó không phải là "chưa cấu hình" — nó là lựa chọn đúng. Trên đĩa quay thì hoàn toàn khác, và những lời khuyên bạn đọc được thường ra đời từ thời đó. Nếu hệ thống của bạn còn chạy đĩa quay, bfq hoặc mq-deadline vẫn tạo khác biệt lớn. Nhân trên máy tôi đo không có bfq nên tôi không đo được nó.

Nút thật sự có tác dụng

Cùng thiết bị, cùng phép đo đọc tuần tự qua bộ đệm trang, chỉ đổi cửa sổ đọc trước:

read_ahead_kb Thông lượng
4 375,9 MB/s
32 1.398,6 MB/s
128 (mặc định) 2.510,5 MB/s
512 3.370,8 MB/s
2.048 4.026,8 MB/s

10,7 lần. Nút được người ta chỉnh nhiều nhất cho 0%. Nút gần như không ai đụng tới cho 10,7 lần. Đây chính là chìa khoá nằm ngoài vùng sáng cột đèn.

Không phải vì bộ lập lịch vô dụng, mà vì phần cứng đã đổi và nó không còn là chỗ tắc nghẽn. Chỗ tắc nghẽn bây giờ là số yêu cầu đang bay — đúng như phần 19 đã đo với iodepth. Đọc trước là cách nhân tự tạo ra độ sâu hàng đợi thay cho bạn, và cửa sổ đọc trước quyết định nó tạo được bao nhiêu.

Đọc trước lớn không miễn phí

Cửa sổ 2 MB nghĩa là mỗi lần chạm vào một chỗ mới, nhân kéo về 2 MB. Với truy cập tuần tự thì tuyệt vời. Với truy cập ngẫu nhiên thì nó lãng phí băng thông cho dữ liệu không bao giờ dùng tới, và đẩy dữ liệu đang nóng ra khỏi bộ đệm trang. Nên quy tắc là chỉnh theo tải, không chỉnh theo thiết bị:

Tải read_ahead_kb
Quét bảng lớn, sao lưu, phân tích dữ liệu 1024 – 4096
Mặc định, tải hỗn hợp 128
Cơ sở dữ liệu OLTP, truy cập ngẫu nhiên 16 – 64
blockdev --setra 2048 /dev/nvme0n1              # don vi 512 byte -> 1 MB
echo 1024 > /sys/block/nvme0n1/queue/read_ahead_kb   # don vi KB

Hai lệnh này chỉnh cùng một thứ với hai đơn vị khác nhau — một nguồn nhầm lẫn quen thuộc. Đặt bền qua khởi động lại bằng udev:

# /etc/udev/rules.d/60-readahead.rules
ACTION=="add|change", KERNEL=="nvme[0-9]n[0-9]", ATTR{queue/read_ahead_kb}="1024"

Hai nút khác đáng biết

cat /sys/block/nvme0n1/queue/nr_requests   # do sau hang doi cua tang khoi
cat /sys/block/nvme0n1/queue/nomerges      # 0 = cho gop yeu cau lien ke
cat /sys/block/nvme0n1/queue/rotational    # 0 = the ran, 1 = dia quay

rotational là cờ mà nhân dùng để chọn mặc định. Máy ảo đôi khi báo sai — nếu nó là 1 trên một ổ SSD, nhân sẽ chọn bộ lập lịch và cửa sổ đọc trước của đĩa quay, và đó là một lỗi cấu hình thật sự đáng sửa.

Giới hạn của phép đo này

Thiết bị tôi đo là tệp loop nằm trên overlay nằm trong máy ảo. Bộ lập lịch của nhân trong máy ảo điều phối một hàng đợi ảo, còn việc sắp xếp thật diễn ra ở tầng dưới mà nó không với tới.

Nghĩa là kết quả "ba bộ lập lịch bằng nhau" được củng cố bởi bố trí này chứ không bị nó phủ định — trong container, bộ lập lịch còn ít việc hơn nữa. Nếu bạn chạy container, đó chính là tình huống của bạn. Trên kim loại trần với đĩa quay, hãy tự đo lại. Đó cũng là điểm chung của cả loạt bài này: mọi con số đều thuộc về một máy cụ thể, và cái đáng mang đi là cách đo.

Mẫu số chung: chỉnh chỗ sáng, không chỉnh chỗ hỏng

Cái bẫy ở đây — đổ công vào cái nút dễ chỉnh và hay được nhắc, trong khi chỗ tắc nghẽn thật nằm ở nút khác — lặp lại ở mọi tầng, và nó nguy hiểm vì cái nút phổ biến từng đúng, chỉ là ngữ cảnh đã đổi dưới chân nó.

Trong thế giới Java, đây đúng là chuyện tinh chỉnh cờ GC. Người ta dán hàng chục -XX từ một bài blog 2012 vào một ứng dụng mà nút thắt thật là N+1 query hay một synchronized giữ khoá quá lâu — chỉnh GC cho 0% trong khi hồ sơ chỉ thẳng vào chỗ khác. Cột đèn là cờ JVM (dễ đổi, đầy hướng dẫn cũ); chìa khoá nằm trong hồ sơ hiệu năng chưa ai chạy.

Trong Go, cùng chuyện với GOMAXPROCS và đủ loại tinh chỉnh runtime, trong khi phần lớn dịch vụ Go chậm vì gọi mạng tuần tự hoặc cấp phát trong vòng nóng — thứ chỉ pprof mới chỉ ra. Chỉnh cái runtime cho cảm giác đang tối ưu; đo thì mới biết nó không phải chỗ tắc.

Còn ở cơ sở dữ liệu, đây là chuyện nống shared_buffers, work_mem lên theo một bài chỉnh Postgres cũ, trong khi truy vấn chậm vì thiếu một index, hay vì thống kê lỗi thời khiến planner chọn sai — EXPLAIN ANALYZE chỉ đúng chỗ, còn các nút bộ nhớ chỉ là chỗ có sẵn ánh sáng để vặn.

Mẫu số chung: nút được chỉnh nhiều nhất thường là nút dễ chỉnh nhất và có nhiều hướng dẫn nhất — không phải nút quan trọng nhất; và các hướng dẫn đó thường ra đời trong một ngữ cảnh phần cứng/tải đã thay đổi, nên nút xưa từng quyết định nay chỉ còn là nhiễu. Nên trước khi vặn bất cứ tham số nào, câu hỏi đúng không phải "đặt nó bằng bao nhiêu" mà là "tôi đã đo để biết chỗ tắc nghẽn thật nằm đâu chưa, hay đang tìm chìa khoá dưới cột đèn vì ở đó có sẵn ánh sáng" — vì gần như luôn, một lần đo thẳng vào chỗ tắc đáng giá hơn mười lần vặn nút quen tay.

Muốn tự thấy, xem thiết bị của bạn đang dùng gì và thử đổi cửa sổ đọc trước:

for d in /sys/block/*/queue; do
  dev=$(echo $d | cut -d/ -f4)
  case $dev in loop*|ram*) continue;; esac
  printf "%-10s rotational=%s  ra=%-6s sched=%s\n" "$dev" \
    "$(cat $d/rotational)" "$(cat $d/read_ahead_kb)" "$(cat $d/scheduler)"
done

# thu tang cua so doc truoc roi do lai (can root, doi ten thiet bi)
d=nvme0n1
cu=$(cat /sys/block/$d/queue/read_ahead_kb)
for ra in 128 1024; do
  echo $ra > /sys/block/$d/queue/read_ahead_kb
  sync; echo 3 > /proc/sys/vm/drop_caches
  printf "ra=%-6s " $ra
  dd if=/duong/dan/tep-lon of=/dev/null bs=4k count=200000 2>&1 | tail -1
done
echo $cu > /sys/block/$d/queue/read_ahead_kb

Nếu dòng ra=1024 nhanh hơn đáng kể và tải của bạn là quét tuần tự, bạn vừa tìm được một cải thiện lớn hơn mọi thứ mà việc đổi bộ lập lịch có thể cho — chìa khoá, không phải cột đèn.

Phần sau: theo dõi I/O trong thực tế — iostat, iotop và cách đọc chúng cho đúng.