手机在枕头边上震了两下。我摸起来看了一眼,是监控发的,blog.mibase.top 返回 502。
当时是凌晨两点十分。我第一反应是想装死,第二天早上再说。但那台机器上还挂着别的东西,万一是一起挂了,拖到早上就麻烦。
爬起来开电脑,连上去看。
现象
|
|
状态是 active (running),但错误日志里一直在刷:
|
|
Too many open files,这个报错我见过,但一直以为离自己很远。
什么是文件描述符
Linux 里一切皆文件。不只是磁盘上的文件,网络连接、管道、设备、socket,在内核眼里都是一个文件,用一个整数来标识,叫文件描述符,简称 fd。
每个进程能同时打开多少个 fd,是有上限的。这个上限来自三个地方,一层压一层。
最外面那层是内核的全局上限,/proc/sys/fs/file-max,一般很大,几十万,通常不会撞到。
往里一层是用户级限制,ulimit -n,这个是每个登录会话的限制。
最里面是进程自己的限制,从父进程继承,可以用 prlimit 查看。
nginx 作为服务跑,它继承的是 systemd 给它的限制,而不是我 SSH 登录之后的 ulimit。这一点很关键,后面会说。
先看当前值
|
|
我那次查出来,nginx 的限制是 1024。
1024 这个数字是老系统的默认值,现在看着确实小。nginx 每个连接要占两个 fd,一个给客户端,一个给上游。1024 意味着理论上最多五百个并发连接,实际还要留一些给日志文件、监听 socket,能用的更少。
我那个站平时并发不高,但那天晚上有人在爬,连接数上去了,直接撞到天花板。
改系统级限制
/etc/security/limits.conf 是老办法:
|
|
这个文件只对通过 PAM 登录的会话生效,也就是 SSH 登录、su 这些。对 systemd 服务不一定管用,因为 systemd 启动服务的时候不走 PAM 的 limits 那一套。
所以还得改 systemd 的配置。
/etc/systemd/system.conf 或者针对单个服务写 override:
|
|
然后:
|
|
验证:
|
|
应该显示 65535。
还有一层:systemd 自己的默认值
Debian 12 和 Ubuntu 22.04 之后,systemd 有个 DefaultLimitNOFILE 的默认值,是 1024:524288。意思是软限制 1024,硬限制 524288。
也就是说,如果你不显式设置,服务拿到的软限制还是 1024,硬限制虽然是 524288,但进程不会自己去把它调高。
这就是为什么很多人改了 limits.conf 之后发现没效果。服务是通过 systemd 起的,不走 PAM。
/etc/systemd/system.conf 里可以全局改:
|
|
改完 daemon-reload,然后重启所有服务才生效。我一般不改全局,只给需要的服务单独写 override,影响面小一点。
nginx 自己的配置也要改
系统限制放开了,nginx 的配置里还有一个上限。
|
|
这个值决定了单个 worker 能同时处理多少个连接。
worker 的数量是 worker_processes,一般设成 CPU 核数或者 auto。总的并发能力大致是 worker_processes × worker_connections。
我把这两个都调了:
|
|
multi_accept on 让一个 worker 一次接受多个新连接,高并发下能减少系统调用。
use epoll 在 Linux 上是默认值,写出来只是为了明确。BSD 系才需要 kqueue。
注意,worker_connections 不能超过 LimitNOFILE。如果系统限制是 1024,你写 8192 也没用,nginx 启动的时候会警告,实际还是被卡在 1024。
一个容易忽略的点:日志文件
nginx 每个 worker 都要打开 access.log 和 error.log。如果开了很多虚拟主机,每个都有自己的日志文件,这个数字会累积。
我那次有七个站点,每个两套日志,加上主配置的,一共十五个文件。乘以 worker 数,就是几十个 fd。虽然不多,但在接近上限的时候,这就是压死骆驼的最后一根稻草。
解决办法是减少日志文件数量,或者用 access_log off 关掉不需要的。我把几个不重要的站的 access log 关了,只留 error log。
排查当时到底谁在占
改配置之前,我想先确认一下是不是真的连接数暴涨。
|
|
输出里有个 TCP: 1234 (estab 890, closed 210, orphaned 0, timewait 130)。890 个 established,对于那个站来说确实异常。
再看是谁:
|
|
有一个 IP 占了三百多个连接。看 User-Agent,是一个很老的爬虫标识,明显是伪造的。
临时封掉:
|
|
连接数立刻下来了。但这只是止血,根本问题还在。
后来做的几件事
最直接的是给 nginx 加连接数限制:
|
|
单个 IP 最多二十个并发连接,超了直接 429。这个比事后封 IP 有效得多。
另一个是 keepalive 的超时。默认 keepalive_timeout 75s,意味着一个空闲连接要占着 fd 七十五秒。我改成 20 秒:
|
|
keepalive_requests 是单个连接最多处理多少个请求,到了就关。默认是 1000,对于被爬的情况,调小能让连接更快轮换出去。
最后是把 LimitNOFILE 写进了所有需要高并发的服务,不只是 nginx。MariaDB、Redis 这些也都有各自的连接数上限,同样受这个限制。
怎么验证改对了
改完之后,我用 ab 压了一下:
|
|
-c 200 是两百个并发。跑的时候盯着 fd 数量:
|
|
这个数字应该随着并发上去,压测结束后回落到一个稳定的值,不报错就说明改对了。
如果还是报 Too many open files,那就是某一层的限制没改到位,按 ulimit → systemd override → nginx 配置这个顺序再检查一遍。
一点延伸
这个限制不只在 nginx 上有。
MySQL 的 open_files_limit、Redis 的 maxclients、Java 应用的 socket 数量,最后都会落到这个 fd 上限上。
还有一个容易被忽略的,就是 Docker 容器。容器里的进程继承的是宿主机的限制,但如果你在 compose 里没设,拿到的可能是 Docker daemon 的默认值。可以在 compose 里显式指定:
|
|
这个写法比在容器里改 /etc/security/limits.conf 靠谱,因为容器里那个文件经常不生效。
现在
那天晚上折腾到快四点。改完配置,压测通过,才回去睡。
现在这台机器的 LimitNOFILE 是 65535,nginx 的 worker_connections 是 8192。跑了几个月,没再出过这个问题。
回头看,这个故障本身不复杂,但它的排查过程逼着我把 Linux 的资源限制体系理了一遍。fd 上限、ulimit、PAM、systemd、cgroup,这几层的关系搞清楚之后,很多类似的问题都能举一反三。
运维这件事,最怕的不是不懂,是以为自己懂。