c++多线程实现反向代理 QPS 达到 Haproxy/Nginx 的 3 倍

2023-09-12 16:32:08 +08:00
 shaoyie

NiubiX.

项目地址

实验性项目,NiubiX 只提供反向代理功能,大家轻拍有不好的地方可以留言或提 issue/pr. 觉得好就点个 star ,我会持续完善它

与 Nginx/Haproxy 对比测试

Linux 5.19.0-1030-gcp #32~22.04.1-Ubuntu
Instacne 1 GCP cloud VM, 2 cores, 4GB RAM 10.146.0.2 (nginx,haproxy, niubix run at here)
Instacne 2 GCP cloud VM, 2 cores, 4GB RAM 10.146.0.3 (backend, wrk run at here)

nginx version config

nginx version: nginx/1.18.0 (Ubuntu)

server {
    listen       8082 reuseport;
    server_name  localhost;

    access_log  off;
    error_log off;

    location / {
        proxy_pass http://10.146.0.3:8080;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

root         516       1  0 Aug24 ?        00:00:00 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
www-data  417322     516  0 12:13 ?        00:00:06 nginx: worker process
www-data  417323     516  0 12:13 ?        00:00:08 nginx: worker process

haproxy version config

HAProxy version 2.4.22-0ubuntu0.22.04.2 2023/08/14

listen niubix
    bind 0.0.0.0:8083
    mode http
    option forwardfor
    server s1 10.146.0.3:8080

ps -eLf | grep haproxy
root      449421       1  449421  0    1 15:11 ?        00:00:00 /usr/sbin/haproxy -Ws -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -S /run/haproxy-master.sock
haproxy   449423  449421  449423  0    2 15:11 ?        00:00:05 /usr/sbin/haproxy -Ws -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -S /run/haproxy-master.sock
haproxy   449423  449421  449429  0    2 15:11 ?        00:00:05 /usr/sbin/haproxy -Ws -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -S /run/haproxy-master.sock

单独测试后端程序处理能力, 确保不存在吞吐量瓶颈

run at 10.146.0.2

wrk -t 2 -c 100 -d 10s  http://10.146.0.3:8080/xxx
Running 10s test @ http://10.146.0.3:8080/xxx
  2 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   520.95us  203.98us   4.09ms   68.03%
    Req/Sec    59.25k     2.68k   63.62k    52.50%
  1179133 requests in 10.00s, 173.17MB read
Requests/sec: 117888.45
Transfer/sec:     17.31MB

为了数据真实性,我只取了 1 次测试结果,连续对 3 个服务测试截图

对于 nginx 的数据声明一下:只有偶尔能跑到 1.7w 的 qps ,如果 proxy_pass http://10.146.0.3:8080; 换到 127.0.0.1:8080 ,qps 能到 9000 qps ,至于局域网内为什么这么低通过 strace 也没看到异常,而且 cpu 也通跑满,不知道它在干嘛

tcpdump tcp port 8080 抓包查看 niubix 实际数据,包含 X-Real-IP, XFF ,并且响应在微秒级

目前具备功能:

测试声明

接下来开发计划

8289 次点击
所在节点    程序员
125 条回复
realpg
2023-09-13 19:12:36 +08:00
@u20237 #100
100W 并发的后端自从 golang 出来以后 猴子都写得出来 没啥 io 的立刻返回一个简易逻辑
realpg
2023-09-13 19:13:24 +08:00
你们这帮人宁可相信 OP 的鬼扯,都不相信我是秦始皇 V 我 50
shaoyie
2023-09-13 19:42:19 +08:00
@u20237 后端很简单 不解析协议,在我的仓库 reactor 的 example 目录中,qps 主要看机器配置
shaoyie
2023-09-13 19:43:17 +08:00
@u20237 是的,后端不能掉链子,所以我最开始贴了后端的能力测试
shaoyie
2023-09-13 19:47:11 +08:00
@realpg 我信你,你就是秦始皇最宠爱的小赵
rrfeng
2023-09-13 19:50:14 +08:00
@shaoyie

timeout 1s yes "hello,world" > log
wc -l log

35086854 不多也就三千五百万 QPS 还是带全量日志落盘的,你可以去吃了。
shaoyie
2023-09-13 20:07:28 +08:00
@rrfeng 牛逼 666 给你个双击
E1n
2023-09-13 20:17:07 +08:00
脑残:)
Dart
2023-09-13 20:25:28 +08:00
请问你们真的有这么大的实际业务流量吗?
shaoyie
2023-09-13 20:40:05 +08:00
那就给有这么大流量的公司提供一种选项呗
lesismal
2023-09-13 22:40:38 +08:00
@shaoyie #97

> 减少 syscall 次数,而且有了这个前提 大部分情况 ET 模式反而会浪费一次 syscall

这个可能不太准确:Edo while 条件+ONESHOT 需要重新添加事件,这种才会需要更多 syscall ,如果都是单次读完当前数据的话,ET 和 LT 是一样的。

> 而在水平模式下,读出来的数量小于你的 buf 长度 就可以了,不需要再尝试一次

除非你的读 buf 长度大于 socket 设置的读缓冲区 size ,否则不管 ET 还是 LT ,读本身是没法保障单次读出来的数量小于 buf 长度的,因为有可能 socket 读缓冲区数据量大于读 buf 长度

这里的 do while 条件也不是尽量读完,例如 socket 读缓冲区有 33k 数据,buf 是 32k ,本次读到 32k ,则不满足你的 (ret == -1 && errno == EINTR) 条件。
但这也不能算 bug ,因为你默认用的是 LT ,即使本次没读完、下一轮 event loop 也会继续触发读,只是相比于单次读完,这样需要内核在下一轮 event loop 继续派发可读事件、这样未必最优。

> 庆幸,昨晚程序能跑起来后我就第一时间做了对比测试,数据很惊讶

niubix 实现的功能本身就不是完整 http 相关功能、比 nginx 、haproxy 的逻辑少很多,所以比它们快也应该是意料之中,OP 为此惊讶这件事让我感到狠惊讶!
lesismal
2023-09-13 23:02:16 +08:00
#111 编辑的时候窜行了,更正下

> 这个可能不太准确:Edo while 条件+ONESHOT 需要重新添加事件,这种才会需要更多 syscall ,如果都是单次读完当前数据的话,ET 和 LT 是一样的。

更正为:

> 这个可能不太准确:ET+ONESHOT 需要重新添加事件,这种才会需要更多 syscall ,如果都是单次读完当前数据的话,ET 和 LT 是一样的。
shaoyie
2023-09-13 23:06:19 +08:00
@lesismal
1. man 7 epoll 说的很清楚
The suggested way to use epoll as an edge-triggered (EPOLLET) interface is as follows:
a) with nonblocking file descriptors; and
b) by waiting for an event only after read(2) or write(2) return EAGAIN.
你必须要读到返回 errno=EAGAIN ,如果我是水平模式,不没必要

