Tưởng tượng bạn bấm giờ ba vận động viên, mỗi người chạy đúng một lượt, rồi trao huy chương vàng cho người nhanh nhất. Nghe hợp lý — cho tới khi bạn nhớ ra hôm đó người thắng chạy xuôi gió, người về chót vừa vấp ở vạch xuất phát. Một lượt chạy không nói được ai nhanh hơn; nó chỉ nói ai may hơn trong lượt đó. Bài này là bài tôi mắc đúng cái lỗi ấy rõ nhất trong cả loạt — và tôi để nguyên nó lại đầu bài, vì cách nó lộ ra đáng giá hơn mọi con số phía sau.

Ba hệ tệp, cùng một máy, cùng một ảnh loop 2 GB, cùng một bộ phép đo.

Sai lầm trước, kết quả sau

Lần đo đầu tiên tôi chạy một lần mỗi hệ tệp cho phép ghi tuần tự:

ext4    3.908 MB/s
xfs     1.231 MB/s
btrfs   2.535 MB/s

Kết luận rõ ràng: ext4 nhanh gấp 3,2 lần xfs. Một con số đẹp, dễ nhớ, dễ viết thành tiêu đề. Rồi tôi chạy lại ba lần mỗi bên:

Lần 1 Lần 2 Lần 3
ext4 3.683 5.120 5.885 MB/s
xfs 4.923 4.063 5.689 MB/s
btrfs 3.012 3.631 3.606 MB/s

Hai dải trùm hoàn toàn lên nhau. Kết luận "3,2 lần" là nhiễu, không phải kết quả.

Con số 1.231 MB/s của xfs ở lần đầu thấp hơn cả ba lần đo sau, và tôi không biết chắc vì sao — có thể là lần chạy đầu tiên trên hệ tệp vừa tạo, có thể là máy chủ bên dưới đang bận. Điều duy nhất chắc chắn là một lần đo không phải một phép đo. Chuyện tương tự xảy ra với phép đo siêu dữ liệu: lần đầu ext4 cho 53.388 tệp/giây, các lần sau đều trên 138.000. Con số đầu là lúc bộ đệm còn lạnh — người chạy còn chưa khởi động.

Kết quả sau khi lặp lại

Phép đo ext4 xfs btrfs
Ghi tuần tự (MB/s) 3.683 / 5.120 / 5.885 4.923 / 4.063 / 5.689 3.012 / 3.631 / 3.606
Đọc ngẫu nhiên (IOPS) 32.165 35.861 32.858
Tạo tệp (tệp/giây) 138k / 154k / 151k 165k / 175k / 175k 180k / 184k / 184k
Ghi ngẫu nhiên + fsync (IOPS) 14.210 / 15.641 / 14.638 14.538 / 10.950 / 11.241 9.025 / 7.414 / 8.407

Thứ hạng đổi theo từng phép đo:

  • Ghi tuần tự: ext4 và xfs không phân biệt được; btrfs thấp hơn khoảng 30%.
  • Đọc ngẫu nhiên: cả ba như nhau. Không có gì để chọn.
  • Tạo tệp: btrfs ≥ xfs > ext4, chênh khoảng 1,25 lần.
  • Ghi ngẫu nhiên có fsync: ext4 nhanh nhất và ổn định nhất; btrfs chậm hơn khoảng 1,7 lần.

Không hệ tệp nào thắng ở cả bốn. Câu hỏi "hệ tệp nào nhanh hơn" không có câu trả lời; phải hỏi "nhanh hơn ở việc gì".

Vì sao btrfs chậm ở ghi ngẫu nhiên

btrfs là hệ tệp sao chép khi ghi: sửa 4 KB giữa một tệp không ghi đè tại chỗ mà ghi ra chỗ mới rồi cập nhật cây tham chiếu trỏ tới đó. Với fsync, cả chuỗi cập nhật cây phải xuống đĩa. Đó là cái giá đổi lấy ảnh chụp tức thời, tổng kiểm tra dữ liệu, và khả năng quay lui — những thứ ext4 không có.

Với cơ sở dữ liệu, cái giá đó thường không đáng, và có cách giảm:

chattr +C /var/lib/postgresql/data     # tat sao chep khi ghi cho thu muc nay
mount -o nodatacow,noatime /dev/... /data

chattr +C phải đặt trên thư mục rỗng — tệp đã có dữ liệu không đổi được thuộc tính này.

Vì sao ext4 ổn định hơn ở fsync

Nhìn lại ba lần đo của xfs: 14.538, 10.950, 11.241. Lần đầu ngang ext4, hai lần sau thấp hơn 25%. ext4 thì 14.210, 15.641, 14.638 — dao động 10%.

Với một dịch vụ mà fsync nằm trên đường đi của mỗi giao dịch, độ ổn định quan trọng hơn giá trị trung bình. Một hệ thống nhanh hơn 5% nhưng thỉnh thoảng chậm đi 25% cho trải nghiệm xấu hơn. Đây là lý do thực dụng khiến ext4 vẫn là mặc định cho nhiều triển khai cơ sở dữ liệu, dù xfs thắng ở nhiều bảng so sánh.

Chọn theo tính chất, không theo con số

