• 一次网络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数据,能正常收到,没报错
  • 相关阅读:
    JQ优化性能
    CSS3 Filter的十种特效
    立即执行函数: (function ( ){...})( ) 与 (function ( ){...}( )) 有什么区别?
    EasyUI DateBox
    Java8接口的默认方法
    MySQL -- insert ignore语句
    建数据库表经验总结
    IntelliJ IDEA 实用快捷键
    从 Java 代码到 CPU 指令
    使用ImmutableMap简化语句
  • 原文地址:https://www.cnblogs.com/3workman/p/5760067.html
Copyright © 2020-2023  润新知