2. 你这属于抬杠,在多少情况是 socket 缓冲区暴满了?要考虑大部分情况的代码分支走向
1 )程序处理的慢,读不过来,堆积了
2 )业务类型就是客户端不停的 send (也得看处理的快慢)
这都不是关键问题,多路复用同步的读取 你可以把 recv buf 搞得很大,因为它是共享的,不存在浪费内存的问题。

另外,我回复中写的是 EAGAIN ,不是 EINTER 。

3. 是不完整,我惊讶的是给我的性能发挥空间还很大啊,如果只是性能超过 haproxy 20%,那我就没啥好惊讶的,等我完善 完善 可能这 20%就被抹平了
shaoyie
2023-09-13 23:13:14 +08:00
@lesismal 我说的读的情况多一次 syscall ,指的是 read ,不是 epoll_ctl
lesismal
2023-09-14 00:33:19 +08:00
> 1. man 7 epoll 说的很清楚
> The suggested way to use epoll as an edge-triggered (EPOLLET) interface is as follows:
> a) with nonblocking file descriptors; and
> b) by waiting for an event only after read(2) or write(2) return EAGAIN.
> 你必须要读到返回 errno=EAGAIN ,如果我是水平模式,不没必要

> 2. 你这属于抬杠,在多少情况是 socket 缓冲区暴满了?要考虑大部分情况的代码分支走向
> 1 )程序处理的慢,读不过来,堆积了
> 2 )业务类型就是客户端不停的 send (也得看处理的快慢)
> 这都不是关键问题,多路复用同步的读取 你可以把 recv buf 搞得很大,因为它是共享的,不存在浪费内存的问题。

你好像没看懂我在说什么,我说的是你当前这个 LT 单次 event 不读完的实现,与 ET 单次读完的区别,我没说你 bug 啊也没说你一定需要读完啊,你再看下 #111

> 你这属于抬杠,在多少情况是 socket 缓冲区暴满了?要考虑大部分情况的代码分支走向

单次读完相比于你当前的 LT 单次不一定读完,也并不多浪费什么代码,也就循环里多个 if EAGAIN 的判断。而且我也没说必须这样做,只是分析你的 LT 实现与 ET 的区别,没说你这个实现就影响性能了或是怎么样,而是说 `这样未必最优`,我可不是说你这样一定不如单次读完啊。

