V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
shaoyie
V2EX  ›  程序员

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

  •  
  •   shaoyie · 2023-09-12 16:32:08 +08:00 · 7115 次点击
    这是一个创建于 471 天前的主题,其中的信息可能已经有所发展或是发生改变。

    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 ,并且响应在微秒级

    目前具备功能:

    • master/worker 模式,worker 采用多线程,支持配置优雅的 Reload(像 nginx 一样),master 还是守护进程,当 worker 进程异常会马上 fork 一个新的
    • 只支持 Linux (将来也不准备跨平台)
    • 主体逻辑无锁,简单高效,可靠
    • 优雅的 acceptor/connector ,高效实现异步监听+连接

    测试声明

    • niubix 仅提供反向代理功能
    • niubix 支持 X-Real-IP, X-Forwarded-For, 其他 Header 并没有解析
    • http parser 只是简单的解析, 并没有完全实现
    • 只是初步测试, 并没有做冒烟测试和稳定性测试以及多条件下复杂测试
    • niubix 均衡策略使用的是 roundrobin(别的也还没实现呢), nginx/haproxy 也是一样的策略
    • backend 测试程序 code
    • niubix 不解析 response 内容
    • 功能逐步完善中, 基本框架是过硬的, 我相信这是一个好的开始

    接下来开发计划

    • Better HTTP parser
    • TCP protocol + Proxy Protocol
    • Https
    第 1 条附言  ·  2023-09-13 18:05:29 +08:00

    nginx的问题搞定了,wrk 加上 -H 'Connection: keep-alive'了,wrk默认不带,nginx按照 Connection: close处理了。

    重新更正一下连续测试结果

    wrk -t 2 -c 100 -d 10s -H 'Connection: keep-alive' http://10.146.0.2:8081/xxx
    Running 10s test @ http://10.146.0.2:8081/xxx
      2 threads and 100 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     1.22ms  800.49us  19.71ms   94.57%
        Req/Sec    26.67k     1.92k   29.53k    76.00%
      530996 requests in 10.01s, 77.99MB read
    Requests/sec:  53032.21
    Transfer/sec:      7.79MB
    (base) root@instance-1:~# wrk -t 2 -c 100 -d 10s -H 'Connection: keep-alive' http://10.146.0.2:8082/xxx
    Running 10s test @ http://10.146.0.2:8082/xxx
      2 threads and 100 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    10.16ms   13.47ms  93.49ms   85.88%
        Req/Sec     8.64k     7.59k   23.31k    68.50%
      172028 requests in 10.01s, 26.41MB read
    Requests/sec:  17188.44
    Transfer/sec:      2.64MB
    (base) root@instance-1:~# wrk -t 2 -c 100 -d 10s -H 'Connection: keep-alive' http://10.146.0.2:8083/xxx
    Running 10s test @ http://10.146.0.2:8083/xxx
      2 threads and 100 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     6.49ms    8.64ms 141.16ms   97.99%
        Req/Sec     8.89k     1.25k   13.48k    85.86%
      176005 requests in 10.00s, 21.82MB read
    Requests/sec:  17598.35
    Transfer/sec:      2.18MB
    
    第 2 条附言  ·  2023-09-13 20:06:04 +08:00
    总结一下啊,很开心有这么多人关注,大家怼的也很开心,正所谓淫者见淫,智者见智。

    我写这个东西的肝了好几晚,初衷就是想看一下 nginx 功能拆分后(包括扔掉历史包袱)对性能的提升能有多少,我有信心在网络架构上不会输给 nginx ,细节和专业度就不好意思提了,肯定赶不上

    庆幸,昨晚程序能跑起来后我就第一时间做了对比测试,数据很惊讶,后来我又拉 haproxy 一起测了下,确认了结论

    我没贬低 nginx haproxy ,也没吹牛逼,只是尽量尽量把数据搞得经得起推敲(开始 nginx 的问题确实没搞定),我定位的只是方向代理功能,在我的测试条件下 nginx 大部分功能都是不会触发的,只是在解析协议上有些差距(当然这只是我的浅薄之见),包括只做代理的 haproxy

    我在实现过程也做了一些优化,比如 epoll ptr 的使用,在链接成功后就尝试匹配后端,多线程下互斥的避免,这些其实在我的测试中都遇到过问题,包括 coredump ,不过都解决了

    每一种结果都能给我们提供一种可行性,都有它存在的价值

    大家多一些思考,不要盲目判断
    第 3 条附言  ·  2023-09-20 15:27:15 +08:00
    2023-09-20 更新
    1. 基于状态机的 http parser ,解析效率超过 nginx 模式 1 倍
    2. 灵敏的健康检查机制
    3. 增加 POST/DELETE/PUT/HEAD 的支持(主要是 Content-Length 的支持)
    4. Frontend Active Check
    5. 增加 Admin api web server (简易版),可以通过 http 请求动态更新配置(部分配置)
    6. 通过 Host 匹配 app (还不支持模糊匹配)
    以上,性能未减

    计划
    1. Transfer-Encoding: chunked
    2. 支持 https (有经验的兄弟可以联系我,这块我没碰过)
    3. ip hash 均衡策略
    4. 支持 Proxy protocol
    完成以上就可以发 v0.1 了
    125 条回复    2023-09-21 13:49:45 +08:00
    1  2  
    realpg
        101
    realpg  
       2023-09-13 19:12:36 +08:00
    @u20237 #100
    100W 并发的后端自从 golang 出来以后 猴子都写得出来 没啥 io 的立刻返回一个简易逻辑
    realpg
        102
    realpg  
       2023-09-13 19:13:24 +08:00
    你们这帮人宁可相信 OP 的鬼扯,都不相信我是秦始皇 V 我 50
    shaoyie
        103
    shaoyie  
    OP
       2023-09-13 19:42:19 +08:00 via iPhone
    @u20237 后端很简单 不解析协议,在我的仓库 reactor 的 example 目录中,qps 主要看机器配置
    shaoyie
        104
    shaoyie  
    OP
       2023-09-13 19:43:17 +08:00 via iPhone
    @u20237 是的,后端不能掉链子,所以我最开始贴了后端的能力测试
    shaoyie
        105
    shaoyie  
    OP
       2023-09-13 19:47:11 +08:00 via iPhone
    @realpg 我信你,你就是秦始皇最宠爱的小赵
    rrfeng
        106
    rrfeng  
       2023-09-13 19:50:14 +08:00   ❤️ 1
    @shaoyie

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

    35086854 不多也就三千五百万 QPS 还是带全量日志落盘的,你可以去吃了。
    shaoyie
        107
    shaoyie  
    OP
       2023-09-13 20:07:28 +08:00 via iPhone
    @rrfeng 牛逼 666 给你个双击
    E1n
        108
    E1n  
       2023-09-13 20:17:07 +08:00
    脑残:)
    Dart
        109
    Dart  
       2023-09-13 20:25:28 +08:00
    请问你们真的有这么大的实际业务流量吗?
    shaoyie
        110
    shaoyie  
    OP
       2023-09-13 20:40:05 +08:00 via iPhone
    那就给有这么大流量的公司提供一种选项呗
    lesismal
        111
    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
        112
    lesismal  
       2023-09-13 23:02:16 +08:00
    #111 编辑的时候窜行了,更正下

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

    更正为:

    > 这个可能不太准确:ET+ONESHOT 需要重新添加事件,这种才会需要更多 syscall ,如果都是单次读完当前数据的话,ET 和 LT 是一样的。
    shaoyie
        113
    shaoyie  
    OP
       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
        114
    shaoyie  
    OP
       2023-09-13 23:13:14 +08:00
    @lesismal 我说的读的情况多一次 syscall ,指的是 read ,不是 epoll_ctl
    lesismal
        115
    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
        116
    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
        117
    shaoyie  
    OP
       2023-09-14 00:50:14 +08:00
    好吧,每次你聊都容易扯远,扯着扯着就忘了最开始提出的疑问了,聊 niubix 的问题呢,你非要聊其他模式的应用。这里不继续讨论了,
    rrfeng
        118
    rrfeng  
       2023-09-14 10:02:04 +08:00 via Android
    虽然前面嘲讽了一波,op 态度好起来了那就正经评价一下:
    你在完全不懂 Nginx 的情况下写了个超越 Nginx 的玩意,就差不多等于「我不认识你,但你是个傻逼」。哪怕你花半天找到任何一个 Nginx 实现不合理的地方然后用自己的方式写出来。

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

    所以发出来供大家评判的东西,需要准备充分一点,否则就接受冷嘲热讽。受嘲讽恼羞成怒还要喷回去,就更小孩子气了。
    rrfeng
        119
    rrfeng  
       2023-09-14 10:03:33 +08:00 via Android
    另外 c++ 多线程,了解下 envoy ?
    shaoyie
        120
    shaoyie  
    OP
       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 这中间的性能差异,可不是你们觉得功能不完善就能跑出来的。这中间的空间留给我发挥的空间很大
    shaoyie
        121
    shaoyie  
    OP
       2023-09-14 14:01:02 +08:00
    大部分怼我的可能你们没有理解反向代理它的程序逻辑是怎么走的,数据流是流转的,只是觉得心中的神被对比了,就喷
    shaoyie
        122
    shaoyie  
    OP
       2023-09-20 15:28:45 +08:00
    2023-09-20 更新
    1. 基于状态机的 http parser ,解析效率超过 nginx 模式 1 倍
    2. 灵敏的健康检查机制
    3. 增加 POST/DELETE/PUT/HEAD 的支持(主要是 Content-Length 的支持)
    4. Frontend Active Check
    5. 增加 Admin api web server (简易版),可以通过 http 请求动态更新配置(部分配置)
    6. 通过 Host 匹配 app (还不支持模糊匹配)
    以上,性能未减

    计划
    1. Transfer-Encoding: chunked
    2. 支持 https (有经验的兄弟可以联系我,这块我没碰过)
    3. ip hash 均衡策略
    4. 支持 Proxy protocol
    完成以上就可以发 v0.1 了
    shaoyie
        123
    shaoyie  
    OP
       2023-09-20 15:34:23 +08:00
    @shaoyie 测试一下,是不是只有回复才会更新排名
    barriosdillot
        124
    barriosdillot  
       2023-09-21 11:06:45 +08:00
    @shaoyie 兄弟可以,能坚持就值得鼓励!
    shaoyie
        125
    shaoyie  
    OP
       2023-09-21 13:49:45 +08:00
    1  2  
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3338 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 11:33 · PVG 19:33 · LAX 03:33 · JFK 06:33
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.