Io_uring Không Có Readahead: Hiểu Sâu Về Hiệu Suất I/O Trong Turso

Công nghệ01 tháng 9, 2026·6 phút đọc

Bài viết phân tích chi tiết cách io_uring hoạt động khi không có readahead trong cơ sở dữ liệu Turso, thông qua việc đo đạc benchmark TPC-H. Tác giả chỉ ra tầm quan trọng của việc triển khai readahead ở tầng ứng dụng khi dùng O_DIRECT, cũng như so sánh chi phí CPU, cache miss và mô hình polling giữa các backend khác nhau.

Io_uring Không Có Readahead: Hiểu Sâu Về Hiệu Suất I/O Trong Turso

Io_uring Không Có Readahead: Bài Học Hiệu Suất Từ Turso

Bài viết này phân tích sâu về tác động của readahead trong io_uring thông qua một PR triển khai tính năng này trong cơ sở dữ liệu Turso. Tác giả đã thực hiện các phép đo benchmark TPC-H để so sánh hiệu suất giữa hai backend, từ đó khám phá ra những yếu tố ảnh hưởng đến tốc độ đọc dữ liệu.

Kết quả cho thấy việc triển khai readahead ở tầng ứng dụng giúp tăng tốc độ đáng kể nhờ khả năng hợp nhất request ở tầng block layer, đồng thời phơi bày sự khác biệt quan trọng giữa buffered I/O và O_DIRECT về mặt cache CPU.

Bối Cảnh: Hai Backend Của Turso

Turso sử dụng hai backend khác nhau để đọc dữ liệu. Backend đầu tiên dùng syscall pread(2) thông thường. Backend thứ hai sử dụng io_uring và mở file database với cờ O_DIRECT, không có tùy chọn dùng buffered I/O. Điều này dẫn đến một vấn đề thú vị: O_DIRECT loại bỏ cơ chế readahead của kernel, vì vậy muốn có readahead thì phải tự triển khai ở tầng ứng dụng.

PR được đề cập trong bài đã thực hiện đúng điều đó, và kết quả đo đạc rất ấn tượng. io_uring với application buffer chạy nhanh hơn đáng kể.

Vấn Đề Concurrency Và Request Merging

Khi chưa có readahead, io_uring chỉ phát hành một entry tại một thời điểm. Điều này tạo ra điểm nghẽn: mỗi lần đọc phải chờ lần đọc trước hoàn thành. Cụ thể, khi ứng dụng cần page 100, Turso submit SQE (Submission Queue Entry) để đọc page đó rồi chờ kết quả. Tiếp tục đến page 101, lại submit một SQE mới.

Với readahead được bật, mọi thứ thay đổi hoàn toàn. Khi Turso cần page 100 và phát hiện truy cập tuần tự, thay vì yêu cầu một page, nó submit cùng lúc 32 read SQE (từ page 100 đến 131). Bây giờ có 32 request đang chạy song song.

Số Liệu Đo Đạc Từ TPC-H

Tác giả sử dụng benchmark TPC-H trên database 1.2 GiB để đo hiệu suất. Query Q6 thực hiện full scan trên bảng lineitem – bảng lớn nhất trong benchmark.

Kết quả so sánh rất rõ ràng:

  • SQEs submitted: 195,207 (off) so với 218,212 (on)
  • Device requests: ~196,000 (off) so với ~16,300 (on)
  • rareq-sz (kích thước đọc trung bình): 4.37 KiB (off) so với 56.53 KiB (on)
  • %rrqm (phần trăm request được merge): ~0% (off) so với 91-93% (on)

Khi bật readahead, Turso phát hành thêm 23,005 SQE và đọc nhiều bytes hơn mức cần thiết, nhưng thiết bị nhận được ít request hơn đáng kể. Lý do là block layer có khả năng gộp các request liền kề thành một request lớn hơn – nhưng chỉ khi chúng xuất hiện trong queue cùng lúc.

Phân Tích Polling Thread

Turso cài đặt io_uring với sqpoll – một thread chạy vòng lặp kiểm tra xem có work mới hay không. Khi đo thời gian với prefetch_pages=32, kết quả rất bất ngờ:

  • Wall time: 8.22 giây
  • User time: 3.70 giây
  • System time: 8.46 giây (lớn hơn cả wall time!)