Hệ tệp Mạnh ở Nên chọn khi
ext4 ổn định, ít bất ngờ, công cụ sửa lỗi trưởng thành mặc định; cơ sở dữ liệu; khi không muốn nghĩ nhiều
xfs tệp rất lớn, siêu dữ liệu song song, mở rộng online máy chủ tệp, kho dữ liệu, phân vùng lớn
btrfs ảnh chụp, tổng kiểm tra, nén, RAID mềm máy trạm, máy chủ sao lưu, nơi cần quay lui

Khoảng cách hiệu năng giữa chúng — trừ trường hợp fsync của btrfs — nhỏ hơn nhiều so với khoảng cách giữa việc cấu hình đúng và cấu hình mặc định. Một tuỳ chọn gắn kết duy nhất cho nhiều hơn cả việc đổi hệ tệp:

mount -o noatime,nodiratime /dev/sdb1 /data

noatime bỏ việc cập nhật thời gian truy cập — nghĩa là mỗi lần đọc không còn kéo theo một lần ghi siêu dữ liệu. Với tải đọc nhiều, đây là thay đổi lớn hơn mọi thứ trong bảng trên. (Nhân hiện đại mặc định relatime nên chỉ ghi khi mốc cũ hơn một ngày, đã đỡ nhiều — nhưng noatime vẫn hơn.)

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

Ba hệ tệp nằm trên ảnh loop 2 GB trong một máy ảo, không phải trên phân vùng thật của ổ NVMe. Con số tuyệt đối không mang đi được.

Thứ mang đi được là hình dạng: đọc thì cả ba như nhau, tạo tệp thì btrfs và xfs hơn, fsync thì btrfs kém rõ rệt. Ba kết luận đó khớp với kiến trúc của chúng và sẽ lặp lại trên phần cứng thật. Và mang đi được, quan trọng hơn cả: lặp lại phép đo ba lần trước khi tin bất cứ tỷ lệ nào.

Mẫu số chung: một lần chạy là giai thoại, không phải số liệu

Cái lỗi ở đầu bài — phong vô địch cho một lượt chạy đơn lẻ — không riêng gì đo hệ tệp. Ở đâu có nhiễu (mà ở đâu cũng có nhiễu), một phép đo đơn lẻ chỉ là một mẫu từ một phân bố, và tin nó như thể nó là sự thật là cách sai lầm phổ biến nhất khi so sánh hiệu năng.

Trong thế giới Java, đây đúng là lý do JMH tồn tại: chạy microbenchmark một lần cho con số vô nghĩa vì JIT chưa kịp biên dịch nóng, GC chưa vào nhịp, CPU chưa lên xung. JMH bắt buộc có vòng khởi động (warmup) và nhiều lần lặp cùng đo độ phân tán — nó không cho bạn một con số, nó cho bạn một con số kèm khoảng tin cậy, đúng cái mà "chạy ba lần" ở đây cố làm thô sơ.

Trong Go, testing.B chạy vòng lặp b.N tự tăng cho tới khi thời gian đủ ổn định, và cả một hệ sinh thái quanh benchstat để so hai bộ kết quả bằng kiểm định thống kê — vì các tác giả Go biết rằng "bản mới nhanh hơn 8%" là vô nghĩa nếu độ nhiễu là 15%. So sánh đúng là so phân bố, không so hai con số.

Còn ở cơ sở dữ liệu, đây là chuyện chạy một câu query một lần rồi kết luận nó nhanh: lần đầu buffer cache lạnh, kế hoạch chưa cache, lần sau nóng hẳn — chênh cả chục lần; và một EXPLAIN ANALYZE đơn lẻ trên hệ đang tải biến động chỉ là một lát cắt. Phải chạy nhiều lần, cả nóng lẫn lạnh, mới biết hình dạng thật.

Mẫu số chung: một phép đo đơn lẻ là một mẫu rút từ một phân bố có nhiễu, nên mọi tỷ lệ tính từ nó (A nhanh gấp x lần B) đều có thể hoàn toàn là nhiễu — chỉ khi lặp lại đủ nhiều và thấy độ chênh giữa các nhóm lớn hơn độ chênh trong mỗi nhóm thì tỷ lệ đó mới là kết quả. Nên câu hỏi đúng trước khi tin một con số so sánh không phải "chênh nhau bao nhiêu" mà là "tôi đã chạy mỗi bên mấy lần, và độ dao động trong một bên có nhỏ hơn khoảng cách giữa hai bên không" — vì nếu chưa, bạn đang trao huy chương vàng cho người chạy xuôi gió.

Muốn tự thấy, so hai hệ tệp trên chính máy bạn, có lặp lại:

for d in /mnt/a /mnt/b; do          # doi thanh hai duong dan that cua ban
  echo "=== $d ($(df -T $d | tail -1 | awk '{print $2}')) ==="
  for i in 1 2 3; do
    rm -rf $d/many; mkdir -p $d/many; sync
    t0=$(date +%s%N)
    i2=0; while [ $i2 -lt 10000 ]; do : > $d/many/f$i2; i2=$((i2+1)); done
    sync; t1=$(date +%s%N)
    echo "  lan $i: $(( 10000000000 / (t1-t0) )) tep/giay"
    rm -rf $d/many
  done
done

Nếu ba lần của cùng một hệ tệp chênh nhau hơn mức chênh giữa hai hệ tệp, bạn chưa đo được gì cả — và đó là tình huống phổ biến hơn nhiều so với người ta tưởng.

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