修改 go websocket server 启动方式, 内存占用立省 40% !

2023-04-27 14:03:20 +08:00
 Nazz

最近发布了 gws v1.4.7 更新, 支持从 tcp conn 直接解析 websocket 协议, 降低内存占用. 大部分人使用 go websocket server, 都是复用的 http server, 这种劫持http连接升级的方式, 最大的弊端就是浪费内存. 由于 go http hijack 的缺陷, 一些内存一直得不到释放, 大概每个连接 10KB. 测试 10000 个连接的场景, 换用 Demo2 方式, 内存占用立省 42.86%!

Demo 1: hijack

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())
	}
}

Demo 2: direct

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())
	}
}

2094 次点击
所在节点    Go 编程语言
38 条回复
lysS
2023-04-27 16:17:03 +08:00
第一种方式不用可以和 http 同时使用、复用 http 的路由、不影响已经存在的 http api ,同时支持多条 ws ,不用另开端口。
lysS
2023-04-27 16:18:33 +08:00
喔,不对,上面说的有问题
Nazz
2023-04-27 16:21:33 +08:00
@lysS 缺点就是太费内存, 看官方什么时候修复吧
lesismal
2023-04-27 18:06:48 +08:00
这种 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 ,说不定还有一些其他可以清理的
lesismal
2023-04-27 18:10:55 +08:00
另外,upgrader.Accept 和 socket.Listen 这两个命名实在是有点难受,Accept 和 Listen 都与实际的行为意义不匹配,OP 啥时候改成和大家一致的比如也叫 upgrader.Upgrade ,另一个叫 ReadLoop()之类的,老接口可以保留,提供个新的命名就行。。。
Nazz
2023-04-27 18:18:12 +08:00
@lesismal 这两个命名跟我想到一块去了👍🏻
lesismal
2023-04-27 18:18:19 +08:00
还有一点,在 handler 里其实可以不用新开协程的,socket.Listen() 就可以了、不要 go ,这样就复用了 http server 原来的那个协程、避免了一次不必要的协程重建和一些变量逃逸。比如前面说到的 http conn 挂载的读写 buffer ,因为是默认 4k ,1.18 还是哪个版本之后来着、协程栈好像默认是 8k ,所以一般而言,复用了原来的协程,则挂载的这个读 buffer 至少不太涉及跨协程的逃逸,所以应该是能使用原来这个协程的栈空间,除非你要设置得很大 size
lesismal
2023-04-27 18:20:30 +08:00
单就主帖内容,TCP 这个可能会误导一些人、以为可以替换了原来的方案了,所以特地来回复一下,顺便又 review 了几眼
lesismal
2023-04-27 18:27:39 +08:00
@lesismal #7 我只是肉眼分析,实际差别应该不会特别大。可以实测对比下玩玩看,需要排除掉多次启动基础内存占用可能不同的一些差别、比如多跑几轮
Nazz
2023-04-27 18:36:01 +08:00
@lesismal 能用于浏览器的,你试试. 实测 net.Buffer 稍快点,而且能省掉 bufio.Writer 的内存
Nazz
2023-04-27 18:38:21 +08:00
@lesismal 我跑了好几次,结果都差不多的,截图里面跑了五六分钟
Nazz
2023-04-27 18:45:10 +08:00
@lesismal demo 里面加 go 是因为我想让请求上下文被 gc 掉, 结果还是有副作用
Nazz
2023-04-27 18:51:44 +08:00
@lesismal 还以为 net.Buffer 能减少一次拷贝呢, 没想到底层还是会拷贝. net/http 里的东西改不来, 理顺逻辑要费不少功夫, 已经给官方提 issue 了: https://github.com/golang/go/issues/59567
Nazz
2023-04-27 19:05:57 +08:00
@lesismal WebSocket over TCP 是我在 github 写的 title , 没想到好的命名 😂
lesismal
2023-04-27 19:14:09 +08:00
> 能用于浏览器的,你试试.

刚看了下代码,这。。。

我之前说不能用于浏览器是因为你这句 "支持从 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 复用协程也是挺给力而且不是高频行为,剩下的主要是逃逸
Nazz
2023-04-27 19:38:42 +08:00
我做了个简单的 parser ,做了大 header 的防范,设置了 Deadline ,不知道还有没有其他风险,改天找个正经 http parser 看下
lesismal
2023-04-27 19:46:04 +08:00
我又测了下 net.Buffers vs user buffer append:
https://gist.github.com/lesismal/a40c420511252aa79b054cbd2acc896e

我的机器上还是 user buffer append 性能略好,而且内存更优,你可以自己环境跑下看看不同环境是否有差异
Nazz
2023-04-27 19:54:39 +08:00
@lesismal 我回家之后测一下看看
Nazz
2023-04-27 20:47:46 +08:00
@lesismal user buffer append 确实更优些,gws 1000 connections iops 峰值从 1200 提高到了 1400
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 可能更好点

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/935912

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX