DNS 那事儿刚消停没几天,另一个客户的系统就给我寄来一份"新剧本",比上回还邪门:服务进程活着,心跳正常,端口也在监听,用 telnet 一连,通;可业务请求发过去,全部超时。客户运维在群里发了句我至今记得的话:"它好像醒着,但在装睡。"
我到现场那天,已经是这毛病发作的第三天。规律准得像闹钟:每天下午两点四十左右开始批量超时,重启服务立刻满血复活,第二天照旧。客户的老运维已经总结出经验了——"到点重启,别问为什么"。我问日志呢,他说日志干干净净,连个 error 都翻不出来。
这话反而让我起了疑心。你想啊,一个服务每天准时"装死",却一声 error 都不喊,要么是它真没毛病,要么是它想喊喊不出来。CPU 不忙、内存正常、磁盘剩一大把,我把常规嫌疑人挨个排了一遍,最后剩下一个平时最没人搭理的指标:文件描述符。
这里先补个课。文件描述符(File Descriptor,简称 fd)是 Linux 给每个打开的"东西"发的号牌——不光是文件,网络连接、管道、读写日志,都得先领号牌才能干活。Linux 说"一切皆文件",我那天对这句话有了生理性理解:连 TCP 连接都在排队领号牌。一个进程能领多少张,是有额度的,管这个额度的就是 ulimit(user limit,用户级资源上限),默认值往往就 1024。
我用 lsof(list open files,把进程手里所有号牌列出来的命令)数了一下:1018。过十分钟再看:1020。我干脆搬了个凳子坐在那儿盯着,看它一分钟涨一两张,稳稳当当爬到 1024,然后一动不动。那一刻我基本就明白了:号牌领光了。新连接进门,系统想发新号牌,发不出来,accept(服务端接收新连接的动作)直接报 Too many open files——客人堵在门口,门就是不开。老连接还撑着场面,所以进程"活着";新客一个进不来,所以它"装睡"。
顺便打岔一句,有个细节特别损:打日志这个动作本身,也得先领一张号牌。号牌没了,报错根本写不进文件。所以日志干干净净不是没出事,是它喊救命的那张嘴,恰好被它自己弄丢的号牌堵上了。这是我那次学到的最反常识的一课。
接下来就是揪出谁在偷偷领号牌不还。lsof 往下翻,九成多的号牌都指向同一个目录:/data/tmp/req_dump_*.json。我盯着这串文件名愣了三秒,脸有点热——这是我两周前写的。当时客户反馈一个接口返回内容诡异,我为了留证据,塞了个"诊断开关":每个请求进来,把请求体 dump 成文件存到临时目录。文件确实存了,可写文件的流,我只管开、忘了关。
平时没人发现,是因为漏得慢:QPS(每秒请求数)低的时候,一小时也就漏百来张,1024 张额度够撑一整天。偏偏那阵子客户搞活动,流量翻了一倍多,泄漏点提前引爆,于是每天下午两点四十准时"发病"。重启能好也好解释——进程一死,号牌全数上缴,第二天从头再漏。客户的"到点重启大法",本质上是用改密码的办法对付一个不停借钱的账户。
修复说穿了就两步:先把漏点堵上,再把额度放宽。漏点那头,我把诊断开关改成 try-with-resources(Java 里"用完自动还"的写法,借了号牌自动归还),顺手给临时目录加了定时清理;额度那头,在 systemd(Linux 上负责拉起和管理后台服务的管家)的服务配置里加 LimitNOFILE=65535,等于告诉管家:这个服务的号牌额度放大到 65535。这里有个坑替你踩过了:我一开始改的是 /etc/security/limits.conf,改完重启服务,上限纹丝不动。后来才搞明白,systemd 拉起来的服务压根不看那个文件,人家只认自己配置里的 LimitNOFILE。号牌谁发的,提额就得找谁。
打岔第二回:我一直觉得"重启治百病"是运维界的止痛药,不治病,但确实不疼。那三天客户靠它续命,我笑不出来——因为顺手一查,我们自己网关服务的 ulimit 也还是默认的 1024,量一大就是同一颗雷,只是还没轮到它炸。
多说一句,fd 泄漏不止这一种长相。有人是 HTTP 客户端每次请求都新建一个不复用,连接关不掉;有人是连接池配了上限却没配回收,号牌借出去就不还;还有的更隐蔽——某个库在抛异常的路径上没写 close,平时一点事没有,一报错就开始漏。所以案例你背不完,记一个动作就行:服务上线后隔三差五跑一下 lsof -p 进程号 | wc -l,这个数应该是一条平稳的波浪线,而不是一条只涨不跌的楼梯。
这套东西适合谁?只要你写过后端服务,或者管着别人写的服务,都值得花半小时搞懂——它不挑语言,Java、Go、Python 的进程一样会漏。但如果你现在只在本地跑跑脚本,或者服务流量小、额度闲置一大把,倒不用焦虑,知道"号牌"这回事就行,等你第一次撞上"进程活着却没人连得上",今天这篇就是排查地图。
我现在养成了个习惯,跟上篇记物业电话一样土但好用:新接手一个服务,先看一眼 ulimit -n 的值,再看一眼进程当前的 fd 数。你呢?有没有遇到过"进程活着、端口通着、就是不干活"的灵异现场?最后揪出来的凶手是谁?评论区聊聊——我猜不少人会写"重启之后就再没复现过",那不是好了,那是它在憋大招。
下一篇预告:号牌是进程的事,还有一种更安静的死法——磁盘明明没存多少东西,空间却满了,查到最后发现连"文件名"本身都成了负担。下篇聊聊 inode 耗尽的故事。