从一次文件句柄泄漏排查说起:Linux 服务稳定性检查清单
从一次文件句柄泄漏排查说起:Linux 服务稳定性检查清单
一次持续压测进行到后半程时,接口延迟突然升高,但 CPU、内存和磁盘 I/O 都没有明显异常。应用日志里只有零星的连接超时,重启后又很快恢复。最终排查发现,问题并不是某个慢查询,而是服务打开的文件句柄数量持续上涨,触及进程上限后,新建连接和读取配置都开始失败。
这类问题很适合用来理解 Linux 服务的排查顺序:先用量化指标缩小范围,再沿资源生命周期寻找未释放的位置,最后把一次性修复变成可持续的监控规则。
一、先确认“哪个资源”在恶化
不要一上来就翻代码。先记录同一时间窗口内的 CPU、内存、线程数、文件句柄、网络连接和错误率。对文件句柄,可以先执行:
PID=$(pgrep -f my-service | head -n 1)
cat /proc/$PID/limits | grep -i files
lsof -p "$PID" | wc -l
ss -s
如果错误率上升时 lsof 数量呈阶梯式增长,而 limits 中的 Max open files 已接近上限,就可以把怀疑范围收敛到连接、文件、管道或日志句柄未关闭。这里的关键是连续采样,不要只看某一刻的绝对值。
二、沿资源生命周期定位
文件句柄泄漏通常不是“少写了一个 close”这么简单。应按“获取—使用—异常—回收”四段检查:
- 获取处是否设超时,失败后是否仍占着部分资源;
- 使用中是否有早退分支、异常分支跳过释放;
- 是否把连接或流放进了长期缓存,却没有失效策略;
- 线程池、定时任务和异步回调是否重复创建资源。
Java 服务可重点检查未关闭的 InputStream、Socket、FileChannel 以及连接池租约;Go 服务可关注 defer resp.Body.Close() 是否位于正确的错误检查之后。第三方 SDK 也可能隐藏连接创建逻辑,因此要结合调用栈和句柄类型,而不是只搜索 close。
三、修复时优先解决根因
把 ulimit 临时调大只能争取排查时间,不能替代修复。我的做法是先用受控流量复现,记录增长速率,再逐项修正资源所有权:能由框架管理的连接交给框架,必须手动创建的资源使用确定性的释放结构,重试逻辑加入指数退避和最大次数,避免失败时不断堆积新连接。
修复后至少验证三种路径:正常请求、上游超时、业务异常。只测成功路径,很容易遗漏异常分支中的泄漏。
四、把经验变成自动化防线
上线后我会增加三条低成本监控:
- 每分钟采集进程文件句柄数,并配置相对基线增长告警;
- 同时观察连接数与请求量,区分正常流量增长和异常堆积;
- 在压测中加入持续短连接、上游超时和重试场景,验证资源能否回落。
指标最好保留足够长的时间窗口,因为句柄泄漏往往增长缓慢,单看五分钟图并不明显。
五、上线前检查清单
发布前可以快速确认:所有网络调用是否有超时;异常路径是否释放资源;连接池是否设置最大连接数和空闲回收;重试是否有上限;日志是否可能无限增长;监控能否回答“句柄数为什么上涨”。
文件句柄泄漏并不可怕,可怕的是只看到接口超时,却没有把超时和系统资源联系起来。把资源增长、异常率和关键调用链放在同一张图里,再通过可重复的异常场景验证回收行为,通常比盲目重启更快接近根因。
- 点赞
- 收藏
- 关注作者
评论(0)