“服务爆满”这四个字,很多人一听就头大。我处理过的线上故障里,至少有三分之一都能归到这一类: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_max | 32768 或 4194304 | /proc下目录数 | 进程数过大导致内核内存压力 |
fs.file-max | 系统自动 | fs/file-nr第一列 | 内存占用增加 |
LimitNOFILE | 1024 | ls /proc/PID/fd | 无符号整数溢出 |
LimitNPROC | 系统默认 | pids.current | 线程过载 |
TasksMax | 512(部分系统) | cgroup pids.current | 失去资源隔离 |
vm.swappiness | 60 | swap使用率 | 调低后内存压力增大 |
调参一定要小步验证,一次改一个变量,观察至少十分钟。改两个以上参数同时出问题,责任就很难说清楚了。
5. 常见问题与排查技巧速查
5.1 故障现象与原因匹配表
| 现象 | 可能原因 | 第一时间查看的命令 |
|---|---|---|
| ssh 登录后敲命令卡死 | 进程表满 | ps -eLf、cat /proc/loadavg |
| 服务一直 restarting | 启动超时或崩溃循环 | systemctl status、journalctl -u |
日志大量Too many open files | FD 耗尽 | 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 不高 | 父进程未 wait | ps -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,否则崩溃循环会让“自动重启”变成“自动加速消耗资源”。这个坑很多人没注意,等到服务反复崩溃把系统拖垮才后悔。
服务爆满的原因再多,归根结底是资源管理和代码质量两个层面的问题。资源管理靠基线、监控和调参,代码质量靠排查日志时多问一句“为什么会这样”。排查多了之后你会发现,大部分看似复杂的故障,背后原因都很朴素——配置没跟上业务变化,或者代码没管好生命周期。希望这篇文章能帮你少走点弯路。