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

go websocket rps, cpu, latency 全面测评

  •  1
     
  •   Nazz · 2023-02-18 10:34:46 +08:00 · 4034 次点击
    这是一个创建于 629 天前的主题,其中的信息可能已经有所发展或是发生改变。

    本来想测试四款websocket package的, 但是gobwas/ws数据太异常还是不放了. 今天测试的三个库分别是

    测试代码地址: github

    Env

    env

    WebSocket Protocol

    正确性高于性能, 首先测试一下WebSocket协议. 每个包基本都是用的默认配置,为了节省时间,本项测试关闭了压缩. 可以看到,尽管 gorilla/websocket 和 nhooyr/websocket 宣称通过了所有 autobahn-testsuite 测试,但可能还需要开发者额外写一些代码.

    • 命令
    docker run -it --rm \
      -v ${PWD}/config:/config \
      -v ${PWD}/reports:/reports \
      crossbario/autobahn-testsuite \
      wstest -m fuzzingclient -s /config/fuzzingclient.json
    
    • 结果
    package Pass Info Non-Strict Unclean Failed
    lxzan/gws 294 3 4 0 0
    gorilla/websocket 223 3 0 85 75
    nhooyr/websocket 173 3 0 0 125

    testsuite

    RPS

    // 1000 connections, 500 messages/second, 1000 Byte Payload
    tcpkali -c 1000 --connect-rate 500 -r 500 -T 30s -f assets/1K.txt --ws 127.0.0.1:${port}/connect
    
    • gws
    Destination: [127.0.0.1]:8000
    Interface lo address [127.0.0.1]:0
    Using interface lo to connect to [127.0.0.1]:8000
    Ramped up to 1000 connections.
    Total data sent:     12919.8 MiB (13547411965 bytes)
    Total data received: 12854.5 MiB (13478908970 bytes)
    Bandwidth per channel: 7.178⇅ Mbps (897.2 kBps)
    Aggregate bandwidth: 3594.175↓, 3612.441↑ Mbps
    Packet rate estimate: 316194.9↓, 581166.7↑ (3↓, 2↑ TCP MSS/op)
    Test duration: 30.0017 s.
    
    • gorilla
    Destination: [127.0.0.1]:8001
    Interface lo address [127.0.0.1]:0
    Using interface lo to connect to [127.0.0.1]:8001
    Ramped up to 1000 connections.
    Total data sent:     7077.0 MiB (7420776528 bytes)
    Total data received: 7089.8 MiB (7434174595 bytes)
    Bandwidth per channel: 3.961⇅ Mbps (495.1 kBps)
    Aggregate bandwidth: 1982.319↓, 1978.746↑ Mbps
    Packet rate estimate: 272613.9↓, 173441.2↑ (2↓, 12↑ TCP MSS/op)
    Test duration: 30.0019 s.
    
    • nhooyr
    Destination: [127.0.0.1]:8002
    Interface lo address [127.0.0.1]:0
    Using interface lo to connect to [127.0.0.1]:8002
    Ramped up to 1000 connections.
    Total data sent:     5103.5 MiB (5351431830 bytes)
    Total data received: 5140.6 MiB (5390317539 bytes)
    Bandwidth per channel: 2.856⇅ Mbps (357.0 kBps)
    Aggregate bandwidth: 1437.359↓, 1426.990↑ Mbps
    Packet rate estimate: 135048.1↓, 124004.1↑ (1↓, 14↑ TCP MSS/op)
    Test duration: 30.0012 s.
    

    Latency

    • 1000 connections, 100 messages/second, 1000 Byte Payload

    gorilla

    nhooyr

    gws

      PID   USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
      18305 caster    20   0  720780  38116   7332 S 248.8   1.0  24:29.55 gorilla-linux-amd64
      18325 caster    20   0  720952  52544   7180 S 161.1   1.3  15:57.80 gws-linux-amd64
      18346 caster    20   0  721460  50064   7364 R 311.3   1.3  20:49.94 nhooyr-linux-amd64
    
    • 10000 connections, 10 messages/second, 1000 Byte Payload

    gorilla

    nhooyr

    gws

      PID   USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
      19430 caster    20   0 1070196 395408   6924 S 294.0   9.9   3:44.56 gws-linux-amd64
      19618 caster    20   0  930480 267108   7268 S 313.0   6.7   9:01.10 gorilla-linux-amd64
      20939 caster    20   0 1067980 372916   7236 R 455.8   9.3  12:12.72 nhooyr-linux-amd64
    

    Final Result

    可以看到, 除了内存, 每一项都是

    gws > gorilla >> nhooyr
    
    第 1 条附言  ·  2023-02-18 16:24:19 +08:00

    追加一下gobwas/ws测试数据:

    RPS:

    Destination: [127.0.0.1]:8003
    Interface lo address [127.0.0.1]:0
    Using interface lo to connect to [127.0.0.1]:8003
    Ramped up to 1000 connections.
    Total data sent:     3364.6 MiB (3528061499 bytes)
    Total data received: 3440.3 MiB (3607388324 bytes)
    Bandwidth per channel: 1.893⇅ Mbps (236.7 kBps)
    Aggregate bandwidth: 961.961↓, 940.808↑ Mbps
    Packet rate estimate: 89305.6↓, 84530.8↑ (1↓, 18↑ TCP MSS/op)
    Test duration: 30.0003 s.
    

    Latency

    • 1000 connections

    PID  USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
    2797 caster    20   0  721068  20932   7048 S 322.6   0.5  23:07.91 gobwas-linux-amd64
    
    • 10000 connections

    PID  USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
    3845 caster    20   0  791984  90576   7096 S 426.6   2.3  20:13.87 gobwas-linux-amd64
    
    41 条回复    2023-02-26 05:17:50 +08:00
    contractswif
        1
    contractswif  
       2023-02-18 13:49:21 +08:00
    很棒,收藏了
    Nazz
        2
    Nazz  
    OP
       2023-02-18 14:08:58 +08:00 via Android
    grafana 配置上传到 assets/config 下面了
    Trim21
        3
    Trim21  
       2023-02-18 15:52:24 +08:00 via Android
    gobwas/ws 怎么个异常法…
    Nazz
        4
    Nazz  
    OP
       2023-02-18 16:00:57 +08:00 via Android
    @Trim21 RPS 只有预期的一半
    Nazz
        5
    Nazz  
    OP
       2023-02-18 16:05:17 +08:00
    @Trim21 gobwas/ws 单独跑数据是正常的, 晚点放一下数据 😂
    Nazz
        6
    Nazz  
    OP
       2023-02-18 16:33:32 +08:00
    @Trim21 数据放出来了, gobwas/ws 的 higher-level example, rps, cpu 表现都是最差的
    lesismal
        7
    lesismal  
       2023-02-18 17:31:49 +08:00   ❤️ 1
    @Trim21 @Nazz
    1m-go-websocket 和 gobwas/ws 都是错误的方案,有类似线头阻塞或者 for loop 内阻塞的问题的:io for loop 内单个 conn 阻塞(ws Upgrade 、Read 等调用都有可能当前只收到了 half-packet 而导致阻塞读的等待),这样会导致该 poller 的 for loop 内其他 conn 等待,我有在它的 repo 和 example 的 repo 里聊到相关的,但该作者似乎并不想解决问题、或者他们目前的方案(只是自定制了事件监听、读写并不是非阻塞)无法解决反而是 close issue 假装看不见来解决问题。。。
    lesismal
        8
    lesismal  
       2023-02-18 17:33:04 +08:00
    所以,没有必要测试 gobwas/ws ,它还不适合用于商业项目
    FightPig
        9
    FightPig  
       2023-02-18 17:33:55 +08:00
    gorilla/websocket 不维护了吧
    Nazz
        10
    Nazz  
    OP
       2023-02-18 17:40:27 +08:00 via Android
    @FightPig 是的
    Nazz
        11
    Nazz  
    OP
       2023-02-18 17:42:18 +08:00 via Android
    @lesismal 一开始和 nhooyr 测试的 gobwas ,rps 只有预期的一半😂
    后面单独测试,rps 这块还是垫底的
    Nazz
        12
    Nazz  
    OP
       2023-02-18 17:53:12 +08:00 via Android
    @lesismal 正确的道路太艰难,纯异步地解析应用层协议😂
    Trim21
        13
    Trim21  
       2023-02-18 17:54:54 +08:00 via Android
    @lesismal 看了你的 issue ,没理解错的话,只要少数恶意 ws 客户端就能造成 gobwas/ws 服务器拒绝服务?
    lesismal
        14
    lesismal  
       2023-02-18 18:08:50 +08:00
    @Trim21
    对。
    使用 grilla/websocket 或者其他多数基于标准库的 ws 库,如果有广播业务,也要注意不只是处理读要单独协程,处理写也要单独协程,否则单个 conn 的写可能阻塞,造成类似的 for loop 内其他 conn 等待的问题。
    基于 OP 的库也需要注意同样的问题。melody 对 gorilla/websocket 的封装是单独的写协程,代码质量还不错,可以作为参考
    lesismal
        15
    lesismal  
       2023-02-18 18:11:17 +08:00
    @Trim21
    我总结了下,使用 gobwas/ws 的项目大概需要几个“靠”:
    1. 一靠运气好(公网速度稳定)
    2. 二靠没人搞
    3. 三靠业务小

    手动狗头:dog:
    liuxu
        16
    liuxu  
       2023-02-18 18:12:00 +08:00
    gws 在 write 时非常直接,一个锁就发数据了,其他 2 个框架 write 前或多或少的用管道或锁处理了下别的状态情况,然后才一个锁发数据
    对比来看,gws 用一个全局 emitError 来处理各种错误,有点像整个项目用一个 try 处理,只返回 http code 错误码,可以说是简洁,也可以说是简陋,看个人喜好了

    gorilla 现在被 fasthttp 接着开发了: https://github.com/fasthttp/websocket
    lesismal
        17
    lesismal  
       2023-02-18 18:14:50 +08:00
    @Nazz #11 很正常,for loop 处理单个慢、其他要等,我在其他帖子里说的跨协程的各种逃逸成本、亲和性差之类的,异步部分只有在连接数达到阈值才有优势,所以我自己的库以前只支持异步的时候性能就是干不过基于标准库的,无奈加上了多种 io 模式支持后、阻塞 io 的 conn 就能干过了。
    基于 fasthttp 的 fastws 那个实现好像是个前几年的在校生搞的,性能和资源占用好像更拉跨,也可以对比下看看
    lesismal
        18
    lesismal  
       2023-02-18 18:18:53 +08:00
    @liuxu
    以前基于 fasthttp 的有个 github.com/dgrr/fastws ,好像有点拉跨。这个基于 gorilla 的改天我也看看

    > gws 在 write 时非常直接,一个锁就发数据了,其他 2 个框架 write 前或多或少的用管道或锁处理了下别的状态情况,然后才一个锁发数据

    不管是锁还是 chan ,只要不是发送队列而是直接 conn write ,就都需要注意广播业务存在 #14 的问题
    Nazz
        19
    Nazz  
    OP
       2023-02-18 18:20:25 +08:00
    @liuxu gws IO 错误最终会通过 OnError 返回给用户, 实际上 gorilla 的 IO 错误它内部也已经处理好了, Read/Write 返回的错误是用来退出循环的
    lesismal
        20
    lesismal  
       2023-02-18 18:27:19 +08:00
    @Nazz 这样似乎有个问题,就是执行 WriteMessage 的地方没有收到 err 、出错了不好处理后续流程、比如应该踢掉连接,OnError 里收到 err 却不知道 WriteMessage 的上下文
    Nazz
        21
    Nazz  
    OP
       2023-02-18 18:32:00 +08:00 via Android
    @lesismal 断开连接是自动的
    DefoliationM
        22
    DefoliationM  
       2023-02-18 18:32:21 +08:00
    前几天也搞了个 weboscket,写的比较简单,就是包装 header+data,直接写到 tcp 里,感觉又简单性能还好。
    Nazz
        23
    Nazz  
    OP
       2023-02-18 18:33:09 +08:00 via Android
    @lesismal 使用 channel 异步写会增加一倍的常驻协程,我更倾向于广播的时候开启一小批临时协程
    Nazz
        24
    Nazz  
    OP
       2023-02-18 18:42:08 +08:00 via Android
    @DefoliationM CloseCode 和错误处理比较恶心
    Nazz
        25
    Nazz  
    OP
       2023-02-18 18:48:49 +08:00 via Android
    @lesismal 以我多年 crud 的经验来看,似乎很少有人关心 write 是否返回了错误. 一般来说,在 write 之前业务逻辑都处理好了,或者开启了协程去处理错误,有错误关闭连接退出就好
    Nazz
        26
    Nazz  
    OP
       2023-02-18 18:50:13 +08:00 via Android
    @lesismal 笔误,开启了协程去处理业务逻辑.
    Nazz
        27
    Nazz  
    OP
       2023-02-18 19:19:22 +08:00 via Android
    @lesismal 标准库对于广大中小企业够用了;大厂没准会自研
    Nazz
        28
    Nazz  
    OP
       2023-02-18 19:28:56 +08:00 via Android
    @lesismal 异步 IO 适合「海量连接,少量活跃」的场景,连接太少跑 benchmark 就吃亏了😆

    我就和基于 std net 的库比一比
    Nazz
        29
    Nazz  
    OP
       2023-02-18 19:37:37 +08:00 via Android
    @lesismal async write 这块我还有个 idea ,可以维护一个全局的 WriteMessageQueue
    lesismal
        30
    lesismal  
       2023-02-18 21:45:36 +08:00
    @Nazz

    > 断开连接是自动的
    > 以我多年 crud 的经验来看,似乎很少有人关心 write 是否返回了错误. 一般来说,在 write 之前业务逻辑都处理好了,或者开启了协程去处理错误,有错误关闭连接退出就好

    不是能不能断开的问题,而是 WriteMessage 后及时性的问题。按 go 的习俗,应该是 if err != nil 就处理了,但是 WriteMessage 不返回 err ,如果这后面还有其他逻辑,就造成了后面逻辑代码的浪费,虽然整体可能不影响,但你提供 err 返回毕竟也没什么复杂度。标准库也好、其他 repo 也好大家都是这样,你这里的设计会显得不符合习惯。
    而且其实这样做,相当于是基于标准库的同步 io 、能够顺序代码的情况下,糅杂进来了异步框架的回调机制。nbio 的 OnError 这些也是回调,但那是因为 io 和逻辑协程是在不同协程,为了避免用户再占用一个协程来处理,只能回调,并且 nbio 的 WriteMessage 这些也是提供 err 返回的,所以也还算符合 go 的习惯

    > 使用 channel 异步写会增加一倍的常驻协程,我更倾向于广播的时候开启一小批临时协程
    > async write 这块我还有个 idea ,可以维护一个全局的 WriteMessageQueue

    这两种实际使用的前提都是:业务无所谓,卡了就卡了吧

    但对于工程严谨性和高实时要求的业务而言,都无法允许你说的这两种策略,因为都解决不了我说的问题,只要你是在单个循环中处理多个 conn ,就都可能存在一些健康 conn 被某些网络不佳的 conn 造成卡顿的问题。
    举个例子,RPG 游戏,地图上广播的消息非常多,如果某个玩家的连接卡了,其他人都跟着卡,这是不可接受的,否则游戏服务提供者上线用不了多久就可以解散项目了,除非项目本来也没人、本来挣不到钱。
    其他的游戏类型,比如 FPS 、Moba 类、其他 PVP 的动作类,都是同样的无法忍受这种一人卡多人的问题的。
    复杂的 APP 业务同样会存在类似的消息推送及时性需要。
    既然是做通用框架,就不要投机取巧了。协程多了硬件不够用,你还可以加机器解决,但是业务卡了公司都可能直接倒闭的,这是不行的。技术方案该硬刚的地方需要硬刚,该加机器的时候就要加机器。

    > 我就和基于 std net 的库比一比

    nbio 是支持直接使用标注库 std net 、不使用 nbio 自己的 poller io 的,这里例子代码就是基于标准库的,另外也有描述多种 io 模式的特点、可以根据自家业务特性来 cover 不同场景:
    https://github.com/lesismal/nbio/releases/tag/v1.3.5
    lesismal
        31
    lesismal  
       2023-02-18 21:52:49 +08:00   ❤️ 1
    @Nazz
    对于异步写的问题,其实 gws 只对标 gorilla/websocket 的话就可以不考虑它,因为只是提供 ws 的基础库,gorilla/websocket 和 net.TCPConn 也同样需要用户自己去封装读写。
    但是用户使用涉及广播时,应该建议用户自己注意写阻塞可能导致的问题。
    而且读写个一个协程虽然并不复杂,但细节也并不那么简单,一致性、时序性、timeout 等细节,都需要仔细处理。而且我个人就遇到过 5+以上的朋友来让我帮忙 review 他们 ws 的代码,其中有些代码的封装,其实他们是知道写阻塞可能导致的问题的,所以他们封装了单独协程+chan 写的代码,但一些细节上仍然存在问题比如导致 panic 、状态不一致、timer 泄露甚至协程泄露等。
    Nazz
        32
    Nazz  
    OP
       2023-02-18 22:05:40 +08:00   ❤️ 1
    @lesismal 下次更新把写操作的错误返回加上吧. 不打算在 gws 里面对广播场景做优化了, 如果有需要, 开发者可以自己为每个连接多开一个协程, 对于不需要处理广播的业务场景就节省了一般的协程. 确实, 用少量协程处理大量连接写入, 碰到大量慢连接的场景会很卡.
    Nazz
        33
    Nazz  
    OP
       2023-02-18 22:07:11 +08:00   ❤️ 1
    @lesismal 好的, 我去 README 里面提醒下
    lizhenda
        34
    lizhenda  
       2023-02-19 00:13:48 +08:00   ❤️ 1
    支持,自己项目还没替换 gorilla/websocket ,等有空了试试
    lizhenda
        35
    lizhenda  
       2023-02-19 00:16:44 +08:00
    gorilla/websocket 目前用的不爽的地方是如果是主动关闭链接,需要手动调用 CloseHandler ,而不是自动的
    Nazz
        36
    Nazz  
    OP
       2023-02-19 07:35:48 +08:00 via Android
    @lizhenda gws api 更清晰一些
    Nazz
        37
    Nazz  
    OP
       2023-02-19 09:09:59 +08:00
    @lizhenda gws 在连接生命周期管理这块可以说是自动挡, IO 过程中一旦出现错误就会关闭连接
    Trim21
        38
    Trim21  
       2023-02-25 19:54:34 +08:00
    @lesismal 之前的回复没提醒,又看到 v2 有 websocket 相关的帖子才想起来这个帖子...

    那个作者还写了一篇 medium 博文,他是用在 mailru 的,应该不算是业务小吧 ...
    lesismal
        39
    lesismal  
       2023-02-25 22:30:57 +08:00
    @Trim21
    不知道他有没有把 gobwas/ws 用于他们大业务量的项目。
    但 gobwas/ws 的实际问题是存在的,所以跟它用到哪里了不是直接关系,有问题就是有问题,只是可能问题影响范围他们可以接受,但并不代表别人的业务也能接受,尤其是被别人盯上随便来攻击一下的时候,影响要更大得多。
    而且你看他的 issue 列表里也确实是别人提出了生产环境遇到了问题,我在那里讨论,他都是想着 timeout 之类的方式去解决,但那并不能解决问题。所以我才说它需要“三靠”才能稳定。。。但总不能大家项目都“三靠”保平安吧。。。
    Trim21
        40
    Trim21  
       2023-02-26 03:51:45 +08:00 via Android
    @lesismal 会不会他们的 ws 服务器是运行在反向代理后面的,反向代理做了 buffer 所以没遇到这个问题…
    lesismal
        41
    lesismal  
       2023-02-26 05:17:50 +08:00
    @Trim21
    有这种可能,但也得是反代跟 ws 服务内网或者至少同区、网络稳定才行,否则跨了公网就可能更高概率卡一下。

    另一方面,既然它的目标也是解决海量并发问题,而且 ws 的协议功能没那么琐碎,自家反代加的这层节点成本也不低,还不如直接云 CDN 直接回源 ws 服务划算。
    还有就是,gobwas 只能做到低消耗,相比其他 ws 框架,它性能几乎是最差的,我估计他们的业务本身可能就比较低频,所以即使用了问题也不大。

    但按照你说的 medium 的博文搜了下,看到这句:
    "Mail polling involves about 50,000 HTTP queries per second, 60% of which return the 304 status, meaning there are no changes in the mailbox."
    原来 http 轮询的方式是 5w/s ,改为 ws 主动推新,加上一点心跳,频率要低得多,如果只是这个数量级,其实总量没多大,即使不用 poller 方案、多部署几个节点也随便搞定了

    实际情况如何就不清楚了,但通用框架依赖这些特定因素才能稳定总不是好事情,哪天整个环境某个链条改变一下,服务就不稳了

    我之前并不知道 gobwas/ws 这个库,还是之前一个人来我 repo issue 推荐我看,去扫了几眼就觉得不对,代码验证一下就确认问题了:
    https://github.com/lesismal/arpc/issues/2#issuecomment-746694287
    https://github.com/lesismal/arpc/issues/2#issuecomment-747258191
    后来 nbio 里搞了更多,百万连接也没 gobwas/ws 这问题,终于可以省心了
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1201 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 18:15 · PVG 02:15 · LAX 10:15 · JFK 13:15
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.