System time lớn hơn wall time chỉ có thể xảy ra khi hai thread sử dụng CPU đồng thời. Và quả thực, io_sq_thread (kernel polling thread) chiếm tới 65% số cycles trong khi chạy Q6.

Khi thử nghiệm với io_uring thông thường (không sqpoll), kết quả thay đổi: wall time tăng nhẹ lên 8.62 giây, user time gần như không đổi (3.62s), nhưng system time giảm mạnh xuống còn 1.27 giây. Điều này cho thấy polling thread không phải lúc nào cũng tối ưu.

Cache Misses: Điểm Khác Biệt Lớn Nhất

Khi tắt SQ polling, tác giả đo lại Q6 với cả hai backend:

  • io_uring: 8.55 giây
  • syscall (pread): 3.02 giây

Sự khác biệt 5.5 giây là rất đáng kể. Để hiểu tại sao, tác giả dùng perf để đếm số instructions và cache misses:

BackendCyclesInstructionsIPCCache MissesMiss Rate
io_uring plain5.374 B21.497 B3.99910.666 M13.255%
syscall4.734 B21.297 B4.4995.889 M7.691%

Io_uring chạy thêm 0.2 tỷ instructions (gần như không đáng kể), nhưng có thêm 4.8 triệu cache misses. Nguyên nhân nằm ở cơ chế hoạt động:

Với buffered read, kernel sao chép dữ liệu từ page cache vào process buffer. Quá trình copy này sử dụng CPU và có tác dụng phụ: dữ liệu được đưa vào CPU caches (L1/L2/L3). Với O_DIRECT, disk ghi dữ liệu trực tiếp vào process buffer mà không qua bước copy, nghĩa là CPU không tham gia và không có gì được đưa vào caches.

Giải thích này là giả thuyết, nhưng các con số cho thấy mức độ hợp lý của nó.

Bài Học Về Chi Phí

Sau tất cả các phép đo, tác giả rút ra một vài nhận định quan trọng:

io_uring với O_DIRECT và không có readahead là cấu hình chậm nhất. Chỉ có một SQE trong ring đồng nghĩa với zero concurrency, và không có gì để block layer merge.

sqpoll là lựa chọn hợp lý nếu máy có nhiều hơn một vCPU. Nhưng nếu ứng dụng đã sử dụng CPU nặng, polling thread sẽ cạnh tranh tài nguyên. Trong trường hợp đó, nên đo đạc tác động của việc dành riêng một vCPU cho kernel thread trước khi bật sqpoll.

io_uring thông thường cũng hợp lý vì nó có khả năng batching – chỉ cần một syscall io_uring_enter(2) cho nhiều SQEs. Một nghiên cứu từ SYSTOR 2022 đo được 1.01 syscalls mỗi I/O ở queue depth 64. Turso hiện chưa batching nên phải trả một syscall cho mỗi page.

Về chi phí tổng thể, có hai điểm đáng lưu ý:

  • Buffered read có bước copy dữ liệu từ page cache sang process buffer, giúp dữ liệu "nóng" trong CPU caches. O_DIRECT bỏ qua bước copy này, nhưng dữ liệu vẫn phải được nạp vào CPU trong lúc query – nhưng dưới dạng cache misses.
  • Readahead làm tốn thêm công việc: Turso submit thêm 23,005 SQEs và đọc thêm bytes không dùng đến. Nhưng số request thừa này giữ cho queue luôn đầy, tránh tình trạng queue chỉ chứa một request như trước.

Bài viết cung cấp cái nhìn sâu sắc về cách io_uring hoạt động trong thực tế, đồng thời cho thấy rằng không có giải pháp nào là hoàn hảo – mọi lựa chọn đều có những đánh đổi về concurrency, cache và tài nguyên CPU. Đối với các nhà phát triển Việt Nam đang xây dựng ứng dụng database hoặc hệ thống I/O intensive, những bài học này rất đáng giá để tối ưu hiệu suất.

Biểu đồ so sánh hiệu suất io_uringBiểu đồ so sánh hiệu suất io_uring

Chia sẻ:FacebookX
Nội dung tổng hợp bằng AI, mang tính tham khảo. Xem bài gốc ↗