单机跑满百万tcp长连接在go中必须调优,默认配置必然失败:需显式设置readtimeout、writetimeout(均≤15s)、idletimeout(如60s)和maxheaderbytes(如64kb),否则将因goroutine泄漏、缓冲区内存爆炸和文件描述符耗尽导致服务崩溃。
单机跑满百万 tcp 长连接在 go 里不是“能不能”的问题,而是“不改默认配置就必然失败”的问题。核心矛盾在于:每个连接默认绑一个 goroutine + 缓冲区 + tls 上下文,100 万连接直接吃掉 2–3gb 内存、触发高频 gc、耗尽文件描述符,netstat -an | grep established | wc -l 看似达标,cat /proc/pid/status | grep vmrss 才告诉你真相。
为什么 net/http.Server 默认撑不住百万连接
Go 的 http.Server 是阻塞式模型,底层调用 conn.Read() 会挂起 goroutine,直到数据来或超时。这意味着:
- 每个活跃长连接(Keep-Alive)独占一个 goroutine,栈+缓冲区起步 6KB,100 万就是 ~6GB 虚拟内存压力
-
ReadBufferSize/WriteBufferSize默认 4KB,仅缓冲区就额外占用 ~8GB 地址空间(即使 RSS 没打满,内核页表和 TLB 压力已飙升) - 不设
IdleTimeout时,空闲连接永不释放,ESTABLISHED数持续上涨,最终卡死在accept()或epoll_wait() -
http.Request.Body是io.ReadCloser,但底层net.Conn生命周期由 server 控制,你无法主动归还或复用连接
必须显式覆盖的四个超时参数
漏掉任意一项,百万连接就是内存泄漏现场。这些不是“建议”,是硬性准入条件:
-
ReadTimeout:防慢请求卡住 goroutine,建议 ≤ 15s;上传类场景可放宽,但必须有上限 -
WriteTimeout:防响应生成慢或客户端接收阻塞,同样 ≤ 15s;注意它包含 header 写入时间 -
IdleTimeout:百万连接的生命线,必须设(如60 * time.Second),否则连接永远不退 -
MaxHeaderBytes:限制头部大小(如64 ),防恶意大 header 触发 OOM
示例设置:
server := &http.Server{
Addr: ":8080",
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 64 <h3>sync.Pool 复用高频临时对象</h3><p>每秒万级请求下,<code>json.Decoder</code>、<code>bytes.Buffer</code>、<code>http.Header</code> 这类对象是 GC 主力军。不用 <code>sync.Pool</code>,堆分配压力线性上涨;用了,可降 40–70% <code>Mallocs</code>:</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/learn/7564" title="使用Go语言搭建家庭相册系统-相关课件"><img
src="https://img.php.cn/upload/webcode/000/000/164/636a2b4d84031727.png" alt="使用Go语言搭建家庭相册系统-相关课件" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/learn/7564" title="使用Go语言搭建家庭相册系统-相关课件" class="overflowclass">使用Go语言搭建家庭相册系统-相关课件</a>
<p class="overflowclass">使用Go语言搭建家庭相册系统-相关课件</p>
</div>
<a rel="nofollow" href="/xiazai/learn/7564" title="使用Go语言搭建家庭相册系统-相关课件" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div>
- 为
json.Decoder单独建池:var jsonDecoderPool = sync.Pool{New: func() interface{} { return json.NewDecoder(nil) }} - 使用时
dec := jsonDecoderPool.Get().(*json.Decoder); dec.Reset(r.Body),注意Reset()必须调用 -
Put()前确保对象不持有外部引用(比如闭包捕获了大 slice 底层 array),否则内存无法回收 - 避免把
*http.Request或http.ResponseWriter放进池——它们生命周期由 runtime 控制,放进去等于制造悬挂指针
绕过 http.Server 直接接管 net.Conn
真要压到百万级,http.Server 的抽象层已成负担。必须下沉到 net.Listener 层,自己做 accept() + SetDeadline() + epoll 事件分发:
- 用
net.Listen("tcp", addr)获取 listener,禁用http.Server.Serve() - 启动固定数量 goroutine(如
runtime.NumCPU())轮询listener.Accept(),每个 goroutine 绑定一个epoll实例(或用gobwas库) - 对每个
net.Conn显式调用SetReadDeadline()和SetWriteDeadline(),替代超时字段 - 消息读写走
bufio.Reader/Writer+ 自定义缓冲池,避免每次分配[]byte
此时你不再依赖 http.Request 解析逻辑,而用自定义协议帧(如 length-prefixed binary),彻底甩开 HTTP 头部解析开销。
真正难的不是代码写出来,而是每项优化都得配合监控验证:runtime.ReadMemStats 看 HeapInuse 是否平稳、pprof/goroutine 查是否有 goroutine 泄漏、ss -s 核对 socket 状态分布。没这些数据,所谓“百万连接”只是 netstat 里的幻觉。










