runtime.LockOSThread() 对网络服务无效,因为 Go 运行时仅维护一个全局 netpoller 且绑定单个 OS 线程,导致 epoll_wait 无法多核并行,且后续 Serve() 会因监听器占用而报错。

为什么 runtime.LockOSThread() 对网络服务无效
在 HTTP 服务中调用 runtime.LockOSThread() 并启动多个 goroutine 执行 http.Server.Serve(),不会让 epoll_wait 分散到多核——因为 Go 运行时只维护一个全局网络轮询器(netpoller),它始终绑定在单个 OS 线程上。你看到的“只有一个线程调用 epoll_wait”是设计使然,不是 bug。更糟的是,后续 Serve() 调用会因监听器已被占用而立即返回 http: Server closed 错误,但若没检查 err,就以为“起了多个实例”,实际只是假并发。
SO_REUSEPORT 是多进程方案的前提
Linux 3.9+ 支持 SO_REUSEPORT,允许多个进程绑定同一端口,内核自动做连接分发。Go 1.11+ 提供了标准方式启用它:
l, err := net.ListenConfig{
Control: func(network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) {
syscall.SetsockoptInt32(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEPORT, 1)
})
},
}.Listen(context.Background(), "tcp", ":8080")
不加这个配置,多个进程 bind 同一地址会直接报 address already in use。注意:macOS 不支持 SO_REUSEPORT 的连接级负载均衡,仅支持端口复用,需额外代理层(如 nginx)分发。
绑定进程到 CPU 核心要分两步走
Go 本身不提供跨平台的 CPU 绑定 API,必须依赖系统调用。关键点在于:绑定对象是 OS 线程(M),不是 goroutine(G)。所以得在进程启动后、业务逻辑开始前完成绑定:
- 先用
unix.SchedSetaffinity(0, &cpuset)(Linux)或syscall.SetThreadAffinityMask(Windows)设置当前线程亲和性 - 再调用
runtime.LockOSThread()防止后续 goroutine 被调度到其他线程,破坏绑定效果 - 若进程含 CPU 密集型任务(如编解码),建议每个进程只绑 1–2 个物理核心,并避开超线程逻辑核(例如只用 CPU 0、2、4…)
NUMA 节点感知比单纯绑 CPU 更重要
在双路服务器或 ARM big.LITTLE 架构下,跨 NUMA 节点访问内存延迟可能高 2–3 倍。此时只绑 CPU 不够,还得绑内存节点:
启动时用 numactl --cpunodebind=0 --membind=0 ./myserver,或在代码中调用 unix.MigratePages(需 root 权限)。Go 程序默认分配的内存来自当前线程所在节点的本地内存池,但若进程 fork 出子进程或 mmap 大块内存,仍可能跨节点分配——这点极易被忽略,却是高吞吐场景下性能抖动的主因。


















