《Go组件设计与实现》-netpoll的总结

主要针对字节跳动的netpoll网络库进行总结。netpoll网络库相比于go本身的net标准库更适合高并发场景。

基础知识

netpoll与go.net库一样使用epoll这种IO多路复用机制处理网络请求。

基本理解

我们知道linux万物皆文件,每个文件有个文件标识符fd,我们可以想象linux提供给我们的socket fd就是操作系统将传输层及以下的协议进行封装抽象化的一个接口。我们可以简单把socket理解成对应的一次tcp连接。 那么网络操作根本上也是针对网卡的IO操作,我们需要读取数据/写入数据,那么如何更加高效地处理数据呢?目前大多数网络库都使用IO多路复用机制,在linux系统中最先进的io多路复用就是epoll机制。

epoll工作方式

1642059597458-57fb017e-7e9a-4bcf-b78b-e06c239b3b6c.png

  • 事件通知机制
  • epoll_ctl/epoll_wait
  • ET(边缘触发)/LT(水平触发)
事件通知机制
  • 注册事件:epoll需要注册一些可读的事件
  • 监听事件:监听到可读的数据
  • 触发事件:通知数据可读

主要还是有两个系统调用:

  • epoll_ctl
  • epoll_wait
工作模式

epoll有两种触发工作模式:ET和LT

  1. ET也叫边缘触发,注册的事件满足条件之后,epoll只会触发一次通知。就算你这一次的读写事件的数据没有处理完,下一次epoll_wait也不会再触发通知。
  2. LT也叫水平触发,注册的事件满足条件之后,不管数据是否读写完成,每一次epoll_wait都会通知当前监听的fd事件。

BIO/NIO

1642063229291-6b59adf6-3116-483c-ab02-b547b08152dc.png

  1. BIO:blocking I/O,阻塞I/O。就是当我们向一个socket发起read的时候,数据读取完成之前一直是阻塞的。
  2. NIO:nonblocking I/O,非阻塞I/O。就是read数据的时候不阻塞,立即返回

那么我们每次发现socket中有可读数据的时候,我们就会开启一个goroutine读取数据。

Netpoll的优化点

go的net库是BIO的,浪费了更多的goroutine在阻塞,并且难以对连接池中的连接进行探活。 netpoll采用了LT的触发方式,这种触发方式也就导致编程思路的不同

ET

1642064579869-64deb77c-4845-4103-b735-94a0eb19937b.png

LT

1642064598829-68afa0a4-1e15-4598-aca0-a4b924e1645c.png

netpoll采用LT的编程思路 由于netpoll想在 系统调用 和 buffer 上做优化,所以采用LT的形式。

优化系统调用

syscall这个方法其实有三步:

  1. enter_runtime
  2. raw_syscall
  3. exit_runtime

由于系统调用是一个耗时的阻塞操作,容易造成goroutine阻塞,所以需要加入一些runtime的调度流程。 但是,epoll_wait触发的事件,保证不会被阻塞,所以netpoll直接采用RawSyscall方法做系统调用,跳过了runtime的一些逻辑。

优化调度

使用msec动态调参和runtime.Gosched主动让出P

msec动态调参

epoll_wait的系统调用有个参数是,等待时间,设置成-1是无限等待事件到来,0是不等待。

1642066647357-64124af2-f093-4edd-bb41-d7117ce77b4d.png

这样就有事件到来的时候下次循环的epoll_wait采用立即返回,没有事件就一直阻塞,减少反复无用的调用。

runtime.Gosched主动让出P

如果msec为-1的话会立即进入下一次循环,开启新的epoll_wait调用,那么调用就阻塞在这里,goroutine阻塞时间长了之后会被runtime切换掉,只能等到下一次执行这个goroutine才行,导致时间浪费。 netpoll调用runtime.Gosched方法主动将GMP中的P让出,减少runtime的调度过程

优化buffer

我们在读取和写入数据的时候需要使用到buffer。 多数框架使用环形buffer,可以做到流式读写。但是环形buffer容量是死的,需要扩容的话,需要重新copy数组,引入了很多的并发问题。

LinkBuffer

netpoll使用的buffer实现包括:

  • 链表解决扩容copy问题
  • sync.Pool复用链表节点
  • atomic访问size,规避data race和锁竞争

还有一些nocopy方面的优化,减少了write和read的次数,从而提高了读取和发送的时候的编解码效率。

更多信息看:https://www.cloudwego.io/zh/blog/2021/10/09/字节跳动在-go-网络库上的实践/#nocopy-buffer

posted @ 2022-01-13 18:10  aaayi  阅读(1042)  评论(1编辑  收藏  举报