本来想测试四款websocket package
的, 但是gobwas/ws
数据太异常还是不放了. 今天测试的三个库分别是
测试代码地址: github
正确性高于性能, 首先测试一下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 |
// 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
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.
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.
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.
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
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
可以看到, 除了内存, 每一项都是
gws > gorilla >> nhooyr
追加一下gobwas/ws
测试数据:
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.
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
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
1
contractswif 2023-02-18 13:49:21 +08:00
很棒,收藏了
|
2
Nazz OP grafana 配置上传到 assets/config 下面了
|
3
Trim21 2023-02-18 15:52:24 +08:00 via Android
gobwas/ws 怎么个异常法…
|
6
Nazz OP @Trim21 数据放出来了, gobwas/ws 的 higher-level example, rps, cpu 表现都是最差的
|
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 假装看不见来解决问题。。。 |
8
lesismal 2023-02-18 17:33:04 +08:00
所以,没有必要测试 gobwas/ws ,它还不适合用于商业项目
|
9
FightPig 2023-02-18 17:33:55 +08:00
gorilla/websocket 不维护了吧
|
11
Nazz OP @lesismal 一开始和 nhooyr 测试的 gobwas ,rps 只有预期的一半😂
后面单独测试,rps 这块还是垫底的 |
13
Trim21 2023-02-18 17:54:54 +08:00 via Android
@lesismal 看了你的 issue ,没理解错的话,只要少数恶意 ws 客户端就能造成 gobwas/ws 服务器拒绝服务?
|
14
lesismal 2023-02-18 18:08:50 +08:00
@Trim21
对。 使用 grilla/websocket 或者其他多数基于标准库的 ws 库,如果有广播业务,也要注意不只是处理读要单独协程,处理写也要单独协程,否则单个 conn 的写可能阻塞,造成类似的 for loop 内其他 conn 等待的问题。 基于 OP 的库也需要注意同样的问题。melody 对 gorilla/websocket 的封装是单独的写协程,代码质量还不错,可以作为参考 |
15
lesismal 2023-02-18 18:11:17 +08:00
|
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 |
17
lesismal 2023-02-18 18:14:50 +08:00
@Nazz #11 很正常,for loop 处理单个慢、其他要等,我在其他帖子里说的跨协程的各种逃逸成本、亲和性差之类的,异步部分只有在连接数达到阈值才有优势,所以我自己的库以前只支持异步的时候性能就是干不过基于标准库的,无奈加上了多种 io 模式支持后、阻塞 io 的 conn 就能干过了。
基于 fasthttp 的 fastws 那个实现好像是个前几年的在校生搞的,性能和资源占用好像更拉跨,也可以对比下看看 |
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 的问题 |
19
Nazz OP @liuxu gws IO 错误最终会通过 OnError 返回给用户, 实际上 gorilla 的 IO 错误它内部也已经处理好了, Read/Write 返回的错误是用来退出循环的
|
20
lesismal 2023-02-18 18:27:19 +08:00
@Nazz 这样似乎有个问题,就是执行 WriteMessage 的地方没有收到 err 、出错了不好处理后续流程、比如应该踢掉连接,OnError 里收到 err 却不知道 WriteMessage 的上下文
|
22
DefoliationM 2023-02-18 18:32:21 +08:00
前几天也搞了个 weboscket,写的比较简单,就是包装 header+data,直接写到 tcp 里,感觉又简单性能还好。
|
23
Nazz OP @lesismal 使用 channel 异步写会增加一倍的常驻协程,我更倾向于广播的时候开启一小批临时协程
|
24
Nazz OP @DefoliationM CloseCode 和错误处理比较恶心
|
25
Nazz OP @lesismal 以我多年 crud 的经验来看,似乎很少有人关心 write 是否返回了错误. 一般来说,在 write 之前业务逻辑都处理好了,或者开启了协程去处理错误,有错误关闭连接退出就好
|
28
Nazz OP |
29
Nazz OP @lesismal async write 这块我还有个 idea ,可以维护一个全局的 WriteMessageQueue
|
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 |
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 泄露甚至协程泄露等。 |
32
Nazz OP @lesismal 下次更新把写操作的错误返回加上吧. 不打算在 gws 里面对广播场景做优化了, 如果有需要, 开发者可以自己为每个连接多开一个协程, 对于不需要处理广播的业务场景就节省了一般的协程. 确实, 用少量协程处理大量连接写入, 碰到大量慢连接的场景会很卡.
|
34
lizhenda 2023-02-19 00:13:48 +08:00 1
支持,自己项目还没替换 gorilla/websocket ,等有空了试试
|
35
lizhenda 2023-02-19 00:16:44 +08:00
gorilla/websocket 目前用的不爽的地方是如果是主动关闭链接,需要手动调用 CloseHandler ,而不是自动的
|
38
Trim21 2023-02-25 19:54:34 +08:00
@lesismal 之前的回复没提醒,又看到 v2 有 websocket 相关的帖子才想起来这个帖子...
那个作者还写了一篇 medium 博文,他是用在 mailru 的,应该不算是业务小吧 ... |
39
lesismal 2023-02-25 22:30:57 +08:00
@Trim21
不知道他有没有把 gobwas/ws 用于他们大业务量的项目。 但 gobwas/ws 的实际问题是存在的,所以跟它用到哪里了不是直接关系,有问题就是有问题,只是可能问题影响范围他们可以接受,但并不代表别人的业务也能接受,尤其是被别人盯上随便来攻击一下的时候,影响要更大得多。 而且你看他的 issue 列表里也确实是别人提出了生产环境遇到了问题,我在那里讨论,他都是想着 timeout 之类的方式去解决,但那并不能解决问题。所以我才说它需要“三靠”才能稳定。。。但总不能大家项目都“三靠”保平安吧。。。 |
40
Trim21 2023-02-26 03:51:45 +08:00 via Android
@lesismal 会不会他们的 ws 服务器是运行在反向代理后面的,反向代理做了 buffer 所以没遇到这个问题…
|
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 这问题,终于可以省心了 |