最近发布了 gws v1.4.7
更新, 支持从 tcp conn
直接解析 websocket
协议, 降低内存占用. 大部分人使用 go websocket server
, 都是复用的 http server
, 这种劫持http
连接升级的方式, 最大的弊端就是浪费内存. 由于 go http hijack
的缺陷, 一些内存一直得不到释放, 大概每个连接 10KB. 测试 10000 个连接的场景, 换用 Demo2 方式, 内存占用立省 42.86%!
package main
import (
"github.com/lxzan/gws"
"log"
"net/http"
)
func main() {
upgrader := gws.NewUpgrader(new(gws.BuiltinEventHandler), nil)
http.HandleFunc("/connect", func(writer http.ResponseWriter, request *http.Request) {
socket, err := upgrader.Accept(writer, request)
if err != nil {
log.Printf(err.Error())
return
}
go socket.Listen()
})
if err := http.ListenAndServe(":3000", nil); err != nil {
log.Fatalf(err.Error())
}
}
package main
import (
"github.com/lxzan/gws"
"log"
)
func main() {
srv := gws.NewServer(new(gws.BuiltinEventHandler), nil)
if err := srv.Run(":3001"); err != nil {
log.Panicln(err.Error())
}
}
1
lysS 2023-04-27 16:17:03 +08:00
第一种方式不用可以和 http 同时使用、复用 http 的路由、不影响已经存在的 http api ,同时支持多条 ws ,不用另开端口。
|
2
lysS 2023-04-27 16:18:33 +08:00
喔,不对,上面说的有问题
|
4
lesismal 2023-04-27 18:06:48 +08:00 2
这种 over TCP 的不能用于浏览器相关的领域,对于绝大多数 Websocket 用户是与 Web 浏览器的前段交互,所以绝大多数用户都不能用这种直接 over TCP 的 Websocket ,所以这样对比其实本身就没什么意义。
之前也有人在我这问基于 TCP 的,开放了一些字段,现在随便哪里的数据传递给 Websocket 解析器就可以了,基于 TCP/Unix/QUIC/KCP 或者随便什么协议都可以: https://github.com/lesismal/nbio/issues/240#issuecomment-1304804444 如果是游戏、App 之类的用 TCP 通常也都是自家封装的协议、更简单更定制化。 内存占用的问题,标准库 HTTP 这块确实是,conn 上面挂载的读、写 buffer ,Hijack 的时候又新建了个写 buffer 传给用户,我看 OP 代码里是把新建的这个写 buffer=nil 释放了,写的时候是用 net.Buffers ,但这样不一定是最佳: 1. 标准卡库 Hijack 时创建的写 Buffer 虽然一瞬间又销毁了、但毕竟不是 c free ,不能立即释放。但正常业务 Upgrade 并不是超级频繁的动作,所以影响也不算大 2. net.Buffers 调用 TCPConn 这种,最后是 syscall.Writev ,以前做测试用 net.Buffers 性能比应用层自己拼接 buffer 然后 Write 要稍微差一点点。syscall.Writev 的内核 c 实现也是创建大 buffer 拷贝上去然后在 write ,但是用 syscall.Writev 可能消耗更多的内核资源和时间、而内核是整个系统共用资源时间比较宝贵且竞争,所以可能反倒不如让应用层来做这种划算,所以或许直接复用 Hijack 传过来的那个 Writer 也是相当于应用层的 Writev ,性能也还好。但是我很久没再做这个 syscall.Writev 与应用层自己 Writev 的对比了,不知道现在新版本如何。我自己的库一些实现是避免使用 net.Buffers 的,但是不管怎么用,性能差别应该不大 标准库 HTTP 的解析有些地方也是比较浪费的,重复拷贝、循环之类的,之前想去 pr 一波来着,但是标准库的实现太复杂了、功能也多、并发流也多,有的地方是一个连接可能多个协程处理的。pr 流程也很麻烦,要想 pr 成功太耗费精力了,所以放弃了 OP 可以试下去把标准库 Hijack()这块的代码优化下,Hijack 之后就把 conn 自己身上挂载的读写 buffer=nil ,说不定还有一些其他可以清理的 |
5
lesismal 2023-04-27 18:10:55 +08:00 1
另外,upgrader.Accept 和 socket.Listen 这两个命名实在是有点难受,Accept 和 Listen 都与实际的行为意义不匹配,OP 啥时候改成和大家一致的比如也叫 upgrader.Upgrade ,另一个叫 ReadLoop()之类的,老接口可以保留,提供个新的命名就行。。。
|
7
lesismal 2023-04-27 18:18:19 +08:00
还有一点,在 handler 里其实可以不用新开协程的,socket.Listen() 就可以了、不要 go ,这样就复用了 http server 原来的那个协程、避免了一次不必要的协程重建和一些变量逃逸。比如前面说到的 http conn 挂载的读写 buffer ,因为是默认 4k ,1.18 还是哪个版本之后来着、协程栈好像默认是 8k ,所以一般而言,复用了原来的协程,则挂载的这个读 buffer 至少不太涉及跨协程的逃逸,所以应该是能使用原来这个协程的栈空间,除非你要设置得很大 size
|
8
lesismal 2023-04-27 18:20:30 +08:00
单就主帖内容,TCP 这个可能会误导一些人、以为可以替换了原来的方案了,所以特地来回复一下,顺便又 review 了几眼
|
9
lesismal 2023-04-27 18:27:39 +08:00
@lesismal #7 我只是肉眼分析,实际差别应该不会特别大。可以实测对比下玩玩看,需要排除掉多次启动基础内存占用可能不同的一些差别、比如多跑几轮
|
10
Nazz OP @lesismal 能用于浏览器的,你试试. 实测 net.Buffer 稍快点,而且能省掉 bufio.Writer 的内存
|
13
Nazz OP @lesismal 还以为 net.Buffer 能减少一次拷贝呢, 没想到底层还是会拷贝. net/http 里的东西改不来, 理顺逻辑要费不少功夫, 已经给官方提 issue 了: https://github.com/golang/go/issues/59567
|
15
lesismal 2023-04-27 19:14:09 +08:00 2
> 能用于浏览器的,你试试.
刚看了下代码,这。。。 我之前说不能用于浏览器是因为你这句 "支持从 tcp conn 直接解析 websocket 协议" ,以为你是和我发的那个类似、直接 tcp 上的数据解析 websocket ,但其实不是,你这个仍然是先解析 http request 然后 upgrade ,只是没用标准库的 http 、而是自己实现了解析 http request 这步。 所以准确说,是使用自实现的 http request 解析进行 ws upgrade 。 但这个解析不够完备、没有非法字符之类的协议规范的判断,我不确定会不会有一些风险。 > 实测 net.Buffer 稍快点,而且能省掉 bufio.Writer 的内存 可能你对比的是 bufio.Writer ,我之前对比的是 buffer append > net.Buffer 能减少一次拷贝呢, 没想到底层还是会拷贝 这个可以看下代码,跟进去,TCPConn 这种就是调用的 syscall.Writev 了。 内核 c 语言实现的 writev 也看下就知道了 > demo 里面加 go 是因为我想让请求上下文被 gc 掉, 结果还是有副作用 单就 upgrade 这个 request 而言,可能上下文的代价比新 go 更大一点,但实际应该也差不了太多,runtime 复用协程也是挺给力而且不是高频行为,剩下的主要是逃逸 |
16
Nazz OP 我做了个简单的 parser ,做了大 header 的防范,设置了 Deadline ,不知道还有没有其他风险,改天找个正经 http parser 看下
|
17
lesismal 2023-04-27 19:46:04 +08:00
我又测了下 net.Buffers vs user buffer append:
https://gist.github.com/lesismal/a40c420511252aa79b054cbd2acc896e 我的机器上还是 user buffer append 性能略好,而且内存更优,你可以自己环境跑下看看不同环境是否有差异 |
19
Nazz OP @lesismal user buffer append 确实更优些,gws 1000 connections iops 峰值从 1200 提高到了 1400
|
20
lesismal 2023-04-27 21:12:02 +08:00
@Nazz #19 我又测了下 bufio.Writer ,跟 buffer append 性能差不多,但 bufio.Writer 是在线连接长期占用这个,buffer append 的方式是当前有写才会占用下、可以复用 pool ,而且并发写有 mutex 的、一个 conn 同时最多也就占一个,总数量<= conn num ,而且核心数没那么多、并发多数时候没有那么多协程达到并行,所以实际应该是小于 conn num ,所以 buffer append 可能更好点
|
24
lesismal 2023-04-27 22:08:40 +08:00
> bytes.Buffer 应该比手撸的更快
并不是。首先它一样的是挂载在 conn 上需要持续占用大段 buffer ,其次它源码你看下就知道了,还是那些个 buffer 的操作、只是封装些常用方法方便用户罢了。buffer 操作这种事情并没有什么性能提升的秘诀,就是谁的逻辑代码消耗更少、拷贝更少之类的 |
28
Nazz OP @lesismal 现在改成用户态拼接 buffer 的方式了, bytes.Buffer 没有 Discard 方法, 压缩那块写得有点丑
|
32
lesismal 2023-04-28 14:56:56 +08:00
@lysS 跟是不是浏览器应该没关系,而是 ws 协议就是这么规定的,go 的 client 也是要发这个握手的 request 的。
|
33
lesismal 2023-04-28 15:00:45 +08:00
> 但毕竟不是 c free ,不能立即释放
#4 更正,c free 也不一定是立即归还的、而且多数时候不是立即归还,要看分配器实际情况了 > 单就 upgrade 这个 request 而言,可能上下文的代价比新 go 更大一点 #15 更正,单就 upgrade 这个 request 而言,可能相比于上下文的代价、新 go 代价更大一点 |
34
lesismal 2023-04-28 15:05:10 +08:00
> 现在改成用户态拼接 buffer 的方式了, bytes.Buffer 没有 Discard 方法, 压缩那块写得有点丑
> 忽然发现 next 就相当于 Discard :) @Nazz 主要是你 write frame 的场景,就一 head+body 拼接,太简单了,pool+append 足矣 |
37
lesismal 2023-04-28 18:24:08 +08:00
> copy 比 append 更快些
恩。 前提是得比较明确 size cap 这些,很多地方是 buffer size 可能不够,即使 copy 也是得先 append 扩容。 预先知道 size 并且分配了足够 size 的 buffer ,我也是 copy: https://github.com/lesismal/nbio/blob/master/nbhttp/websocket/conn.go#L263 |
38
lesismal 2023-04-28 18:25:37 +08:00
> 不开新 goroutine 好点, 反正 http 包里面很多东西 gc 不了.
是。标准库让人又爱又恨的 |