Skip to content

做了一点优化,实际测试效果提升不是非常大,约8%左右的提升#463

Closed
sunvim wants to merge 13 commits intolesismal:masterfrom
sunvim:master
Closed

做了一点优化,实际测试效果提升不是非常大,约8%左右的提升#463
sunvim wants to merge 13 commits intolesismal:masterfrom
sunvim:master

Conversation

@sunvim
Copy link
Contributor

@sunvim sunvim commented Mar 1, 2025

优化列表:

  1. 内存管理实现了分级内存池策略(mempool/tiered_allocator.go),优化了WebSocket帧处理中的内存拷贝,使用零拷贝技术提高性能
  2. 并发模型优化,添加了支持优先级任务的策略
  3. IO优化实现了自适应缓冲区大小调整(adaptive_buffer.go),根据历史使用情况自动调整缓冲区大小,添加了支持io_uring
    4.其他小细节

没有进行系统性测试, repo主有的话,请测试一下,有问题 请反馈!

@lesismal
Copy link
Owner

lesismal commented Mar 1, 2025

感谢发起PR!

buffer pool 部分

内存管理实现了分级内存池策略(mempool/tiered_allocator.go),优化了WebSocket帧处理中的内存拷贝,使用零拷贝技术提高性能

分级buffer是不太适合http部分的,因为很多append,buffer append后size就改变了,很容易造成从小size的池子里拿出来、放回的时候到了大size的池子里,反倒导致复用率低。

IO优化实现了自适应缓冲区大小调整(adaptive_buffer.go),根据历史使用情况自动调整缓冲区大小,添加了支持io_uring

这个的Put如果不是currentsize会重新分配并放回,重新分配完全是多余的,如果觉得size不适合直接就return了,应该是类似进程fork的 copy on write原则——真正用到的时候再分配、也就是说应该在Malloc的地方分配

这个RecordRead都有锁,高并发每次读完都RecordRead都有锁并且maybeResize里都有至少一次time.Since,这个也是比较浪费的。

而且,最重要的,这里传给OnData是读取到的size的部分,但是borrow、payback之间buffer size是不变的,而且我看好像只有engine设置poller读取时这个buffer用到这个、其他地方都没有用到,那其实size是一直不变的,没必要进行RecordRead相关的计算:
https://github.com/lesismal/nbio/blob/master/poller_epoll.go#L276
https://github.com/lesismal/nbio/blob/master/poller_epoll.go#L281
并且,现在默认的配置,epoll默认使用的是固定的同一段读buffer,比这个效率更高,而且为了减少syscall、这个buffer设置得大一些,也没必要动态适应:
https://github.com/lesismal/nbio/blob/master/engine.go#L444
https://github.com/lesismal/nbio/blob/master/engine.go#L428

另外,我们无法预估任何时段的流量和内存状况,应用层自己做这些动态适配,我觉得不如就交给runtime,自己加的额外计算的方案往往能在定制的压测中有优势,在千变万化不可预测的现实中反倒可能是负优化,fasthttp里用的bytesbufferpool里的动态我个人也是觉得没太大必要的,所以也没有做类似的实现。

另外,整体上对于buffer pool,nbio都是支持可配置的,用户可以自定制、然后设置成自己想要的方案,比如http的部分用不按size对齐的,body部分用size对齐的。nbio默认使用同一个不按size对齐的是考虑更大复用率,如果用户想自己配置nbhttp.Engine.BodyAllocator就可以了。
这些并不需要在nbio内部必须支持一个默认实现,所以这种分级或者动态调整size之类的pool的实现可以留给用户自行处理更好些。

@lesismal
Copy link
Owner

lesismal commented Mar 1, 2025

taskpool

这个实现竟然使用time sleep,这是非常不合理的:

  1. sleep粒度较大,高并发系统不应考虑这么大粒度,这属于拖慢系统的常见姿势。。
  2. runtime go和推出后回收、复用,效率都挺高的,没必要使用常驻协程,在这基础上,1就更没必要、即使sleep粒度小也没必要,同样的在这基础上,这些各种计算来动态调整常驻协程的逻辑也是多余的
  3. 协程池、任务队列通常就是先到先执行,没必要区分worker局部和全局,所以这个实现里类似runtime调度,又弄了worker本地queue和全局queue是多余的
  4. 没有size限制,理论上存在无限任务堆进来的可能性。nbio使用chan就是为了能做到自动流控、上下游生产者消费者的动态均衡,避免队列无限堆积、内存爆棚的问题。

与buffer pool类似,这些都是可配置项,nbio选择的实现方案是比较均衡的,如果有特殊需要,可以自己设置 nbhttp.Engine.Execute/ClientExecute 就好了

@sunvim
Copy link
Contributor Author

sunvim commented Mar 1, 2025

感谢发起PR!

buffer pool 部分

内存管理实现了分级内存池策略(mempool/tiered_allocator.go),优化了WebSocket帧处理中的内存拷贝,使用零拷贝技术提高性能

分级buffer是不太适合http部分的,因为很多append,buffer append后size就改变了,很容易造成从小size的池子里拿出来、放回的时候到了大size的池子里,反倒导致复用率低。

IO优化实现了自适应缓冲区大小调整(adaptive_buffer.go),根据历史使用情况自动调整缓冲区大小,添加了支持io_uring

这个的Put如果不是currentsize会重新分配并放回,重新分配完全是多余的,如果觉得size不适合直接就return了,应该是类似进程fork的 copy on write原则——真正用到的时候再分配、也就是说应该在Malloc的地方分配

这个RecordRead都有锁,高并发每次读完都RecordRead都有锁并且maybeResize里都有至少一次time.Since,这个也是比较浪费的。

而且,最重要的,这里传给OnData是读取到的size的部分,但是borrow、payback之间buffer size是不变的,而且我看好像只有engine设置poller读取时这个buffer用到这个、其他地方都没有用到,那其实size是一直不变的,没必要进行RecordRead相关的计算: https://github.com/lesismal/nbio/blob/master/poller_epoll.go#L276 https://github.com/lesismal/nbio/blob/master/poller_epoll.go#L281 并且,现在默认的配置,epoll默认使用的是固定的同一段读buffer,比这个效率更高,而且为了减少syscall、这个buffer设置得大一些,也没必要动态适应: https://github.com/lesismal/nbio/blob/master/engine.go#L444 https://github.com/lesismal/nbio/blob/master/engine.go#L428

另外,我们无法预估任何时段的流量和内存状况,应用层自己做这些动态适配,我觉得不如就交给runtime,自己加的额外计算的方案往往能在定制的压测中有优势,在千变万化不可预测的现实中反倒可能是负优化,fasthttp里用的bytesbufferpool里的动态我个人也是觉得没太大必要的,所以也没有做类似的实现。

另外,整体上对于buffer pool,nbio都是支持可配置的,用户可以自定制、然后设置成自己想要的方案,比如http的部分用不按size对齐的,body部分用size对齐的。nbio默认使用同一个不按size对齐的是考虑更大复用率,如果用户想自己配置nbhttp.Engine.BodyAllocator就可以了。 这些并不需要在nbio内部必须支持一个默认实现,所以这种分级或者动态调整size之类的pool的实现可以留给用户自行处理更好些。

嗯,有道理! 但层级内存管理 不是这样的, 各种尺寸的池,比如 1K/2K/4K/8K/16K 各 64个, 正常的请求大概都在1K左右,那么自然使用1K的chunk 就多一些,以此类推,更合理的管理内存, 采用统一分配的确实现简单一些,但在小body请求很多的时候就太浪费内存了, 我认为是值得去优化的,NBIO这个库定位是 高性能,关键设施的,无论是计算资源还是存储资源抑或是IO资源 都值得精细化管理

不过有些地方确实还是需要持续优化的,最好能在无所的情况下实现 层级管理

@lesismal
Copy link
Owner

lesismal commented Mar 1, 2025

另外,如果再发起PR,文件中请不要使用中文

@lesismal
Copy link
Owner

lesismal commented Mar 1, 2025

这个PR整体上不会被合并,所以我先关闭了,如果需要讨论可以继续留言

@lesismal lesismal closed this Mar 1, 2025
@sunvim
Copy link
Contributor Author

sunvim commented Mar 1, 2025

这个PR整体上不会被合并,所以我先关闭了,如果需要讨论可以继续留言

okkk, 那就关了吧

@lesismal
Copy link
Owner

lesismal commented Mar 1, 2025

嗯,有道理! 但层级内存管理 不是这样的, 各种尺寸的池,比如 1K/2K/4K/8K/16K 各 64个, 正常的请求大概都在1K左右,那么自然使用1K的chunk 就多一些,以此类推,更合理的管理内存, 采用统一分配的确实现简单一些,但在小body请求很多的时候就太浪费内存了, 我认为是值得去优化的,NBIO这个库定位是 高性能,关键设施的,无论是计算资源还是存储资源抑或是IO资源 都值得精细化管理

只有不需要被append的buffer,可以使用这种分级的,因为get put是对应的相同的池。如果会被append,那就不可控了,每次请求的size不可控,上次put回来和下次get出去的顺序不可控,buffer基本就只朝着一个方向流动、就是从小size到大size的pool。

另外还有ws zero copy的,因为nbio epoll管理的conn,每个ws message解析出来后默认是协程池去异步执行的,这是可能上个message还没处理完,bytesCached被put回pool并且被别的conn拿到用于处理解析从poller的读buffer拷贝数据过来、然后脏内存了

@lesismal
Copy link
Owner

lesismal commented Mar 1, 2025

不过有些地方确实还是需要持续优化的,最好能在无所的情况下实现 层级管理

分级buffer pool的管理本身不需要锁,就是[]sync.Pool,按照size对齐取index操作sync.Pool就可以了,剩下的都是sync.Pool自己内部的锁、这个避免不掉了

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants