go不优化系统调用,而是通过绕过、缓冲、分流、超时等手段规避其瓶颈:如用so_reuseport分流accept、bufio缓冲i/o、context超时控制阻塞调用、http/2替代http/1.1提升连接复用效率。

Go 本身不“优化”系统调用,而是让你别轻易触发它、别在错误位置卡住、别让 syscall 成为调度瓶颈——真正有效的手段是绕过、缓冲、分流、超时。
net.Listener.Accept 阻塞是最大单点瓶颈
默认 http.ListenAndServe 只启一个 listener goroutine,所有连接都挤在同一个 socket listen queue 上。压测时 CPU 很低但 QPS 上不去,ss -s 显示 ListenOverflows,基本就是它卡死了。
- 必须用
net.ListenConfig+SO_REUSEPORT,让内核把新连接 hash 分发到多个 listener goroutine - listener 数量建议等于
runtime.NumCPU(),每个跑自己的for { conn, _ := lis.Accept() } - Linux 3.9+、Go 1.11+ 才支持;macOS 有类似机制;Windows 需 fallback 到单 listener + 连接队列分发
- 别在 handler 里直接
accept()后自己处理连接:应accept后立刻syscall.Dup复制 fd,再把新 fd 交给 worker
小块 I/O 会把 syscall 开销放大十倍
每次 conn.Read 或 os.File.Write 都是一次系统调用,而上下文切换成本远高于内存拷贝。高频小读写(比如每行日志都 WriteString)会让性能掉到十分之一。
- 一律用
bufio.NewReader和bufio.NewWriter包装连接或文件,显式设缓冲区大小(如64 * 1024) - 写入后必须调用
writer.Flush(),否则数据可能滞留用户态缓冲区不落网/不落盘 - 别在循环里反复
new bufio.Reader:要么复用,要么从sync.Pool取 - 大文件别用
os.ReadFile,改用io.CopyBuffer(dst, src, buf)流式处理,buf从池里拿
goroutine 卡在 netpoll 不等于“忙”,而是“等内核通知”
pprof 看到大量 goroutine 处于 runtime.netpoll 状态,不是它们在计算,是在等 socket 就绪、文件可读、定时器触发——这是正常现象,但若数量持续上涨,说明上层没及时消费或分发。
- 检查是否在 handler 里做了阻塞操作(如未设超时的
http.Client.Do、裸调time.Sleep) - 用
context.WithTimeout包裹所有外部依赖调用,避免一个慢请求拖垮整条 worker 链 - 别让 goroutine 直接
accept()后自己处理连接;应 accept 后立刻syscall.Dup复制 fd,再把新 fd 交给 worker - 若真要手写 epoll(仅限 Linux 网关类服务),必须
runtime.LockOSThread(),否则 M 切换导致 epoll fd 失效
HTTP/2 和连接池比调参更直接有效
HTTP/1.1 的 Keep-Alive 是“串行复用”,单连接同一时间只能处理一个请求;HTTP/2 才是真正的并发复用——单连接上可同时发多个 request,响应乱序返回也不影响。
- Go 1.6+ 原生支持 HTTP/2,但硬性前提:必须用 TLS 启动,且证书合法(自签名需客户端显式信任)
- 纯 HTTP 端口(如
:8080)无法协商 HTTP/2;得用mkcert+ ALPN,h2才算成功 -
http.DefaultClient在高并发下会崩:默认MaxIdleConnsPerHost = 0,每发一次http.Get就新建 TCP 连接 -
MaxConnsPerHost是硬上限(活跃 + 空闲总和),MaxIdleConnsPerHost只是“空闲中可保留多少”
真正难的不是知道该用 SO_REUSEPORT 或 bufio,而是你得在代码里主动放弃“简单写法”——比如不用 http.ListenAndServe,不裸调 conn.Read,不在 handler 里起 goroutine 而不控速。这些地方不改,再多的 go 关键字也救不了 syscall 瓶颈。











