958 字
3 分钟
Go netpoll:高并发网络的秘密武器
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 的问题
| 机制 | 复杂度 | 连接数限制 | 问题 |
|---|---|---|---|
| select | O(n) | 1024(FD_SETSIZE) | 每次调用需传入全部 fd |
| poll | O(n) | 无限制 | 每次调用需传入全部 fd |
| epoll | O(1) | 无限制 | 只通知就绪的 fd |
epoll 的三个 API
int epoll_create(int size); // 创建 epoll 实例int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // 注册/修改/删除 fdint 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() | 初始化 epoll | netpoll_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 } }}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 来检查就绪事件:
func schedule() { // ... 调度逻辑
// 检查 netpoll list := netpoll(0) // 非阻塞检查 if !list.empty() { injectglist(&list) // 将就绪的 goroutine 注入调度队列 }}netpoll 被调用的时机:
- schedule():每次调度时检查
- sysmon():后台监控线程定期检查
- findRunnable():找不到可运行的 G 时检查
- 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.Read3. net.conn.Read → net/fd_posix.go: FD.Read4. 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 切换到运行其他 goroutine10. [内核:数据到达,epoll 通知]11. sysmon/schedule → runtime.netpoll(0)12. netpoll → epoll_wait(0) → 返回就绪 fd13. netpoll → goready(等待的 goroutine)14. goroutine 被放入运行队列15. goroutine 恢复执行16. 再次 syscall.Read(fd, buf) → 成功读取数据17. 返回数据给用户代码性能特征与调优
为什么 Go 网络性能好?
| 特性 | Go 的实现 | 好处 |
|---|---|---|
| 非阻塞 IO | 所有网络 fd 设为非阻塞 | 不阻塞 M |
| epoll 集成 | netpoll 封装 epoll | O(1) 事件通知 |
| goroutine 挂起 | gopark 挂起而非阻塞线程 | M 可运行其他 G |
| 批量唤醒 | netpoll 返回就绪列表 | 减少调度次数 |
调优参数
// 设置 epoll 的最大等待时间runtime.GOMAXPROCS(n) // 更多 P = 更多并发调度
// 使用 SO_REUSEPORT 允许多个 listenerlc := 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 fdQ5:一个 goroutine 阻塞在网络 IO 上,最多等多久?
取决于调度器调用 netpoll 的频率。sysmon 每 10ms 检查一次,schedule 每次调度时检查。所以最坏情况下,goroutine 可能在数据就绪后 10ms 才被唤醒。
小结
- netpoll 封装 epoll,将 Linux 事件机制与 Go 调度器深度集成
- 非阻塞 IO + goroutine 挂起:网络 IO 不阻塞 M,只挂起 G
- epoll_wait 在多个时机被调用:schedule、sysmon、findRunnable
- goroutine 唤醒链路:epoll_wait → goready → injectglist → 调度
- 文件 IO 不走 netpoll,会阻塞 M
参考资料
- Go Runtime Source: netpoll_epoll.go — epoll 实现
- Go Runtime Source: netpoll.go — netpoll 核心逻辑
- Go Runtime Source: proc.go — 调度器集成
- The Go netpoller — 深度分析文章
- Linux epoll(7) — epoll 手册
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
Go netpoll:高并发网络的秘密武器
https://blog.souloss.com/posts/golang/go-netpoll/ 部分信息可能已经过时
相关文章 智能推荐
1
Go 系统调用机制:从用户态到内核态的穿越
golang 深度解析 Go 的系统调用机制——entersyscall/exitsyscall、sysmon 监控、与非阻塞 IO 的配合
2
Go 程序的启动
golang 从汇编入口 `rt0_linux_amd64.s` 出发,追踪 Go 程序启动的完整调用链——runtime 初始化、goroutine 调度器初始化、全局变量初始化,直到用户 main 函数被调用的每一个关键节点。
3
Go map 底层实现:从 hmap 到桶的完整解析
golang 深度解析 Go map 的底层实现——hmap 结构、桶机制、扩容策略、哈希冲突处理与并发安全
4
Go defer/panic/recover 底层实现:延迟调用与栈展开
golang 深度解析 Go defer/panic/recover 的底层实现——_defer 结构体、延迟链表、栈展开机制与性能优化
5
Go slice 与 string 底层实现:从 runtime 结构到性能陷阱
golang 深度解析 Go slice 和 string 的底层实现——runtime 结构、扩容策略、字符串不可变性、与 C 的互操作






