io_uring được giới thiệu như bước nhảy của I/O trên Linux. Bài này đếm chính xác số lời gọi hệ thống mà nó tiết kiệm được — và câu trả lời phụ thuộc hoàn toàn vào cách bạn cấu hình.
Đếm lời gọi hệ thống
strace -c, đọc 40.000 khối 4 KB, O_DIRECT, iodepth=32:
| Cách đọc | Lời gọi hệ thống | Mỗi thao tác |
|---|---|---|
pread64 (psync) |
40.000 | 1,00 |
libaio |
40.000 io_submit + 39.970 io_getevents = 79.970 |
2,00 |
io_uring, mặc định |
40.002 io_uring_enter |
1,00 |
io_uring, gom 32 |
1.857 | 0,05 |
io_uring, gom 64 |
1.250 | 0,03 |
Hai dòng giữa là điều đáng ngạc nhiên.
libaio tốn gấp đôi pread. Mỗi thao tác cần một lời gọi để nộp và một lời gọi để lấy kết quả. Nó bất đồng bộ, nhưng nó không hề tiết kiệm lời gọi hệ thống — nó tiêu nhiều hơn.
io_uring với cấu hình mặc định tốn đúng bằng pread. Vòng nộp và vòng kết quả nằm trong bộ nhớ chung, nhưng nếu chương trình nộp một yêu cầu rồi gọi io_uring_enter ngay, nó vẫn là một lời gọi cho một thao tác.
Cái vòng chung không tự gom giúp bạn. Nó chỉ cho phép gom. Bảo nó gom 64 thì số lời gọi tụt 32 lần.
Thông lượng theo đúng thứ tự đó
Đọc ngẫu nhiên 4 KB, ba lần đo mỗi cấu hình:
iodepth = 1:
| Cách | IOPS |
|---|---|
psync |
23.614 / 24.669 / 28.823 |
libaio |
26.703 / 26.966 / 25.564 |
io_uring |
19.935 / 19.557 / 19.788 |
io_uring chậm nhất. Ở độ sâu 1 thì không có gì để gom, và bộ máy vòng chỉ thêm việc.
iodepth = 64:
| Cách | IOPS |
|---|---|
libaio |
84.346 / 41.076 / 41.619 |
io_uring, mặc định |
47.809 / 52.224 / 47.976 |
io_uring, có gom |
246.485 / 250.408 / 310.408 |
Từ 47.809 lên 246.485 — hơn 5 lần, chỉ bằng hai tuỳ chọn cấu hình, không đổi một dòng logic nào.
So với pread ở độ sâu 1 (khoảng 25.000 IOPS), io_uring có gom nhanh hơn 10 lần.
(Ba lần đo của libaio ở độ sâu 64 dao động từ 41.076 tới 84.346. Với dải rộng như vậy tôi không xếp hạng nó so với io_uring mặc định.)
Điều đáng nhớ
Nói lại theo cách khác vì nó dễ bị bỏ qua: io_uring không phải một công tắc bật là nhanh. Nó là một cơ chế cho phép bạn gom, và lợi ích tỷ lệ thuận với việc bạn có gom hay không.
Đổi --ioengine=psync thành --ioengine=io_uring trong một tệp cấu hình, rồi báo cáo rằng "đã nâng cấp sang io_uring", là một thay đổi làm chậm hệ thống đi 20%.
Điều này áp dụng cho cả các thư viện: một thư viện có "hỗ trợ io_uring" chưa chắc đã gom. Cách kiểm tra duy nhất là đếm:
strace -f -c -e trace=io_uring_enter ./chuong-trinh-cua-ban
Chia số lời gọi cho số thao tác I/O. Bằng 1 nghĩa là bạn đang trả tiền cho một cơ chế mà không dùng nó.
Ba mức lợi ích
io_uring có ba nấc, và mỗi nấc đòi hỏi thêm việc từ phía ứng dụng:
Nấc một — vòng chung thay cho lời gọi nộp. Yêu cầu được ghi vào bộ nhớ dùng chung với nhân. Vẫn cần io_uring_enter để đánh thức nhân. Đây là mặc định, và như bảng trên, nó không cho gì.
Nấc hai — gom. Nộp N yêu cầu rồi gọi io_uring_enter một lần. Số lời gọi chia cho N. Đây là nấc cho phần lớn lợi ích, và trong fio nó là hai tuỳ chọn:
--iodepth_batch_submit=32 --iodepth_batch_complete_min=32
Trong liburing, nó là việc gọi io_uring_submit() sau khi đã xếp nhiều sqe chứ không phải sau mỗi cái.
Nấc ba — SQPOLL, không lời gọi hệ thống nào cả. Một luồng của nhân quét vòng nộp liên tục. Ứng dụng chỉ ghi vào bộ nhớ.
struct io_uring_params p = {0};
p.flags = IORING_SETUP_SQPOLL;
p.sq_thread_idle = 2000; /* ms truoc khi luong nhan ngu */
io_uring_queue_init_params(256, &ring, &p);
Cái giá là một nhân CPU bị chiếm để quay vòng. Chỉ đáng với hệ thống I/O rất dày; với tải trung bình thì đó là đốt một nhân để tiết kiệm vài phần trăm.
Những gì io_uring làm được mà libaio không
libaio chỉ hỗ trợ đúng một thứ: đọc/ghi O_DIRECT trên tệp thường. Mọi thứ khác — tệp qua bộ đệm, socket, openat, statx — âm thầm rơi về chế độ đồng bộ.
io_uring nhận gần như mọi lời gọi hệ thống liên quan tới I/O: read, write, accept, connect, send, recv, openat, close, statx, fsync, timeout. Nghĩa là một máy chủ mạng có thể chạy toàn bộ vòng lặp sự kiện qua một vòng duy nhất, thay cho epoll cộng với các lời gọi riêng lẻ.
Đó mới là thay đổi kiến trúc thật sự, và nó lớn hơn con số IOPS trong bảng trên.
Cái giá về bảo mật
io_uring đã là nguồn của nhiều lỗ hổng leo thang đặc quyền. Google tắt nó trên ChromeOS và Android; nhiều bản phân phối máy chủ hạn chế nó theo mặc định.
cat /proc/sys/kernel/io_uring_disabled
# 0 = cho phep moi tien trinh
# 1 = chi tien trinh co CAP_SYS_ADMIN
# 2 = tat han
Nếu bạn định dùng io_uring trong sản phẩm, kiểm tra giá trị này trên máy chủ đích trước — chương trình sẽ nhận EPERM ngay ở io_uring_setup, và nhiều thư viện xử lý trường hợp đó bằng cách rơi thầm về epoll mà không báo gì.
Thử ba mươi giây
Đếm xem chương trình của bạn có thật sự gom không:
strace -f -c -e trace=io_uring_enter,io_submit,io_getevents,pread64,read,write \
./chuong-trinh-cua-ban 2>&1 | tail -12
echo "--- io_uring co duoc phep khong ---"
cat /proc/sys/kernel/io_uring_disabled 2>/dev/null || echo "nhan cu, khong co nut nay"
grep -c io_uring /proc/kallsyms
Lấy số lời gọi chia cho số thao tác I/O mà bạn biết chương trình đã làm. Nếu tỷ lệ là 1,0 thì io_uring trong hệ thống của bạn đang ở đúng chỗ mà tôi đo được 19.935 IOPS, còn pread cho 23.614.
Phần sau: TCP — bắt tay, cửa sổ trượt, và những gì xảy ra ở một kết nối chậm.