V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
lesismal
V2EX  ›  程序员

4C-2G 来战 [ Golang Websocket 百万连接测试 ]

  •  
  •   lesismal ·
    lesismal · 320 天前 · 2624 次点击
    这是一个创建于 320 天前的主题,其中的信息可能已经有所发展或是发生改变。

    4C-2G 来战 [ Golang Websocket 百万连接测试 ]

    代码代码: https://github.com/lesismal/go-websocket-benchmark

    使用框架: https://github.com/lesismal/nbio

    总结:4cpu 核心,2G 内存,100 万连接,1k 数据载荷,500 万次 Echo 测试,TPS 10+万,详情请继续往下看。

    看到隔壁帖子一些人上来就说这没用那没用 https://www.v2ex.com/t/945616

    想起之前自己帖子也是类似: https://www.v2ex.com/t/755862

    之前几次也是被好些人说 nbio 这没用那没用,所以有感而发。

    PS:期待 @byte10 大神百忙之中能再来与我讨论技术。

    前置声明:

    1. 绝大多数人不需要百万级连接场景的优化,但确实有公司有项目有人需要,搞这些东西就是为了满足这些需要;
    2. 标题 4C-2G 只是作为一个参考指标,用不是特别高的配置更能体现框架的承载力。采用这个配置并不是鼓励实际场景就要用这么低的配置去处理百万连接,实际场景应从实际出发;
    3. 否定别人只需要动动嘴,但技术是实在的,如果也有兄弟姐妹想说 nbio 之类的 poller 没用,请确认自己真正了解相关知识,然后带上实际的论据观点再来讨论,如果实在想这么讲,也请先看下旧帖。

    以前很多次遇到很多人先入为主地以为异步框架就是要写回调、golang 框架也如此。 为了避免误解,这里也再对 nbio 的同步异步做下简要说明: nbio 底层非阻塞、异步 io ,但使用逻辑协程池处理 http 请求、websocket 消息,由于 golang 协程不像进程线程成本那么高,所以逻辑协程池 size 比 c/cpp 或者其他语言的逻辑线程数量大得多,所以用户仍然可以写同步逻辑,实际上也是这样处理的。

    为了避免既当裁判又当运动员、甚至误导用户,每当有人问我性能时,我通常是建议用户以自己实测得到的性能数据为准,而不是直接相信测试库作者提供的数据。所以建议有兴趣的兄弟姐妹在自己环境进行测试。

    如果测试库代码有误,欢迎 Issue/PR 来指正更正。

    在这里也邀请并欢迎大家来跑下多个 go websocket 框架的测试,并留言到这里供参考: https://github.com/lesismal/go-websocket-benchmark/issues/11

    另外:除了 nbio 以外的其他 go websocket 框架多数主要是基于 golang 标准库、每个连接一个协程,这种普通配置的硬件上无法跑到海量连接,所以百万连接测试的脚本默认只针对 nbio 自己,如果想测试更多参数,请自行修改脚本。 gev 支持百万但不支持 TLS ,gobwas+netpoll 有 for loop 阻塞问题,所以目前没有添加它们,以后可能会添加。

    下面是我的 ubuntu vm 上跑的数据,仅供参考

    环境:

    --------------------------------------------------------------
    os:
    
    Ubuntu 20.04.6 LTS \n \l
    
    --------------------------------------------------------------
    cpu model:
    
    model name	: AMD Ryzen 7 5800H with Radeon Graphics
    --------------------------------------------------------------
                  total        used        free      shared  buff/cache   available
    Mem:       16362568      396988    15151676        1636      813904    15656380
    Swap:             0           0           0
    --------------------------------------------------------------
    # taskset 0-3, nbio server 只占 4 cpu 核心
    run nbio_nonblocking server on cpu 0-3
    --------------------------------------------------------------
    

    压测结果:

    --------------------------------------------------------------
    BenchType  : Connections
    Framework  : nbio_nonblocking
    TPS        : 26545    # 每秒建立连接数
    Min        : 20ns     # 建立单个连接最小耗时
    Avg        : 74.80ms  # 建立单个连接平均耗时
    Max        : 37.67s   # 建立单个连接最大耗时(实际压测并发度大,有一些容易失败,目前测试逻辑会重试、多次重试时间导致最大值时间较长)
    TP50       : 30ns     # 前 50%次建立连接最大耗时
    TP75       : 30ns     # 前 75%次建立连接最大耗时
    TP90       : 30ns     # 前 90%次建立连接最大耗时
    TP95       : 30ns     # 前 95%次建立连接最大耗时
    TP99       : 31ns     # 前 99%次建立连接最大耗时
    Used       : 37.67s   # 总耗时
    Total      : 1000000  # 建立连接数
    Success    : 1000000  # 成功建立连接数
    Failed     : 0        # 建立连接成功数(实际压测并发度大,有一些容易失败,目前测试逻辑会重试、多次重试都失败才算失败)
    Concurrency: 2000     # 并发度( 2000 个协程,每个协程循环建立连接)
    --------------------------------------------------------------
    BenchType  : BenchEcho
    Framework  : nbio_nonblocking
    TPS        : 113789     # 每秒 Echo 次数
    Min        : 182.56us   # 单次 Echo 最小耗时
    Avg        : 435.80ms   # 单次 Echo 平均耗时
    Max        : 1.69s      # 单次 Echo 最大耗时
    TP50       : 407.61ms   # 前 50%次 Echo 最大耗时
    TP75       : 554.56ms   # 前 75%次 Echo 最大耗时
    TP90       : 698.06ms   # 前 90%次 Echo 最大耗时
    TP95       : 800.52ms   # 前 95%次 Echo 最大耗时
    TP99       : 1.07s      # 前 99%次 Echo 最大耗时
    Used       : 43.94s     # 总耗时
    Total      : 5000000    # 测试 Echo 次数
    Success    : 5000000    # 测试 Echo 的成功次数
    Failed     : 0          # 测试 Echo 的失败次数
    Conns      : 1000000    # 测试的连接数
    Concurrency: 50000      # 并发度( 5w 个协程,每个协程循环取当前可用的连接进行 Echo )
    Payload    : 1024       # websocket body size
    CPU Min    : 95.96%     # CPU 最小值(采集开始时较小)
    CPU Avg    : 347.80%    # CPU 平均值
    CPU Max    : 380.94%    # CPU 最大值
    MEM Min    : 1.82G      # MEM 最小值( Benchmark 开始前有进行 Warmup ,所以起始内存最低值已经较大)
    MEM Avg    : 1.92G      # MEM 平均值
    MEM Max    : 1.94G      # MEM 最大值
    ---------------------------------------------------------------------------------------------------
    
    34 条回复    2023-12-12 12:26:04 +08:00
    byte10
        1
    byte10  
       320 天前   ❤️ 1
    嗯,我觉得当初错了,我觉得你做的这个应该是有应用场景的,适合做一些中间件。

    但还是保持一些观点,如果放在业务层去使用,大部分开发人员还是会用 协程去写同步代码,那么就跟你省内存的初衷违背了。
    lesismal
        2
    lesismal  
    OP
       320 天前
    @byte10 之前好多次没回复我,还以为你把我 block 了 :joy: ,感谢回复!

    > 但还是保持一些观点,如果放在业务层去使用,大部分开发人员还是会用 协程去写同步代码,那么就跟你省内存的初衷违背了。

    其实你还是误解了,我解释过好多次了,框架本身是提供了逻辑协程池的,用户仍然是同步代码,比如 http handler ,这个跟使用标准库写同步代码没什么区别:
    https://github.com/lesismal/nbio-examples/blob/master/http/server/server.go#L19

    nbio 里,每个 http 请求到来时取一个协程处理,这个请求处理完了这个协程可以继续服务其他请求,并不是每个连接固定、持续占用一个协程。

    c/cpp 那些框架,线程太贵,所以线程池数量少,很多框架是逻辑单线程,所以需要各种回调。
    但 golang 协程不一样,协程便宜,百万链接那是协程数量太大了,但是 1-10w 协程压力不大,所以逻辑协程池 size 弄个几千几万个协程是可以的。而且 golang 其他的 io ,比如到数据库,也是有连接池限制的。即使 100w 个逻辑协程也是可能被数据库连接池卡着等待,所以太大逻辑协程数量也意义不大,反倒是几千几万这种协程池数量,已经足够动态均衡了。

    总结下就是,逻辑协程数量多但是可配置、不好过硬件能力:
    1. 如果是快业务,每次请求处理很快、协程能很快释放给其他请求去复用
    2. 如果是慢业务,逻辑协程再多也是要被阻塞,但逻辑协程数量通常远多余下游(比如数据库)的限制,所以仍有足够的空闲协程处理其他请求


    所以对于通用需求,根本不存在你先入为主地以为的那种用了 nbio 就要写回调的问题。

    而有一些需求,即使是用标准库,也可能是需要写回调的。特殊问题,特殊处理就可以了。

    比较均衡的并发模式是:
    1. 纵向的不同分层上(比如网络库、框架、业务层),各层限制好自己的资源使用,比如协程池、buffer pool
    2. 横向的不同模块上(比如 A 功能 B 功能 C 功能),各模块限制好自己的资源使用,比如协程池、buffer pool


    架构是灵活的,人也应该灵活,欢迎来试试 nbio
    buffzty
        3
    buffzty  
       320 天前   ❤️ 1
    用了好久了 没啥问题
    lesismal
        4
    lesismal  
    OP
       320 天前
    @byte10
    但如果想基于 poller 4 层 TCP 去自己实现 7 层框架比如 HTTP/Websocket ,那没什么好办法了,还是需要异步。但 nbio 提供的 http/websocket 并不需要用户全去写异步!不需要!不需要!不需要!我都给你说了好多次了而且这个帖子里也写了 “以前很多次遇到很多人先入为主地以为异步框架就是要写回调、golang 框架也如此。 为了避免误解”

    你先看明白了,别再回复我说要异步了!
    我快成复读机了快被你们逼疯了!!!
    😇😇😇😇😇😇😇😇
    lesismal
        5
    lesismal  
    OP
       320 天前
    @buffzty 66666
    byte10
        6
    byte10  
       320 天前
    @lesismal 嗯好的,之前也有点固执😂,我多了解下。
    lesismal
        7
    lesismal  
    OP
       320 天前   ❤️ 1
    @byte10
    嗯嗯,欢迎多来交流,java 只是社区积累的框架多,但性能相关的实在是太不友好了

    平时少把 java 搞,内存杀手不环保
    重心多往 golang 转,护发节能走得远
    monkeyWie
        8
    monkeyWie  
       320 天前   ❤️ 1
    好东西,兼容了标准库确实不错
    wslzy007
        9
    wslzy007  
       320 天前
    唉...C1M 问题很久就有各种方案了(几乎是 10 年前),似乎重要的不是同时多少连接在线(一般只和内存大小相关),而是保持大链接下的高 QPS (调度能力),个人经验 4c 配置 QPS 很容易到达 30w 左右的(小报文)
    blankmiss
        10
    blankmiss  
       320 天前
    用 rust 不更节能 为什么要特别去在意内存
    lesismal
        11
    lesismal  
    OP
       320 天前
    > 唉...C1M 问题很久就有各种方案了(几乎是 10 年前),似乎重要的不是同时多少连接在线(一般只和内存大小相关),而是保持大链接下的高 QPS (调度能力),个人经验 4c 配置 QPS 很容易到达 30w 左右的(小报文)

    @wslzy007

    epoll 异步非阻塞早就解决 C1M 了,这没错,c/cpp 里也早就不是问题了。

    但这个帖子说的不是 c/cpp/rust 或者 java netty 或者其他脚本语言那些基于 c/cpp 这些底层,而是 go ,搞 go 的 poller 目的是解决 go 标准库方案每个连接一个协程导致的内存爆炸 OOM 、GC 负担过重 STW 的问题,主要是针对 go 自己,而不是说用 go 解决了其他语言解决不了的 C1M 问题。
    lesismal
        12
    lesismal  
    OP
       319 天前
    @blankmiss 欢迎来杠。

    我想问一下,那 c/cpp 也都性能杠杠的,为啥还搞 rust ?
    rust 解决 c/cpp 的安全问题

    为啥 go 成为并发、云各种场景下的宠儿?因为它除了性能还不错,更重要的是开发效率高。

    java 开发效率高,但是它对于性能场景和硬件消耗实在太不堪入目了,所以 go 替换 java 可以实现节能环保,并且开发效率也高。
    别给我杠说什么 java 开发功能快,java 发展了多少年积累了多少轮子?而且很多说 go 开发效率低的真的熟悉 go 吗?而且时间久了 go 的轮子也越来越多越来越完善。如果为了当下谁轮子最多就用谁,那性能、占用之类的各种问题永远没法优化,甚至 java 社区自己都不需要进化、因为现在已经能做功能只是消耗高一些罢了。

    你用 rust 更节能环保,你有 go 的这种开发效率吗?再说句不好听的,100 个人学 rust ,有几个能短时间玩熟练的?
    c/cpp 三年不出门,go 三天 curd ,你 rust 学多久能快速开发?
    blankmiss
        13
    blankmiss  
       319 天前
    @lesismal 我觉得 golang 的 if nil 不行,至于 golang 不是 yaml 开发工程师吗。而且我不想在语言上去说什么优劣,谁爱用什么就用什么。(我只是想说 可能现在硬件条件已经很好,动态扩容,不是非常需要说对内存扣扣省省)当然我是个菜比 可能了解的没你深入
    lesismal
        14
    lesismal  
    OP
       319 天前
    @blankmiss

    帖子前置条件里我也说了,绝大多数人没这个扣内存的需求,但是确实也有少量比例的团队需要去扣这里,因为这部分人面临的在线量很大,扣这个能省的成本、对服务稳定性的提高也是很客观的

    早几年 uber 就对海量并发各种 手动 gc 优化,甚至去改 go 源码。但其实如果用 nbio ,可能比他们的这些方式更有效一些

    > 可能现在硬件条件已经很好,动态扩容,不是非常需要说对内存扣扣省省

    动态扩容确实能解决一些问题,但成本也是不小,我知道的一些团队的一些服务,用的 java ,上百个云节点,而且这种公司还不是头部企业的业务量级。如果改成 nbio 这种,往多了说能省 95% 的成本,往少了说,也能省 80%

    nbio 关注的用户里,估计大概三分之一是外国人吧,早期就有外国人是来用 nbio 处理高在线量的业务的
    ruanimal
        15
    ruanimal  
       319 天前
    好奇什么业务会用到百万链接,看测试 echo 都 500ms 以上了
    lysS
        16
    lysS  
       319 天前
    话说怎么测的? 100w 连接端口不够吧?而且 loopback 肯定有优化的
    lysS
        17
    lysS  
       319 天前
    @ruanimal 这么大的连接,几乎做不了重的任务,一般都是简单的 ping-pong ;比如说广告计数、上线通知之类的
    lesismal
        18
    lesismal  
    OP
       319 天前
    > 好奇什么业务会用到百万链接,看测试 echo 都 500ms 以上了
    > 这么大的连接,几乎做不了重的任务,一般都是简单的 ping-pong ;比如说广告计数、上线通知之类的

    低配硬件:高在线、低频的功能,比如推送业务
    高配硬件:高在线、高频的也可以做。主帖已经说明过了,测试用的 4C-2G "只是作为一个参考指标,用不是特别高的配置更能体现框架的承载力",实际业务可以根据业务需要来部署硬件规格


    > 话说怎么测的? 100w 连接端口不够吧?而且 loopback 肯定有优化的

    套接口对四元组包括:server ip, server port, client ip, client port ; port 是 short int 2 字节 64k ,加上系统 port range 配置项这些限制,这四元组在 server ip 、client ip 各只有 1 个的情况下,每个 server port 可以建立小于 64k 个连接,N 个 server port 可以建立 64k*N 个连接。测试为了省去虚拟网络或者 docker 方式的麻烦、使用了 50 个端口:
    https://github.com/lesismal/go-websocket-benchmark/blob/main/config/config.go#L27
    lesismal
        19
    lesismal  
    OP
       319 天前
    @ruanimal @lysS #18 回复忘记 at 了,请查看 #18
    lesismal
        20
    lesismal  
    OP
       319 天前
    qbmiller
        21
    qbmiller  
       308 天前
    nbio VS gnet
    lesismal
        22
    lesismal  
    OP
       308 天前
    > nbio VS gnet

    @qbmiller

    对于七层协议:gnet 不支持 TLS/HTTP/Websocket ,gnet examples 对这些 7 层协议相关的示例似乎并不是完整功能,离实用还差很多,gnet 的 README 里所说的 HTTP 性能并不是真正的完整功能 HTTP 框架,当初那个只是简单地解析 HTTP 换行符之类的这种,所以用于跟别人完整功能的 HTTP 框架对比性能本身就是不合理的。有给 gnet 作者聊过这个问题,他解释了原因:
    https://github.com/panjf2000/gnet/issues/182
    好像有其他人在 gnet 之上封装过 TLS ,但易用性相比于 nbio 应该还差很多吧,结合实际场景的应用不知道效果如何。

    对于四层:我也有做测试: https://github.com/lesismal/go-net-benchmark/issues/1
    nbio 的 used by 列表里有 gnet-io/gnet-benchmarks 但现在的 gnet-io/gnet-benchmarks 好像是没有包含 nbio ,可能是测过之后删掉了吧:
    https://github.com/lesismal/nbio/network/dependents?dependents_after=MjQ3MjQyNjExMzI

    根据我自己压测的情况是,单就四层而言,nbio 和 gnet 性能差不多,跑多轮测试有时候 nbio 高一点有时候 gnet 高一点,我的环境里跑 10 轮,可能 nbio 有 7 、8 次会略高一些。

    因为遇到过多次如下情况:benchmark 库官方提供的压测数据与我自己实际跑他们官方测试代码的数据差异较大,也包括一些类似 gnet HTTP 性能这种压测数据不符合实际场景的情况。
    所以我通常不建议直接以压测仓库作者提供的压测数据作为参考依据,包括我自己写的压测代码。所以我建议有兴趣的兄弟姐妹还是自己跑下实际代码试试看,并且也可以根据实际测试参数、尽量把各个框架的参数、配置对齐来公平对比而不是被作者自己号称的性能忽悠。
    qbmiller
        23
    qbmiller  
       300 天前   ❤️ 1
    @lesismal 感谢大佬解答
    joyanhui
        24
    joyanhui  
       159 天前 via Android
    @lesismal 看到晚了,我早先上了 gnet 的车
    lesismal
        25
    lesismal  
    OP
       158 天前
    @joyanhui 随时欢迎换乘~
    shockerli
        26
    shockerli  
       133 天前   ❤️ 1
    不错,OP 加油
    xbchaonba
        27
    xbchaonba  
       132 天前
    这个 js websocket 怎么发 ping , 我连上后 120 秒总是断开一直有发消息也断
    lesismal
        28
    lesismal  
    OP
       132 天前
    @xbchaonba #27

    为了避免僵尸连接耗尽 server 资源,尤其是进电梯导致信号终断、移动信号切换信号塔、设备掉电等情况时,收不到 TCP 协议的 FIN ,连接一直无法退出。TCP 设置 keepalive 是检测四层链路健康,可以避免这种情况,但它并不能解决七层僵尸连接、慢连接的问题,因为四层链路可能一直是健康的,但这个连接什么数据都不发或者只发很少数据、仍然会耗尽 server 资源,所以七层的 keepalive 是需要的,有了七层、再做四层的 TCP 设置 keepalive 就画蛇添足了、没必要。既然应该做七层,所以就应该都设置读超时比较好,所以与其他很多框架默认不设置读超时不太一样,nbio 默认设置。

    一直有发数据仍然断开是因为 Upgrade 过程中默认设置了 120s 读超时,但后续收到消息时没有自动更新读超时。
    当前版本,server 端设置 upgrader.KeepaliveTime = YourKeepaliveDuration 即可自动更新读超时,设置 upgrader.KeepaliveTime = YourKeepaliveDuration 后,不只是 PING 、其他类型的 message 也都可以,因为是读到消息就更新超时。僵尸连接检测也不是必须要 PING 协议才行、有效的协议即可。
    我考虑下,要不要把这个心跳间隔也做成默认配置、自动更新读超时。

    如果不想设置读超时,可以在 Upgrade 后 SetReadDeadline(time.Time{}) 清空超时关闭。
    lesismal
        29
    lesismal  
    OP
       132 天前   ❤️ 1
    @xbchaonba #27

    dev 修改了下,默认会开启读超时的更新,可以试下:
    go get -u github.com/lesismal/nbio@a81e8e2

    过阵子再发到新版
    xbchaonba
        30
    xbchaonba  
       132 天前
    @lesismal conn.SetReadDeadline(time.Now().Add(time.Second / 120)) 是要用这个方法更新吧,昨天试了好多次 u.KeepaliveTime = time.Second / 120 直接这样重新赋值没有用
    xbchaonba
        31
    xbchaonba  
       132 天前
    @lesismal #29 好的感谢你的解答
    lesismal
        32
    lesismal  
    OP
       130 天前
    @xbchaonba #30

    > conn.SetReadDeadline(time.Now().Add(time.Second / 120)) 是要用这个方法更新吧

    是用这个方法。
    但你这个是 120 分之一秒,设置完后很快就过期了,如果没有立刻收到新消息,就 close 了

    > 昨天试了好多次 u.KeepaliveTime = time.Second / 120 直接这样重新赋值没有用

    同样的问题,估计也是因为 120 分之一秒太快了
    xbchaonba
        33
    xbchaonba  
       130 天前
    @lesismal 32 哈哈,* 被我打成/ 了,后面发现改过来了但是评论删除不了; u.KeepaliveTime 我刚开始把它弄到 onmassage 里面去了;现在看明白了感谢解答
    lesismal
        34
    lesismal  
    OP
       130 天前
    @xbchaonba #33 好嘞!欢迎使用,如果有疑问、bug ,也欢迎来 issue 、PR
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   2944 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 08:49 · PVG 16:49 · LAX 01:49 · JFK 04:49
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.