2021-10-18 16:39:32

Go 网络并发模型

Go 网络并发模型

主要内容来自 潘少 的 gnet开源说。 https://github.com/gocn/opentalk

image-20211018162906994

Go 原生网络模型(netpoller),编程模式是 goroutine- per-connection ,在这种模式下,开发者使用的是同 步的模式去编写异步的逻辑而且对于开发者来说 I/O 是否阻塞是无感知的,也就是说开发者无需考虑 goroutines 甚至更底层的线程、进程的调度和上下文 切换。

而 Go netpoller 最底层的事件驱动技术肯定是基于 epoll/kqueue/iocp 这一类的 I/O 事件驱动技术,只不 过是把这些调度和上下文切换的工作转移到了 runtime 的 Go scheduler,让它来负责调度goroutines,从而极大地降低了程序员的心智负担!

底层原理

首先,client 连接 server 的时候,listener 通过 accept 调用接收新 connection,每一个新 connection 都启动一个 goroutine 处理,accept 调用会把该 connection 的 fd 连带所在的 goroutine 上下文信息封装注册到 epoll 的监听列表里去,当 goroutine 调用 conn.Read 或
者 conn.Write 等需要阻塞等待的函数时,会被 gopark 给封存起来并使之休眠,让 P 去执行本地 调度队列里的下一个可执行的 goroutine,往后 Go scheduler 会在循环调度

的 runtime.schedule() 函数以及 sysmon 监控线程中调用 runtime.netpoll 以获取可运行的 goroutine 列表并通过调用 injectglist 把剩下的 g 放入全局调度队列或者当前 P 本地调度队列去重 新执行。

那么当 I/O 事件发生之后,netpoller 是通过什么方式唤醒那些在 I/O wait 的 goroutine 的?答案是 通过 runtime.netpoll。

基本流程

image-20211018163146436

价值

netpoll 通过使用非阻塞 I/O,避免让操作网络 I/O 的 goroutine 陷入到系统调用从而进入内核态, 因为一旦进入内核态,整个程序的控制权就会发生转移(到内核),不再属于用户进程了,那么也就 无法借助于 Go 强大的 runtime scheduler 来调度业务程序的并发了;而有了 netpoll 之后,借助于 非阻塞 I/O ,G 就再也不会因为系统调用的读写而 (⻓时间) 陷入内核态,当 G 被阻塞在某个 network I/O 操作上时,实际上它不是因为陷入内核态被阻塞住了,而是被 Go runtime 调用 gopark 给 park 住了,此时 G 会被放置到某个 wait queue 中,而 M 会尝试运行下一个 _Grunnable 的 G,如果此时没有 _Grunnable 的 G 供 M 运行,那么 M 将解绑 P,并进入 sleep 状态。

当 I/O available,在 epoll 的 eventpoll.rdr 中等待的 G 会被放到 eventpoll.rdllist 链表里并通 过 netpoll 中的 epoll_wait 系统调用返回放置到全局调度队列或者 P 的本地调度队列,标记 为 _Grunnable ,等待 P 绑定 M 恢复执行。

问题

Go netpoller 的设计不可谓不精巧、性能也不可谓不高,配合 goroutine 开发网络应用的时候就一个字:爽。因此 Go 的网络编程模式是及其简洁高效 的,然而,没有任何一种设计和架构是完美的, goroutine-per-connection 这种模式虽然简单高效,但是在某些极端的场景下也会暴露出问题: goroutine 虽然非常轻量,它的自定义栈内存初始值仅为 2KB,后面按需扩容;海量连接的业务场景下, goroutine-per-connection ,此时 goroutine 数 量以及消耗的资源就会呈线性趋势暴涨,虽然 Go scheduler 内部做了 g 的缓存链表,可以一定程度上缓解高频创建销毁 goroutine 的压力,但是对于瞬 时性暴涨的⻓连接场景就无能为力了,大量的 goroutines 会被不断创建出来,从而对 Go runtime scheduler 造成极大的调度压力和侵占系统资源,然后 资源被侵占又反过来影响 Go scheduler 的调度,进而导致性能下降。

真实的网络服务

连接的状态

image-20211018163403547

Reactor 网络并发模型

目前 Linux 平台上主流的高性能网络库/框架中,大都采用 Reactor 模式,比如 netty、libevent、

libev、ACE,POE(Perl)、Twisted(Python)等。
Reactor 模式本质上指的是使用 I/O 多路复用(I/O multiplexing) + 非阻塞 I/O(non-blocking I/O) 的

模式。

通常设置一个主线程负责做 event-loop 事件循环和 I/O 读写,通过 select/poll/epoll_wait 等系统调 用监听 I/O 事件,业务逻辑提交给其他工作线程去做。而所谓『非阻塞 I/O』的核心思想是指避免 阻塞在 read() 或者 write() 或者其他的 I/O 系统调用上,这样可以最大限度的复用 event-loop 线 程,让一个线程能服务于多个 sockets。在 Reactor 模式中,I/O 线程只能阻塞在 I/O multiplexing 函数上(select/poll/epoll_wait)。

image-20211018163503184

compute计算复杂时,使用异步计算。

image-20211018163549637

本文链接:http://blog.go2live.cn/post/go-network.html

-- EOF --