真正卡住go程序高并发长连接的是linux内核的文件描述符限制,需同时配置systemd的limitnofile、go程序内syscall.setrlimit及全局fs.file-max参数。

Go 程序在单机维持大量长连接(比如几十万 HTTP 客户端连接或百万级 TCP 连接)时,真正卡住你的不是 Go 本身,而是 Linux 内核对单个进程能打开的文件描述符(file descriptor, FD)数量的限制。这个限制默认是 1024,远低于高并发场景需求。
为什么 ulimit -n 改了还是报 too many open files
常见现象:你在 shell 里执行 ulimit -n 65535,然后 go run main.go,结果一压测就崩——too many open files。这是因为:
-
ulimit -n只影响当前 shell 及其直接子进程;如果你用systemd启动服务(比如systemctl start myapp.service),它完全不读limits.conf,也不继承 shell 的 ulimit - 软限制(soft limit)不能超过硬限制(hard limit),而硬限制默认常为 4096,你没改 hard 就改 soft,会静默失败
- Go 程序启动后,内核实际允许的 FD 上限,得看
/proc/<pid>/limits</pid>里的Max open files行,不是你终端里ulimit -n的输出
systemd 服务必须显式配 LimitNOFILE
绝大多数生产环境 Go 服务都走 systemd,而它绕过 PAM 和 limits.conf。不配就永远卡在 1024 或 4096。
- 编辑服务单元文件,例如
/etc/systemd/system/myapp.service - 在
[Service]段下加一行:LimitNOFILE=65536(同时设 soft 和 hard);若需区分,写成LimitNOFILE=65536:65536 - 改完必须执行:
sudo systemctl daemon-reload && sudo systemctl restart myapp - 验证是否生效:
cat /proc/$(pidof myapp)/limits | grep "Max open files",确认两列值都 ≥ 65536
Go 程序内部别漏调 syscall.Setrlimit
即使 systemd 配好了,某些 Go 进程(尤其容器内、或由 supervisord 启动)可能仍拿不到预期值。稳妥做法是在程序启动时主动申请:
import "syscall"
func init() {
var rLimit syscall.Rlimit
rLimit.Cur = 65536
rLimit.Max = 65536
if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil {
log.Fatal("failed to set rlimit:", err)
}
}
- 放在
main()开头或init()里都行,但必须早于任何网络连接建立 - 注意:普通用户进程无法把
Max设得比系统硬限制还高;root 进程可突破,但通常没必要 - 该调用只影响当前进程,不影响子进程(如 exec.Command 启的)
别只盯着进程限制,fs.file-max 也得够用
单个进程 FD 上限只是第一层。内核总句柄池上限 fs.file-max 必须足够大,否则所有进程加起来一超,新连接直接被内核拒绝。
- 查看当前值:
cat /proc/sys/fs/file-max - 临时调高:
sudo sysctl -w fs.file-max=2097152 - 永久生效:在
/etc/sysctl.conf加一行fs.file-max = 2097152,再运行sudo sysctl -p - 建议值:按单机最大预期连接数 × 1.5~2 倍估算,比如目标支撑 100 万连接,
fs.file-max至少设到 200 万
真正容易被忽略的是:fs.file-max 不足时,错误日志里往往没有明确提示,只表现为随机连接失败或 accept 返回 EMFILE,排查时得从全局句柄使用量入手,而不是只盯一个进程。











