lesismal / llib

BSD 3-Clause "New" or "Revised" License
12 stars 9 forks source link

和标准库的差异? #16

Open byene0923 opened 2 years ago

byene0923 commented 2 years ago

hi,非常感谢开源这么优秀的网络库。 我理解golang官方的tls标准库适配net模型,使用的同步模型,为适配nbio,作者应该是改为了异步的方式,想了解下,改为异步的思路?我理解难点在于内存的分配,想知道作者的解决/优化思路?

lesismal commented 2 years ago

异步难点

传统异步的难点主要包括两方面

  1. 异步流解析
  2. 内存优化

异步流解析

需要异步处理半包,涉及半包缓存、新数据来了后的拼接、解析流程的状态机等,这些都比同步解析要麻烦的多。因为同步解析可以根据协议特点,当前需要几个字节就去阻塞读几个字节出来,解析逻辑是同步的,自上而下、逻辑简单。而异步解析,没法自己需要几个字节就阻塞读几个字节,只能等下次来新数据了再解析,下次来的数据够不够也不敢保证

内存优化

标准库同步方案的内存池优化策略也不一样,主要区别在两方面: 一是buffer复用:

  1. 同步方案是for循环中读一个request处理一个,然后再读一个再处理,固定size的buffer也很容易复用,不存在跨协程传递,生命周期非常容易管理
  2. 异步方案,通常要分为IO协程、逻辑协程,如果都在IO协程中处理逻辑,某个request中有阻塞操作,则IO协程中其他Conn就需要等待了。IO与逻辑协程之间的消息投递,就涉及buffer的跨协程了,跨协程了,就不容易像同步方案那种复用单个读buffer了,每次解析出一个request/message,丢给逻辑协程时那个buffer就不能再被之前的IO协程同时使用了,配合前面说的异步解析,这里的逻辑细节也是挺多的
  3. 二是2^N size的不适用:

  4. 同步方案,每个连接一个或者几个2^N size的buffer就可以不同复用,buffer与Conn之间是相对稳定的关系,读完一个处理一个,buffer也不需要异步半包的cache、新数据来了时的拼接,所以2^N size能够稳定复用
  5. 异步方案,比如本次来了1.5个包,解析到1个完整包后,因为要转给逻辑协程,这就需要copy了,而剩下的0.5个包,需要cache起来等新数据来了再拼接和继续解析,这过程中就涉及memmove或者copy之类的很多操作,如果固定使用2^N,因为每次的0.5或者0.3总之不够一个完整包的2^N对齐尺寸都可能是不太一样的,反倒会浪费非常多性能

再有就是不同协议层之间的内存池复用,llib主要是为nbio的tls做的标准库tls魔改,网络层次,咱们用的tcp/tls/http/websocket,4-7层,如果各个层单独一个内存池,也可能是用量加倍,所以nbio是高度定制了这个4-7,都复用同一个内存池了,尽量复用

c/c++那些是直接malloc/free,或者也可以jemalloc/tcmalloc那些来做,并且c/c++没有runtime,虚拟内存的分配释放相对及时、更可控。golang毕竟不同,即使不用了也一来runtime gc,异步流解析+内存池的实现其实跟c/c++之类的相比,难度并不算大

byene0923 commented 2 years ago

感谢作者的解答!作者是把tcp/tls/http/websocket 协议 做了统一适配解析啊。。ORZ,2^N size的buffer 是源自于fasthttp的?还有不知道作者研究过netpoll的LinkBuffer?。 非常感谢,拜读中

lesismal commented 2 years ago

是把tcp/tls/http/websocket 协议 做了统一适配解析啊

这个是指统一的buffer pool

2^N size的buffer 是源自于fasthttp的?

nbio没有使用 2^N,我上面解释了的,2^N 不适用异步库。2^N源于哪里我没有去考究,因为标准库里也有一些地方是这种

不知道作者研究过netpoll的LinkBuffer

这里有一份benchmark,包括net、netpoll、nbio、gnet的对比,其他的一些poller仓库比如evio和其他一些国内作者的仓库我之前测试过,吞吐率都不及gnet,所以没有加入到代码中。issue里的截图数据是我自己环境跑的结果,你可以在自己环境实测为准,然后再看看各个方案的效果: https://github.com/lesismal/go-net-benchmark/issues/1 在测试中发现netpoll内存消耗比其他方案大非常多,甚至超过标准库;并且连接数大了之后,netpoll有时候甚至卡死。

所以我建议是不要看各个benchmark仓库自己提供的数据,而是自己实测好些,包括基于netpoll的kitex框架: https://github.com/cloudwego/kitex-benchmark 至少我和一些小伙伴实测netpoll、kitex得到的结论,与他们仓库里自带的数据存在较大差异,这可能是各方测试的实际环境有所差异,也有可能是其他的原因导致

单就LinkBuffer而言,其实用处不大,反倒使代码逻辑复杂化了、易用性降低了、性能可能下降(至少我实测得到的结论是netpoll的吞吐率是标准库和几个框架里最低的)

lesismal commented 2 years ago

以写为例,通常直接写入fd就可以了,只有socket写缓冲区满等少数情况才会需要先缓存起来然后等待可写事件再写入,绝大多数情况都是直接把buffer写入就ok了,所以直接buffer更简单搞笑、根本不太需要LinkBuffer的。有一些框架用的是RingBuffer,也是同样道理。数据结构并不是看上去高级就一定效果好,得根据实际使用场景考虑具体的得失

而且根据实测效果,压测时netpoll的内存占用是其他方案甚至几十倍,比如别人占用几十M、netpoll占几个G,我压测是去年做的,不知道他们后续的版本有没有优化或者修复

哦看了下,你也是字节的呀,刚好,有兴趣的话可以实测下,顺便也帮我看下会不会是我测试的netpoll代码有问题,如果有问题帮我更正下。。。

byene0923 commented 2 years ago

nbio没有使用 2^N

嗯了解的,最近也在看fasthttpp的源码,顺道提了句。

哦看了下,你也是字节的呀

是的,不过我不是基架那边的同学 :),目前在学习协议和网络编程,看了下各大go的各大网络框架,目前发现只有nbio支持异步tls。比较好奇~,感谢作者的耐心解答

有兴趣的话可以实测下,顺便也帮我看下会不会是我测试的netpoll代码有问题,如果有问题帮我更正下

目前正在看gnet/netpoll/nbio的实现^^, 后续会实测验证下。

lesismal commented 2 years ago

可能是因为 fasthttp 作者把 2^N 单独开源了个仓库,所以关注的人比较多 fasthttp是自己的parser,加上各种pool优化,还有raw []byte字段节约了大量的string转换,性能确实好 nbio的http为了兼容标准库,没办法raw []byte,而且还由于上面讲到的,涉及跨协程了,不容易复用同一个read buffer,所以在普通连接数时候没法做到性能极致,单就http而言甚至不如标准库。主要是解决海量并发的性能、占用和稳定性,或者自定制tcp协议之类的不考虑标准库兼容性,就可以做针对优化性能了