ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux进程管理实战:从CPU爆高到僵尸进程排查

Linux进程管理实战:从CPU爆高到僵尸进程排查 凌晨两点被值班电话吵醒机房一台数据库服务器CPU打满登录上去top一看一个Java进程占了400%。当时第一反应是看它到底在干嘛结果ps、top、jstack轮番上阵折腾了快一个小时才定位到一个线程池满负荷。那会儿我就想Linux下的进程管理很多时候不是“不会命令”而是碰到真问题时脑子里没有一套完整的排查路径。这篇是Linux系列的第4篇专讲进程管理。适合三类人看刚入行的运维、写服务端代码但很少上线的开发以及准备Linux面试的朋友。我不会系统背诵每个命令的全部参数只把最常用、最容易被问到的部分拆开揉碎结合排查案例讲清楚怎么看懂进程状态、怎么控制进程生命周期、怎么给进程改名、进程之间怎么通信以及遇到进程故障时从哪下手。1. 先学会看“全家福”ps、top、pidstat的实测解读进程管理的第一步不是杀进程而是先会看进程。很多新手会背参数但真给他一台服务器让他说出“当前哪些进程最耗CPU”他反而答不上来。原因只有一个没理解每个字段的含义看到输出也不知道该怎么办。1.1 ps aux的每一列到底在说什么ps aux是我在单台机器上用得最多的命令输出长这样数据做了脱敏USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.1 168852 11864 ? Ss Feb20 0:11 /usr/lib/systemd/systemd www 1024 12.3 8.5 745210 534652 ? S 09:30 240:32 /usr/local/php/sbin/php-fpm master www 1025 3.2 3.1 512233 193345 ? S 09:30 55:10 php-fpm: pool www mysql 1560 1.1 9.2 1889200 576120 ? Sl Feb21 89:33 /usr/sbin/mysqld这里最容易被误解的是%CPU。它不是“总CPU的百分比”而是相对于单个核心的占比。如果机器是32核一个进程显示400%意味着它占满了4个核心这在多核机器上完全正常。面试里考“进程CPU占用高怎么看”多半就是看这列。VSZ是虚拟内存RSS是实际驻留物理内存。你只要记住RSS才是真正占用的内存量VSZ看看就好。STAT里藏着的字母才是关键状态S可中断睡眠等待某事件最常见R运行中或等待运行top里排在前面的多半是它D不可中断睡眠通常在等IO这种状态很难被kill掉T被停止比如CtrlZ挂起Z僵尸进程进程已死但父进程还没回收START是启动时间TIME是进程累计消耗的CPU时间注意是累计不是当前占用。看一个进程是否长期吃CPUTIME比%CPU更有参考价值启动才10分钟TIME就有50分钟说明它自从启动后几乎一直在跑。1.2 top的交互操作和负载含义top是动态视图但很多人只用来看前几行这是浪费。第一行load average: 8.26, 7.54, 6.81分别代表1分钟、5分钟、15分钟的平均负载注意这里的负载是“运行队列不可中断睡眠”的平均线程数不是CPU使用率。一台8核机器负载常年8以上基本可以判断CPU一直是满的。在top运行界面里这几个按键建议背下来P按CPU占用排序默认就是M按内存占用排序T按累计CPU时间排序适合找“长期消耗”的进程u输入用户名只看某个用户的进程k输入PID和信号发送killr调整进程优先级H切换线程视图排查多线程应用时非常有用我的习惯是先用top按P找到大头再按M看是否有内存黑洞最后用u过滤出业务进程。1.3 用pidstat做定向追踪top看的是全局想知道某个进程的CPU、内存、IO随时间怎么变化十有八九要用pidstat属于sysstat包没装的话yum install sysstat或apt install sysstat。# 每秒采样一次只追踪PID 1024 pidstat -p 1024 -u 1 # 看内存和缺页情况 pidstat -p 1024 -r 1 # 看IO读写 pidstat -p 1024 -d 1举个例子你怀疑某个PHP-FPM进程在大量读磁盘pidstat -d会显示kB_rd/s和kB_wr/s一眼就知道IO是否是瓶颈。这种逐进程的定向观察top给不了。2. 进程的一生创建、运行、停止与资源回收会看进程之后第二步是搞清楚进程从诞生到消亡的完整链路。因为很多棘手问题就出在被忽略的阶段后台运行、挂起恢复、正常终止、僵尸残骸。2.1 谁创建了进程以及“双胞胎坑”Linux的进程是树状的除了init/systemd每个进程都有父进程PPID。创建机制是fork()exec()fork复制当前进程的内存镜像exec替换成新程序。这里有个经典面试题孤儿进程和僵尸进程的区别。孤儿进程父进程先退出子进程被init/systemd收养变成“有父”的进程。孤儿进程本身无害会被系统自动回收。僵尸进程子进程先退出但父进程没有调用wait()回收它的退出状态进程表里留下一个“已死未回收”的条目。判断手段很简单ps aux里看到STAT是Z的就是僵尸。僵尸进程不占CPU不占内存但它占进程表项。进程PID有上限僵尸堆到满的时候会导致整个系统无法创建新进程。2.2 前后台切换和会话灵魂日常操作里最简单的控制流程就是前后台切换。前台进程一旦占用终端你就无法输入别的命令。按CtrlZ把进程挂起会看到[1] Stopped此时用jobs查看列表bg让它到后台接着跑fg再拉回前台。[roothost ~]# sleep 300 ^Z [1] Stopped sleep 300 [roothost ~]# jobs [1] Stopped sleep 300 [roothost ~]# bg %1 [1] sleep 300 [roothost ~]# fg %1 sleep 300 ^C真正让我踩过坑的是“ssh断开导致进程退出”。用nohup解决了以为万事大吉结果第二天发现进程还是没了。根本原因在于nohup只是屏蔽了SIGHUP信号但如果进程是从当前终端继承了标准输入ssh断开后进程读输入会读到EOF直接退出。正确姿势是把标准输入也重定向走nohup ./app /dev/null app.log 21 后来我干脆用setsid彻底脱离当前会话或者干脆写个systemd service。对于需要长期托管的服务systemd永远是最稳的归宿。2.3 终止进程不是只会kill -9kill这个命令名起得很有误导性它其实是一个信号发送器。不用-9的情况下默认发的是SIGTERM给进程一个优雅退出的机会。流程是先kill PID等几秒不行再kill -9 PID。信号列表用kill -l查看。长用的几个SIGTERM(15)请求终止进程可以自行清理后退出SIGKILL(9)强制终止进程无法捕获也无法清理SIGHUP(1)挂断信号很多服务用它来重新加载配置SIGSTOP(19)暂停进程进程无法捕获SIGCONT(18)让暂停的进程继续我要特别强调一句kill -9不是不能用而是不应该作为首选。数据库、状态机这类进程被强杀后可能留下文件锁、binlog不一致、数据刷盘不完整恢复成本远超“多等两秒”。优先级思路应该是SIGTERM - SIGHUP(重载) - SIGKILL最后打底。批量杀进程的时候也要小心。pkill -f匹配的是完整命令行一个pkill -f test可能把写同样关键词的无关进程误杀。我一般在pkill之前先pgrep -f 关键词看一眼匹配范围确认无误再动手。2.4 nice与reniceCPU资源的“礼让”进程优先级由nice值决定范围是-20到19越小优先级越高。普通用户只能调高让进程赶紧跑或调低自己让出资源root才有权调成负数提升优先级。# 以较低优先级运行任务 nice -n 10 ./backup.sh # 调整已经运行的进程 renice -n 15 -p 5678Cgroups出现之后优先级调度显得“粗糙”但在没有systemd环境的裸机器上nice/renice依然是影响进程CPU占比的唯一简单手段。比如对备份任务执行renice确保它不影响在线业务。3. 给进程“改名换姓”修改进程名称的完整方案这个需求遇到的人不少Python写的Worker启动后ps里看到的全是python3 xxx.py根本分不清哪个是哪个运维监控按进程名做告警分组却发现所有子进程共用同一个名字。网络上“linux修改进程名称”的热度很高但多数教程只说了一个最小实现没有讲懂原理。3.1 先搞清你的“进程名”指的是哪一层Linux下进程名其实有两个载体/proc/PID/comm内核层面的进程名只有15字节上限ps -o comm、top的COMMAND列、killall都基于这一层。/proc/PID/cmdline完整命令行参数ps aux的COMMAND列、pkill -f匹配的是这一层。所以“改名”要分场景只希望监控能看到可读名字改comm就够了希望ps输出也变化那要动argv[0]。3.2 Python和Shell两种改法实测Python在处理多进程任务时最省事的是第三方库setproctitle# pip install setproctitle import setproctitle # 在Work子进程里设置 setproctitle.setproctitle(crawler-worker-1)这个库原理是做了两件事修改进程的argv内容同时通过prctl(PR_SET_NAME)改comm所以ps aux、htop、/proc/PID/comm都会同步变。实测下来很稳推荐优先使用。Shell脚本或者不方便装库的场景可以用exec -a# 让进程在ps里显示为custom-worker exec -a custom-worker /usr/bin/python3 /opt/app/main.py注意这种方式只改了cmdline里的argv[0]/proc/PID/comm不会变。也就是说ps aux看到的是custom-worker /usr/bin/python3 /opt/app/main.py但killall还是会按comm里的python3去找。还见过一种土办法创建一个带业务名字的软链接指向解释器比如ln -s /usr/bin/python3 /usr/local/bin/myworker再执行/usr/local/bin/myworker script.py。效果和exec -a类似纯靠argv[0]变化。3.3 改名之后的隐性问题最典型的坑是改名后killall -9打不中进程。原因是killall匹配comm而comm不是你想改就叫什么。解决方式要么把comm也改掉要么改用pkill -f custom-worker。第二个坑是agent监控的误判。有些监控脚本用ps aux | grep -v grep | grep python3来判断服务在线你一旦改了进程名告警直接炸开。上线改名方案前一定要先同步给监控组。第三点如果你在用systemd管理服务进程名和Unit名经常不一致。比如ExecStart是java -jar app.jarps里看到的是java但systemd的systemctl status会显示Unit名。这时候进程名再改成别的排查时三套名字对不上非常痛苦。我的经验是生产环境改名要“分层统一”comm、cmdline、Unit Description都用同一条业务标识。4. 进程之间怎么协同IPC的实用选型逻辑进程管理从来不只是单进程的事生产上更多是“一堆进程怎么协作”。Linux下进程间通信IPC方式很多面试题也爱横向对比。我不打算背概念只从实际选型角度聊聊。4.1 最轻量的信号和管道信号适合“通知”而不是“传数据”。比如SIGHUP让nginx重载配置SIGUSR1让某个worker重新初始化。我在脚本里常用的方式# 给php-fpm主进程发USR2让它平滑重载 kill -USR2 $(cat /var/run/php-fpm.pid)管道分两种匿名管道|只能用于父子进程之间命名管道FIFO用mkfifo创建不相关的进程也能通。命名管道在简单的一对多广播场景里非常好用比如采集系统分发配置给一组消费进程。4.2 共享内存和消息队列的取舍进程间传递大数据块共享内存性能最高。mmap映射文件到内存或者shm_open创建POSIX共享内存对象。但性能高不是免费的你得自己处理并发冲突——两个进程同时写时怎么办一般配信号量semaphore做锁。消息队列适合数据量中等的异步任务分发。System V的msgget/msgsnd/msgrcv是经典实现POSIX的mq_*接口更干净。但真在业务里用原生消息队列的越来越少因为Redis这类中间件做消息分发已经够用还自带持久化和消费确认。知道原理选型时心里有数就行。4.3 最通用的Socket和D-BusUnix Domain SocketUDS是我个人最偏爱的本地IPC方式。原因是可靠双向的字节流、支持传递文件描述符、支持多客户端连接、不会像共享内存那样必须处理复杂的锁同步。Nginx和php-fpm的通信就是经典的UDS实践。如果进程之间是“服务调用”关系而非“数据搬运”D-Bus是Linux桌面和系统服务里常见的消息总线方案。它负责注册、发现、广播调用gnome等桌面组件、NetworkManager、systemd这些系统服务都在用。但它有性能上限不适合高频大数据传输——别拿D-Bus干共享内存的活。把上面的内容整理成一个选型表方便直接对照场景需求首选方案备选方案通知信号/唤醒事件signalfutex父子进程传数据匿名管道信号不相关进程传小数据命名管道Unix Socket大数据共享mmap信号量共享内存异步任务分发消息队列Redis本地服务调用Unix SocketD-Bus跨机器通信TCP/UDP SocketgRPC/HTTP5. 进程管理翻车现场三个典型故障的完整复盘讲再多命令不如复盘几场真实故障。下面三个案例是我自己在生产环境遇到过或围观过的排查链路都值得抄作业。5.1 CPU爆高但top里找不到元凶现象top第一行load average冲到30以上但列表里没有单个进程特别夸张分布在全机器。排查思路先按P排序没结果怀疑是多线程应用把CPU分散在线程上。此时top中按H切换线程视图或者用ps -L -p PID列出某进程的所有线程。重点看线程的TID。如果是Java进程拿到占用最高的线程TID后转成十六进制printf %x\n 28467然后用jstack PID | grep -A 30 0x6f33立刻能定位到是哪个线程池、哪段代码的行号。如果是C/C进程gdb attach PID看线程栈或者用perf top -p PID看热点函数。这个案例的通用心法是进程高CPU只是结果线程级定位才是过程。系统没有线程视图时很多排查会撞墙养成“先看线程再下结论”的习惯。5.2 僵尸进程堆积导致新进程无法启动现象业务频繁重启但每次重启都失败报错Resource temporarily unavailable。排查链路# 1. 查看僵尸进程数量 ps aux | awk $8 ~ /Z/ | wc -l # 2. 找到僵尸进程和它的父进程 ps aux | awk $8 ~ /Z/ {print $2, $3} # 3. 查父进程是谁 ps -o pid,ppid,stat,cmd -p 上面查到的PPID发现僵尸进程的父进程是个常驻的守护脚本它fork子进程后没调用wait回收子进程退出后全变僵尸。累计几千个后进程数到达上限新进程fork不出来。解法分两步先修父进程——必须重启这个常驻守护脚本让init/systemd接管它的子进程走完自然回收流程同时优化代码waitpid(-1, status, WNOHANG)定期回收子进程退出状态。这提醒我功能上线前要检查所有“起了子进程就不管”的代码尤其脚本里反复nohup ... 起后台任务的地方。5.3 明明nohup了进程还是跟着ssh断开而死现象用nohup python3 app.py 启动服务本地测试一切正常一旦办公网断连或者ssh超时退出进程就没了。复盘原因进程虽然忽略了SIGHUP但它继承了ssh会话的标准输入、输出、错误——也就是挂着tty。tty对象随ssh断开被销毁时进程在写日志或读输入时收到异常最终退出。更隐蔽的是很多程序在stdin读到EOF时直接触发自身退出逻辑。正确做法是一个“三位一体”重定向setsid nohup python3 app.py /dev/null app.log 21 setsid负责让进程完全脱离当前会话nohup负责兜底/dev/null防止EOF误触发。配合echo $! pidfile记录PID方便后续管理。如果想长期托管最稳的仍然是定义为systemd serviceTypesimpleExecStart/opt/app/run.shRestartalways让init进程直接托管不再依赖任何终端。6. 写在后面我常用的进程管理自查清单根据这几年的排障经验我把进程管理相关的操作收敛成一张自查清单新环境排查直接按顺序走能省不少时间先看top确认状态记录load average和CPU/内存占比用ps aux --forest看进程树形态找父子关系是否异常有异常进程时用pidstat做单进程追踪确认是CPU、内存还是IO问题僵尸进程用ps aux | awk $8 ~ /Z/定位父进程后台任务确认是否完成“nohupsetsid三重重定向”跨进程协作先确认通信方式是管道、socket还是共享内存避免结构错配杀进程前默认先SIGTERM确认无效再SIGKILL我个人体会最深的一句话是进程管理会被很多人当成“会几个命令就够”但实际上它是一套从观测、判断、处置到复盘的方法论。碰到过一次半夜的CPU告警你就会明白多背几条命令远不如真正理解一个进程从fork到僵尸的一生来得实在。如果把这篇当作Linux系列的地基下一篇我会接着聊聊几个和进程强相关的子主题比如多线程环境下到底该怎么观察和干预。
返回列表