When Bigger Instances Don't Scale

Bài tóm tắt ScyllaDB 2026 3. Quy mô lớn Độ khó: Khó

ScyllaDB · 2026 · 3. Quy mô lớn · Độ khó: Khó — bài gốc

Series ba phần của nhà cung cấp database: phần 1 (bài này, IO Scheduler) · phần 2 ScyllaDB 2026 (XFS, read-modify-write) · phần 3 ScyllaDB 2026 (checklist tổng kết). Các con số là của ScyllaDB trên phần cứng AWS.

Bối cảnh

  • Trước khi hỗ trợ một loại instance mới, ScyllaDB chạy IOTune (công cụ benchmark đĩa đi kèm Seastar) để đo 4 con số: IOPS đọc/ghi và băng thông đọc/ghi. Bốn số này vào io-properties.yaml; IO Scheduler dựng một model của đĩa từ đó, đại ý read_bw/read_bw_max + write_bw/write_bw_max + read_iops/read_iops_max + write_iops/write_iops_max ≤ 1, rồi điều tiết request để vừa bão hoà đĩa vừa giữ latency.
  • Với họ instance AWS i7i/i7ie, số đo không scale theo cỡ máy: IOPS đọc thấp hơn thông số tới 25% ở i7i.48xlarge, IOPS ghi thấp hơn 25–42%; băng thông thì dừng ở khoảng 8,5 GB/s trong khi fio kéo được tới 40 GB/s.
  • Hệ quả: số đo thấp làm scheduler đánh giá mỗi request đắt hơn thật, bóp nhiều hơn, nên một i7i.48xlarge chạy như một máy rẻ hơn nhiều (i7i.4xlarge). Họ không muốn bật hỗ trợ i7i khi số đo còn sai.

Thay đổi chính

  • Tách hai triệu chứng: IOPS thấp nhưng vẫn scale, băng thông thì đứng hẳn. Coi đó là hai lỗi khác nhau giúp thu hẹp nhanh hơn.
    • IOPS: IOTune không đọc được block size vật lý nên mặc định request 512 byte; đo bằng 4K thì được khoảng 1 triệu IOPS ghi thay vì khoảng 650.000 (i7i.12xlarge). Họ thêm tuỳ chọn chỉ định block size riêng cho đọc và ghi. Lúc đó họ tưởng là lỗi riêng của loại máy này; phần 2 cho thấy nguyên nhân thật nằm ở firmware.
  • Loại trừ dần: IOTune không bị nghẽn CPU; fio cho thấy cần request khoảng 4 MB mới lấp đầy hàng đợi đĩa; io_tester (Seastar) và IOTune đo giống nhau, nên lỗi nằm ở phía Seastar chứ không ở công cụ đo.
  • Bản vá hackathon: một patch bỏ phần lớn logic điều tiết của scheduler, xả request ngay. Chạy lại IOTune: băng thông ghi 9.775 MB/s, đọc 13.617 MB/s, vượt mức 8,5 GB/s. Vậy đúng là scheduler đang bóp.
  • Tracer nhị phân chi phí thấp gắn vào scheduler: ở bản master, một request chờ 83 µs mới được dispatch, bản vá chỉ 1 µs. Token bucket hết token trong khi lẽ ra không giới hạn (IOTune cố ý chạy không có io-properties.yaml, tức token vô hạn).
  • Đo hàng đợi trong đĩa: ở master, với request 1 MB, độ dài hàng đợi trong đĩa đứng ở khoảng 4 dù tăng song song, và latency trong đĩa không tăng: đĩa chưa hề bị ép. Bản vá thì latency tăng tỉ lệ với song song sau khi đĩa bão hoà, đúng như Little's Law.
  • Thử tái hiện không cần đĩa bằng ioinfo: với io-properties.yaml cho sẵn, giới hạn scheduler tính ra nằm trong 5% so với cấu hình, kể cả trên 8 GB/s. Vậy công thức đúng; lỗi chỉ xảy ra ở trường hợp không cấu hình. Họ vẫn thêm test để giữ hành vi này.
  • Nguyên nhân: hai biến nội bộ (giá trị dẫn xuất từ IOPS/băng thông cấu hình) có mặc định std::numeric_limits<int>::max(). Khi có file cấu hình, code tính lại chúng; khi không có thì nhánh đó không chạy, INT_MAX đi thẳng vào công thức fair queue và thành trần khoảng 8,x GB/s. Sửa: đổi mặc định sang uint64_t max. Theo bài, "một dòng sửa cho nhiều tuần làm việc". Họ cũng kiểm lại code có chịu được chi phí request bằng 0 (trường hợp đúng của "không giới hạn") mà không chia cho 0 hay tràn số.

Vì sao

  • Model đĩa là đầu vào của bộ điều tiết. Đo sai thì điều tiết sai, và sai theo hướng phí tiền: trả cho máy lớn nhưng dùng như máy nhỏ (Chi phí hạ tầng và build vs buy).
  • Bộ điều tiết (token bucket, fair queue) có thể tự trở thành nút thắt khi giá trị mặc định ngầm định một trần mà không ai nghĩ tới (Load shedding, rate limit và backpressure).

Bài học cho SWE

  • Khi "máy to hơn không nhanh hơn", so với một công cụ chuẩn (fio) để biết lỗi nằm ở phía mình hay phía phần cứng.
  • Một bản vá thô (tắt hẳn điều tiết) là thí nghiệm rẻ để xác nhận nghi phạm trước khi đọc kỹ công thức.
  • Công cụ quan sát nội bộ (tracer nhị phân, xuất độ dài hàng đợi trong đĩa) đáng đầu tư trước khi có sự cố; ở đây chúng đến từ một hackathon (Observability và monitoring).
  • Giá trị mặc định kiểu INT_MAX cho "không giới hạn" là bẫy: nhánh không cấu hình ít được chạy và ít được test. Kiểm cả trường hợp biên "chi phí bằng 0".
  • Thống kê: phân biệt "đĩa đầy" (latency tăng theo song song) với "mình không gửi đủ" (latency đứng yên). Little's Law cho biết nên nhìn hai số này cùng nhau.

Liên quan