GitHub: xiaoyang-sde/co-uring-http
前段时间我在实现 rust-kernel-riscv (使用 Rust 无栈协程进行上下文切换的操作系统内核) 时, 跟进了一些 Linux Kernel 的特性, 其中印象最深的就是 io_uring. io_uring 作为最新的高性能异步 I/O 框架, 支持普通文件与网络套接字的异步读写, 解决了传统 AIO 的许多问题. 在 Linux 通过隔离内核页表来应对 Meltdown 攻击后, 系统调用的开销是不可忽略的, 而 io_uring 通过映射一段在用户态与内核态共享的内存区域, 显著减少系统调用的次数, 缓解了刷新缓存开销. 关于 io_uring 的使用方法可以参考迟先生的博客: io_uring 的接口与实现.
C++ 20 引入的无栈协程让编写异步程序容易了不少, 之前通过回调函数实现的功能可以全部通过类似同步代码的写法来实现. 协程的性能很优秀, 创建的开销几乎可以忽略不记, 但是当前的标准只提供了基础功能, 还并没有实现易于使用的协程高级库, 导致我尝试自己封装了一套协程原语, 例如 task 与 sync_wait<task>.
为了体验这些特性, 我用 C++ 20 协程与 io_uring 重新实现了一个烂大街项目: HTTP 服务器. 鉴于以前没用过 C++ 写项目, 再加上 GitHub 常见的 HTTP 服务器项目是基于 Reactor 模式与 epoll 实现的, 以至于我在开发的过程中能借鉴 (指复制) 的机会并不多, 希望各位包容一下我的逆天代码. 我会持续维护这个项目, 争取添加更多特性并进一步优化性能.
.devcontainer/Dockerfile 提供了基于 ubuntu:lunar 的容器镜像, 已经配置好了编译环境, 可以直接在 Linux 或者 WSL 上使用. WSL 用户可以参考 Update WSL Kernel 的步骤将 Linux Kernel 升级到 6.3, 但是 Docker Desktop on Mac 用户似乎没办法升级.
cmake -DCMAKE\_BUILD\_TYPE=Release -DCMAKE\_C\_COMPILER:FILEPATH=/usr/bin/clang -DCMAKE\_CXX\_COMPILER:FILEPATH=/usr/bin/clang++ -B build -G "Unix Makefiles"
make -C build -j$(nproc)
./build/co\_uring\_http
为了测试 co-uring-http 在高并发情况的性能, 我用 hey 这个工具向它建立 1 万个客户端连接, 总共发送 100 万个 HTTP 请求, 每次请求大小为 1 KB 的文件. co-uring-http 每秒可以 88160 的请求, 并且在 0.5 秒内处理了 99% 的请求.
测试环境是 WSL (Ubuntu 22.04 LTS, Kernel 版本 6.3.0-microsoft-standard-WSL2), i5-12400 (6 核 12 线程), 16 GB 内存, PM9A1 固态硬盘.
./hey -n 1000000 -c 10000 <http://127.0.0.1:8080/1k>
Summary:
Total: 11.3429 secs
Slowest: 1.2630 secs
Fastest: 0.0000 secs
Average: 0.0976 secs
Requests/sec: 88160.9738
Total data: 1024000000 bytes
Size/request: 1024 bytes
Response time histogram:
0.000 [1] |
0.126 [701093] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.253 [259407] |■■■■■■■■■■■■■■■
0.379 [24843] |■
0.505 [4652] |
0.632 [678] |
0.758 [1933] |
0.884 [1715] |
1.010 [489] |
1.137 [5111] |
1.263 [78] |
// `thread_worker::handle_client()` 协程的简化版代码逻辑
// 省略了许多用于处理 `http_request` 并构造 `http_response` 的代码
// 实现细节请参考源代码
auto thread_worker::handle_client(client_socket client_socket) -> task<> {
http_parser http_parser;
buffer_ring &buffer_ring = buffer_ring::get_instance();
while (true) {
const auto [recv_buffer_id, recv_buffer_size] = co_await client_socket.recv(BUFFER_SIZE);
const std::span<char> recv_buffer = buffer_ring.borrow_buffer(recv_buffer_id, recv_buffer_size);
// ...
if (const auto parse_result = http_parser.parse_packet(recv_buffer); parse_result.has_value()) {
const http_request &http_request = parse_result.value();
// ...
std::string send_buffer = http_response.serialize();
co_await client_socket.send(send_buffer, send_buffer.size());
}
buffer_ring.return_buffer(recv_buffer_id);
}
}