高性能 websocket 库 quickws 发布 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
guonaihong
V2EX    Go 编程语言

高性能 websocket 库 quickws 发布

  •  
  •   guonaihong
    guonaihong 2023-08-29 13:11:13 +08:00 2370 次点击
    这是一个创建于 772 天前的主题,其中的信息可能已经有所发展或是发生改变。

    quickws 是一个高性能的 websocket 库

    Go codecov Go Report Card

    地址

    https://github.com/antlabs/quickws

    example

    服务端

     package main import ( "fmt" "net/http" "time" "github.com/antlabs/quickws" ) type echoHandler struct{} func (e *echoHandler) OnOpen(c *quickws.Conn) { fmt.Println("OnOpen:", c) } func (e *echoHandler) OnMessage(c *quickws.Conn, op quickws.Opcode, msg []byte) { fmt.Println("OnMessage:", c, msg, op) if err := c.WriteTimeout(op, msg, 3*time.Second); err != nil { fmt.Println("write fail:", err) } } func (e *echoHandler) OnClose(c *quickws.Conn, err error) { fmt.Println("OnClose:", c, err) } // echo 测试服务 func echo(w http.ResponseWriter, r *http.Request) { c, err := quickws.Upgrade(w, r, quickws.WithServerReplyPing(), // quickws.WithServerDecompression(), // quickws.WithServerIgnorePong(), quickws.WithServerCallback(&echoHandler{}), quickws.WithServerReadTimeout(5*time.Second), ) if err != nil { fmt.Println("Upgrade fail:", err) return } c.StartReadLoop() } func main() { http.HandleFunc("/", echo) http.ListenAndServe(":9001", nil) } 

    常见问题

    1.为什么 quickws 不标榜 zero upgrade?

    第一:quickws 是基于 std 的方案实现的 websocket 协议。

    第二:原因是 zero upgrade 对 websocket 的性能提升几乎没有影响(同步方式),所以 quickws 就没有选择花时间优化 upgrade 过程,

    直接基于 net/http ,websocket 的协议是整体符合大数定律,一个存活几秒的 websocket 协议由 upgrade(握手) frame(数据包) frame frame 。。。组成。

    所以随着时间的增长, upgrade 对整体的影响接近于 0 ,我们用数字代入下。

    A: 代表 upgrade 可能会慢点,但是 frame 的过程比较快,比如基于 net/http 方案的 websocket

    upgrade (100ms) frame(10ms) frame(10ms) frame(10ms) avg = 32.5ms

    B: 代表主打 zero upgrade 的库,假如 frame 的过程处理慢点,

    upgrade (90ms) frame(15ms) frame(15ms) frame(15ms) avg = 33.75ms

    简单代入下已经证明了,决定 websocket 差距的是 frame 的处理过程,无论是 tps 还是内存占用 quickws 在实战中也会证明这个点。所以没有必须也不需要在 upgrade 下功夫,常规优化就够了。

    2.quickws tps 如何

    在 5800h 的 cpu 上面,tps 稳定在 47w/s ,接近 48w/s 。比 gorilla 使用 ReadMessage 的 38.9w/s ,快了近 9w/s

    quickws.1: 1s:357999/s 2s:418860/s 3s:440650/s 4s:453360/s 5s:461108/s 6s:465898/s 7s:469211/s 8s:470780/s 9s:472923/s 10s:473821/s 11s:474525/s 12s:475463/s 13s:476021/s 14s:476410/s 15s:477593/s 16s:477943/s 17s:478038/s gorilla-linux-ReadMessage.4.1 1s:271126/s 2s:329367/s 3s:353468/s 4s:364842/s 5s:371908/s 6s:377633/s 7s:380870/s 8s:383271/s 9s:384646/s 10s:385986/s 11s:386448/s 12s:386554/s 13s:387573/s 14s:388263/s 15s:388701/s 16s:388867/s 17s:389383/s gorilla-linux-UseReader.4.2: 1s:293888/s 2s:377628/s 3s:399744/s 4s:413150/s 5s:421092/s 6s:426666/s 7s:430239/s 8s:432801/s 9s:434977/s 10s:436058/s 11s:436805/s 12s:437865/s 13s:438421/s 14s:438901/s 15s:439133/s 16s:439409/s 17s:439578/s gobwas.6: 1s:215995/s 2s:279405/s 3s:302249/s 4s:312545/s 5s:318922/s 6s:323800/s 7s:326908/s 8s:329977/s 9s:330959/s 10s:331510/s 11s:331911/s 12s:332396/s 13s:332418/s 14s:332887/s 15s:333198/s 16s:333390/s 17s:333550/s 

    3.quickws 流量测试数据如何 ?

    在 5800h 的 cpu 上面, 同尺寸 read buffer(4k), 对比默认用法,quickws 在 30s 处理 119GB 数据,gorilla 处理 48GB 数据。

    • quickws
    quickws.windows.tcp.delay.4x: Destination: [127.0.0.1]:9000 Interface lo address [127.0.0.1]:0 Using interface lo to connect to [127.0.0.1]:9000 Ramped up to 10000 connections. Total data sent: 119153.9 MiB (124941915494 bytes) Total data received: 119594.6 MiB (125404036361 bytes) Bandwidth per channel: 6.625 Mbps (828.2 kBps) Aggregate bandwidth: 33439.980↓, 33316.752↑ Mbps Packet rate estimate: 3174704.8↓, 2930514.7↑ (9↓, 34↑ TCP MSS/op) Test duration: 30.001 s. 
    • gorilla 使用 ReadMessage 取数据
    gorilla-linux-ReadMessage.tcp.delay: WARNING: Dumb terminal, expect unglorified output. Destination: [127.0.0.1]:9003 Interface lo address [127.0.0.1]:0 Using interface lo to connect to [127.0.0.1]:9003 Ramped up to 10000 connections. Total data sent: 48678.1 MiB (51042707521 bytes) Total data received: 50406.2 MiB (52854715802 bytes) Bandwidth per channel: 2.771 Mbps (346.3 kBps) Aggregate bandwidth: 14094.587↓, 13611.385↑ Mbps Packet rate estimate: 1399915.6↓, 1190593.2↑ (6↓, 45↑ TCP MSS/op) Test duration: 30 s. 

    4.内存占用如何 ?

    quickws 的特色之一是低内存占用。

    1w 连接的 tps 测试,1k payload 回写,初始内存占用约 122MB , 在 240s-260s 之后大约 86MB ,

    22 条回复    2023-09-02 13:57:48 +08:00
    hafung
        1
    hafung  
       2023-08-29 13:38:14 +08:00
    有和 gws 以及 nbio 的对比测试数据吗
    bv
        2
    bv  
       2023-08-29 13:47:16 +08:00
    这种 callback 形式的 ws 库爱不起来呀
    guonaihong
        3
    guonaihong  
    OP
       2023-08-29 13:54:51 +08:00
    @bv callback 主要是为了后面对接 epoll 方便点.
    guonaihong
        4
    guonaihong  
    OP
       2023-08-29 14:12:19 +08:00
    @hafung 你可以下载 https://github.com/guonaihong/bench-ws 自己跑下
    make
    ./script/tps-benchmark.sh

    在.bashrc 下面加上
    sysctl -w fs.file-max=2000500
    sysctl -w fs.nr_open=2000500
    sysctl -w net.nf_conntrack_max=2000500
    ulimit -n 2000500
    sysctl -w net.ipv4.tcp_tw_reuse=1

    历史压数数据在
    https://github.com/guonaihong/bench-ws/issues/3
    Nazz
        5
    Nazz  
       2023-08-29 14:18:40 +08:00
    @bv 所谓 WebSocket 自然源于 Web, JS 里面就是回调风格, 你们的观念受 gorilla/websocket 影响先入为主了.
    Nazz
        6
    Nazz  
       2023-08-29 14:19:24 +08:00
    一个回调而已, 不会有回调地狱问题
    bv
        7
    bv  
       2023-08-29 14:38:30 +08:00
    @Nazz 不知道别人是不是 gorilla/websocket 先入为主,至少我不是:因为之前做 Java 开发的时候,最先接触到的是 Spring Boot 的 websocket 就是 OnXXX 的回调风格。因为阻塞调用加封装一层就很容易变成回调风格,一旦圈定了回调,在此基础上封装层阻塞式就麻烦了。
    Nazz
        8
    Nazz  
       2023-08-29 14:42:15 +08:00
    @bv go runtime 把异步非阻塞 IO 包装成了同步接口, 跟 Java IO 模型差异很大
    Glauben
        9
    Glauben  
       2023-08-29 14:48:08 +08:00
    有个疑问,zero upgrade 是指的什么
    Nazz
        10
    Nazz  
       2023-08-29 14:52:11 +08:00
    @bv go 开发者大部分是受 gorilla/websocket 影响, 循环 ReadMessage, 最受欢迎的几个库都是这种风格. quickws / gws 则是封装了循环 ReadMessage 这一过程, 暴露 Event API, 标准库方案本质都一样.
    Nazz
        11
    Nazz  
       2023-08-29 14:53:49 +08:00
    @Glauben 大概是 zero allocs 的握手, 从 http 升级到 ws
    guonaihong
        12
    guonaihong  
    OP
       2023-08-29 14:56:59 +08:00
    @Glauben gobwas/ws 的作者发明的新词,指通过 websocket upgrade 不分配内存。其实没什么用的概念,只会误导用户。你看我前面的数据代入证明就明白了,zero upgrade 和不 zero upgrade 没什么影响,无论是内存整体占用还是 tps 。
    DDDZZZFFF
        13
    DDDZZZFFF  
       2023-08-29 14:57:58 +08:00
    可以搞个 websocket 网关 ma
    Glauben
        14
    Glauben  
       2023-08-29 15:05:43 +08:00
    @Nazz #11
    @guonaihong #12
    谢谢解答,如果是 zero alloc ,那作用应该是体现在大量新连接时的 GC 压力而不是内存整体占用吧。对于 GC STW 敏感的业务应该还是有其吸引力的。
    guonaihong
        15
    guonaihong  
    OP
       2023-08-29 17:34:59 +08:00
    @Glauben 你说的场景要优化的不是语言,而应该先优化网络, 一次 ping 的 icmp 包是 7ms(随便 ping 个 baidu 或者 qq 的网址),一次 go 的 stw 最长在 <=1ms 。真的有这种担心 stw 的应用,应该先优化下网络 用专线,再考虑服务端的语言 gc 的问题,一个是 1/8 ,另一个是 7/8 的影响。

    普通互联网应用不需要操这个心。上面我已经讲过存活时间越长的 websocket 连接,upgrade 的时间开销越接近于 0.
    guonaihong
        16
    guonaihong  
    OP
       2023-08-31 13:03:19 +08:00
    @DDDZZZFFF 是想了解业务 websocket 网关还是基础网关?前者是业务聚合数据用的,后者如 nginx 之类的.
    smartdoc647
        17
    smartdoc647  
       2023-09-01 17:20:28 +08:00
    你们这个 antlab 名字搞得可以,搞开源看着也比较正经
    eudore
        18
    eudore  
       2023-09-01 19:46:45 +08:00
    稍微看了一下单元测试覆盖也就 66%,跑一下`go get -v github.com/antlabs/quickws ;CGO_ENABLED=1 go test -v -race github.com/antlabs/quickws`出现直接失败,堆栈太长 v2 粘不全。

    ```bash
    [root@node1 tmp]# CGO_ENABLED=1 go test -v -race github.com/antlabs/quickws
    === RUN Test_DefaultCallback
    === RUN Test_DefaultCallback/local:_default_callback
    fatal error: checkptr: pointer arithmetic result points to invalid allocation

    goroutine 22 [running]:
    runtime.throw({0x969273?, 0xc0001162d8?})
    /usr/local/go1.20/src/runtime/panic.go:1047 +0x5d fp=0xc000093480 sp=0xc000093450 pc=0x46e7fd
    runtime.checkptrArithmetic(0x930ae0?, {0x0, 0x0, 0x952406?})
    /usr/local/go1.20/src/runtime/checkptr.go:69 +0xaa fp=0xc0000934b0 sp=0xc000093480 pc=0x43e68a
    github.com/antlabs/wsutil/rsp.ClearRsp({0x9ff570?, 0xc0001162a0})
    /root/go/src/github.com/antlabs/wsutil/rsp/rsp.go:19 +0x249 fp=0xc000093548 sp=0xc0000934b0 pc=0x86cb09
    github.com/antlabs/quickws.upgradeInner({0x9ff570, 0xc0001162a0}, 0xc00011a500, 0xc0000803c0)
    /root/go/src/github.com/antlabs/quickws/upgrade.go:76 +0x17a fp=0xc000093770 sp=0xc000093548 pc=0x87b4da
    github.com/antlabs/quickws.Upgrade({0x9ff570, 0xc0001162a0}, 0x7fb16e226818?, {0xc0000938a0, 0x1, 0x0?})
    /root/go/src/github.com/antlabs/quickws/upgrade.go:53 +0x225 fp=0xc0000937d8 sp=0xc000093770 pc=0x87b2e5
    github.com/antlabs/quickws.Test_DefaultCallback.func1.1({0x9ff570, 0xc0001162a0}, 0x4a8297?)
    /root/go/src/github.com/antlabs/quickws/callback_test.go:34 +0xfd fp=0xc0000938f8 sp=0xc0000937d8 pc=0x8b7d3d
    net/http.HandlerFunc.ServeHTTP(0xc0000cc120, {0x9ff570, 0xc0001162a0}, 0x1?)
    /usr/local/go1.20/src/net/http/server.go:2122 +0x4e fp=0xc000093928 sp=0xc0000938f8 pc=0x829fee
    net/http.serverHandler.ServeHTTP({0xc00009f050?}, {0x9ff570, 0xc0001162a0}, 0xc00011a500)
    /usr/local/go1.20/src/net/http/server.go:2936 +0x683 fp=0xc000093a50 sp=0xc000093928 pc=0x82d8a3
    net/http.(*conn).serve(0xc0001321b0, {0x9ff960, 0xc00009ef30})
    /usr/local/go1.20/src/net/http/server.go:1995 +0xbd5 fp=0xc000093fa0 sp=0xc000093a50 pc=0x8283f5
    net/http.(*Server).Serve.func3()
    /usr/local/go1.20/src/net/http/server.go:3089 +0x59 fp=0xc000093fe0 sp=0xc000093fa0 pc=0x82e559
    runtime.goexit()
    /usr/local/go1.20/src/runtime/asm_amd64.s:1598 +0x1 fp=0xc000093fe8 sp=0xc000093fe0 pc=0x4a5221
    created by net/http.(*Server).Serve
    /usr/local/go1.20/src/net/http/server.go:3089 +0x818
    ```
    guonaihong
        19
    guonaihong  
    OP
       2023-09-01 23:24:02 +08:00
    @eudore 感谢提的问题,我修复下。
    guonaihong
        20
    guonaihong  
    OP
       2023-09-02 00:16:40 +08:00
    @eudore 好了,你试下 v0.1.4 版本
    eudore
        21
    eudore  
       2023-09-02 08:39:35 +08:00
    @guonaihong go get -v -u 已正常

    一段 action 修改建议,使用 matrix 区分 arch ,另外 race 和 cover 可以一起测试,但是 covermode 有个值不支持。

    ```yaml
    strategy:
    max-parallel: 2
    matrix:
    go: [ '1.20']
    arch: [amd64, 386]
    name: Go-${{ matrix.go }} ${{ matrix.arch }}
    env:
    GOARCH: ${{ matrix.arch }}
    steps:
    - name: Run Tests
    run: go test -v -timeout=1m -race -cover -coverprofile=coverage.out ./...
    ```
    guonaihong
        22
    guonaihong  
    OP
       2023-09-02 13:57:48 +08:00
    @eudore 挺好的建议,可以直接 pr 。哈哈。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1154 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 25ms UTC 23:28 PVG 07:28 LAX 16:28 JFK 19:28
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86