首页 / 资讯中心 / 文章详情

Linux服务爆满排查实战:从systemd到进程资源的完整治理指南

Linux服务爆满排查实战:从systemd到进程资源的完整治理指南 ★ FEATURED ARTICLE
凌晨两点手机告警把我从梦里拽出来某台线上服务器的“系统服务爆满”监控面板上systemctl list-units --typeservice --staterunning的数量直接从 80 多跳到了 400 多CPU 和内存双双拉满新请求全部超时。这种场景凡是搞过 Linux 运维的兄弟应该都不陌生——服务列表突然爆炸性增长进程删不完、杀不绝一晚上光盯着ps和journalctl就熬到天亮。“linux系统服务爆满”这类问题最坑的地方在于表象是服务数量多本质往往是配置错误、资源泄漏、依赖崩溃三件事在背后搞鬼。如果你只是机械地systemctl stop一个个关服务大概率是按下葫芦浮起瓢前脚刚停完后脚它又自己拉起来。这篇文章我不打算讲那种“遇到问题先重启”的空话而是把我多次排障积累下来的完整思路拆开先分清“爆满”是哪一种爆再从进程、资源、依赖三个维度逐层定位根因最后给出能落地的治理方案包括配置样例和监控告警参数。适合刚接手服务器的新手也适合被这类故障折腾过的运维、后端开发拿来当排查手册。1. 先别急着杀进程搞清“服务爆满”的确切含义很多人在这一步就栽了。看到“服务爆满”四个字下意识认定是服务开太多了实际上 Linux 里的“服务”这个词很模糊可能是 systemd 管理的 Unit也可能只是用户态的多进程程序甚至可能是线程数暴涨。定义不清楚排查方向就会跑偏。1.1 从一条告警说起系统服务数量究竟为什么会“爆”我遇过的最典型的告警长这样node_systemd_unit_state或者自定义脚本检查到 running 状态的 systemd 服务数量超过阈值同时ps -ef | wc -l显示总进程数破千。这里的“爆满”有两种可能你必须先分清楚Unit 层面的爆满systemd 里同一服务被启动了 N 个实例比如你配了[email protected]模板服务结果 fork 了几十个实例或者某个服务处于activating (auto-restart)状态反复重启Failed列表越积越多。进程层面的爆满某个服务本身是多进程/多线程模型子进程、线程、僵尸进程defunct数量失控把 PID 空间和系统负载打满。判断方法很简单一条命令的事systemctl list-units --typeservice --staterunning | wc -l ps -ef | wc -l前者看 Unit 数量后者看真实进程数。如果 Unit 数量正常但进程数爆炸那问题根本不在 systemd而在服务程序自身——你往下杀多少个进程都没用它马上会拉起来新的子进程因为根因在代码逻辑里。1.2 爆满的三种典型形态对照排查少走弯路结合我自己的排障经验服务爆满基本逃不出下面三种形态建议你把现象对号入座后再动手形态典型现象常见根因单服务多实例爆炸同一 ExecStart 路径下出现大量子进程父进程 PID 相同程序内部 fork 逻辑失控、线程池配置过大、定时任务并发重叠僵尸进程堆积ps 里大量defunct状态进程CPU 不高但进程数多父进程没调用 wait() 收割子进程或父进程卡死failed 单元堆积systemd 服务反复重启systemctl --failed一大串Unit 配置错误、启动依赖缺失、程序启动即崩溃坦白说第三种形态最阴险。systemd 默认某些版本对Restartalways的服务有重启次数限制但如果你踩过StartLimitIntervalSec0这种“关掉限制”的配置服务每一秒崩一次、每崩一次就留一条 failed 记录时间一长整个服务列表全是红标。碰到这种情况别一个一个手动 reset先把配置里的Restart和StartLimit*参数捋清楚。注意systemctl reset-failed是清空 failed 状态最快的方式但它只治标。如果不改 Unit 里的RestartSec和StartLimitIntervalSec同样的爆满会在几分钟后卷土重来。2. 从上到下的排查路径五分钟把范围缩到最小排障最忌讳的就是到处乱翻日志。我自己的习惯是严格按“全局 → 进程 → 资源 → 日志”的顺序来每一层都只花一两分钟一旦发现线索立刻深入效率高很多。2.1 第一件事先看全局状态确认是“满”还是“卡”别急着数服务先瞟一眼系统整体有没有救。三个数字可以决定你接下来的策略负载、内存、PID 余量。uptime free -h cat /proc/sys/kernel/pid_max ps -eLf | wc -luptime的 load average 如果持续大于 CPU 核数的 3 倍以上说明系统已经处于过载状态这时候再做复杂排查会进一步拖垮机器。先快速清理掉明显异常的服务把负载降下来再谈根因。内存方面重点看 available 而不是 free因为 page cache 在 Linux 下会吃掉大量“free”数值实际可用的可能已经见底。PID 余量是个容易被忽略的指标。pid_max默认通常 32768如果系统上线程数接近这个值新进程会直接Resource temporarily unavailable。一旦出现这种情况不管服务数量爆不爆业务都会表现为“服务全部不可用”——这不是某个服务的问题是内核进程号分配空间被打满了。2.2 第二件事过滤出异常服务别在健康服务上浪费时间全局状态没问题那就可以精准定位了。我常用的三连systemctl --failed --no-legend systemctl list-units --typeservice --staterunning --no-pager | head -30 ps -eo pid,ppid,stat,%cpu,%mem,etime,cmd --sort-%cpu | head -30第一条看哪些服务处于失败状态第二条看当前实际 running 的 Unit 有哪些按名称一眼扫过去异常的多实例服务会非常扎眼——比如同一名字的 service 出现 20 行第三条按 CPU 排序把所有进程拉出来看有没有诡异的进程名或者 CPU 占用异常的。我踩过的一个坑是光看ps -ef容易漏掉线程数爆炸的情况。单看进程数可能只有 50 个但每个进程起了上百个线程总线程数上万。所以ps -eLf这个命令至少在初步排查时应该跑一次线程列LWP数量才是最接近真实压力源的指标。2.3 第三件事查系统资源限制很多“爆满”其实是受限后的连锁反应如果进程和线程数量确实在合理范围内但服务仍然报错那就得看资源限制。Linux 下服务跑不起来或频繁重启十有八九和ulimit里的 nofile文件句柄数、nproc进程数、内存锁有关。systemd 管理的服务不走 shell 的 ulimit它有自己的限制项。用下面命令看某个服务的实际限制systemctl show nginx.service -p LimitNOFILE -p LimitNPROC -p TasksMax这里重点解释一下TasksMax很多人不熟悉。它是 systemd 对 cgroup 内任务进程线程总数的限制默认值在部分系统上是 512 或 4096。你如果部署的服务是线程池模型线程一多cgroup 里任务数触顶新线程创建失败服务表现就是“过一段时间莫名假死”。查看 cgroup 当前使用量可以这样cat /sys/fs/cgroup/pids.current cat /sys/fs/cgroup/pids.max如果pids.current已经非常接近pids.max那就是 TasksMax 压制导致的“服务爆满”——表面看服务还在跑实际已经失去响应能力了。这种问题改代码没用要么调大 TasksMax要么优化线程池参数。3. 根因拆解服务进程为什么会“只增不减”定位到具体是哪个服务在爆接下来就是挖根因。我见过太多人卡在这一步知道是某个服务的问题但重启完过一会儿又爆反反复复。下面三类根因几乎覆盖了 90% 的线上场景逐个说透。3.1 systemd 单元配置错误重启风暴是怎么形成的先看一个我实际修过的配置片段某业务的 Unit 文件长这样[Unit] DescriptionDemo API Service [Service] ExecStart/opt/demo/bin/demo-server --config /etc/demo/demo.conf Restartalways RestartSec1 [Install] WantedBymulti-user.target表面上没问题但配合/opt/demo/bin/demo-server这个程序本身环境变量缺失导致进程启动 1 秒内必崩。Restartalways意味着进程退出后立即重启RestartSec1只间隔 1 秒。于是 systemd 就进入“崩溃→重启→崩溃”的循环每秒钟多产生一个 failed 计数而systemctl list-units里该服务始终处于activating (auto-restart)状态。这种场景下systemctl status看到的启动时间永远是几秒前journalctl -u demo-service里全是同一条报错刷屏。修复的最小改动是[Service] Restarton-failure RestartSec5 StartLimitIntervalSec0等等StartLimitIntervalSec0不是关掉了次数限制吗这里有个反直觉的细节如果你希望服务“健康时稳定运行异常时不要无限重启”应该保留默认限制而不是设 0。上面示例里设 0 是为了让它在修复配置前先能起来调试实际生产环境我不建议把重启次数限制去掉否则一旦出现瞬时故障systemd 会变成“永动机”把故障无限放大。实操心得排查服务爆满时systemctl show 服务名 -p NRestarts这个属性一定要看一眼。如果它数值很大说明服务已经反复重启很多次了直接往启动依赖和启动即崩溃的方向查别浪费时间看业务日志。3.2 资源泄漏文件句柄、内存、线程的三重失控服务进程数量没怎么变但系统整体越来越卡、新进程起不来这就要考虑资源泄漏了。我遇到最多的三种文件句柄泄漏程序打开文件或 socket 后没关闭日积月累把进程的 nofile 限制打满。此时该进程无法再打开新文件典型表现是日志开始报Too many open files数据库连接池拿不到新连接服务“看起来活着实际瘫了”。排查命令ls /proc/pid/fd | wc -l lsof -p pid | wc -l如果 fd 数量持续增长——比如第一次查是 3000五分钟后是 5000——那就不用怀疑了代码里有 fd 泄漏。定位到具体是哪些 fd 可以用lsof -p pid | grep -v mem | grep -v cwd | sort -k4 -n | tail -20内存泄漏ps里看%MEM和RSS单次快照看不出趋势要连续采样ps -o pid,rss,vsz,etime,cmd -p pid watch -n 5 ps -o pid,rss,vsz,cmd -p pidRSS 只增不减或者 gc 之后内存不回落基本就是泄漏。注意区分“缓存增长”和“真泄漏”——Java 或 Go 的堆内缓存、Python 的lru_cache都可能导致内存看起来只增不减但只要 GC 后能回落就不算泄漏。线程失控业务代码里每来一个请求就 new 一个线程又不做复用请求量一大线程数直接起飞。ps -eLf | grep 服务进程 | wc -l pstree -p pid | wc -l线程数持续上升最后线程栈占用内存把系统拖垮。这种问题调整TasksMax只能延缓爆炸真正的解法是把线程池化或者用协程替代。排查时如果进程是 Java直接打一份 thread dumpjstack pid /tmp/jstack_$(date %F).log数一数http-前缀的线程数量基本一眼定案。3.3 外部依赖拖垮主服务端口耗尽与连接池堆积的连环爆还有一种“服务爆满”很隐蔽主服务进程本身很健康但下游依赖数据库、缓存、外部 API的端口连接耗尽导致主服务疯狂重试每次重试都新建连接、新开线程。你看到的表象是主服务进程数/线程数暴涨实际根因在下游。端口耗尽尤其常见。Linux 下客户端发起 TCP 连接时会从本地临时端口范围分配一个源端口。如果服务短连接非常多且 TIME_WAIT 状态的连接回收慢可用端口会被占满新连接直接失败。cat /proc/sys/net/ipv4/ip_local_port_range netstat -ant | grep TIME_WAIT | wc -l ss -sTIME_WAIT 数量上万时服务表现就是“间歇性连接失败”监控上能看到网络错误数缓慢爬升直到某个峰值后断崖式故障。对症的 sysctl 参数如下sysctl -w net.ipv4.tcp_tw_reuse1 sysctl -w net.ipv4.tcp_fin_timeout15 sysctl -w net.ipv4.ip_local_port_range1024 65535注意tcp_tw_reuse只对客户端出站连接有效不解决服务端 accept 侧的 TIME_WAIT。如果是服务端大量 TIME_WAIT先查短连接是否来自健康检查、日志采集这类高频请求能合并长连接就合并不能的话把tcp_fin_timeout调小是更稳妥的做法。线程池堆积是另一个变种。某个中间件客户端的连接池配置过小服务在高峰期拿不到连接线程阻塞在获取连接的位置。阻塞线程越积越多最终表现为“服务线程数爆满”CPU 却不高。这种问题用jstack一看便知大量线程卡在getConnection或者你使用的客户端库的borrowObject方法上。解法无非是调大连接池、加超时熔断再加上请求排队的上限保护。4. 两个案例复盘从告警到修复的完整过程理论讲了一堆不如带大家走一遍我实际处理过的两个案例。这两个案例一个 fd 泄漏、一个内存失控都非常典型参数和命令都是我原样操作过的。4.1 案例一某 API 网关的 fd 泄漏排查某天某公司的监控显示 API 网关容器频繁 OOM但重启容器后能恢复一段时间过几个小时又不正常。我去看的时候ps -ef列出来容器内 PID 1 的进程数正常没有多实例但lsof -p pid | wc -l显示 fd 数量到了 5 万多远超默认的 65535 的一半。第一轮排查我先把 fd 按类型归类ls -l /proc/pid/fd | awk {print $5} | sed s/.*- // | awk -F: {print $1} | sort | uniq -c | sort -rn | head -10结果显示绝大多数 fd 是 socket 类型指向同一个后端日志服务器的 8000 端口。后来在代码里找到了根因某个日志上报函数每次调用都会新建一个 TCP 连接但正常情况下应该在 finally 块里 close这里漏写了。每次请求泄漏一个 socket fd访问量一大fd 数量就飞速上涨。修复方式也很粗暴有效在代码里补上defer file.Close()/res.Body.Close()把进程的 nofile 限制从默认值调大到 1048576防止瞬时峰值把进程打死加了监控fd 数量超过阈值直接告警而不是等到 OOM 才惊觉。经验教训如果你在lsof里看到大量TCP类型的 fd别急着改系统参数先找到源头是哪个模块建立的连接。我后来在好几个项目里都发现过类似的日志/上报组件泄漏问题系统参数只是兜底代码修复才是根治。4.2 案例二某定时任务系统的内存失控另一个案例是某定时任务系统用 systemd timer 调度跑批任务每天晚上触发。起初一切正常某次发版后定时任务跑完后内存不释放进程 RSS 从 1GB 慢慢涨到 8GB直接把同一台机器上的其他服务挤爆。排查时我用了 systemd 的 MemoryMax 参数临时“止血”[Service] MemoryMax2G MemoryHigh1.5G设完之后任务内存一旦超过 2G 就会被 cgroup 杀掉不会再拖垮同机的其他服务。但这只是止血根因还是要查。看journalctl发现任务日志里有一段处理“某个大文件”的代码处理完后持有的全局缓存没有清理。修掉之后再把上面两个参数保留下来作为兜底——即便将来代码再出问题也不会波及邻居服务。这个思路我在好几个项目里都推荐过服务必须有个人的限额牺牲自己不能影响全局。排查内存泄漏时可以用/proc/pid/smaps看看哪类内存增长最快awk /^Size|^Rss|^Pss|^Swap/ {print} /proc/pid/smaps | paste - - - - | sort -k2 -n | tail -20不过说实话生产环境最有效的还是连续采样 对比看 RSS 曲线往上走的斜率。你再神也不可能靠一次采样就看透内存分布。5. 长期治理把“爆满”扼杀在报警之前线上故障处理完不意味着事情结束了。系统服务爆满的问题十次有九次是“积重难返”——平时不关注资源使用趋势等到爆发才看到告警于是手忙脚乱。想真正减少这类事故靠的是日常的配置规范、监控手段和简单的自愈机制。5.1 基础兜底给每个服务都上好“紧箍咒”不管什么服务部署上线前至少要在 systemd Unit 里明确写上下面几个参数[Service] LimitNOFILE1048576 LimitNPROC65536 TasksMax512 MemoryMax2G Restarton-failure RestartSec5 TimeoutStopSec30LimitNOFILE和LimitNPROC防止文件句柄和进程数耗尽-TasksMax限制线程总数防止线程失控拖垮机器-MemoryMax设定硬性内存上限cgroup 会在超限时直接 OOM Kill 该服务的进程-Restarton-failure保证非正常退出能快速恢复但不会像always那样连主动 stop 也给你拉起来。这里要特别说明这几个参数不是随便拍的得根据服务实际资源画像来定。我见过有人把MemoryMax设成 512M结果正常流量下就 OOM反而制造了故障。正确做法是先连续压测或观察几天基线峰值在峰值的 1.5 倍左右设限既保证业务弹性又能拦住异常增长。5.2 监控与自愈从被动响应到主动发现监控指标上别只看 CPU、内存这种大路货。跟服务爆满直接相关的几个指标必须覆盖systemd_unit_stateNode Exporter 的 systemd collector——服务是否 active / activating / failedprocess_state里的threads数——单机总线程数趋势node_filefd_allocated/node_filefd_maximum——系统 fd 分配情况/proc/sys/kernel/pid_max接近值——PID 耗尽风险每个服务的MemoryMax使用百分比、TasksMax使用百分比。告警阈值可以这么设线程数或进程数超过基线的 2 倍持续 5 分钟就告警而不是等它翻到 10 倍才响。自愈方面写个定时任务或者部署一个 watchdog 服务每分钟做一次这样的检查if [ $(systemctl --failed --no-legend | wc -l) -gt 0 ]; then systemctl reset-failed fi这是个兜底动作防止 failed 状态堆积导致 systemd 的告警列表失真。真正的自愈不可能靠几条 shell 命令完成——你得把前面讲的根因治理做好脚本只是给恢复争取时间。5.3 容易忽略的配置细节三个让我翻过车的教训最后分享几个平时不起眼、但出事时能坑死人的细节。教训一ExecStart里别省路径。我见过有人写ExecStartdemo-server依赖 PATH 环境变量来找二进制。systemd 环境下 PATH 跟 shell 不一样经常找不到命令服务启动失败后反复重启看着就像“服务爆满”。正确做法是写绝对路径。教训二KillMode的默认值不是万能的。KillModecontrol-group会杀掉整个 cgroup 里的所有进程。如果你在服务里用脚本拉起了一堆子进程想靠systemctl stop停掉全部默认 KileMode 就能做到。但如果改成KillModeprocess只会杀主进程子进程会变成孤儿继续留在系统里久而久之也堆出一堆“服务”。教训三容器场景里 systemd 和 cgroup v2 的联动。在容器里跑 systemd比如某些极简镜像里用 systemd 做 init要留意 cgroup v2 的pids.max可能继承外层限制。容器内创建大量线程时可能先触及容器的 pids 限制而不是虚拟机的TasksMax这时报错信息又指向 systemd很容易让人误判成系统服务爆满。排查时如果/sys/fs/cgroup/pids.current和pids.max数值一样赶紧检查容器编排层是不是限制了 PID 数量。真心建议在测试环境故意压一次你自己的服务把线程数、fd 数、内存都往上顶观察系统在哪个临界点开始表现异常。用这种方式确定你的监控阈值和服务参数比等着线上出事要安心得多。我个人做完这几轮治理后的体会是Linux 服务爆满的根本原因并不可怕可怕的是你对该服务的“正常画像”没有概念。当你清楚地知道某个服务平时应该有多少进程、多少线程、多少 fd、多少内存异常出现时一眼就能看出来排查时间能从几小时压缩到几分钟。这也是我为什么强调基线数据和监控可视化——排障的效率本质上取决于你对系统的熟悉程度。希望这篇文章的排查路径和参数配置能成为你的第一份参考资料下次再遇到服务数量飙升先冷静下来分形态、查根因、定基线别一上来就乱杀进程。
阅读完成 · 觉得有帮助?
咨询建站