
半夜两点手机连着响了三声——不是微信是监控平台的告警生产服务器 CPU 使用率 100%已经持续五分钟。屏幕上 top 刷出来的数据直冲天花板而你连这台机器上跑的是什么服务都得想半分钟。这种情况每个运维都遇到过新手会直接上去 kill 进程老手会先深吸一口气把现场抓下来再动手。这篇文章就是围绕“服务器后台 CPU 飙升到 100% 该怎么防护”这个核心问题写的。我会把应急处理、根因定位、系统兜底策略、典型案例复盘这些内容完整梳理一遍。面向的读者是 Linux 运维、后端开发、以及所有需要自己维护服务器的人。看完你至少能回答三件事现在该不该动手杀进程、用什么思路找到元凶、以及怎样让下一次 CPU 飙高不再让你半夜爬起来。1. 先别慌CPU 100% 的应急处理流程1.1 三分钟内必须完成的现场采样CPU 飙到 100%第一反应不要是“重启大法好”。服务器一旦重启进程里的线程栈、网络连接状态、临时文件全部消失很多线索就断了。正确的做法是先把现场完整抓下来再考虑止血。登录服务器后前三分钟按这个顺序执行# 1. 记录系统整体状态 uptime # 2. 查看 CPU 占用最高的 20 个进程保存结果 top -bn 1 | head -30 /tmp/cpu_top_$(date %Y%m%d_%H%M%S).txt top -bn 1 | sort -k9 -r | head -20 # 3. 记录所有进程的 CPU 和内存快照 ps -eo pid,ppid,%cpu,%mem,cmd --sort-%cpu | head -30 # 4. 记录网络连接状态 ss -s ss -antp | head -50这里有个很多人忽略的细节top默认刷新的间隔是 3 秒直接执行top看不全历史信息。用top -bn 1跑一次批处理模式输出的是当前瞬间的快照适合留档。再加上ps按 CPU 排序的结果基本能锁定可疑进程。除了保存命令输出还要留意/proc目录下的信息。如果怀疑某个进程有问题立刻备份它的状态和命令行参数# 记录可疑进程的启动命令和工作目录 ls -l /proc/PID/exe cat /proc/PID/cmdline cat /proc/PID/status这些操作本身不重不影响生产环境但能为后面排查省下大量时间。记住一个原则现场数据越完整根因定位越轻松。1.2 应急止血哪些进程可以动哪些不能乱动现场抓完才开始考虑“救火”。这里的坑特别多——很多人看到 CPU 100% 就急着眼杀进程结果把数据库主进程、配置中心节点这种关键服务砍了业务直接雪崩。止血之前先确认三个问题第一这个进程是什么用ps -ef看启动命令用systemctl status PID看是不是系统托管服务。如果进程名是mysqld、nginx、java这种基本是正常业务要谨慎处理。第二这个进程的父进程是谁用ps -o ppid -p PID查看。如果父进程是 1init/systemd说明它是守护进程杀了会自动拉起如果父进程是你自己启动的脚本或者某个异常的 shell就要小心是不是被恶意利用了。第三这个进程是不是系统关键组件kswapd、kworker、systemd-journald这类内核线程和系统服务不要轻易 kill杀了要么没作用要么引发连锁问题。确认清楚之后再按严重程度选择止血手段。最柔和的是用renice降低优先级让出 CPU 给核心业务其次是用systemctl stop停掉异常服务最极端才用kill -9。还有一个经常被忽略的选项——用防火墙临时封禁异常外联 IP比如iptables -A INPUT -s 可疑IP -j DROP很多时候 CPU 飙升是因为被扫描或攻击先把流量挡住进程本身未必需要杀。提示任何时候执行kill -9之前先确认你保留了对应用户态和内核态的现场数据。没有现场就动手后面就只能靠猜了。2. 从定位到根因CPU 飙高的深度诊断方法2.1 进程、线程、指令三级定位法top能告诉我们哪个进程占 CPU 高但进程只是第一层。一个 Java 应用里有几十个线程一个 Nginx worker 进程里也有多个线程在跑到底是哪个线程在烧 CPU这需要逐层往下拆。第一层是进程级定位也就是top或ps的结果锁定具体进程 PID。第二层是线程级定位用top -Hp PID查看该进程下所有线程的 CPU 占用找到“冒烟”的那个线程 IDTID。第三层是代码级定位把线程 ID 转成十六进制配合语言层面的工具抓取调用栈。Java 程序用jstack PID抓线程栈搜索对应线程名的十六进制 nidC/C 程序用gdb attach PID或者pstack通用的可以用perf top -p PID -g直接看热点函数。举一个实际的 Java 排障例子# 找到 CPU 占用最高的 Java 进程 top -bn 1 | grep java # 假设 PID 是 12345查看它的线程 CPU 情况 top -Hp 12345 # 发现线程 12399 占 CPU 95%转十六进制 printf %x\n 12399 # 输出 306f用 jstack 抓线程栈 jstack 12345 /tmp/thread_dump.txt # 在 dump 文件里搜 0x306f就能看到具体在跑什么代码这套方法还有第三个层级如果线程栈显示在同一个函数里出不来大概率是死循环或者正则回溯如果显示在park/wait等待状态那就是锁竞争并不是这个线程在真的“干活”。理解这个区别很重要——CPU 100% 不一定是一个线程在疯狂计算也可能是大量线程在自旋等待锁把 CPU 活活吃光。2.2 如何区分是“业务真忙”还是“代码卡死”同样是 CPU 100%原因可能天差地别一个是业务量真的上来了程序在拼命处理请求这是“幸福的烦恼”另一个是程序逻辑出 bug在死循环里空转这叫“程序的烦恼”。两者的处理方式完全相反前者要扩容、限流后者要修代码、发补丁。区分的方法看top输出里的 CPU 时间分布。重点关注下面几个字段ususer用户态 CPU 占比业务代码在跑。sysystem内核态 CPU 占比程序在做系统调用、内核处理。waiowait等待磁盘 IO 的占比。sisoftirq和hihardirq软中断和硬中断处理占比。如果us很高说明业务代码逻辑本身占用了大量计算资源优先查线程栈找热点函数如果sy很高可能是系统调用过于频繁、锁竞争激烈或者中断风暴用vmstat 1观察上下文切换cs和中断in数据如果wa很高那 CPU 飙高其实是假象真实瓶颈在磁盘把进程杀掉解决不了任何问题。还有一个实用的判断办法——用mpstat -P ALL 1看每个 CPU 核的负载分布。如果单核 100% 而其他核空闲多半是单线程程序遇到死循环或者 GC 线程在某个核上疯狂工作如果所有核都占满则更像是水平扩展线程池被打满或者多个线程同时出问题。历史数据也是重要的参考维度。如果服务器上开了sysstat服务用sar -u -f /var/log/sa/sa$(date -d yesterday %d)查看昨天的 CPU 记录能判断是突然飙升还是缓慢爬升。突然飙升大多是流量尖峰、恶意攻击、定时任务撞车缓慢爬升则可能是内存泄漏引发持续 GC、日志文件无限增长导致 IO 压力逐步变大。3. 五大典型诱因这些场景我都在生产环境见过3.1 高并发流量压垮应用线程最常见的 CPU 飙高场景就是流量突然上涨应用线程池配置跟不上导致线程频繁创建销毁、上下文切换暴涨。典型的例子是应用配置的tomcat.max-threads200但线上并发请求到了 300此时应用会不断创建新线程去处理请求每个线程都在抢 CPU线程切换开销陡增。排查方法很简单ss -s看 socket 连接数lsof -p PID | wc -l看进程打开的连接数如果连接数和线程数都远超合理值基本就是并发压力导致。这种情况的解决思路不是杀进程而是在 Nginx/LVS 层做限流比如limit_req zonereq_zone burst20 nodelay挡住超量请求。如果是应用自身的问题调大线程池上限或者改造成异步非阻塞模型。最直接的还是扩容——加机器、加容器副本把流量分散掉。记住流量导致的 CPU 高说明业务在增长这是需求信号不是故障信号。碰见这种场景优先考虑负载均衡和水平扩展而不是想着怎么压榨单机性能。3.2 代码死循环与正则表达式灾难程序逻辑 bug 导致的 CPU 飙升是最令人头疼的类型因为表面上看没有任何流量异常连接数平稳内存正常但 CPU 就是掉不下来。死循环好理解while条件永远满足代码在里面空转。但最坑的是“半死循环”——循环本身不会永远跑但每次执行的数据量爆炸性增长。比如一个列表操作从 O(n) 变成 O(n²)数据量一大CPU 立刻被打满。另一个人人踩过的大坑是正则表达式灾难性回溯。经典案例是处理用户输入时写了个复杂的正则比如^([a-z])*!$当输入是几十个并不匹配的字符时回溯次数会呈指数级增长一个请求就能把一个核占满。排查这两类问题Java 用jstack连续抓取三次线程栈间隔 3 秒如果三次都停在同一行代码上基本可以确认是死循环如果是不同调用栈但 CPU 都高则要考虑是不是大量并发请求触发了相同的热点逻辑。C/C 服务用perf record -p PID -g抓性能数据再用perf report看热点函数一目了然。解决方案也分两层。临时止血用kill -9重启服务先把业务恢复长期修复要改代码、规范日志打印、加上单元测试覆盖边界输入。3.3 定时任务与日志清理的“隐形炸弹”很多服务器 CPU 飙高发生在整点或者凌晨业务没流量代码没更新但 CPU 就是爆了。这种时候把怀疑的目光转向 crontab 定时任务是绝对正确的。我遇到过好几个典型的坑第一find /var/log -type f -mtime 7 -delete这类清理命令在文件极少时很快但如果日志文件几十万个find 的目录遍历和删除操作会引发大量磁盘 IOCPU 的wa和sy同时飙升。第二多个定时任务扎堆执行。每天早上 3 点备份脚本、日志压缩、数据清理同时启动磁盘和 CPU 同时吃紧。crontab -l看一下把任务执行时间错开就解决了。第三压缩命令tar czf或gzip是 CPU 密集型的压缩几十 GB 的日志能把 CPU 直接打满。正确做法是降级到gzip -1最快压缩级别或者用pigz并行压缩配合ionice -c 3让压缩任务让路给业务 IO。排查定时任务问题直接看 syslog 和 cron 日志grep CRON /var/log/syslog | tail -50结合 CPU 飙高的时间点对一下基本能锁定新增任务。这个问题本质上是任务调度策略的设计问题错峰、限速、资源限制都该在配置 cron 时就考虑进。3.4 恶意程序与挖矿木马伪装CPU 飙升有一个让人血压拉满的原因——服务器被入侵了跑着挖矿木马。这类问题在公网服务器上尤其常见。木马的典型特征进程名伪装成正常系统服务比如kswapd0、systemd-network、crond但路径在/tmp/、/var/tmp/之类异常位置。网络连接异常主动连接不常见的外部 IP 的 443/80 端口。会创建守护脚本藏在 cron 或 systemd 定时器里杀了进程会重新拉起。排查三步走# 1. 找异常进程。重点关注进程名看着正常但启动路径异常的 ps -eo pid,ppid,user,%cpu,cmd --sort-%cpu | head -20 ls -l /proc/PID/exe # 2. 找异常网络连接外联 IP 为重点 netstat -antp | grep ESTABLISHED | grep -v :80\|:443 # 3. 找守护脚本看 cron 和 systemd 定时器 crontab -l cat /etc/crontab ls -la /etc/cron.d/ systemctl list-timers确认是挖矿木马后处理流程要特别注意顺序先断外联、封 IP、删定时任务然后再杀进程。如果直接 kill守护脚本会立刻重新拉起进程白忙一场。具体操作# 封禁可疑外联 IP iptables -A OUTPUT -d 可疑IP -j DROP # 删除恶意定时任务 crontab -r 或者手动编辑 crontab -e 删除恶意行 # 清理文件 rm -f /tmp/恶意文件 # 最后杀进程 kill -9 恶意PID处理完之后还要做安全审计查一下是通过什么漏洞进来的——是 Redis 未授权访问、SSH 弱口令还是应用框架漏洞。不堵住入口清理完第二天又会被种回来。这个方向我在第 5 章案例里会再详细展开。3.5 虚拟化环境与硬件层干扰虚拟化环境里还有一种特殊情况你的服务本身没忙但 CPU 跑满了。看top输出里的%ststeal time如果这个值很高说明宿主机上的其他虚拟机在抢 CPU 资源你的云服务器 CPU 被“偷走”了。%st高的情况下本地无论怎么优化代码都无效正确的应对方式正常业务向云服务商提工单要求排查宿主机资源争抢。非关键期错峰运行重任务尽量避开其他租户的繁忙时段。有预算升配到独享型实例避免与其他租户共享物理核。硬件层面也要检查。服务器长期高负载运转散热不良会导致 CPU 温度过高触发降频保护表面上 CPU 使用率高实际性能却下降了。用sensors查看温度如果温度持续偏高要考虑清理灰尘、增加风扇、调整机房温控。CPU 虚焊这类硬件故障虽然不常见但一旦出现机器会频繁重启、性能骤降这时需要走硬件更换流程。服务器硬件的稳定性是靠冗余设计和机房环境保障的单机出问题是概率问题重要的是监控能不能及时暴露问题。4. 系统级防护与兜底策略4.1 用 cgroup 和 systemd 给 CPU 上“笼头”应急处理能救一时但不能只靠救火。真正成熟的防护策略是在 CPU 飙高之前就把单个进程能占用的资源限制住避免“一颗老鼠屎坏了一锅汤”。现代 Linux 系统上最常用的手段是 systemd 的资源控制参数。如果你管理的服务通过 systemd 托管在 unit 文件里加上[Service] # 限制该服务最多使用 200% CPU即 2 个核心 CPUQuota200% # 只允许使用 CPU 0 和 CPU 1 AllowedCPUs0-1 # 进程优先级 Nice5修改后执行systemctl daemon-reload和systemctl restart 服务名生效。对于非 systemd 托管的进程可以直接操作 cgroup。手动创建 cgroup 并限制mkdir /sys/fs/cgroup/cpu/limit_group echo 100000 /sys/fs/cgroup/cpu/limit_group/cpu.cfs_period_us echo 50000 /sys/fs/cgroup/cpu/limit_group/cpu.cfs_quota_us echo 12345 /sys/fs/cgroup/cpu/limit_group/cgroup.procs含义是CPU 的 CFS 调度周期为 100ms限制该组内进程最多使用 50ms也就是最多占用 0.5 个核心。这样即使这个进程发疯最多吃一半 CPU其余核心还能处理其他业务。容器场景下更简单。Docker 启动参数加--cpus0.5或--cpu-shares256Kubernetes 里设置resources.limits.cpu和requests.cpu。核心思路都一样给每个服务划好 CPU 预算谁也别想越界。4.2 进程优先级与资源隔离限制 CPU 之外还可以调整进程优先级。nice值范围从 -20 到 19数值越低优先级越高。默认优先级为 0对普通用户来说调整自己的进程只能往低了降提高 nice 值不能往高提降低 nice 值。运维中有一个常见操作把非核心服务的优先级调低让核心业务优先使用 CPU。# 启动时指定 nice -n 10 /usr/bin/backup_script # 对已运行进程调整 renice 10 -p 12345调低优先级之后如果 CPU 充足服务没有任何感知如果 CPU 紧张调度器会优先把 CPU 让给高优先级进程。这是成本最低、风险最小的资源控制手段。实时调度策略要特别谨慎。把某个进程设置成SCHED_FIFO或SCHED_RR通过chrt命令会让它抢占系统中绝大多数 CPU一旦这个进程有 bug整个系统都可能卡死。没有充分测试的情况下不要在生产环境使用实时调度。4.3 监控、告警与自愈闭环防护的第三步是监控告警。没有监控CPU 飙到 100% 只能靠用户反馈才发现这已经是事故级别的体验了。基础监控工具组合Prometheus node_exporter Grafana开源方案能保存历史数据支持配置告警规则。Netdata单机轻量监控分钟级数据自带图表。sysstatsar历史数据留存适合事后回溯。告警规则建议这样设计单核 CPU 使用率超过 90% 持续 5 分钟触发 Warning。所有核 CPU 使用率超过 90% 持续 10 分钟触发 Critical。系统负载load average超过 CPU 核数的 2 倍持续 10 分钟触发 Critical。%ststeal 时间持续高于 30%触发 Warning虚拟化环境特有。有了告警还不够现代运维讲究“自愈”。配合 systemd 的自动重启机制和健康检查可以把部分故障在用户感知之前解决掉[Service] Restarton-failure RestartSec5s StartLimitBurst3在一些场景下也可以写简单的自愈脚本——比如进程卡死超过阈值就自动重启配合cron或者 systemd timer 运行。但要记住一点自愈是兜底不是治疗。频繁自动重启本质上是掩耳盗铃最终还是要把根因找出来。5. 从被动救火到主动设防三个真实案例复盘5.1 凌晨的“Nginx 100%”原来是一个爬虫在扫接口当时是凌晨两点告警平台弹出来 Nginx 所在服务器 CPU 100%。上去一看连接数从平时的 500 飙到了 8000全部集中在某个 API 接口上。top排在前面的是多个 Nginx worker 进程CPU 被吃满。用ss -anp | grep :443查看来源 IP发现连续几百个连接都来自同一网段。再用 Nginx 访问日志定位这个 IP 的请求特征确认是爬虫在暴力遍历接口参数每个请求的参数组合都在变化导致后端不停查询数据库。CPU 高只是一个表象真正的压力点其实是数据库被大量无效查询打满。处理步骤# 1. 防火墙封禁来源 IP 段 iptables -A INPUT -s 来源IP -j DROP # 2. 在 Nginx 配置里针对该接口做限流 limit_req_zone $binary_remote_addr zoneapi_limit:10m rate10r/s; # 3. 配置 WAF 规则拦截异常 UA 或高频访问这轮操作十分钟内就把 CPU 压下来了。但这个案例给我们的教训是对外暴露的接口必须做限流。没有限流的接口就像没有检票口的景区拥堵只是时间问题。5.2 定时任务引发的“CPU 尖峰”有段时间一台数据库服务器每天凌晨 3 点整 CPU 都会冲到 95%持续 20 分钟后回落到正常。业务侧在这个时间段根本没有流量数据库连接数也平稳排查起来很反常。先看监控曲线CPU 尖峰的时间非常规律——每天都在 3 点 05 分开始3 点 25 分结束。据此怀疑是定时任务查看系统 crontabgrep -r 3 \* \* \* /etc/crontab /etc/cron.d/ /var/spool/cron/找到了罪魁祸首运维同事之前加了一个备份脚本把数据库备份文件在凌晨 3 点用tar gzip压缩。压缩几十 GB 的数据文件CPU 和 IO 同时被打满刚好和业务高峰期错开却直接和监控巡检重合了。解决方式很简单把压缩任务的执行时间挪到凌晨 4 点半。用pigz多线程压缩压缩时间从 20 分钟降到 6 分钟。用ionice -c 3让备份任务的磁盘 IO 降级不让它抢占业务 IO。这类问题的核心教训是所有定时任务都要评估对 CPU、内存、磁盘 IO 的影响并且同一时间点不要堆叠多个重量级任务。5.3 伪装成 kswapd 的挖矿进程这个案例是最典型的恶意程序场景。客户的一台公网服务器 CPU 持续 100%top排第一的进程名显示kswapd0。懂 Linux 的人都知道真正的 kswapd 是内核线程不可能出现在用户进程列表里。但不懂的人看到“系统进程”就不会多想这恰恰是木马的伪装策略。排查过程# 查看可执行文件路径发现 /tmp/.X11-unix/kswapd0路径异常 ls -l /proc/可疑PID/exe # 查看网络连接发现连接到一个陌生 IP 的 8443 端口 ss -antp | grep 可疑PID # 查看定时任务发现了下载脚本的后门 crontab -l确认是挖矿木马后按“断网、删后门、杀进程”的顺序处理# 先封外联 IP iptables -A OUTPUT -d 陌生IP -j DROP # 删除 cron 里的恶意任务 crontab -e # 删除恶意文件 rm -rf /tmp/.X11-unix/ # 最后杀进程 kill -9 恶意PID这一步处理完了只是止血。后续安全审计发现入侵入口是 Redis 服务未授权访问——Redis 监听在公网 IP 的 6379 端口没有设置密码攻击者通过 Redis 写文件功能把公钥写进了 root 的 authorized_keys。修复动作包括Redis 绑定内网 IP、设置强密码、禁用CONFIG命令、检查所有 SSH 公钥、全盘扫描可疑文件。这个案例提醒所有运维CPU 100% 未必是性能问题也可能是安全问题。每次 CPU 异常飙高都要例行检查外联连接和计划任务。6. 常见问题速查与长期运维清单6.1 CPU 飙高的排查七问速查表下面是我反复在内部文档里用的一张速查表把 CPU 飙高排查中最关键的七个问题列出来按顺序走一遍绝大多数场景都能定位。问题查看方式结论指向哪个进程 CPU 最高top -bn 1按 %CPU 排序直接锁定问题进程CPU 时间花在用户态还是内核态top里 us/sy 字段us 高查业务代码sy 高查系统调用和锁是哪个线程在烧 CPUtop -Hp PID线程级定位配合 jstack/perf有没有大量上下文切换vmstat 1看 cs 字段cs 超高说明线程频繁切换负载高但 CPU 不高uptime和top对比可能瓶颈在 IO 或锁等待外联连接是否异常ss -antp可疑 IP 可能是挖矿或 C2 回连是否是定时任务触发对比 CPU 时间点和 cron 日志任务撞车或任务本身过重这张表看着简单但实战价值很高。我见过太多人拿到问题就一头扎进代码里忘记了先回答“是不是流量问题”“是不是定时任务问题”这种基础问题。6.2 日常巡检与预防清单最后整理一份长期运维的预防清单。CPU 飙高防护从来不是“出现一次处理一次”而是把预防措施做到前面。第一业务层面做到有备无患新上线服务必须做压测摸清单机承载上限。所有对外接口配置限流不管是云盾、Nginx limit_req 还是应用层限流组件。代码 Review 阶段关注循环、正则、递归的复杂度问题。第二系统层面做到资源隔离所有服务通过 systemd 托管配置 CPUQuota 和内存限制。核心业务与非核心业务分离部署避免互相干扰。定时任务统一管理错峰执行限制 CPU 和 IO 优先级。第三监控层面做到早发现早处理确保 node_exporter 或 sysstat 在跑历史数据保留至少 30 天。配置 CPU、内存、磁盘、网络四类核心指标的告警阈值。每周看一次监控报表关注变化趋势而不是只看告警。第四安全层面做到入门口没漏洞公网服务最小化暴露SSH 禁用密码登录、改用密钥。Redis、MongoDB、ES 等中间件禁止监听公网使用强认证。定期检查系统登录日志和 cron 任务建立基线。我自己在操作中的体会是CPU 飙升的防护70% 的工作在平时就要做好——限流配置、资源限制、监控覆盖、权限收紧。剩下的 30% 才是真正遇到问题时的应急排查。如果你现在管理的服务器还没有做资源限制和监控告警建议今天就补上。等到 CPU 飙到 100% 的时候再想这些已经晚了。