Featured image of post 使用C++协程+liburing的HTTP server 和 epoll 的 HTTP server 性能对比

Uring和epoll性能对比

使用C++协程+liburing实现了一个简单的 HTTP server,并与epoll版本进行了性能对比。测试结果显示,liburing在高并发场景下表现出更优的性能,尤其是在处理大文件时,QPS提升显著。

前段时间,使用C++协程+liburing实现了一个简单的 echo server,文章地址

都说使用 uring 可以获得更好的性能,但是究竟能提升多少呢?接下来,我们来对比一下 liburing 和 epoll 的性能。

测试方式

在原来的 echo server 基础上改动了一下,把 echo server 改成了一个简单的 HTTP server,返回一个固定的 HTML 内容。

然后使用 go-stress-testing 这个工具进行压力测试。

测试工具在windows 10 上。HTTP 服务运行这个windows 10 的 VM虚拟机上。系统是 debian13,linux内核是 6.12.41-amd64

编译器使用 clang-18 编译参数 -O2 stdlib=libc++ -std=c++23 进行编译。

uring 服务和 epoll 服务都是单线程模型,单线程处理所有连接。

测试代码

这里就不贴所有代码了,主要贴一下关键部分。

uring 版本

 1Task<false> IoUring::startSession(int fd, uint64_t connId) {
 2  std::string buffer;
 3  buffer.resize(1024);
 4  bool closed = false;
 5  while (!closed) {
 6    auto aRead = AwaitableRead(this, fd, buffer);
 7    while (true) {
 8      int res = co_await aRead;
 9      if (res <= 0) { // 连接关闭或者读取错误
10        closed = true;
11        break;
12      }
13      if (res == 1) { // 读完了
14        break;
15      }
16    }
17    if (closed) {
18      break;
19    }
20
21    // std::cout << "Received data: ";
22
23    std::string response;
24    std::string body =
25        "<!DOCTYPE html><html><head><title>Test Page</title></head><body>"
26        "<p>Hello, this is a test HTML content for HTTP response.</p>"
27        "</body></html>";
28    response.reserve(128 + body.size()); // 减少拷贝
29
30    response += "HTTP/1.1 200 OK\r\n"
31                "Content-Length: " +
32                std::to_string(body.size()) +
33                "\r\n"
34                "Content-Type: text/html; charset=UTF-8\r\n"
35                "Connection: close\r\n"
36                "Date: Mon, 21 Oct 2024 13:24:24 GMT\r\n\r\n";
37    response += body;
38
39    auto aWrite = AwaitableWrite(this, fd, std::move(std::string(response)));
40    while (true) {
41      int res = co_await aWrite;
42      if (res < 0) { // 写出错了
43        closed = true;
44        break;
45      }
46      if (res == 0) { // 写完了
47        break;
48      }
49    }
50  }
51  close(fd);
52  co_return;
53}

在原来的 echo server 基础上,改成了 HTTP server。然后使用协程处理每个连接。

epoll 版本

 1void Epoll::run()
 2{
 3    std::cout << "Epoll::run" << std::endl;
 4    epoll_event events[EVENT_SIZE];
 5    while (true)
 6    {
 7        int nfds = epoll_wait(ePollFd_, events, EVENT_SIZE, -1);
 8        if (nfds < 0)
 9        {
10            std::cerr << "epoll_wait failed nfds:" << nfds << "errno:" << errno << std::endl;
11            break;
12        }
13        if (errno == EINTR)
14        {
15            // 系统调用被中断,继续重试
16            continue;
17        }
18        for (int i = 0; i < nfds; ++i)
19        {
20            if (events[i].events & EPOLLERR || events[i].events & EPOLLHUP)
21            {
22                std::cout << "epoll_wait failed errno:" << errno << std::endl;
23                delFd(events[i].data.fd);
24                close(events[i].data.fd);
25                continue;
26            }
27            if (events[i].events & EPOLLIN)
28            {
29                if (events[i].data.fd == listenFd_)
30                {
31                    sockaddr_in cliAddr{};
32                    socklen_t length = sizeof(cliAddr);
33                    int clientFd = accept4(listenFd_, reinterpret_cast<sockaddr*>(&cliAddr), &length,
34                                           0);
35                    if (clientFd < 0)
36                    {
37                        std::cerr << "accept failed fd:" << clientFd << "errno:" << errno << std::endl;
38                        continue;
39                    }
40                    std::cout << "new client fd" << clientFd << std::endl;
41                    addRead(clientFd);
42                }
43                else
44                {
45                    char buff[1024];
46                    int n = ::read(events[i].data.fd, buff, sizeof(buff));
47                    if (n <= 0)
48                    {
49                        std::cout << "close fd:" << events[i].data.fd << std::endl;
50                        delFd(events[i].data.fd);
51                        close(events[i].data.fd);
52                        continue;
53                    }
54                    std::string response;
55                    response.reserve(128 + image.size()); // 减少拷贝
56                    response += "HTTP/1.1 200 OK\r\n"
57                        "Content-Length: " + std::to_string(image.size()) + "\r\n"
58                        "Content-Type: image/jpeg\r\n"
59                        "Connection: close\r\n"
60                        "Date: Mon, 21 Oct 2024 13:24:24 GMT\r\n\r\n";
61                    response += image;
62                    write(events[i].data.fd, response.data(), response.size());
63                }
64            }
65        }
66    }
67}

epoll 版本没有使用协程,直接在事件循环中处理所有连接。两个HTTP server 处理逻辑是一样的,都是读取请求,然后返回一个固定的 HTML 内容。

测试结果

这个HTTP server 返回一个简单的 HTML 内容:

1<!DOCTYPE html><html><head><title>Test Page</title></head><body>
2    <p>Hello, this is a test HTML content for HTTP response.</p>
3</body></html>

使用 go-stress-testing 进行压力测试,测试命令如下:

开启 100 个并发连接,每个连接发送 100 次请求。

1go-stress-testing-win.exe -u "http://192.168.1.19:8088" -c 100 -n 100

uring 版本测试结果

可以看到 QPS 达到了 4800+,平均响应时间 20ms。

epoll 版本测试结果

可以看到 QPS 只有 4000+,平均响应时间 23ms。

uring 版本的性能比 epoll 版本提升了 20% 左右。

测试2

上面的测试中,HTML返回了一个简单的网页,现在改成返回一个148KB 的图片,为了防止网络成为瓶颈,这次并发数改成 50,连接数改成 50。

在进程启动的时候,把图片读到内存中,避免每次都读磁盘,然后每次请求都返回这个图片。

这个改动比较小,只一下 response 的内容:

 1    response.reserve(128 + image.size()); // 减少拷贝
 2
 3    response += "HTTP/1.1 200 OK\r\n"
 4                "Content-Length: " +
 5                std::to_string(image.size()) +
 6                "\r\n"
 7                "Content-Type: image/jpeg\r\n"
 8                "Connection: close\r\n"
 9                "Date: Mon, 21 Oct 2024 13:24:24 GMT\r\n\r\n";
10    response += image;

使用 go-stress-testing 进行压力测试,测试命令如下:

1go-stress-testing-win.exe -u "http://192.168.1.19:8088" -c 50 -n 50

开启 50 个并发连接,每个连接发送 50 次请求。

uring 版本测试结果

可以看到 QPS 达到了 1300+,平均响应时间 37ms。

epoll 版本测试结果

可以看到 QPS 只有 520+,平均响应时间 93ms。

可以看到,uring 版本的性能是 epoll 版本的 2.5 倍左右。这个提升还是非常明显的。

总结

通过上面的测试,可以看到 liburing 相比 epoll 在高并发场景下,不管是 QPS 还是 响应耗时,性能提升还是比较明显的。

具体来说,uring 版本在处理小文件时的 QPS 比 epoll 版本高出 20% 左右,而在处理大文件时的 QPS 则高出 2.5 倍左右。这些测试结果表明,liburing 在高并发场景下具有更好的性能表现,尤其是在读取和写入大文件时,优势更加明显。这也是因为 liburing 能够更高效地利用内核的异步 I/O 能力,减少了系统调用的开销,从而提升了整体的吞吐量和响应速度。

发表了65篇文章 · 总计159.57k字
本博客已稳定运行
© QX
使用 Hugo 构建
主题 StackJimmy 设计