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 · 2020 次点击
    这是一个创建于 485 天前的主题,其中的信息可能已经有所发展或是发生改变。

    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   ·   实用小工具   ·   1063 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 19:09 · PVG 03:09 · LAX 11:09 · JFK 14:09
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.