你这么容易觉得我在抬杠,没必要。人在觉得发现新大陆、搞了大进步的时候最容易自我陶醉、也最容易听不进去跟自己不同的观点,很正常。
你可以先让自己冷静下来再看看,或许能吸收些新东西。


另外:
> 你必须要读到返回 errno=EAGAIN

并不是必须这样的,自己想做流控的话,可以自行选择读多少、什么时候继续读,比如 golang ,有数据来了 net.TCPConn 就可读,但是你应用层没有调用 net.TCPConn.Read 也没关系啊,你什么时候想读直接能读就醒了,如果当前没数据不可读、阻塞在那等 runtime event 来了唤醒就可以了

man 手册离的是那是 `The suggested way`, 因为 ET 数据没读完、没有新数据到来是不会再触发可读的、如果用户解析处理逻辑有 bug 并且不继续读和处理就可能僵尸连接了。
但请你看清楚,那 `You must do it`,所以 `必须要读到返回 errno=EAGAIN` 这说法也是不准确的。


> 另外,我回复中写的是 EAGAIN ,不是 EINTER 。

我上面说的不是你回复其他人的 EAGAIN ,而是说你源码中的这块,我上面漏贴了代码链接:
https://github.com/shaovie/niubix/blob/main/src/io_handle.cpp#L24

> 3. 是不完整,我惊讶的是给我的性能发挥空间还很大啊,如果只是性能超过 haproxy 20%,那我就没啥好惊讶的,等我完善 完善 可能这 20%就被抹平了

那可以惊讶的事情可真是太多了,你继续加油提高性能天花板吧

> 我说的读的情况多一次 syscall ,指的是 read ,不是 epoll_ctl

读的时候,ET 怎么就可能比 LT 多一次 syscall 了呢?同样一次读事件到来,同样的 read buf 。
你是不是又搞混了什么。。
lesismal
2023-09-14 00:36:47 +08:00
#115

man 手册离的是那是 `The suggested way`, 因为 ET 数据没读完、没有新数据到来是不会再触发可读的、如果用户解析处理逻辑有 bug 并且不继续读和处理就可能僵尸连接了。
但请你看清楚,那 `You must do it`,所以 `必须要读到返回 errno=EAGAIN` 这说法也是不准确的。

->

man 手册里的那是 `The suggested way`, 因为 ET 数据没读完、没有新数据到来是不会再触发可读的、如果用户解析处理逻辑有 bug 并且不继续读和处理就可能僵尸连接了。
但请你看清楚,那不是 `You must do it`,没人强迫你必须读到 EAGAIN ,也不是只有 event loop 里才能进行读、其他地方就不能读了,所以 `必须要读到返回 errno=EAGAIN` 这说法也是不准确的。
shaoyie
2023-09-14 00:50:14 +08:00
好吧,每次你聊都容易扯远,扯着扯着就忘了最开始提出的疑问了,聊 niubix 的问题呢,你非要聊其他模式的应用。这里不继续讨论了,
rrfeng
2023-09-14 10:02:04 +08:00
虽然前面嘲讽了一波,op 态度好起来了那就正经评价一下:
你在完全不懂 Nginx 的情况下写了个超越 Nginx 的玩意,就差不多等于「我不认识你,但你是个傻逼」。哪怕你花半天找到任何一个 Nginx 实现不合理的地方然后用自己的方式写出来。

and HTTP proxy 不支持 HTTP 协议,更是贻笑大方。哪怕你只支持 HTTP 1.0 。

所以发出来供大家评判的东西,需要准备充分一点,否则就接受冷嘲热讽。受嘲讽恼羞成怒还要喷回去,就更小孩子气了。
rrfeng
2023-09-14 10:03:33 +08:00
另外 c++ 多线程,了解下 envoy ?
shaoyie
2023-09-14 13:58:48 +08:00
@rrfeng 我不认识你,但你是个傻 X

基于你好好说话,我也好好回复你,尊重都是相互的。上边这句不是送给你的,但确是你扣给我的,我从来没有贬低 nginx/haproxy ,只是用他们做对比测试而已。
谁说不支持 http 协议?不支持 wrk 怎么跑出来数据的?
我声明了,只是没有全部解析所有 http header 而已,因为反向代理也不需要解析 Accept, Expires ,Date, Etag, Last-Modified, Cache-* 等等这些部分,也不需要实现 web server 的功能
我只需要解析需要的就可以了(当然现在功能不完善,可能还要解析 cookie ,还不支持 POST Content-Length ),这就是我提到的功能拆分后带来的性能提升

envoy 确实功能很多,值得参考

我还是那句话,要辩证的看数据,1.7w ~ 5.3w 这中间的性能差异,可不是你们觉得功能不完善就能跑出来的。这中间的空间留给我发挥的空间很大

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://ex.noerr.eu.org/t/973075

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX