一次网络IO优化的讨论

看项目的tcp通信模块时跟同事的偶然讨论,意外拉出来好几样东西,体验非常棒,记录下来~( ̄0  ̄)y

 

周梦飞 12:08:27
我们的tcp网络直接用的conn,之前在看别人项目里见到有用bufio.Reader优化的
张明 12:13:45
io.readatleast 不是也是使用一个分配好的缓冲区吗。
周梦飞 12:14:35
不是的,用的传进去的那个
周梦飞 12:15:28
这里的r.Read是原始的conn.Read
周梦飞 12:15:59
bufio.NewReader(conn)可以用bufio包上一层
周梦飞 12:16:31
代替conn传进io.readatleast
张明 12:17:14
能看到源吗,这多包了一层,这个读和次数就减少了吗,什么原理。
周梦飞 12:17:33
稍等,我翻下那篇文章
周梦飞 12:19:55
https://zhuanlan.zhihu.com/p/21369473

func ReadFull(r Reader, buf []byte) (n int, err error) {
return ReadAtLeast(r, buf, len(buf))
}
周梦飞 12:20:06
他用的io.ReadFull
周梦飞 12:20:11
跟我们实际是一样的
周梦飞 12:22:24
作者解释的说,bufio帮我们内置一个buf,io发生时数据先写进那个buf里了,我们去取时,先取buf里的,要是不够才会调底层io.Read,以此减少io调用次数
张明 12:25:49
我明白这个意思, 换句话说,就应该像C++的收包过程一样,一性有多少字节,全部收下来,再去解析数据,用逻辑去分包。而不是用读操作去分包。
周梦飞 12:26:45
嗯,是这意思,有io就缓存,逻辑去取缓存的数据
张明 12:27:56
是的,我一开始是打算这样做的,当时还有内存池的一些东西也没有想完善,就用了这种最简 的做法。
张明 12:28:51
是的,现在一个包的内容,还要分三次读。
周梦飞 12:28:56
代码用法也很简单,用bufio包装conn,换下就OK了
张明 12:29:03
理论上是多个包的内容,尽可能一次读。
周梦飞 12:29:11
周梦飞 12:29:40
下午改着跑跑试下
张明 12:29:43
C++的处理粘包也有点麻烦,这个bufio这么好用吗,
张明 12:29:49
我有时间也看看。
周梦飞 12:30:19
看例子,业务层的代码差别不大
张明 12:30:41
还有一个优化也准备后面做,就是还要加一个内存池,不让GC回收。
周梦飞 12:31:02
这个叫达达的很厉害,一直做go服务器的,出了蛮多干货
张明 12:34:12
你看一看,顺便了解三个问题,1, bufio内部是怎么分配的内存,2, 分配多大的内存,3,这个内存是分配一次,还是多次。
周梦飞 12:40:22
OK
张明 12:47:49
想了一下,bufio也不行,肯定也不高效。
周梦飞 12:48:36
什么原因呢?
张明 12:48:38
最高效的做法,还是学C++的处理服务器包的方式。
周梦飞 12:49:31
c++也是recvBuf不断接收io数据,逻辑操作的总是recvBuf
张明 12:50:14
如果bufio每次都新建buf,那用都不能用,现在假设bufio接受用的一个固定大小的buf.
周梦飞 12:50:52
不会每次都新建,不然就数据就被丢了,肯定是一条连接一个buf的
张明 12:51:07
我们每read一次,这个buf的前一部分字节被我们读出来的,他内部是不是要进行数据的移动。
周梦飞 12:51:20
看看底层怎么控制buf的增长、缩短~
周梦飞 12:51:41
这个要看buf的设计了
周梦飞 12:51:54
数组试的,肯定会移动
周梦飞 12:52:06
circle式的就不用
张明 12:52:18
是的,我说的就是这个移动的次数会比我们自己实现移动次数多。
周梦飞 12:52:26
还有游标式的,只有内存不够才会移动
张明 12:53:06
这个我估计我们自己用一个固定的缓冲区来做,比这个会高效一点。
周梦飞 12:53:45
嗯,是有顾虑,看下bufio内存的缓冲咋写的,要是不好,可以参照逻辑写个类似的
张明 12:54:03
我们用一个固定的缓冲区来接收,解析出一个包,移动一次。
张明 12:54:30
包就定为4k。
张明 12:54:48
不用变动,我们目前所有TCP包,都很小.
周梦飞 12:55:23
好的
张明 13:02:06
但其实这样的优化不是很大, @周梦飞 你研究过C++服务器,知道C++服务器里每一个连接都有一个接收缓冲区, 其GO底层就己经这个做了。
张明 13:04:43
这个优化我们需要做,不如C++那边的优化那么大。C++如果没有缓冲区就直接读的网络上的数据,像这样一个包读三次,服务器就完了。
张明 13:05:31
在GO语言里,我们一个包在我们这层逻辑上读了三次,在GO底层,其实一次性就读完了所有的数据。
周梦飞 13:07:12
不一定吧,如果数据还没到,io.Read会阻塞当前线程等数据,数据到了重新唤起
张明 13:07:49
数据没有到,不应该阻塞吗。
张明 13:08:45
GO在底层有一个读网络数据的逻辑,有大概8kb的缓存/
周梦飞 13:13:59
异步的网络架构不用,这个另一个问题了,写法差别也挺大

这个8k是缓存了从系统tcp层取来的数据……那io.Read平常的消耗应该不会很重
如果conn本身的io.Read不是取系统层的数据,那确实没多大必要再包一层buf
张明 13:15:05
嗯,你可以查一些资料确认一下,我也是之前不记得在哪看的。
张明 13:16:09
不过8k缓存了TCP层的数据,但每次取也有锁,代价不大的情况下,包一层buf效率也有好处
周梦飞 13:17:38
待会看下bufio的实现……之前有瞅见过别人跟io.Read(还是Write?)解释调用路径的,找找看
周梦飞 13:19:35
IO调用的开销是什么呢?这得从Go的runtime实现分析起,假设我们这里用到的是一个TCP连接,从`TCPConn.Read()`为入口,我们可以定位到`fd_unix.go`这个文件中的`netFD.Read()`方法。

这个方法中有一个循环调用`syscall.Read()`和`pd.WaitRead()`的过程,这个过程有两个主要开销。

首先是`syscall.Read()`的开销,这个系统调用会在应用程序缓冲区和系统的Socket缓冲区之间复制数据。

其次是`pd.WaitRead()`,因为Go的核心是CSP模型,要让一个线程上可以跑多个Goroutine,其中的关键就是让需要等待IO的Goroutine让出执行线程,当IO事件到达的时候再重新唤醒Go
routine,这样来回切换是有一定开销的。

而我们的这个分包协议的包头很小,有极大的概率是包头和一部分包体甚至是整个包已经在Socket缓冲区等待我们读取,这种情况就很适合使用`bufio.Reader`来优化性能。
周梦飞 13:19:55
就是这段,他跟过系统调用路径,说明消耗点
张明 14:31:05
go在底层,使用的也是iocp, iocp接收数据,怎么会应用层调read的时候,才去复制数据。
周梦飞 14:34:44
如果是用的iocp,那这个syscall.Read很可能只是投递了一次RecvIO,要等完成端口的回调,这个过程中就有系统socket的拷贝了……只是猜测
张明 14:38:40
嗯,不管怎么实现,我们上面需要做的优化,肯定都要做。 
周梦飞 14:43:01
看了下bufio.Read的实现,内部用的游标式buf,不会频繁移动内存

读完后游标会归零,复写之前的数据区,缓冲默认是4096
周梦飞 14:47:22
这个buf貌似不会自动增长,满了后调panic("bufio: tried to fill full buffer")
有个接口NewReaderSize(2)可以指定buf的长度
张明 14:48:17
如果不是circle的,就不可能不移动数据吧
周梦飞 14:49:57
游标式的,如果设计成写满就挂,那也不用移动……c++里的游标到尾巴了,如果前面用掉的区域够,就移动内存块到buf前面,如果还不够就会resize了
张明 14:51:43
这个还是不太好,最好的还是一个固定内存,一次性读完数据(小于4K), 解析所有完整的包,就把剩余的数据(不完整的包),往最前面移动的一次
张明 14:52:25
这个内存不存在resize的问题, IO也会减少。
周梦飞 15:03:56
模仿c++的做法,也可以,readRoutine是个单独的线程,阻塞了没啥
周梦飞 15:16:53
发现bufio实际已经是这样的了,整个文件里调底层Read的只有两个地方
一个是外界传入的[]byte超过4096时候,直接调底层Read,就是你讲的一次读完了
另一个是内部缓存已被读完,调了fill(),里面会用剩余的缓冲去读“b.rd.Read(b.buf[b.w:])”也是足够大的

效果上就是固定大内存去一次性尽可能读全部数据
周梦飞 15:50:38
加上bufio了,再改了下doWrite的写法,用select省下每次都要的判断
张明 15:51:17

Good ( ̄. ̄)+

周梦飞 15:53:37
试了间隔200毫秒发100byte数据,能正常收到,没报错
posted @ 2016-08-11 10:40  *蚂蚁*  阅读(820)  评论(0编辑  收藏  举报