news 2026/10/16 11:02:58

Linux服务爆满排查实战:从进程数、文件描述符到systemd调优

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux服务爆满排查实战:从进程数、文件描述符到systemd调优

“服务爆满”这四个字,很多人一听就头大。我处理过的线上故障里,至少有三分之一都能归到这一类:systemd 服务状态异常、进程数莫名其妙冲到几千、端口探针一抓一大把、文件句柄报错刷满日志。表面看是资源被打爆,实际根因往往藏在几个不起眼的配置里。这篇文章就把这些年踩过的坑掰开揉碎了讲,从现象到原理再到操作命令,一条线直接拉到底,适合刚接手服务器运维、或者已经在排查但总找不到头的同学做参考。

先说说适合什么场景:你发现服务器卡死、SSH 连不上、业务接口超时、systemctl执行报错——这些都有可能是“服务爆满”的连锁反应。我会先讲清楚“爆满”到底指什么,再讲排查思路,然后是实战命令和常见问题的避坑记录。全程用我自己在测试环境反复演练过的方案,你可以直接照着敲命令。

1. 先把“爆满”这个词拆透

1.1 服务爆满在不同人嘴里是完全不同的意思

运维群里喊一句“服务爆满了”,不同人会想到完全不同的画面。我最初排查这类问题走了不少弯路,就是因为没先确认对方说的“爆满”是哪一种。后来我把常见的“爆满”分成了四类,排查思路也就清晰了。

第一类是进程数爆满。ps -ef一刷出来几百上千个相同名字的进程,top里 CPU 占用倒不一定高,但系统负载快速飙到几十甚至上百。这类问题的典型原因是某个服务反复重启、定时任务失控派生出大量子进程、或者代码里 fork 之后没有回收。

第二类是端口和连接数爆满。ss -tlnp一看,监听端口没剩几个,TCP 连接数堆积到几万。最明显的表现是新的访问请求迟迟无法建连,业务侧报“connect timeout”。

第三类是文件描述符和句柄爆满。日志里开始刷Too many open files,服务进程突然起不来,systemctl start提示资源问题。这类问题特别隐蔽,因为平时不太会关注ulimit -n,但高并发环境下句柄耗尽非常常见。

第四类是systemd 管理单元本身爆满。服务没启动几个,但systemctl list-units能看到一大堆 dead 或 failed 的残留记录,甚至执行systemctl命令都慢得让人抓狂。

把“爆满”的类型分清楚之后,排查的路径才能对得上。否则光是盯着 CPU 看半天,问题在句柄上,方向就完全错了。

1.2 用停车场的例子理解资源上限

把服务器想象成一个停车场。CPU、内存、磁盘就是车位数,进程就是要求进场的车。正常情况下车来车走,车位一直够用。但如果一个保安忘了闸门,或者一堆车堵在出口不走,车位就会越来越少。

进程表(task list)就是停车场门口那块登记板。内核给登记板画了上限,这个上限由pid_max决定。登记板满了,任何新进程都创建不了,shell 都启动不起来。

文件描述符表就像是停车场的闸机卡。每次服务打开文件、建立网络连接,都会领一张卡。卡的额度由ulimit -n以及 systemd 服务文件里的LimitNOFILE共同决定。卡发完了,再正常的请求也兑现不了。

cgroup 里的tasks限制则相当于停车场分区管理,一个区域最多停多少辆车。systemd 管理服务时经常自动套一个 cgroup,里面有TasksMax限制,到了阈值就拒绝新进程。

理解了这层映射关系,看到“爆满”就不会慌——你只是在跟内核的计数器和资源配置博弈。

2. 从“水土不服”到“根因定位”:排查思路总览

2.1 先看现象定位层,再顺藤摸瓜

排查服务爆满,我的习惯是分三层去看:主机层、OS资源层、服务管理层。

