9

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

 2 years ago
source link: https://www.v2ex.com/t/936812
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

V2EX  ›  分享创造

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

  xAsiimov · xiaoyang-sde · 3 小时 47 分钟前 · 279 次点击

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 的完成队列中的事件. 对于每个事件, 它会识别等待该事件的协程, 并恢复其执行.

</div


Recommend

  • 50

    协程 JNI 进程间通信 写在前面的话: 笔者在学习《UDP》中的网络模型之后,已经尝试使用Java语言写过阻塞IO模型、非阻塞IO模型、IO多路复用模型以及异步IO模型。每种网络模型的...

  • 58

    当前 Linux 对文件的操作有很多种方式,最古老的最基本就是 read 和 write 这样的原始接口,这样的接口简洁直观,但是真的是足够原始,效率什么自然不是第一要素,当然为了符合 POSIX 标准,我们需要它。一段...

  • 78
    • www.tuicool.com 6 years ago
    • Cache

    Linux异步IO新时代:io_uring

    Linux 5.1合入了一个新的异步IO框架和实现:io_uring,由block IO大神Jens Axboe开发。这对当前异步IO领域无疑是一个喜大普奔的消息,这意味着,Linux native aio的时代即将成为过去,io_uring的时代即将开启。 从Linux IO说起

  • 6
    • zhuanlan.zhihu.com 4 years ago
    • Cache

    有栈协程与无栈协程

    有栈协程与无栈协程李明亮等100万人斯拉夫传承者,嘴炮学家 转载自笔者的同名博客

  • 14

    本文原题“程序员应如何理解高并发中的协程”,本次收录已征得作者同意,转载请联系作者。即时通讯网收录时有改动。 本文已同步发布于52im社区: http://www.52im.net/thread-3306-1-1.html( 点击“阅读原文”进入...

  • 5

    Monoio:基于 io-uring 的高性能 Rust Runtime

  • 8
    • www.cnblogs.com 3 years ago
    • Cache

    ASP.NET Core高性能服务器HTTP.SYS

    如果我们只需要将ASP.NET CORE应用部署到Windows环境下,并且希望获得更好的性能,那么我们选择的服务器类型应该是HTTP.SYS。Windows环境下任何针对HTTP的网络监听器/服务器在性能上都无法与HTTP.SYS比肩。一、HTTP.SYS简介 二、Messa...

  • 9

    无栈协程:用户态的Linux进程调度 作者:底层技术栈 2022-12-30 07:50:05 pthread库对线程函数的定义是void* (*run)(void*),它是一个参数和返回值都是void*的函数指针:这么定义的线程函数,可以给它传递任何类型...

  • 8

    字节开源 Monoio :基于 io-uring 的高性能 Rust Runtime 本文介绍了 Rust 异步机制、Monoio 的设计概要、Runtime 对比选型和应用等。 By CloudWeGo Rust Team |

  • 2

    jasyncfio:Java中基于linux io_uring的高性能IO操作库 解道Jdon ...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK