前段时间,使用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 能力,减少了系统调用的开销,从而提升了整体的吞吐量和响应速度。