主机层先看四个维度的实时数据:CPU、内存、磁盘空间、inode 用量。很多人只看磁盘空间百分比,没注意 inode 已经耗尽——文件挤占了所有索引节点,新文件根本创建不了,服务自然频繁报错。

OS资源层看的是pid_max、当前进程数、线程数、文件描述符分配情况、TCP连接状态分布。这一层的数据最能直接解释“到底满在哪里”。

服务管理层则聚焦 systemd:服务是否处于 activating/auto-restart 状态?失败次数是不是累计到了阈值?unit 文件的资源限制跟系统默认的差距有没有异常?

排查顺序我一般是这样:先top看负载和CPU,再free -h看内存,然后df -i看 inode。如果负载很高但 CPU 不高,那就去看 IO 或 D 状态进程。如果进程数上万,直接查pid_max。如果连接数异常,ss -s看总量,ss -state established按状态分类。

2.2 关键命令的“组合拳”用法

单独敲一条命令往往看不出名堂,组合起来才有意义。我常用的四条命令几乎能应付大部分场景:

# 看系统整体的运行队列和负载 uptime # 看进程状态分布,D(不可中断)和 Z(僵尸)尤其要注意 ps -eo stat,pid,ppid,comm | awk '$1 ~ /D|Z/ {print}' | head -50 # 看当前系统总进程数和线程数 ps -eLf | wc -l # 看 PID 空间最大值和当前溢出情况 cat /proc/sys/kernel/pid_max

然后进入服务侧:

# 列出异常状态的服务 systemctl list-units --state=failed --no-pager systemctl list-units --state=activating --no-pager # 查看某个服务的详细状态和重启计数 systemctl status rsyslog.service -l --no-pager

这里要提醒一句:systemctl list-units默认不显示 inactive 的单元,但“爆满”场景下恰恰是那些残留的 inactive/dead unit 在拖垮性能。排查时要加上--all参数。

2.3 一个完整的定时收集方法

线上故障最怕“事后诸葛”。如果手上有一个历史基线,排查会快很多。我习惯写一个小脚本丢在 cron 里跑,每分钟采集一次关键指标:

#!/bin/bash LOG_DIR=/var/log/sysdiag mkdir -p $LOG_DIR TS=$(date +%Y%m%d-%H%M%S) { echo "=== $TS ===" uptime echo "--- loadavg ---" cat /proc/loadavg echo "--- process count ---" ps -eLf | wc -l echo "--- fd count ---" cat /proc/sys/fs/file-nr echo "--- zombie count ---" ps -eo stat | grep -c '^Z' echo "--- systemd failed units ---" systemctl list-units --state=failed --no-legend --no-pager | wc -l echo "--- top memory consumers ---" ps -eo pid,ppid,%mem,%cpu,comm --sort=-%mem | head -20 } >> $LOG_DIR/sysdiag.log

这个脚本不到二十行,但爆满前半小时的系统数据几乎都能覆盖。有过几次故障之后你就知道这个习惯有多救命了。

3. 常见根因逐一拆解

3.1 杀手一:PID 空间被耗尽

Linux 下每个进程都有一个唯一 PID,内核通过pid_max来控制 PID 编号的上限。默认值通常是 32768 或 4194304,看起来不小,但某些环境下会被调得很低。

为什么会爆满:大量短生命周期进程被循环创建,比如代码里不断 fork 子进程,完成任务后没有正常 wait,子进程变成僵尸进程。僵尸进程不消耗 CPU 和内存,但会占 PID,直到父进程把它们回收。

还有一种情况是 systemd 服务的Restart=always搭配频繁崩溃,每次崩溃都拉起新进程,旧进程如果没完全释放,PID 消耗就会加速。

怎么确认:

# 当前已用 PID 在 /proc 下的目录数 ls /proc | grep -E '^[0-9]+$' | wc -l # 看僵尸进程数量 ps -eo stat | grep -c '^Z' # 看 PID 上限 cat /proc/sys/kernel/pid_max

怎么处理:如果确认僵尸进程来自某个服务,先重启服务试试,因为重启会让父进程重新接管子进程。如果父进程就是残留进程本身(比如已经变成 defunct 的 init 子进程),可能需要重启整个系统或在维护窗口内处理。

调pid_max只能缓解,不能治本:

echo 4194304 > /proc/sys/kernel/pid_max

要永久生效就写进/etc/sysctl.conf:

kernel.pid_max = 4194304

但核心逻辑是:PID 只是表象,进程生命周期管理出了问题才是根源。日志里如果能看到同一进程反复启停,优先去查业务日志和启动脚本。

3.2 杀手二:systemd 单元数量失控

systemd 把每个服务、套接字、挂载点都当成一个单元来管理。正常情况下一个系统几十到几百个单元完全没问题。但某些情况下,单元数量会像细胞分裂一样膨胀。

最容易触发的是模板实例问题。服务名里带@符号就是模板实例,例如redis@6380.service。如果监控软件或自动扩缩容脚本不断生成新的实例文件,旧的又不清理,systemctl list-units会变得非常臃肿。

另外有种情况跟 socket 激活有关。systemd 可以为每个端口创建一个 socket 单元,如果一个进程多次监听不同端口而且每次都生成新单元文件,unit 数量也会涨上去。

怎么确认:

# 查看所有已加载的 unit 总数 systemctl list-unit-files --no-pager | wc -l # 查看 active 和 dead 的对比 systemctl list-units --all --no-pager | awk '{print $2}' | sort | uniq -c

