I/O Linux thông lượng cao trong Rust: io_uring, Tokio & Zero-Copy Networking

Mục lục bài viết(16 mục)
Hiệu năng I/O của Linux rất quan trọng đối với các dịch vụ mạng quy mô lớn. I/O bất đồng bộ truyền thống dựa trên epoll, mặc dù hiệu quả, vẫn gây ra chi phí đáng kể do các syscall lặp đi lặp lại và việc sao chép dữ liệu giữa không gian kernel và không gian người dùng. io_uring, được giới thiệu trong nhân Linux 5.1, tái kiến trúc cơ bản điều này bằng cách cung cấp một giao diện I/O bất đồng bộ mạnh mẽ, giảm thiểu syscall và cho phép các hoạt động zero-copy. Hướng dẫn này trình bày chi tiết cách tận dụng io_uring với Rust, đặc biệt là tích hợp nó với tokio để đạt được hiệu năng mạng thông lượng cao, zero-copy.
Sự thay đổi mô hình của io_uring
io_uring hoạt động dựa trên cơ chế bộ đệm vòng chia sẻ giữa không gian người dùng và kernel. Thay vì phát hành các syscall riêng lẻ cho mỗi hoạt động I/O, các ứng dụng xếp hàng các Submission Queue Entry (SQE) vào một Submission Queue (SQ). Kernel sau đó xử lý các SQE này một cách bất đồng bộ, đặt các Completion Queue Entry (CQE) vào một Completion Queue (CQ) khi các hoạt động hoàn tất. Việc xử lý theo lô này giúp giảm đáng kể việc chuyển đổi ngữ cảnh và chi phí syscall.
Các tính năng chính của io_uring:
- Xử lý theo lô: Nhiều hoạt động I/O được gửi bằng một syscall
io_uring_enterduy nhất. - Bất đồng bộ: Các hoạt động hoàn tất mà không chặn luồng gọi.
- Polling: Kernel có thể chủ động thăm dò SQ, loại bỏ nhu cầu
io_uring_entertrong một số trường hợp, giảm độ trễ hơn nữa. - Zero-Copy: Các hoạt động như
IORING_OP_SENDMSGvàIORING_OP_RECVMSGcó thể trực tiếp hoạt động trên các bộ đệm trong không gian người dùng, tránh sao chép dữ liệu. - Bộ đệm/Tệp cố định: Đăng ký bộ đệm và bộ mô tả tệp với
io_uringcho phép kernel tối ưu hóa quyền truy cập và tránh các tra cứu lặp lại.
epoll so với io_uring: So sánh kiến trúc
| Tính năng | epoll (Tokio tiêu chuẩn) | io_uring (tokio-uring) |
|---|---|---|
| Mô hình I/O | Kích hoạt cạnh, hướng sự kiện | Bất đồng bộ, bộ đệm vòng |
| Chi phí Syscall | Cao (1 syscall cho mỗi thao tác + epoll_wait) | Thấp (xử lý theo lô, io_uring_enter cho nhiều thao tác) |
| Sao chép dữ liệu | Sao chép người dùng-kernel cho read/write | Có thể zero-copy (MSG_ZEROCOPY) |
| Quản lý bộ đệm | Người dùng quản lý, truyền theo từng syscall | Bộ đệm cố định được đăng ký bởi kernel |
| Bộ mô tả tệp | Truyền theo từng syscall | Tệp cố định được đăng ký bởi kernel |
| Độ trễ | Cao hơn do syscall/sao chép | Thấp hơn do xử lý theo lô/zero-copy |
| Thông lượng | Tốt cho nhiều kết nối, bị giới hạn bởi syscall | Tuyệt vời cho I/O khối lượng lớn |
| Phiên bản Kernel | Linux 2.5.44+ | Linux 5.1+ |
| Độ phức tạp | API đơn giản hơn | API phức tạp hơn, đường cong học tập cao hơn |
Tích hợp Rust: tokio-uring
Mặc dù các ràng buộc io_uring trực tiếp tồn tại (ví dụ: crate io-uring), việc tích hợp io_uring với một runtime bất đồng bộ hiện có như tokio thường được mong muốn. Crate tokio-uring cung cấp một runtime tương thích tokio được xây dựng trên io_uring. Nó cung cấp các tương đương được hỗ trợ bởi io_uring cho các nguyên thủy I/O tokio phổ biến như TcpStream, UdpSocket và I/O tệp.
Thiết lập tokio-uring
Thêm đoạn sau vào Cargo.toml của bạn:
[dependencies]
tokio-uring = { version = "0.6", features = ["net", "fs"] }
bytes = "1.4"
log = "0.4"
env_logger = "0.10"
Tính năng net cho phép TcpStream, UdpSocket, v.v., trong khi fs cho phép I/O tệp.
Máy chủ TCP Echo cơ bản tokio-uring
Ví dụ này minh họa một máy chủ TCP echo đơn giản sử dụng tokio-uring. Lưu ý macro tokio_uring::start(), nó khởi tạo runtime io_uring.
use tokio_uring::net::{TcpListener, TcpStream};
use tokio_uring::buf::IoBuf;
use bytes::{BytesMut, BufMut};
use log::{info, error};
const BUFFER_SIZE: usize = 4096;
#[tokio_uring::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
let addr = "127.0.0.1:8080";
let listener = TcpListener::bind(addr).await?;
info!("Listening on {}", addr);
loop {
match listener.accept().await {
Ok((socket, peer_addr)) => {
info!("Accepted connection from {}", peer_addr);
tokio_uring::spawn(async move {
if let Err(e) = handle_connection(socket).await {
error!("Error handling connection from {}: {}", peer_addr, e);
}
});
}
Err(e) => {
error!("Error accepting connection: {}", e);
}
}
}
}
async fn handle_connection(mut socket: TcpStream) -> Result<(), Box<dyn std::error::Error>> {
let mut buf = BytesMut::with_capacity(BUFFER_SIZE);
loop {
// Prepare a buffer for receiving data.
// `bytes::BytesMut` implements `IoBuf`, making it suitable for `tokio-uring`.
let (res, filled_buf) = socket.recv(buf.split_off(0).limit(BUFFER_SIZE)).await;
let bytes_read = res?;
if bytes_read == 0 {
info!("Client disconnected.");
break; // Client disconnected
}
// Advance the buffer to reflect the bytes read.
// `filled_buf` is the original buffer, but its `IoBuf` trait implementation
// ensures it's correctly sliced for the `recv` operation.
// We need to manually advance the `BytesMut` to reflect the data.
buf.unsplit(filled_buf);
buf.advance_mut(bytes_read);
info!("Received {} bytes: {:?}", bytes_read, &buf[..bytes_read]);
// Send the received data back.
// `send` takes an `IoBuf` and returns the buffer back after completion.
let (res, _) = socket.send(buf.split_to(bytes_read)).await;
let bytes_written = res?;
info!("Sent {} bytes.", bytes_written);
if bytes_written == 0 {
info!("Failed to send data, client likely disconnected.");
break;
}
}
Ok(())
}
Để kiểm tra điều này, bạn có thể sử dụng netcat: nc 127.0.0.1 8080. Nhập một số văn bản và nhấn enter; máy chủ sẽ trả lời lại.
Mạng Zero-Copy với MSG_ZEROCOPY
Sức mạnh thực sự của io_uring đối với mạng nằm ở khả năng thực hiện các hoạt động zero-copy. Điều này đạt được bằng cách sử dụng hoạt động IORING_OP_SENDMSG với cờ MSG_ZEROCOPY. Khi cờ này được đặt, kernel ánh xạ trực tiếp bộ đệm không gian người dùng vào không gian bộ nhớ của chính nó để truyền, tránh syscall copy_from_user truyền thống. Ứng dụng được thông báo qua một CQE khi bộ đệm an toàn để sử dụng lại (tức là dữ liệu đã được truyền hoặc xếp hàng để truyền).
tokio-uring hiển thị chức năng này thông qua phương thức send_zc trên TcpStream.
Triển khai truyền tải TCP Zero-Copy
Ví dụ này minh họa một máy chủ nhận dữ liệu và sau đó gửi một phản hồi cố định bằng cách sử dụng zero-copy. Điều quan trọng là phải quản lý cẩn thận vòng đời của bộ đệm.
use tokio_uring::net::{TcpListener, TcpStream};
use tokio_uring::buf::IoBuf;
use bytes::{BytesMut, BufMut};
use log::{info, error};
use std::sync::Arc;
const BUFFER_SIZE: usize = 4096;
const RESPONSE_DATA: &[u8] = b"HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello, World!";
#[tokio_uring::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
let addr = "127.0.0.1:8080";
let listener = TcpListener::bind(addr).await?;
info!("Listening on {}", addr);
loop {
match listener.accept().await {
Ok((socket, peer_addr)) => {
info!("Accepted connection from {}", peer_addr);
tokio_uring::spawn(async move {
if let Err(e) = handle_zero_copy_connection(socket).await {
error!("Error handling zero-copy connection from {}: {}", peer_addr, e);
}
});
}
Err(e) => {
error!("Error accepting connection: {}", e);
}
}
}
}
async fn handle_zero_copy_connection(mut socket: TcpStream) -> Result<(), Box<dyn std::error::Error>> {
let mut recv_buf = BytesMut::with_capacity(BUFFER_SIZE);
let send_buf = Arc::new(BytesMut::from(RESPONSE_DATA)); // Use Arc for shared buffer for zero-copy send
loop {
// Receive data (standard copy, as MSG_ZEROCOPY is for send)
let (res, filled_buf) = socket.recv(recv_buf.split_off(0).limit(BUFFER_SIZE)).await;
let bytes_read = res?;
if bytes_read == 0 {
info!("Client disconnected.");
break;
}
recv_buf.unsplit(filled_buf);
recv_buf.advance_mut(bytes_read);
info!("Received {} bytes from client.", bytes_read);
// Perform zero-copy send
// `send_zc` returns a future that completes when the kernel is done with the buffer.
// The buffer is returned, allowing reuse.
let (res, returned_buf) = socket.send_zc(send_buf.clone().slice(..)).await;
let bytes_written = res?;
info!("Zero-copy sent {} bytes.", bytes_written);
if bytes_written == 0 {
info!("Failed to zero-copy send data, client likely disconnected.");
break;
}
// Clear the receive buffer for the next read
recv_buf.clear();
}
Ok(())
}
Để kiểm tra điều này, bạn có thể sử dụng curl: curl http://127.0.0.1:8080. Bạn sẽ nhận được "Hello, World!".
Những cân nhắc quan trọng đối với Zero-Copy
- Vòng đời bộ đệm: Bộ đệm được truyền cho
send_zcphải còn hiệu lực và không thay đổi cho đến khi futuresend_zchoàn tất.tokio-uringxử lý điều này bằng cách trả về bộ đệm, đảm bảo nó không bị hủy sớm. Đối với dữ liệu tĩnh, được chia sẻ,Arc<BytesMut>hoặcArc<[u8]>là phù hợp. - Hỗ trợ Kernel:
MSG_ZEROCOPYyêu cầu nhân Linux 5.2 trở lên. - Xử lý lỗi: Nếu
MSG_ZEROCOPYthất bại (ví dụ: do bộ nhớ kernel không đủ hoặc kernel cũ hơn),send_zcsẽ trả về lỗi. Có thể cần một phương án dự phòng chosendtiêu chuẩn trong môi trường sản xuất. - Đăng ký bộ đệm: Để đạt hiệu suất tối đa, đặc biệt là với việc gửi lặp lại cùng một dữ liệu, hãy cân nhắc đăng ký bộ đệm với
io_uringbằng cách sử dụngIORING_REGISTER_BUFFERS.tokio-uringcung cấptokio_uring::buf::BoundedBufvàtokio_uring::buf::FixedBufcho việc này.
Kiểm tra hiệu năng 100k kết nối WebSocket đồng thời
Kiểm tra hiệu năng io_uring cho 100k kết nối WebSocket đồng thời yêu cầu một thiết lập mạnh mẽ. Chúng tôi sẽ phác thảo cách tiếp cận và kết quả mong đợi thay vì cung cấp một máy khách/máy chủ kiểm tra hiệu năng đầy đủ, điều này sẽ rất rộng.
Kiến trúc máy chủ
Một máy chủ WebSocket dựa trên tokio-uring sẽ bao gồm:
tokio_uring::net::TcpListener: Chấp nhận các kết nối mới.- WebSocket Handshake: Thực hiện nâng cấp HTTP. Phần này là HTTP tiêu chuẩn và có thể sử dụng
httparsehoặc tương tự. - WebSocket Framing: Triển khai khung của giao thức WebSocket (masking, opcode, length).
tokio_uring::net::TcpStream::recv: Để nhận các khung WebSocket.tokio_uring::net::TcpStream::send_zc: Để gửi các khung WebSocket, đặc biệt là cho các tin nhắn broadcast hoặc dữ liệu lớn. Đây là nơi zero-copy tỏa sáng.
Kiến trúc máy khách
Một máy khách kiểm tra hiệu năng cần:
- Thiết lập 100k kết nối TCP: Điều này yêu cầu giới hạn bộ mô tả tệp đáng kể (
ulimit -n). - Thực hiện WebSocket handshakes: Cho mỗi kết nối.
- Duy trì kết nối: Gửi pings/pongs để giữ kết nối sống.
- Gửi/Nhận dữ liệu: Mô phỏng lưu lượng ứng dụng.
- Đo độ trễ và thông lượng: Theo dõi thời gian khứ hồi và tốc độ dữ liệu.
Lợi ích hiệu suất mong đợi
Với io_uring và zero-copy, đối với một máy chủ gửi các tin nhắn thường xuyên, giống hệt nhau (ví dụ: dữ liệu thị trường, cập nhật trạng thái trò chơi) đến nhiều máy khách, chúng tôi mong đợi:
- Giảm sử dụng CPU: Ít syscall và sao chép dữ liệu hơn có nghĩa là ít thời gian CPU kernel hơn.
- Thông lượng cao hơn: Nhiều dữ liệu được xử lý hơn trên mỗi đơn vị thời gian.
- Độ trễ thấp hơn: Đặc biệt đối với các hoạt động gửi, vì dữ liệu được xếp hàng trực tiếp để truyền.
- Mật độ kết nối tăng: Nhiều kết nối có thể được xử lý trên mỗi phiên bản máy chủ do giảm chi phí trên mỗi kết nối.
Môi trường kiểm tra hiệu năng
- Kernel: Linux 5.10+ (LTS) hoặc 6.x để có các tính năng
io_uringtối ưu. - Phần cứng: CPU có số lõi cao, RAM dồi dào, NIC nhanh.
ulimit -n: Đặt thành giá trị > 100.000 (ví dụ:ulimit -n 1048576).- Điều chỉnh mạng: Các tham số
sysctlnhưnet.core.somaxconn,net.ipv4.tcp_max_syn_backlog,net.ipv4.tcp_tw_reuse,net.ipv4.tcp_fin_timeout.
Những vấn đề và cách khắc phục trong sản xuất
io_uringKhông khả dụng/Được bật:- Triệu chứng: Các hoạt động
io_uringthất bại vớiENOSYShoặc các lỗi tương tự. - Nguyên nhân: Phiên bản Kernel < 5.1, hoặc mô-đun
io_uringchưa được tải/biên dịch. - Khắc phục: Nâng cấp kernel lên 5.1+ (khuyến nghị 5.10+ để ổn định/tính năng). Xác minh
CONFIG_IO_URING=ytrong cấu hình kernel.
- Triệu chứng: Các hoạt động
ulimit -nQuá thấp:- Triệu chứng: Lỗi
Too many open fileskhi chấp nhận kết nối hoặc tạo socket. - Nguyên nhân: Giới hạn bộ mô tả tệp mặc định (thường là 1024) không đủ cho độ đồng thời cao.
- Khắc phục: Tăng
ulimit -ncho người dùng chạy ứng dụng. Đối với các dịch vụ systemd, đặtLimitNOFILEtrong tệp đơn vị dịch vụ.
- Triệu chứng: Lỗi
- Lỗi
MSG_ZEROCOPY:- Triệu chứng:
send_zctrả vềEOPNOTSUPPhoặc các lỗi khác. - Nguyên nhân: Phiên bản Kernel < 5.2, hoặc giới hạn trình điều khiển mạng cụ thể.
- Khắc phục: Nâng cấp kernel. Triển khai phương án dự phòng cho
TcpStream::sendnếusend_zcthất bại.
- Triệu chứng:
- Vấn đề quản lý bộ đệm với Zero-Copy:
- Triệu chứng: Hỏng dữ liệu, lỗi sử dụng sau khi giải phóng (use-after-free), hoặc hành vi không mong muốn.
- Nguyên nhân: Sử dụng lại hoặc sửa đổi bộ đệm trước khi
send_zchoàn tất và trả về nó. - Khắc phục: Đảm bảo vòng đời của bộ đệm được quản lý chặt chẽ.
tokio-uring'ssend_zctrả về bộ đệm, cho biết nó an toàn để sử dụng lại. Đối với dữ liệu tĩnh,Arcvàsliceđược sử dụng để đảm bảo tính bất biến trong quá trình xử lý của kernel.
- Sử dụng CPU cao với Polling
io_uring:- Triệu chứng: Các luồng worker
io_uringtiêu thụ 100% CPU ngay cả khi tải thấp. - Nguyên nhân:
IORING_SETUP_SQPOLL(polling kernel) có thể quá tích cực. Nếu không được cấu hình đúng cách, nó có thể quay vòng chờ. - Khắc phục: Đảm bảo
io_uringđược cấu hình phù hợp với khối lượng công việc của bạn.tokio-uringthường quản lý điều này, nhưng nếu bạn đang sử dụngio-uringthô, hãy lưu ý các cờ polling. Đối với hầu hết các khối lượng công việc máy chủ,io_uringhướng sự kiện (không polling) là đủ, với polling được dành cho các kịch bản độ trễ cực thấp.
- Triệu chứng: Các luồng worker
- Áp lực bộ nhớ:
- Triệu chứng: Lỗi OOM, trao đổi quá mức.
- Nguyên nhân: Số lượng kết nối lớn, mỗi kết nối có bộ đệm nhận riêng, hoặc các bộ đệm cố định đã đăng ký lớn.
- Khắc phục: Tối ưu hóa kích thước bộ đệm. Cân nhắc sử dụng một nhóm bộ đệm cho các hoạt động nhận. Đối với các bộ đệm cố định, chỉ đăng ký những gì cần thiết.
Các câu hỏi thường gặp
- Tôi có thể sử dụng
tokio-uringvới mãtokiohiện có không? Có,tokio-uringcung cấp một runtime tương thíchtokio. Bạn có thể sử dụngtokio_uring::spawnđể chạy các future được hỗ trợ bởiio_uringcùng với các futuretokio::spawnthông thường, mặc dù nhìn chung nên giữ I/O cụ thể củaio_uringtrên runtimetokio-uringđể đạt hiệu suất tối ưu. - Những lợi ích chính của
io_uringso vớiepollđối với mạng là gì? Những lợi ích chính là giảm chi phí syscall thông qua việc xử lý theo lô và khả năng thực hiện truyền dữ liệu zero-copy giữa không gian người dùng và kernel. Điều này dẫn đến việc sử dụng CPU thấp hơn, thông lượng cao hơn và độ trễ thấp hơn, đặc biệt đối với các hoạt động I/O khối lượng lớn. io_uringluôn nhanh hơnepollkhông? Không phải lúc nào cũng vậy. Đối với các ứng dụng có độ đồng thời thấp, thông lượng thấp, chi phí thiết lậpio_uringcó thể lớn hơn lợi ích của nó.io_uringtỏa sáng trong các kịch bản có độ đồng thời cao, khối lượng I/O lớn hoặc khi zero-copy là rất quan trọng. Nó cũng yêu cầu một nhân Linux hiện đại.tokio-uringxử lý việc quản lý bộ đệm cho zero-copy như thế nào? Phương thứctokio-uring'ssend_zcnhận mộtIoBuf(nhưBytesMuthoặcArc<[u8]>) và trả về nó sau khi kernel đã hoàn thành với bộ đệm. Điều này đảm bảo vòng đời của bộ đệm được quản lý chính xác, ngăn ngừa các vấn đề sử dụng sau khi giải phóng. Đối với dữ liệu tĩnh,Arcđược sử dụng để chia sẻ quyền sở hữu.- Các yêu cầu kernel đối với
io_uringvàMSG_ZEROCOPYlà gì?io_uringyêu cầu nhân Linux 5.1 trở lên.MSG_ZEROCOPYđặc biệt yêu cầu nhân Linux 5.2 trở lên. Đối với sản xuất, kernel 5.10 (LTS) hoặc kernel 6.x gần đây được khuyến nghị để ổn định và đầy đủ tính năng.
Kết luận
io_uring đại diện cho một bước tiến đáng kể trong I/O bất đồng bộ của Linux, mang lại hiệu suất vượt trội cho các ứng dụng thông lượng cao. Rust, với hệ thống kiểu mạnh mẽ và đặc tính hiệu suất, là một ngôn ngữ lý tưởng để tận dụng khả năng của io_uring. Crate tokio-uring cung cấp một cách mạnh mẽ và tiện lợi để tích hợp io_uring vào các ứng dụng dựa trên tokio, cho phép các nhà phát triển xây dựng các dịch vụ mạng hiệu quả cao với các tính năng như truyền dữ liệu zero-copy. Mặc dù đường cong học tập cho io_uring có thể dốc hơn so với epoll truyền thống, nhưng lợi ích hiệu suất cho các khối lượng công việc đòi hỏi là đáng kể và xứng đáng với sự đầu tư.
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

Bằng chứng không kiến thức trong Rust & Circom: Xác minh SnarkJS & Hướng dẫn sản xuất
Hướng dẫn toàn diện về bằng chứng không kiến thức trong Rust & Circom: xác minh SnarkJS & hướng dẫn sản xuất với kiến trúc cấp độ sản xuất và các ví dụ mã.
Read more
eBPF trong Môi Trường Production: Quan Sát Hệ Thống Linux Với Overhead Cực Thấp, Tracing và Profiling Nhân Hệ Điều Hành
Triển khai quan sát nhân Linux (kernel observability) với overhead gần như bằng không bằng eBPF. Đo độ trễ system call, theo dõi cấp phát bộ nhớ và giám sát socket mạng mà không cần sidecar.
Read more
Xây dựng Microservice hiệu suất cao với Rust và Axum: Hướng dẫn sản xuất hoàn chỉnh
Hướng dẫn toàn diện về xây dựng microservice hiệu suất cao bằng Rust và Axum: hướng dẫn sản xuất hoàn chỉnh với kiến trúc cấp độ sản xuất và các ví dụ mã.
Read more