次要针对字节跳动的 netpoll 网络库进行总结。netpoll 网络库相比于 go 自身的 net 规范库更适宜高并发场景。
基础知识
netpoll 与 go.net 库一样应用 epoll 这种 IO 多路复用机制解决网络申请。
根本了解
咱们晓得 linux 万物皆文件,每个文件有个文件标识符 fd,咱们能够设想 linux 提供给咱们的 socket fd 就是操作系统将传输层及以下的协定进行封装抽象化的一个接口。咱们能够简略把 socket 了解成对应的一次 tcp 连贯。那么网络操作基本上也是针对网卡的 IO 操作,咱们须要 读取数据 / 写入数据 ,那么如何更加高效地解决数据呢?目前大多数网络库都应用IO 多路复用机制,在 linux 零碎中最先进的 io 多路复用就是epoll 机制。
epoll 工作形式
- 事件告诉机制
- epoll_ctl/epoll_wait
- ET(边缘触发)/LT(程度触发)
事件告诉机制
- 注册事件:epoll 须要注册一些可读的事件
- 监听事件:监听到可读的数据
- 触发事件:告诉数据可读
次要还是有两个零碎调用:
- epoll_ctl
- epoll_wait
工作模式
epoll 有两种触发工作模式:ET 和 LT
- ET 也叫 边缘触发,注册的事件满足条件之后,epoll 只会触发一次告诉。就算你这一次的读写事件的数据没有解决完,下一次 epoll_wait 也不会再触发告诉。
- LT 也叫 程度触发,注册的事件满足条件之后,不论数据是否读写实现,每一次 epoll_wait 都会告诉以后监听的 fd 事件。
BIO/NIO
- BIO:blocking I/O,阻塞 I /O。就是当咱们向一个 socket 发动 read 的时候,数据读取实现之前 始终是阻塞 的。
- NIO:nonblocking I/O,非阻塞 I /O。就是 read 数据的时候 不阻塞,立刻返回。
那么咱们每次发现 socket 中有可读数据的时候,咱们就会开启一个 goroutine 读取数据。
Netpoll 的优化点
go 的 net 库是 BIO 的,节约了 更多的 goroutine 在阻塞,并且难以对连接池中的连贯进行探活。netpoll 采纳了 LT 的触发形式,这种触发形式也就导致编程思路的不同
ET
LT
netpoll 采纳 LT 的编程思路 因为 netpoll 想在 零碎调用 和 buffer 上做优化,所以采纳 LT 的模式。
优化零碎调用
syscall 这个办法其实有三步:
- enter_runtime
- raw_syscall
- exit_runtime
因为零碎调用是一个耗时的阻塞操作,容易造成 goroutine 阻塞,所以须要退出一些runtime 的调度流程。然而,epoll_wait 触发的事件,保障不会被阻塞,所以 netpoll 间接采纳 RawSyscall 办法做零碎调用,跳过了 runtime 的一些逻辑。
优化调度
应用 msec 动静调参和 runtime.Gosched 被动让出 P
msec 动静调参
epoll_wait 的零碎调用有个参数是,等待时间,设置成 - 1 是有限期待事件到来,0 是不期待。
这样就有事件到来的时候下次循环的 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/b…