怎么处理:先定位到是哪个服务产生的大量实例,然后清理对应的 unit 文件。常见位置是/etc/systemd/system/*.service和/run/systemd/system/*.service。清完后跑:

systemctl daemon-reload systemctl reset-failed

这里有一个特别的坑:systemctl reset-failed会清掉服务的失败计数,但如果单元文件本身还在,下次故障又会重新积累。做清理前一定要确认源头,否则只是治标。

3.3 杀手三:文件描述符和句柄耗尽

文件描述符(File Descriptor)是进程访问文件、网络连接、管道等资源的通行证。系统对所有进程的总 FD 数有限制,单个进程也有自己的限制。

典型的泄漏路径:代码里打开文件、建立连接之后忘记 close;日志库在轮转时没有正确关闭旧文件句柄;监控工具不断连新端口但旧 socket 处于 TIME_WAIT 状态迟迟不回收。这些问题积少成多,最终把 FD 配额吃光。

怎么确认:

# 系统当前 FD 使用量(实际/上限) cat /proc/sys/fs/file-nr # 输出三列:已分配、未使用、最大值 # 查看某个服务的 FD 使用数 ls /proc/<PID>/fd | wc -l # 查看最耗 fd 的进程 for pid in $(ls /proc | grep -E '^[0-9]+$'); do if [[ -d /proc/$pid/fd ]]; then count=$(ls /proc/$pid/fd 2>/dev/null | wc -l) echo "$count $pid $(cat /proc/$pid/comm 2>/dev/null)" fi done | sort -rn | head -20

调大限额的方法:

临时调:

ulimit -n 65535 ulimit -u 65535

永久调,改/etc/security/limits.conf:

* soft nofile 65535 * hard nofile 65535 * soft nproc 65535 * hard nproc 65535

但如果服务由 systemd 管理,limits.conf 里的设置未必生效,因为 systemd 会用自己的单元级限制覆盖它。要在/etc/systemd/system/<service>.service.d/override.conf里写:

[Service] LimitNOFILE=65535 LimitNPROC=65535 TasksMax=infinity

然后:

systemctl daemon-reload systemctl restart <service>

关于 MAX值怎么选:不要盲目调到几十万,先估算业务并发量。假设每个连接需要 10 个 FD,你的服务峰值并发是 1000,那 20000 左右的 npfle 已经够用。调到百万级反而会让系统内存浪费,因为内核要给每个进程预留表项。

3.4 杀手四:cgroup 的限制与系统限制打架

systemd 服务启动时,默认会被放入独立的 cgroup,这个 cgroup 里有TasksMax参数,控制该服务最多可以创建多少个任务。如果服务本身是并发模型(比如 PHP-FPM、Java 线程池),TasksMax 默认值 512 可能根本不够用。

现象非常迷惑:服务明明没超内存也没死循环,但新请求就是进不来。查看日志能看到cgroup: fork rejected by pids controller之类的提示。

怎么确认:

systemctl show <service>.service -p TasksMax -p TasksCurrent cat /sys/fs/cgroup/pids/system.slice/<service>.service/pids.max cat /sys/fs/cgroup/pids/system.slice/<service>.service/pids.current

如果pids.current逼近pids.max,基本就是撞上限了。

怎么处理:

systemctl set-property <service>.service TasksMax=4096

或者写进 override.conf:

[Service] TasksMax=4096

同样要注意:TasksMax=infinity虽然省事,但等于放弃了资源隔离的保护机制。我见过有人把所有服务都设成 infinity,结果一个服务线程泄漏,把整台机器的进程表撑爆,连 sshd 都起不来。资源和稳定性的平衡才是目的,不是一刀切。

3.5 杀手五:内存碎片和频繁 swap 的连锁反应

磁盘、CPU、内存都还有余量,但服务就是“起不来、起不稳”。这种情况常见于长期运行的机器上,swap 频繁使用,内存碎片化严重,加上部分服务用了大量共享内存,内存分配器的行为和平时完全不同。

怎么确认:

cat /proc/meminfo | grep -E 'MemTotal|MemFree|MemAvailable|SwapTotal|SwapFree' # 看内存碎片化程度 cat /proc/buddyinfo

处理方式:如果确认是内存碎片,临时手段是触发一次内存压缩:

echo 1 > /proc/sys/vm/compact_memory

长期手段是调整vm.swappiness,降低 swap 使用倾向:

sysctl -w vm.swappiness=10

但说句实话,这类问题最干脆的解法就是滚动重启集群节点。碎片化是运行时间累积出来的,没有银弹。

4. 实操过程与核心环节实现

4.1 一次真实故障的完整排查记录

为了把上面的思路串起来,我模拟一次常见故障:某业务凌晨开始周期性报警,起初不多,后来干脆完全无响应,SSH 能连上但敲命令都卡。

第一步:保持冷静,先看全局。

uptime # 输出 load average: 3.45, 2.10, 1.08

负载在涨但不算极端。

free -h # 内存还剩不少,没有到 swap 极限
df -h df -i # 根分区 70%,inode 还在安全范围

基础资源看起来都活着,那就往下深挖。

第二步:看进程数和 FD 数。

ps -eLf | wc -l # 输出 120345,这个数字大得离谱 ls /proc | grep -E '^[0-9]+$' | wc -l # 输出 78211,进程数明显异常
cat /proc/sys/fs/file-nr # 输出 389120 0 1048576 # 已分配接近 40%,总上限 1M,还没到死亡线

第三步:找到杀手进程。

for pid in $(ls /proc | grep -E '^[0-9]+$'); do if [[ -d /proc/$pid/fd ]]; then count=$(ls /proc/$pid/fd 2>/dev/null | wc -l) echo "$count $pid $(cat /proc/$pid/comm 2>/dev/null)" fi done | sort -rn | head -20

结果出来吓一跳,某个 worker 类进程 PID 只有几百,但 FD 数拥到 6000 多。显然是连接句柄泄漏。接着看它的父进程和启动信息:

ps -fp <PID> systemctl status <服务名>

果然,该服务有几百个子进程,都是因为父进程不断 fork。查看服务的日志,发现每个子进程处理完任务后都没有正常退出,而是挂在 TIME_WAIT 上等待回收。

第四步:针对性处理。

先把问题进程暂停:

systemctl stop <服务名>

然后清理僵尸进程:

kill -HUP 1

此时ps -eo stat | grep -c '^Z'的数量开始下降。等 PID 回落后,查看服务配置:

systemctl cat <服务名>.service

发现Restart=always存在,但TimeoutStopSec没设置,进程在处理完任务前不会响应 SIGTERM——这解释了为什么旧进程迟迟不退出。我给该服务加了:

[Service] TimeoutStopSec=15 Restart=on-failure

并调大了句柄限制:

LimitNOFILE=65535 TasksMax=1024

然后:

systemctl daemon-reload systemctl restart <服务名>

重启后观察:

ps -eLf | wc -l # 回落到几百 systemctl list-units --state=failed # 无异常 ss -tan | wc -l # 连接数恢复正常

整个过程的核心是:先确认哪个层满了,再用数据定位谁在吃资源,最后改配置并验证。

4.2 调优参数对照参考表

参数默认参考值排查时重点观测点调大后的注意隐患
kernel.pid_max32768 或 4194304/proc下目录数进程数过大导致内核内存压力
fs.file-max系统自动fs/file-nr第一列内存占用增加
LimitNOFILE1024ls /proc/PID/fd无符号整数溢出
LimitNPROC系统默认pids.current线程过载
TasksMax512(部分系统)cgroup pids.current失去资源隔离
vm.swappiness60swap使用率调低后内存压力增大

调参一定要小步验证,一次改一个变量,观察至少十分钟。改两个以上参数同时出问题,责任就很难说清楚了。

5. 常见问题与排查技巧速查

5.1 故障现象与原因匹配表

现象可能原因第一时间查看的命令
ssh 登录后敲命令卡死进程表满ps -eLf、cat /proc/loadavg
服务一直 restarting启动超时或崩溃循环systemctl status、journalctl -u
日志大量Too many open filesFD 耗尽cat /proc/sys/fs/file-nr、ulimit -n
新连接建不上,旧连接正常端口耗尽/TIME_WAIT堆积ss -tan state time-wait、netstat -s
cgroup 报 fork 失败TasksMax 超限systemctl show -p TasksMax
磁盘显示还有空间但创建文件失败inode 耗尽df -i
大量僵尸进程但 CPU 不高父进程未 waitps -eo stat,pid,ppid,comm

5.2 我踩过的三个高频坑

第一个坑是只调系统级 limit,忽略 systemd 级覆盖。费了半天口舌教别人改limits.conf,结果对方服务是 systemd 管的,根本不生效。一定要先确认服务是不是 systemd unit,如果是,直接在 override.conf 里改LimitXXX。

第二个坑是僵尸进程用 kill 解决。对普通进程 kill 能清理僵尸,但对已经 defunct 的进程 kill 无效——僵尸进程已经死了,只是等父进程收尸。正确做法是处理父进程。如果父进程是 PID 1,少数情况下需要重启系统才能清干净。

第三个坑是忽略日志中的周期性节奏。很多服务爆满不是突然的,而是每到整点或某个定时任务触发时暴涨。我在日志分析时特意看时间戳的分布,如果异常集中在每分钟的第 30 秒或每小时的第 0 分,多半是 crontab 任务没锁或超时重叠。这个时候去优化业务调度,比无限调大资源配额有针对性得多。

5.3 关于排查效率的四个建议

第一,建基线。正常运行时就把关键指标记录下来,故障时一对比就知道异常程度。

第二,善用 systemd 的journalctl -u service -b查看本次启动以来的日志,而不是全部历史日志,省去大量噪音。

第三,用timeout命令给可能卡死的命令加超时。排查时本身系统就很紧张,如果执行的命令又卡住,会雪上加霜。例如:

timeout 10 top -bn1 timeout 10 systemctl list-units --failed --no-pager

第四,排查过程中做的每一步操作都记录下来。故障复盘时能清晰还原现场,而不是靠回忆。

6. 收尾:一个值得养成的运维习惯

上面讲了一堆命令和方法,但真要避免服务爆满,功夫还在平时。我在实际运维中最深的体会是:不能等报警响了才查。建议每周低频执行一次巡检,把 systemd failed 单元数、PID 使用率、FD 使用率、僵尸进程数量这四个关键数字记下来。万一哪天某项数字异常,翻一下历史记录就能快速锁定变化时间点。

另外一个小技巧:给关键服务的重启行为加上保护。比如用了Restart=always的服务,务必同时设置StartLimitIntervalSec和StartLimitBurst,否则崩溃循环会让“自动重启”变成“自动加速消耗资源”。这个坑很多人没注意,等到服务反复崩溃把系统拖垮才后悔。

服务爆满的原因再多,归根结底是资源管理和代码质量两个层面的问题。资源管理靠基线、监控和调参,代码质量靠排查日志时多问一句“为什么会这样”。排查多了之后你会发现,大部分看似复杂的故障,背后原因都很朴素——配置没跟上业务变化,或者代码没管好生命周期。希望这篇文章能帮你少走点弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 11:01:32

基于Vue3响应式架构的Three.js 3D场景编辑器实践

简介&#xff1a;面向 Vue3 前端开发者的一套 Three.js 三维场景编辑器打包成果&#xff0c;基于 vue3、three.js、vite 构建&#xff0c;可本地启动服务直接查看。编辑器支持场景导入、导出与自定义配置&#xff0c;导出的场景文件可交给 scene-view 解析器渲染&#xff0c;整…

作者头像 李华
网站建设 2026/10/16 11:00:57

Java服务端部署Sidecar:日志采集、健康检查与动态配置的零侵入实践

接触 Java 服务端开发的朋友&#xff0c;大概率经历过下面几个场景&#xff1a;接口需要临时加一条响应日志&#xff0c;必须改代码重新发版&#xff1b;线上排查问题&#xff0c;进服务器翻半天日志才能定位&#xff1b;灰度调整权重&#xff0c;要登录机器改配置再重启进程。…

作者头像 李华
网站建设 2026/10/16 11:00:45

PromQL实战:高频查询表达式与监控告警排障经验

干过几年监控告警的人大概都有这种经历&#xff1a;线上CPU飙到100%&#xff0c;Prometheus里数据明明全都有&#xff0c;可就是写不出一条能准确表达“CPU到底是谁在用”的promeQL查询。promeQL是Prometheus监控体系里的查询语言&#xff0c;它的作用就是在一堆带时间戳和标签…

作者头像 李华
网站建设 2026/10/16 10:59:02

CLion下STM32+RS485实现Modbus从站:从协议到调试全流程

简介&#xff1a;STM32-RS485-Modbus 是一份基于 STM32 HAL 库的嵌入式通信工程源码&#xff0c;围绕 RS485 总线接口与 Modbus RTU 协议实现展开&#xff0c;并演示了中菱轮毂电机 ZLAC8015D 的控制方法。项目在 CLion 环境中以 CMake 构建&#xff0c;代码结构清晰&#xff0…

作者头像 李华
网站建设 2026/10/16 10:58:16

Java @Override报错排查:方法未重写超类方法的常见原因与修复策略

如果你用过Override注解&#xff0c;大概率见过这句话&#xff1a;「方法未从其超类重写方法」。它的英文原文是method does not override method from its superclass&#xff0c;是 Java 编译器在编译阶段报出来的一个典型错误。说实话&#xff0c;我第一次遇到这个报错时完全…

作者头像 李华
网站建设 2026/10/16 10:58:05

支付宝手机网站支付接入实战:异步通知与订单闭环处理

1. 支付接入前的基础认知与整体设计思路1.1 为什么支付宝手机网站支付值得单独梳理一套接入逻辑做Java后端开发的朋友&#xff0c;只要碰过电商、知识付费、会员充值这类带交易闭环的项目&#xff0c;迟早要面对支付接入。支付宝的手机网站支付&#xff08;alipay.trade.wap.pa…

作者头像 李华