mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
958 字
3 分钟
Go netpoll:高并发网络的秘密武器
2023-03-09

Go 能轻松处理百万级并发连接,核心秘密就是 netpoll——将 Linux epoll(或 macOS kqueue)与 Go 调度器深度集成。当一个 goroutine 在网络 IO 上阻塞时,它不会阻塞操作系统线程,而是挂起 goroutine,将 M 让出来运行其他 goroutine。当数据就绪时,epoll 唤醒对应的 goroutine 继续执行。

本文要点#

  • epoll 基础:为什么比 select/poll 高效
  • Go netpoll 的架构:pollDesc 结构体
  • 非阻塞 IO + epoll 的工作流程
  • goroutine 挂起与唤醒的完整链路
  • netpoll 与调度器的集成点
  • conn.Read() 的底层全流程
  • 性能特征与调优

epoll 基础#

select/poll 的问题#

机制复杂度连接数限制问题
selectO(n)1024(FD_SETSIZE)每次调用需传入全部 fd
pollO(n)无限制每次调用需传入全部 fd
epollO(1)无限制只通知就绪的 fd

epoll 的三个 API#

int epoll_create(int size); // 创建 epoll 实例
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // 注册/修改/删除 fd
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); // 等待就绪
flowchart TD A["epoll_create()"] --> B["创建 epoll 实例"] B --> C["epoll_ctl(ADD)<br/>注册 fd 到 epoll"] C --> D["epoll_wait()<br/>等待就绪事件"] D --> E{"有 fd 就绪?"} E --> |"是"| F["返回就绪的 fd 列表"] E --> |"否"| G["阻塞等待<br/>(或超时返回)"] F --> H["处理就绪事件"] H --> D style F fill:#4CAF50,color:#fff

Go netpoll 架构#

pollDesc 结构体#

每个网络连接在 Go 中对应一个 pollDesc

// src/runtime/netpoll.go (简化版)
type pollDesc struct {
link *pollDesc // 链表:全局 poller 链表
fd uintptr // 文件描述符
// 读写等待的 goroutine
rg uintptr // 等待读的 goroutine(或 pdReady/pdWait)
wg uintptr // 等待写的 goroutine
}

netpoll 的核心函数#

函数作用源码
netpollinit()初始化 epollnetpoll_epoll.go
netpollopen(fd, pd)注册 fd 到 epoll同上
netpollclose(fd)从 epoll 删除 fd同上
netpoll(delay)调用 epoll_wait 获取就绪事件同上

非阻塞 IO + epoll 工作流程#

连接建立#

sequenceDiagram participant G as Goroutine participant PD as pollDesc participant EP as epoll G->>PD: socket() → 设置非阻塞 G->>PD: connect() → EINPROGRESS G->>EP: epoll_ctl(ADD, fd, EPOLLOUT) EP-->>G: epoll_wait → fd 可写 G->>PD: 连接建立成功

读取数据#

flowchart TD A["goroutine 调用 conn.Read()"] --> B["syscall.Read(fd, buf)"] B --> C{"返回 EAGAIN?"} C --> |"否(有数据)"| D["直接返回数据"] C --> |"是(无数据)"| E["pd.WaitRead()"] E --> F["将当前 g 存入 pd.rg"] F --> G["gopark() — 挂起 goroutine"] G --> H["M 继续运行其他 goroutine"] H --> I["epoll_wait 返回:fd 可读"] I --> J["netpoll() 获取就绪的 goroutine 列表"] J --> K["goready() — 唤醒等待的 goroutine"] K --> L["goroutine 恢复执行"] L --> M["再次 syscall.Read()"] M --> N["这次有数据,返回"] style G fill:#FF9800,color:#fff style K fill:#4CAF50,color:#fff

关键代码路径#

// src/internal/poll/fd.go (简化版)
func (fd *FD) Read(p []byte) (int, error) {
for {
n, err := ignoringEINTRIO(syscall.Read, fd.Sysfd, p)
if err != syscall.EAGAIN {
return n, err // 有数据或真实错误
}
// EAGAIN:没有数据,等待
if err = fd.pd.WaitRead(); err != nil {
return n, err
}
}
}
src/internal/poll/fd_poll_runtime.go
func (pd *pollDesc) WaitRead() error {
return pd.wait('r')
}
func (pd *pollDesc) wait(mode int) error {
// 将当前 goroutine 注册到 pollDesc
netpollblock(pd, mode) // 挂起 goroutine
return nil
}

goroutine 挂起与唤醒#

挂起:netpollblock#

// src/runtime/netpoll.go (简化版)
func netpollblock(pd *pollDesc, mode int) bool {
gpp := &pd.rg // 读等待
if mode == 'w' {
gpp = &pd.wg // 写等待
}
// 设置 pdWait 标记
for {
old := atomic.Loaduintptr(gpp)
if old == pdReady {
atomic.Casuintptr(gpp, pdReady, 0)
return true // 已经就绪,不需要等待
}
if atomic.Casuintptr(gpp, 0, pdWait) {
break // 成功设置为等待状态
}
}
// 挂起当前 goroutine
gopark(netpollblockcommit, unsafe.Pointer(gpp), waitReasonIOWait, traceEvGoBlockNet, 5)
return true
}

