co-uring-http: 基于 C++ 无栈协程与 io_uring 的高性能 HTTP 服务器 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
xAsiimov
V2EX    分享创造

co-uring-http: 基于 C++ 无栈协程与 io_uring 的高性能 HTTP 服务器

  •  
      xAsiimov 2023-05-02 08:44:58 +08:00 2453 次点击
    这是一个创建于 895 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

    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<T>sync_wait<task<T>>.

    为了体验这些特性, 我用 C++ 20 协程与 io_uring 重新实现了一个烂大街项目: HTTP 服务器. 鉴于以前没用过 C++ 写项目, 再加上 GitHub 常见的 HTTP 服务器项目是基于 Reactor 模式与 epoll 实现的, 以至于我在开发的过程中能借鉴 (指复制) 的机会并不多, 希望各位包容一下我的逆天代码. 我会持续维护这个项目, 争取添加更多特性并进一步优化性能.

    主要特性

    • 使用 C++ 20 协程简化服务端与客户端的异步交互
    • 使用 io_uring 管理异步 I/O 请求, 例如 accept(), recv(), send()
    • 使用 ring-mapped buffers 减少内存分配的次数, 减少数据在内核态与用户态之间拷贝的次数 (Linux 5.19 新特性)
    • 使用 multishot accept 减少向 io_uring 提交 accept() 请求的次数 (Linux 5.19 新特性)
    • 实现线程池进行协程调度, 充分利用 CPU 的所有核心
    • 使用 RAII 类管理 io_uring, 文件描述符, 以及线程池的生命周期

    编译环境

    .devcontainer/Dockerfile 提供了基于 ubuntu:lunar 的容器镜像, 已经配置好了编译环境, 可以直接在 Linux 或者 WSL 上使用. WSL 用户可以参考 Update WSL Kernel 的步骤将 Linux Kernel 升级到 6.3, 但是 Docker Desktop on Mac 用户似乎没办法升级.

    • Linux Kernel 6.3 或更高版本
    • CMake 3.10 或更高版本
    • Clang 14 或更高版本
    • libstdc++ 11.3 或更高版本 (只要装 GCC 就可以)
    • liburing 2.3 或更高版本
    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] | 

    设计文档

    组件简介

    • task (task.hpp): task 类表示一个协程, 在被 co_await 之前不会启动.

    • thread_pool (thread_pool.hpp): thread_pool 类实现了一个线程池来调度协程.

    • file_descriptor (file_descriptor.hpp): file_descriptor 类持有一个文件描述符. file_descriptor.hpp 文件封装了一些支持 file_descriptor 类的系统调用,例如 open()pipe()splice().

    • server_socket (socket.hpp): server_socket 类扩展了 file_descriptor 类, 表示可接受客户端的监听套接字. 它提供了一个 accept() 方法, 记录是否在 io_uring 中存在现有的 multishot accept 请求,并在不存在时提交一个新的请求.

    • client_socket (socket.hpp): client_socket 类扩展了 file_descriptor 类, 表示与客户端进行通信的套接字. 它提供了一个 send() 方法, 用于向 io_uring 提交一个 send 请求, 以及一个 recv() 方法, 用于向 io_uring 提交一个 recv 请求.

    • io_uring (io_uring.hpp): io_uring 类是一个 thread_local 单例, 持有 io_urin 的提交队列与完成队列.

    • buffer_ring (buffer_ring.hpp): buffer_ring 类是一个 thread_local 单例, 向io_uring 提供一组固定大小的缓冲区. 当收到一个 HTTP 请求时, io_uringbuffer_ring 中选择一个缓冲区用于存放收到的数据. 当这组数据被处理完毕后, buffer_ring 会将缓冲区还给 io_uring, 允许缓冲区被重复使用. 缓冲区的数量与大小的常量定义于 constant.hpp, 可以根据 HTTP 服务器的预估工作负载进行调整.

    • http_server (http_server.hpp): http_server 类为 thread_pool 中的每个线程创建一个 thread_worker 任务, 并等待这些任务执行完毕. (其实这些任务是个无限循环, 根本不会执行完毕.)

    • thread_worker (http_server.hpp):thread_worker 类提供了一些可以与客户端交互的协程. 它的构造函会启动 thread_worker::accept_client()thread_worker::event_loop() 这两个协程.

    • thread_worker::event_loop() 协程在一个循环中处理 io_uring 的完成队列中的事件, 并继续运行等待该事件的协程.

    • thread_worker::accept_client() 协程在一个循环中通过调用 server_socket::accept() 来提交一个 multishot accept 请求到 io_uring. (由于 multishot accept 请求的持久性, server_socket::accept() 只有当之前的请求失效时才会提交新的请求到 io_uring.) 当新的客户端建立连接后, 它会启动 thread_worker::handle_client() 协程处理该客户端发来的 HTTP 请求.

    • thread_worker::handle_client() 协程调用 client_socket::recv() 来接收 HTTP 请求, 并且用 http_parser (http_parser.hpp) 解析 HTTP 请求. 等请求解析完毕后, 它会构造一个 http_response (http_message.hpp) 并调用 client_socket::send() 将响应发给客户端.

    // `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); } } 

    工作流程

    • http_serverthread_pool 中的每个线程创建一个 thread_worker 任务.
    • 每个 thread_worker 任务使用 SO_REUSEPORT 选项创建一个套接字来监听相同的端口, 并启动 thread_worker::accept_client()thread_worker::event_loop() 协程.
    • 当新的客户端建立连接后, thread_worker::accept_client() 协程会启动 thread_worker::handle_client() 协程来处理该客户端的 HTTP 请求.
    • thread_worker::accept_client() 或 thread_worker::handle_client() 协程等待异步 I/O 请求时, 它会暂停执行并向 io_uring 的提交队列提交请求, 然后把控制权还给 thread_worker::event_loop().
    • thread_worker::event_loop() 处理 io_uring 的完成队列中的事件. 对于每个事件, 它会识别等待该事件的协程, 并恢复其执行.

    参考文献

    5 条回复    2023-05-05 15:16:40 +08:00
    Nazz
        1
    Nazz  
       2023-05-02 09:42:42 +08:00 via Android
    很犀利
    artnowben
        2
    artnowben  
       2023-05-02 10:17:04 +08:00
    我看到一些评论说, io_uring 对大吞吐有比较好的效果,对 latency 敏感的业务肯能帮助不大,不知道楼主是否测试过 latency ?
    J1sen
        3
    J1sen  
       2023-05-04 02:43:02 +08:00
    最近也想做这个协程+io_uring 的, 搜 github 的时候也看到这个项目
    beetlerx
        4
    beetlerx  
       2023-05-04 12:01:32 +08:00
    Kernel 6.3 要求有点高啊, 好多新系统也才 5.几
    xAsiimov
        5
    xAsiimov  
    OP
       2023-05-05 15:16:40 +08:00
    @beetlerx 这是个以实验 kernel 新功能为主的项目, 所以选了最新的 kernel 版本
    关于     帮助文档     自助推广系统     博客     API &nbp;   FAQ     Solana     5818 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 28ms UTC 03:28 PVG 11:28 LAX 20:28 JFK 23:28
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86