systemd服务的LimitNOFILE必须显式配置,因其默认不读取/etc/security/limits.conf;需在服务单元文件[Service]段添加LimitNOFILE=65536或LimitNOFILE=65536:65536,执行daemon-reload与restart后,再通过/proc/PID/limits验证软硬限制是否生效。

systemd 服务的 LimitNOFILE 必须显式配置
Go 程序跑在 systemd 下却卡在 1024 句柄,不是 Go 写得有问题,而是 systemd 根本不读 /etc/security/limits.conf。哪怕你配了 * soft nofile 65536,systemctl start myapp 启动的进程照样继承默认限制。
实操要点:
- 编辑服务单元文件,例如
/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 或非登录 shell 启动时,LimitNOFILE 可能被绕过或延迟应用。靠操作系统“给”不如自己“要”。
实操建议:
- 在
main()最开头就调用:syscall.Setrlimit(syscall.RLIMIT_NOFILE, &syscall.Rlimit{Cur: 65536, Max: 65536}) - 务必检查返回 error,失败时应 panic 或 log,不能静默忽略
- 不要依赖
ulimit -n的输出来判断——它只反映当前 shell 环境,和 Go 进程实际限制无关 - 若程序 fork 子进程(如 exec.Command),子进程会继承父进程的 rlimit,无需重复设置
http.Transport 的 KeepAlive 不关,句柄涨得比配置还快
Go 的 http.Client 默认启用连接复用,http.Transport 会缓存空闲连接。压测时若并发高、请求频次密,连接池会迅速膨胀,大量连接卡在 ESTABLISHED 或 CLOSE_WAIT 状态,FD 数直逼上限。
关键参数与取舍:
- 设
DisableKeepAlives: true:每次请求新建连接并立即关闭,彻底避免复用,适合短生命周期调用(如 CLI 工具、批处理) - 更稳妥的做法是调优连接池:
MaxIdleConns: 100、MaxIdleConnsPerHost: 100、IdleConnTimeout: 30 * time.Second - 漏掉
defer resp.Body.Close()是高频泄漏源,尤其在 error 分支里——一旦没关,fd 就永远卡住 - 注意:TLS 握手本身也占 fd,
InsecureSkipVerify: true不减少句柄消耗,只跳过证书校验
fs.file-max 是全局兜底,但日常根本碰不到它
看到 Too many open files 就去调 fs.file-max?大概率白忙。这个内核参数控制的是整个系统能分配的最大 fd 总数(比如 2M),而绝大多数问题出在单进程限制(1024/4096)上。
什么时候才真要动它:
- 你有成百上千个进程,每个都开几千 fd,
cat /proc/sys/fs/file-nr第二列接近file-max值 - 临时调:
sudo sysctl -w fs.file-max=2097152 - 永久调:写入
/etc/sysctl.conf,加fs.file-max = 2097152,再sudo sysctl -p - 注意:每个 fd 占约 1KB 内核内存,设太高会吃光 slab,一般 2M 足够撑住百万级连接场景
真正卡住的从来不是内核,而是你没看清限制在哪一层生效:systemd 不认 limits.conf,Go 不自动申请上限,http.Transport 默认攒着连接不放——三层叠加,1024 就成了铁壁。动手前先看 /proc/<pid>/limits</pid>,别让配置躺在文件里睡大觉。