唤醒:netpoll#

调度器在多个时机调用 netpoll 来检查就绪事件:

src/runtime/proc.go
func schedule() {
// ... 调度逻辑
// 检查 netpoll
list := netpoll(0) // 非阻塞检查
if !list.empty() {
injectglist(&list) // 将就绪的 goroutine 注入调度队列
}
}

netpoll 被调用的时机:

  1. schedule():每次调度时检查
  2. sysmon():后台监控线程定期检查
  3. findRunnable():找不到可运行的 G 时检查
  4. GC 的 stopTheWorld 之后:恢复前检查

netpoll 与调度器的集成#

graph TD subgraph "调度器" S1["schedule()"] S2["findRunnable()"] S3["sysmon()"] end subgraph "netpoll" N1["netpoll(0) — 非阻塞"] N2["epoll_wait(0)"] N3["返回就绪 goroutine 列表"] end subgraph "goroutine 唤醒" R1["injectglist()"] R2["goready()"] R3["放入本地或全局队列"] end S1 --> N1 S2 --> N1 S3 --> N1 N1 --> N2 N2 --> N3 N3 --> R1 R1 --> R2 R2 --> R3 style N2 fill:#4CAF50,color:#fff style R3 fill:#FF9800,color:#fff

conn.Read() 的完整底层流程#

1. 用户代码:n, err := conn.Read(buf)
2. net.Conn.Read → net.TCPConn.Read → net.conn.Read
3. net.conn.Read → net/fd_posix.go: FD.Read
4. FD.Read → syscall.Read(fd, buf) [非阻塞]
5. syscall.Read 返回 EAGAIN(无数据)
6. FD.Read → pollDesc.WaitRead()
7. WaitRead → runtime.netpollblock()
8. netpollblock → runtime.gopark() [挂起 goroutine]
9. 调度器:M 切换到运行其他 goroutine
10. [内核:数据到达,epoll 通知]
11. sysmon/schedule → runtime.netpoll(0)
12. netpoll → epoll_wait(0) → 返回就绪 fd
13. netpoll → goready(等待的 goroutine)
14. goroutine 被放入运行队列
15. goroutine 恢复执行
16. 再次 syscall.Read(fd, buf) → 成功读取数据
17. 返回数据给用户代码

性能特征与调优#

为什么 Go 网络性能好?#

特性Go 的实现好处
非阻塞 IO所有网络 fd 设为非阻塞不阻塞 M
epoll 集成netpoll 封装 epollO(1) 事件通知
goroutine 挂起gopark 挂起而非阻塞线程M 可运行其他 G
批量唤醒netpoll 返回就绪列表减少调度次数

调优参数#

// 设置 epoll 的最大等待时间
runtime.GOMAXPROCS(n) // 更多 P = 更多并发调度
// 使用 SO_REUSEPORT 允许多个 listener
lc := net.ListenConfig{
Control: func(network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) {
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEPORT, 1)
})
},
}

常见问题 FAQ#

Q1:Go 的 netpoll 和 nginx 的 epoll 有什么区别?#

核心机制相同(都使用 epoll),但集成方式不同:nginx 是事件驱动回调模型,Go 是 goroutine 阻塞/唤醒模型。Go 的优势是编程模型更简单(同步写法),nginx 的优势是没有调度器开销。

Q2:为什么 Go 不用 io_uring?#

Go 团队正在评估 io_uring,但目前 netpoll+epoll 已经足够好。io_uring 的优势在大量小 IO 场景,但 Go 的 goroutine 模型已经通过并发掩盖了 IO 延迟。Go 1.25+ 有实验性 io_uring 支持。

Q3:netpoll 能用于文件 IO 吗?#

不能。文件 IO 在 Linux 上总是就绪的(不返回 EAGAIN),epoll 对常规文件无效。Go 的文件 IO 使用系统调用,会阻塞 M。

Q4:如何监控 netpoll 的状态?#

# 使用 Go 执行追踪器
$ go test -trace=trace.out
$ go tool trace trace.out
# 查看 epoll 状态
$ cat /proc/$(pidof myprogram)/fdinfo/3 # 3 是 epoll fd

Q5:一个 goroutine 阻塞在网络 IO 上,最多等多久?#

取决于调度器调用 netpoll 的频率。sysmon 每 10ms 检查一次,schedule 每次调度时检查。所以最坏情况下,goroutine 可能在数据就绪后 10ms 才被唤醒。

小结#

  1. netpoll 封装 epoll,将 Linux 事件机制与 Go 调度器深度集成
  2. 非阻塞 IO + goroutine 挂起:网络 IO 不阻塞 M,只挂起 G
  3. epoll_wait 在多个时机被调用:schedule、sysmon、findRunnable
  4. goroutine 唤醒链路:epoll_wait → goready → injectglist → 调度
  5. 文件 IO 不走 netpoll,会阻塞 M

参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

Go netpoll:高并发网络的秘密武器
https://blog.souloss.com/posts/golang/go-netpoll/
作者
Souloss
发布于
2023-03-09
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时