gnet
gnet copied to clipboard
优化网络连接的存储方式,避免 big map 下的 GC 延迟
看到数据结构:connections ,udpSockets 维护链接信息。但是大量连接,例如200w个tcp,这个内存很大,但是GC定时扫描这个大内存,导致cpu间隙很高。(无任何业务请求下) type eventloop struct { ln *listener // listener idx int // loop index in the engine loops list cache bytes.Buffer // temporary buffer for scattered bytes engine *engine // engine in loop poller *netpoll.Poller // epoll or kqueue buffer []byte // read packet buffer whose capacity is set by user, default value is 64KB connCount int32 // number of active connections in event-loop udpSockets map[int]*conn // client-side UDP socket map: fd -> conn connections map[int]*conn // TCP connection map: fd -> conn eventHandler EventHandler // user eventHandler }
同一个连接大量大写的情况下遇到一样的问题。特别是开10个连接大量读写的时候,尤其明显。性能直线下降
同一个连接大量大写的情况下遇到一样的问题。特别是开10个连接大量读写的时候,尤其明显。性能直线下降
不管是什么网络框架,针对你说的这种大量读写的比如一个包几MB大的场景,性能肯定是会下降的,没有例外,还有 gnet 默认的 buffer 大小只有 64KB,如果是这种大包的场景可以通过调大读写 buffer 大小来适配:
https://pkg.go.dev/github.com/panjf2000/gnet/v2#WithReadBufferCap https://pkg.go.dev/github.com/panjf2000/gnet/v2#WithWriteBufferCap
hi panjf2000 ,[FJSDS]说的和我不是同一个问题。我的问题是可以解决的。这个是golang对map数据结构存储的限制,不能存储含有指针的超大内存。否则GC间歇性的扫描大内存,没有任何业务,cpu间歇性能够上升到70%,无法接受 。
hi panjf2000 ,[FJSDS]说的和我不是同一个问题。我的问题是可以解决的。这个是golang对map数据结构存储的限制,不能存储含有指针的超大内存。否则GC间歇性的扫描大内存,没有任何业务,cpu间歇性能够上升到70%,无法接受 。
你说的没错,big map 存指针的确会对 GC 造成很大的负担,但是你们一个进程连了几百万个 TCP 连接吗?这个我的确是没想到。。。
嗯。那没有这么高。但是一台电脑配置,开一个服务,怎么也要连接20w长连接(我们是websocket),也是作为接入层的要求吧。但是其中1/8 用户是活跃状态.....这个正是你开源这个库(epoll)意义所在,否则<1w连接(含1w),使用原生的net即可(一个连接,一个携程的模型)。
可以的话,我提交一份git修改。并附带一份测试200w的连接~
可以的话,我提交一份git修改。并附带一份测试200w的连接~
可以的
能先说一下你的解决思路是什么吗?我昨天想了一下,最简单的做法就是用 slice 代替 map,fd 作为 slice 下标,但缺点是需要预分配固定长度的 slice,或者使用 map[int]int + slice,可以实现动态分配,但是又有可能在不断扩容 slice 的时候导致大量的拷贝,目前的思路是参考 bigcache 的思路实现一个精简版的,基本思路也是 map[int]int + slice 但是更高效,看看你有没有更好的思路? @situnan
能先说一下你的解决思路是什么吗?我昨天想了一下,最简单的做法就是用 slice 代替 map,fd 作为 slice 下标,但缺点是需要预分配固定长度的 slice,或者使用 map[int]int + slice,可以实现动态分配,但是又有可能在不断扩容 slice 的时候导致大量的拷贝,目前的思路是参考 bigcache 的思路实现一个精简版的,基本思路也是 map[int]int + slice 但是更高效,看看你有没有更好的思路? @situnan
刚好也在梳理这一块,map[int]int + slice确实是一个比较好的思路,但也会附带其他的问题,如:高并发锁竞争问题、扩容、缩容的问题... 目前也还没想到比较好的方案! @panjf2000 后续是否有计划调整udpSockets map[int]*conn、connections map[int]*conn的类型?没看细读代码,connections貌似没用锁保证线程安全或是不需要?
能先说一下你的解决思路是什么吗?我昨天想了一下,最简单的做法就是用 slice 代替 map,fd 作为 slice 下标,但缺点是需要预分配固定长度的 slice,或者使用 map[int]int + slice,可以实现动态分配,但是又有可能在不断扩容 slice 的时候导致大量的拷贝,目前的思路是参考 bigcache 的思路实现一个精简版的,基本思路也是 map[int]int + slice 但是更高效,看看你有没有更好的思路? @situnan
刚好也在梳理这一块,map[int]int + slice确实是一个比较好的思路,但也会附带其他的问题,如:高并发锁竞争问题、扩容、缩容的问题... 目前也还没想到比较好的方案! @panjf2000 后续是否有计划调整udpSockets map[int]*conn、connections map[int]*conn的类型?没看细读代码,connections貌似没用锁保证线程安全或是不需要?
gnet
整个框架是无锁的,每个event-loop里的数据都是隔离的,只能在各自的 goroutine 里访问和更新,所以不需要锁。
嗯。map[int]int + slice 模型。请参考:
https://github.com/legougames/slice_map
嗯。map[int]int + slice 模型。请参考:
https://github.com/legougames/slice_map
他这个不是线程安全的...
嗯。线程安全要独立考虑~
嗯。线程安全要独立考虑~
不需要是线程安全的,gnet 里这个 map 不会被并发访问。
嗯。map[int]int + slice 模型。请参考:
https://github.com/legougames/slice_map
他这个实现有点简单了,而且在 slice 不断扩容的情况下会导致大量的内存拷贝,可以参考下 bigcache 或者 fastcache 的思路,做一个精简版的。
- slice替代map按fd存conn指针(比如[]ConnStruct, not pointer, conn = &conns[fd])会存在顶号/窜号的问题,比如网络层已经关闭这个fd,但用户应用层代码不同的异步模块仍然持有这个conn、应用层尚未完成的异步操作后续操作这个conn时、这个fd被新的连接使用了,然后旧操作操作到了新的conn上,会发生混乱,虽然概率低但毕竟不严谨。框架层虽然也能限制应用层、但是go不像c/cpp指令那样性能强可以逻辑单线程,go仍然需要逻辑多协程,所以不方便像逻辑单线程那样让复杂的多异步模块系统的应用层处理顶号问题
- 如果是想用非conn本身的去替代conn本身的结构,则可能需要应用层用户自己必须持有conn,否则框架层的conn无处持有可能被gc掉了、下次事件循环虽然拿到了原来的结构地址但已经不是原来那个conn了。有些简单场景用户应用层可能不会持有conn比如简单的转发。如果确保用户都会自行持有,则像netpoll那样直接使用syscall把conn作为epoll参数传给内核、事件来时取出来就可以了,框架自己不需要map或者其他结构来存conn了
所以我现在仍然用 []*Conn,fd做下标,每次fd对应的是不同的Conn指针,能解决顶/号窜号的问题。 想去掉这个框架层持有,直接由内核持有,但是不想去逼用户自己持有,尤其是一些half-packet时用户可能尚未持有,比如http框架,因为无状态,所以不需要给所有的conn进行统一管理,所以比如一个请求过来时本次只读取到一个字节,则还没有创建Request/Response这些,所以也没地方去持有conn,如果框架层不持有conn,本轮局部读取完conn就被gc了
@situnan 这里有什么进展吗?你要是没时间搞的话我就自己弄了。
最简单的做法就是用 slice 代替 map,fd 作为 slice 下标,但缺点是需要预分配固定长度的 slice,或者使用 map[int]int + slice,可以实现动态分配,但是又有可能在不断扩容 slice 的时候导致大量的拷贝
其实分配较大的固定长度slice问题不大,内存实际分配应该是类似cow机制,真正使用该页时才会触发内核的实际分配,否则只是个占位变量。而且int在64位也才8字节,1-2m连接数也才8-16m内存,即使内核自己在发生实际页分配时需要内存搬运,拷贝的内存也才8-16m,这个很快,而且这种搬运发生的次数也不会太大,也不需要缩容,而且uni* fd是连续且自回收的,不用每个poller loop一个,全局一个就可以了
我知道是一开始只是分配虚拟内存,但是固定长度的话不太灵活,如果超出了怎么处理又是一个问题,要么拒绝新连接,要么扩容。
的
我提供了MaxOpenFiles的配置项,默认1m直接分配了1m个,正打算改成2m个:joy:,用户如果需要更大可以自己修改该值、设置成能满足自己服务需要的值(通常大于连接数的值,为其他类型的fd冗余)
是否断开连接,由7层用户自己持有conn并做相关的在线数统计管理更好,比如超过负载了accept后直接断开,4层的框架可以不去管这个
而且即使是数组存指针,应该是仍然要扫描的。不存指针直接存结构体的话,就存在我前面说的那个窜号的问题
数组是连续内存,扫描的时候比 map 快多了
数组是连续内存,扫描的时候比 map 快多了
那倒是。
业务层的持有也难于避免扫描,而且业务层通常还是按map来存
如果实在大量到影响gc了,可能还是得业务人员调整GOGC间隔、配合手动GC来减少消耗了
如果能想办法规避掉串号的问题,结构体数组内存大些,但是可以避免框架层的指针了,暂时是想不出,因为没法强制业务层的持有
我最近有点忙~~~不好意思。