ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux进程管理与计划任务全解析:从原理到生产环境避坑

Linux进程管理与计划任务全解析:从原理到生产环境避坑 接手一台Linux服务器第一件让人心里有底的事就是搞清楚这台机器上到底跑了些什么以及什么时间该自动去干什么。进程管理和计划任务这两块东西单独拎出来看都是基础得不能再基础的操作可实际一上手就发现问题不少写好的脚本在终端里跑得好好的放进crontab就一言不发集群里某个节点突然CPU飙高你连是哪个进程干的都找不到半夜一个计划任务把磁盘塞满了第二天早上才发现。这篇文章我打算把进程和计划任务这两块放在一起讲透从原理到实操再把我这些年踩过的坑一并交代出来希望对正在折腾Linux的朋友有点用处。1. 先把进程管理的骨架搭明白进程管理是Linux系统运维的地基不管是排查性能问题还是在写脚本时正确处理后台任务都绕不开进程这个概念。很多人用Linux好几年ps、top这些命令倒是敲得很顺可真问起来进程到底是什么、为什么会有僵尸进程、D状态又代表什么意思一下就被问住了。这一节我先把进程管理的底层逻辑拆开再一步步带出常用的命令和实操场景。1.1 进程到底是什么PID和父子关系先搞清楚进程说白了就是正在运行的程序实例。程序是硬盘上躺着的一堆二进制文件进程是操作系统把它加载进内存之后、正在被CPU一条条执行的那个活物。同一个程序可以同时启动多个进程比如你开了两个终端跑同一个脚本就有两个独立的进程它们各自的变量、内存、状态完全隔离互不干扰就像同一个剧本被两个不同的演员同时表演演的虽然是同一出戏但彼此不共享任何台下的记忆。每一个进程都有一个唯一的进程ID就是PID。PID从1开始分配通常逐渐递增一个进程结束之后它的PID还可能被后来者复用。Linux系统里的第一个进程是PID 1也就是systemd老的SysVinit时代叫init整个用户空间的所有进程都是它的子孙后代。与PID对应的还有PPID也就是父进程ID通过它可以追溯每个进程是从哪里来的。进程之间的父子关系最直接的感受就是你在终端里跑一个命令这个命令就是当前shell的子进程你用shell脚本启动了一个服务服务就是脚本的子进程。有时候你会发现一个进程莫名其妙消失了可能是它的父进程把它带走了也可能是父进程先挂掉它变成了孤儿进程。Linux处理孤儿进程有一套机制孤儿会被统一收养到PID 1下面也就是init/systemd管理这样就不会出现那种完全没人管的野进程。查看进程树最直观的办法是用ps加一个森林选项命令是ps -ef --forest它会用树状缩进展示父子关系。我排查启动脚本问题的时候特别喜欢用这个命令一眼就能看出service到底是不是由预期进程拉起来的。1.2 进程状态机运行、睡眠、停止、僵尸Linux里的进程不是只有“运行”和“没运行”两种状态它有一套完整的生命周期用ps命令看进程时STAT那一列显示的几个字母就是当前进程的状态码。常见的状态有这些状态码含义说明Rrunning / runnable正在运行或者正在等待CPU调度随时可以运行Sinterruptible sleep可中断睡眠。进程正在等待某个事件如IO完成可以被信号唤醒Duninterruptible sleep不可中断睡眠。通常是在等待磁盘IO无法响应普通信号Tstopped停止状态。一般是被CtrlZ暂停或者被kill -STOP信号挂起Zzombie僵尸状态。进程已经结束但父进程还没有回收它的退出信息初学者最容易困惑的就是僵尸进程和D状态。僵尸进程不是真的还活着它其实已经执行完了只是在进程表里留下了一个“已退出但无人收尸”的条目等待父进程调用wait()来读取它的退出码。如果父进程一直不调用wait僵尸就一直留在进程表里。僵尸本身不吃CPU也不吃内存但如果大量堆积进程号会被耗尽导致无法创建新进程。D状态比僵尸更麻烦。进程在等待IO比如磁盘卡顿、NFS挂载点无响应时会进入不可中断睡眠此时你给它发kill -9它都没反应。这类进程通常只能等IO恢复或者重启系统没有别的太好办法。我遇到过某云盘挂载点出问题一堆进程全部卡在D状态怎么kill都纹丝不动最后只能把整个挂载点重启。停下来多看一眼STAT列非常有价值。比如你用ps aux看到一堆Z开头的进程说明父进程代码有bug没有正确回收子进程看到一堆D就要先检查存储链路。1.3 常见的进程查看手段与信息解读排查进程问题最常用的两个命令是ps和top。ps适合抓快照一次性看所有进程的静态信息top适合持续观察动态刷新CPU和内存占用。两者各有不可替代的作用配合起来用基本能满足绝大多数场景。先聊ps。虽然ps支持很多参数组合但我实际最常用的是这两个ps -ef显示所有进程的完整命令行ps aux以BSD风格显示所有进程包括CPU和内存占用率对比一下两者的差异ps -ef不带%CPU和%MEMps aux带。但ps aux开头有个奇怪的USER列加上很多发行版对ps aux的“始终显示进程自身”有历史行为差异所以我一般推荐用ps -ef --forest拿进程关系用ps aux看资源占用。ps aux输出里几个关键字段我举个小栗子来说明USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1234 0.3 1.2 162144 23000 ? S 09:12 0:02 /usr/sbin/nginx -c /etc/nginx/nginx.confVSZ是虚拟内存大小表示进程可以访问的地址空间大小单位是KBRSS是常驻内存大小表示进程实际驻留在物理内存中的大小。类比一下VSZ相当于你出门前带上的所有信用卡总额度RSS才是你真的揣在兜里的现金。看一个进程吃多少内存重点看RSS。top就直观多了默认按CPU占用排序。进入top之后有几个非常常用的交互键按P按CPU排序按M按内存排序按k可以给进程发信号按c可以展开完整命令行按1可以看每个CPU核心的负载。还有个更容易入门的htop有颜色有树状视图甚至可以直接鼠标操作适合不习惯纯键盘交互的朋友。不过很多生产环境没有预装htop建议还是把top练熟起码到任何机器上都能直接用。提到性能排查还有一个被系统管理员低估的命令叫pidstat它是sysstat包里的工具。比如你想看看某个进程5分钟内的CPU波动用pidstat -p PID 1 300就能每秒采集一次、采集300次然后自己看趋势。跟top相比pidstat更适合脚本化监控和导出数据。2. 进程管理实操从命令行到脚本看懂进程状态只是第一步真正考验功力的地方是实际动手管理进程尤其是处理前台后台切换、让服务在终端关闭后继续存活、调整优先级还有最考验自己的杀进程。这些操作看似一个个分散的小命令但在真实环境下组合出问题的情况特别多我挨个讲。2.1 前台任务与后台任务的切换技巧在终端窗口里直接执行的命令就是前台任务它会一直占着这个终端直到执行完毕。假如你启动了一个需要长时间运行的命令比如一个大文件的压缩解压或者一个临时起的长任务Web服务整个终端就被卡住了其他命令都敲不了。这时候怎么办很多人第一反应是再开一个终端窗口。其实完全不用Linux早就为这种情况设计好了前后台切换机制。在任务运行过程中按CtrlZ可以把它挂起此时进程会进入T状态stopped任务停在那里等你后续处理。挂起之后你会回到shell提示符这时候可以继续敲其他命令也可以用jobs查看所有被挂起的任务列表。jobs输出第一列的中括号数字就是job号比如[1]、[2]。想把某个挂起的任务拉回前台继续运行用fg %任务号想让它在后台继续跑用bg %任务号。直接举例[rootlocalhost ~]# sleep 300 ^Z [1] 已停止 sleep 300 [rootlocalhost ~]# jobs [1] 已停止 sleep 300 [rootlocalhost ~]# bg %1 [1] sleep 300 [rootlocalhost ~]# ps -ef | grep sleep注意CtrlZ挂起的进程和后台进程是两回事。后台进程是直接通过符号启动的比如sleep 300 它本来就在后台运行CtrlZ挂起只是暂停让它继续运行得用bg切换。在这里有个非常容易踩的坑后台进程虽然不在前台占着终端但它的输出还是会直接打到当前终端上而且当你在同一个终端里执行了exit退出shell时后台进程通常会收到SIGHUP信号直接被杀掉。想让进程彻底不受终端生命周期影响得用下面讲的nohup或者setsid。2.2 让进程脱离终端的正确姿势后台运行并不等于守护进程化这是很多刚接触Linux的人最大的误解。用sleep 300 启动的进程虽然终端看起来解放了但它仍然是当前shell的子进程shell退出时给所有子进程发SIGHUP信号这个后台进程大概率就跟着没了。要验证这个现象很简单你在一个终端里启动后台任务然后退出终端重新登录后发现进程已经不存在了。要让任务在终端关闭后继续存活有几个成熟的办法。第一个是nohup全称no hang up专门用来忽略SIGHUP信号nohup /opt/app/run.sh /var/log/app/run.log 21 nohup 是经典组合。nohup保证进程不响应SIGHUP让它放到后台。命令最后的 /var/log/app/run.log 21把标准输出和标准错误都重定向到同一个日志文件里不然nohup会默认生成一个nohup.out文件放在当前目录时间一长容易把目录搞乱。第二个是setsid它直接让进程开启一个新的会话彻底脱离当前控制终端。场景上比nohup更干净不用理会SIGHUP的问题因为进程连会话都换掉了。不过实际使用中nohup足够覆盖绝大多数需求setsid主要用在那种需要双fork守护进程化、对环境有洁癖的场景。第三个是disown它是shell内建命令把已经启动的后台任务从shell的任务表中移除这样shell退出时就不会给它发SIGHUP信号了。用法是sleep 300 disown -h %1注意disown一定要在任务启动之后、shell退出之前执行它管不了已经开始逃跑的进程。我个人的习惯是临时后台跑东西用nohup写systemd服务用服务文件管理这两个套路最不容易出幺蛾子。2.3 进程优先级与资源限制调整Linux是一个多任务系统CPU时间片怎么分配很大程度取决于进程的优先级。优先级在ps -l命令输出里对应PRI列在top里对应PR列和NI列。NI就是nice值取值范围从-20到19默认是0。nice值越小优先级越高越容易抢到CPU时间片nice值越大优先级越低越谦让。普通用户只能把nice值调大也就是让优先级变低不能调小因为调小会影响其他用户的公平性。root用户则可以任意调整。启动命令时直接设置优先级用nice命令对已经运行的进程调整用renice命令# 以较低优先级启动压缩任务 nice -n 10 tar czf /data/backup/$(date %F).tar.gz /data/app # 修改已有进程的nice值 renice -n 15 -p 12345实际运维中你会用到这个功能的典型场景是一台服务器上既有线上服务又有临时数据压缩任务如果不调整优先级压缩任务很可能把CPU都吃光导致线上接口响应变慢。我给这类任务设计规范时一般要求所有批处理任务必须显式设置nice值禁止裸奔启动。top里按r键可以交互式renice一个进程会提示你输入PID和nice值。这个操作适合临时性调整如果是一个长期存在的服务应该写进systemd服务配置里的Nice字段稳定可控。2.4 杀进程的正确姿势先学会确认再动手进程管理里最危险的操作就是杀进程尤其在生产环境。我见过太多因为杀错进程导致线上事故的情况大多数都是因为没确认清楚就在那里kill -9。这里不夸张地说kill命令用好了是利器用不好就是灾难。Linux的信号不止SIGKILL9号一个但很多人下意识只会kill -9。其实每个信号都有它该用的场景关键是要理解进程收到信号后的反应信号编号行为常用场景SIGHUP1挂起信号默认终止进程让守护进程重新读取配置文件如nginx -s reloadSIGINT2键盘中断默认终止进程相当于CtrlCSIGTERM15终止信号默认终止进程优雅杀进程给进程自己做清理的机会SIGKILL9强制终止无法捕获和忽略终极手段进程将直接消失SIGSTOP19暂停进程无法捕获和忽略挂起进程相当于CtrlZSIGCONT18继续执行暂停的进程恢复被SIGSTOP的进程为什么要先用SIGTERM而不是直接上SIGKILL因为SIGTERM给了进程执行清理动作的机会比如保存状态、释放锁、关闭文件描述符很多服务都能在收到SIGTERM时正常退出。SIGKILL是直接从内核层面干掉进程进程完全没有反应时间你kill掉的可能是它正在写一半的文件也可能是它正准备落盘的数据库事务。我总结的杀进程安全流程是三步确认目标。不管是ps、pgrep还是top先把要杀的进程PID和命令行看清楚最好能看一眼父进程和子进程关系。先温和后暴力。先kill默认SIGTERM等几秒观察进程还在不在再决定要不要kill -9。杀完验证。ps确认进程确实消失端口确实释放关联服务没有异常。另外要特别提醒pgrep -f这个匹配的坑。pgrep -f是对完整命令行做模糊匹配一旦你匹配的字符串写宽了很容易把一堆不相关的进程一起匹配上。比如你想杀的是跑在/tmp/test.sh的脚本写了pgrep -f test结果把名字里包含test的所有进程全杀了。我的习惯是pgrep -a先列出匹配到的所有PID和命令行确认无误再kill。3. 计划任务的核心cron这件事怎么玩明白进程管理管的是“现在跑什么”计划任务管的是“到什么时间自动跑什么”。Linux系统里最传统也最通用的计划任务工具就是cron几乎每个发行版都带。cron本身不复杂但配置里到处是细节写错一个语法、漏了一个环境变量任务就安安静静地不出声了。这一节从机制到实操把cron撸明白。3.1 cron的工作机制与两条配置线crond是Linux的后台守护进程它每分钟醒来一次检查当前时间是否匹配某个计划任务的配置匹配就触发对应命令。这个“每分钟检查一次”的机制决定了cron的精度只有分钟级你想让任务每秒执行一次写在crontab里是实现不了的得靠脚本内部循环或者别的工具。配置计划任务有两条线系统任务和用户任务。系统级别的任务写在/etc/crontab文件里以及/etc/cron.d/目录下用户级别的任务用crontab -e编辑存储在/var/spool/cron/目录下每个用户对应一个文件。很多人分不清/etc/crontab和crontab -e的区别这里说清楚/etc/crontab是系统配置文件它比用户crontab多了用户字段格式上需要指定以哪个用户身份执行命令。直接搬个例子# 系统crontab示例命令前面要写用户名 0 3 * * * root /usr/local/bin/backup.sh # 用户crontab示例不需要写用户名 0 3 * * * /usr/local/bin/backup.sh大多数个人的、按用户隔离的任务用crontab -e就足够涉及到需要在机器上统一管理的系统级任务比如日志清理、监控采集放/etc/cron.d/目录里更合适因为运维脚本可以通过配置管理工具统一部署这个目录下的文件不用挨个用户去改。另外还有个快速使用技巧crontab -l查看当前用户的计划任务crontab -r清空当前用户的计划任务这个操作没有默认确认提示一旦误触全没了慎用crontab -u用户名可以管理其他用户的任务仅限root。3.2 crontab时间字段到底怎么填crontab的语法是五段式时间加命令看起来很简单但坑就藏在细节里分 时 日 月 周 命令五个字段分别表示分钟0-59、小时0-23、日期1-31、月份1-12、星期0-70和7都表示周日。每个字段还支持星号所有可能的值、逗号枚举多个值、减号范围、斜杠步长。下面这几个写法是最高频的建议直接背下来# 每天凌晨2点30分执行 30 2 * * * /usr/local/bin/backup.sh # 每10分钟执行一次 */10 * * * * /usr/local/bin/check.sh # 每周一到周五早上9点执行 0 9 * * 1-5 /usr/local/bin/report.sh # 每月的1日和15日的3点执行 0 3 1,15 * * /usr/local/bin/clean.sh # 每两小时执行一次 0 */2 * * * /usr/local/bin/health.sh斜杠前面如果省略其实就是从范围最小值开始按步长走。/10的意思是每10分钟如果要每35分钟写/35就能实现吗不能因为*/35只会在0、35分执行不是每35分钟执行因为小时位不会跟着调整。很多新手在这里被坑过产生“为什么我的任务怎么只在整点和35分跑是不是坏了”的疑问。还有一个非常经典的易错点日和星期同时指定时cron的处理逻辑是“或”关系而不是“且”关系。比如下面这个写法0 0 1 * 1这个任务会在每月的1号执行同时还会在每个周一执行而不是“只有既是1号又是周一才执行”。如果你想让任务只在每月1号不管周几执行把周字段写成星号想让任务只在周一执行且必须是当月的某种条件就得在脚本里做日期判断。3.3 用实际案例写出可落地的计划任务光会填字段还不够真正写出来的计划任务要能稳定运行几个月不炸才算合格。我拿三个最常见的场景举例子你基本可以照着改。第一个是日志清理。应用日志增长很快是常事用磁盘空间报警前就把旧日志清掉。# 每天凌晨3点清理7天前的应用日志 0 3 * * * find /data/log/app -name *.log -mtime 7 -exec rm -f {} \;这里mtime 7表示修改时间超过7天。把清理动作放到find的-exec里而不是管道后面可以避免文件名带空格导致的分词问题。不过这种方式如果日志文件特别多执行时间会比较长我见过日志量大的机器上这个命令跑了十几分钟建议量大的场景写成脚本用find配合循环分批删。第二个是数据备份。直接想当然地在crontab里写备份命令会埋雷因为备份脚本通常依赖数据库客户端、压缩工具、环境变量这些在cron环境里都不一定好使。正确的姿势是写一个独立脚本然后crontab只负责调脚本# 每天晚上2点执行数据库备份脚本 0 2 * * * /usr/local/bin/mysql_backup.sh备份脚本内部要包含完整的逻辑进入备份目录、执行导出、压缩、清理7天前的旧备份、记录日志。脚本开头最好显式设置所需要的PATH变量比如export PATH/usr/local/mysql/bin:/usr/local/bin:/usr/bin:/bin然后再执行命令否则很可能出现“交互终端里跑得好好的crontab一执行就报command not found”。第三个是接口健康检查。定时探测某个HTTP接口失败时发提醒。这个在crontab里写一行命令就能对付# 每5分钟检查接口状态失败时触发告警脚本 */5 * * * * curl -fsS http://127.0.0.1:8080/health || /usr/local/bin/alert.shcurl的-f参数在HTTP错误码时失败-s静默模式-S出错时显示错误信息。配合||逻辑接口挂掉时执行告警脚本。注意这里告警脚本要事先确认它的执行路径是绝对路径不能用相对路径。3.4 计划任务的权限与安全控制crontab不是每个人都能随便用的。系统管理员可以通过/etc/cron.allow和/etc/cron.deny两个文件控制哪些用户可以创建计划任务。如果两个文件都不存在默认逻辑因发行版而异有些系统只有root能使用crontab有些系统所有用户都可以。生产环境建议只保留cron.allow文件里面显式列出允许使用的用户名这样权限边界最明确。更需要在意的安全细节是root的crontab权限实在太大能执行任何命令。所以我们应该尽量以最小权限运行计划任务能用普通用户跑的任务就不要用root。比如备份脚本如果只需要读数据库的特定库就用专门创建的备份账号不要用root账号连数据库。另外还要提防计划任务被利用来提权如果你有任何目录对普通用户可写而root的计划任务恰好会执行这个目录下的脚本普通用户就能替换脚本内容来获取root权限。这类安全隐患在安全加固检查里属于高频问题我的习惯是所有被crontab引用的脚本目录权限一律设为750以上属主设为root杜绝被低权限用户篡改。4. 计划任务不执行这套排查思路送给你几乎每个用cron的人都会遇到“任务没跑”的灵异事件。实际上cron的灵异事件90%都是可以快速定位的只是很多人排查时没有完整的思路东看一下子、西摸一下子浪费时间还找不到问题。我按排查顺序整理了一套方法按这个流程走一遍基本都能水落石出。4.1 先看服务状态、再来一遍日志任务没执行第一件事是确认crond服务本身是活着的。systemd环境下执行systemctl status crond确认是active (running)。在容器或者精简环境里crond经常没有被安装或者没有启动这是最常见的原因之一。服务正常再看cron日志。日志位置在多数发行版上是/var/log/cron。用tail命令看最近记录tail -n 200 /var/log/cron日志里每一行对应一次任务触发大致长这样Dec 15 03:00:01 hostname CROND[12345]: (root) CMD (/usr/local/bin/backup.sh)关键在于如果日志里都没有这一行说明crond根本没匹配到这条配置问题出在时间字段或者crontab文件本身如果日志里有CMD记录但任务看起来没生效问题就出在脚本执行部分。还有一种情况是cron执行了但脚本报错比如权限不够、命令不存在这些信息默认不会进cron日志而是通过邮件发送给当前用户或者被丢弃。系统上如果没配置邮件服务你又没做输出重定向错误就被吞了这也是“任务消失了”的最大原因之一。4.2 环境变量和路径问题是最大坑终端里手动执行脚本一切正常放到cron里就是不行排到最后的元凶十有八九是环境变量。cron执行命令时的PATH是非常精简的通常只有/usr/bin:/bin而你交互shell里的PATH可能带着/usr/local/bin、/opt/xx/bin等一堆自定义路径。于是脚本里直接用python3、javacron环境下就变成command not found。解决方式有两种我觉得都要做第一脚本内部开头设置PATH。比如#!/bin/bash export PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin第二脚本内部所有命令尽量写绝对路径或者把关键工具路径定义成变量再用。比如MYSQL/usr/local/mysql/bin/mysql然后调用$MYSQL。这样做的好处是脚本不会再因为PATH不同而行为分裂。还有一个坑cron不会加载你的shell配置文件没有~/.bash_profile没有~/.bashrc。如果脚本依赖一些自定义环境变量比如JAVA_HOME、ORACLE_HOME这些是不会自动进入cron环境的。要么在脚本里重新export要么用source命令把配置文件加载进来但加载配置有副作用可能引入一堆用不到的东西我的习惯是宁可脚本里显式写自己需要的那几个变量不让它全量加载。4.3 百分号转义和输出重定向的小细节crontab配置文件里%是有特殊含义的%在cron中会被解释为换行符。如果你直接在crontab命令行里写date %Y%m%d执行结果会完全不对因为这个%会让后面的内容变成换行。正确写法是把它转义成%# 错误写法输出会异常 0 2 * * * tar czf /data/backup/$(date %Y%m%d).tar.gz /data/app # 正确写法 0 2 * * * tar czf /data/backup/$(date \%Y\%m\%d).tar.gz /data/app这是新手最容易踩的坑在终端里测试明明好好的一放进crontab文件名就变成了乱七八糟的样子。或者更简单的方法把整个逻辑写进脚本日期在脚本里用正常的$(date %Y%m%d)这样crontab里避开了%号的解析问题。另一个绝对值得养成习惯的是输出重定向。cron中如果任务有标准输出或标准错误输出默认会以本地邮件形式发给当前用户邮箱。服务器上通常没有完整邮件系统这些邮件会堆积在/var/mail/用户目录下时间长了占空间不说出了问题想看也麻烦。正确做法是把输出重定向到日志文件0 3 * * * /usr/local/bin/cleanup.sh /var/log/cleanup.log 21这样执行日志长期落盘出问题随时可以回头查。如果脚本本身不产生输出但对执行成功状态有要求可以在脚本内通过exit code判断cron的机制是只看exit code为0才算成功否则会向用户发邮件。4.4 一次性任务的替代方案at与systemd定时器cron适合周期性任务但在“几小时后执行一条命令”“今天下午4点跑一下脚本”这种一次性场景里就不合适这时候用at命令更顺手。# 下午4点执行 at 16:00 /usr/local/bin/deploy.sh 按CtrlD结束输入 # 查看等待中的任务 atq # 删除指定编号任务 atrm 5at本身的原理也是后台守护进程atd每秒钟检查一次任务队列精度比cron高很多。不过生产环境我还是建议大家尽量少依赖at因为一次性任务容易遗漏不便于自动化审计。新一代计划任务首选其实是systemd timer。它比cron多了几个优势任务执行状态可以在systemd的日志体系里统一管理journalctl查看可以声明服务依赖可以为错过的定时任务做持久化补执行Persistenttrue比如系统关机时任务没跑开机后自动补上还能设置随机延时防止多台机器同时执行造成服务器冲击。缺点是配置比crontab多几个文件难记一些。一个最简单的systemd timer需要两个文件服务单元和定时器单元。比如/usr/local/lib/systemd/system/backup.service和backup.timerservice里写明要执行的命令timer里用OnCalendar指定时间再用systemctl enable --now backup.timer启动。之后日常管理用systemctl status backup.timer就能看到上次触发时间、下次触发时间、最近执行结果体验比cron全靠日志猜来得好。5. 实战经验合集进程和计划任务联动避坑指南前面讲的是各模块的知识但真实运维里进程和计划任务常常是一对搭档计划任务半夜启动了批量任务批量任务跑挂了一堆子进程或者导致系统负载飙升你排查时又发现杀不掉的进程其实是计划任务反复拉起来的。这一节把我实际用过、多次验证过的综合案例和习惯沉淀出来照这个思路走能省掉大量无谓的折腾。5.1 几个真实场景复盘半夜备份抢CPU、僵尸堆积、任务重叠先讲一个最典型的场景备份。备份通常是凌晨低峰期跑的但低峰期不是没业务很多服务的夜间批处理任务也在跑。如果你备份脚本启动压缩任务的时候不设置nice值而且服务器CPU核心数不多备份任务很可能把负载直接拉满导致线上服务夜间也能感知到延迟。我的解决方案是备份命令统一加nice调低优先级同时用ionice限制磁盘IO优先级让备份任务“谦让”着跑nice -n 10 ionice -c2 -n7 tar czf /data/backup/$(date \%F).tar.gz /data/app第二个场景是僵尸进程堆积。如果某台机器长期运行着一个有bug的父进程它不停地创建子进程但从不回收进程表里Z状态就会越堆越多pid_max到达上限后任何新进程都起不来。处理方法是先定位僵尸进程的父进程是谁ps -ef | grep defunct能看到僵尸进程的PID和PPID确认父进程后把它停掉或者重启然后系统init进程会把僵尸的退出信息回收调。杀僵尸本身没用僵尸是杀不死的根子在父进程。第三个场景是任务重叠。计划任务本身执行时间超过周期上一个还没跑完下一个又启动了。比如日志清理任务是每5分钟一次但某次日志量爆炸导致单次清理耗时10分钟你就同时在跑两个清理进程互相抢文件、重复加锁最终把磁盘IO拖垮。解决方式是在脚本内部加锁业界比较通用的做法是用flock#!/bin/bash exec 200/var/lock/cleanup.lock flock -n 200 || { echo [$(date)] 已有实例在运行本次跳过 /var/log/cleanup.log exit 1 } # 实际清理逻辑 find /data/log -name *.log -mtime 7 -delete这样同一时间只有一个清理进程能拿到锁抢不到锁的实例直接退出不会发生并发冲突。5.2 让计划任务更稳的三个习惯既然计划任务是无人值守的就更需要在设计上保证它遇到问题时不会把环境搞得更糟。我总结下来有三个习惯对稳定性影响最大。第一脚本里必须设置超时控制。一条命令卡住不退出在手动场景你还能CtrlC在cron场景它就是一直挂着占用进程和资源。我习惯用timeout命令包一层# 最多跑30分钟超时就杀掉并记录异常 timeout 1800 /usr/local/bin/backup.shtimeout执行完还会返回124退出码可以在外壳层判断是否是超时退出再做额外告警。第二所有输出都要留痕。不管成功失败执行完把结果写到日志里最好是“固定路径 日期后缀”的方式历史记录按天保留。清理日志的老化策略再配合cron自己来清就形成了一个闭环。我最常见的一个败笔就是早期写脚本从不打日志出了问题完全不知道脚本执行到哪一步只能靠猜。第三做幂等设计。一个计划任务重复执行两次结果应该是一致的不能因为上一次的残留状态导致第二次执行出错。比如备份脚本如果检查到备份目录已经存在同名文件要么覆盖要么跳过不能直接中断清理脚本如果发现日志目录不存在应该正常退出而不是报错。幂等性是非交互脚本的安全底线。5.3 把进程快照和计划任务结合起来做监控最后分享一个我觉得特别实用的小技巧用计划任务周期性采集进程快照留作事后排查依据。具体做法是写一个收集脚本每隔一段时间把关键进程的CPU、内存、线程数、进程树信息追加到当日快照文件#!/bin/bash SNAPSHOT_DIR/var/log/process_snapshot mkdir -p $SNAPSHOT_DIR SNAPSHOT_FILE$SNAPSHOT_DIR/snapshot_$(date \%F).log { echo $(date %F %T) ps -eo user,pid,ppid,pcpu,pmem,stat,etime,cmd --sort-pcpu | head -n 30 echo ----- 进程总数统计 ----- ps -e | wc -l echo ----- 僵尸进程列表 ----- ps -ef | awk $3 ~ /Z/ {print} || true } $SNAPSHOT_FILE # 清理30天前的快照 find $SNAPSHOT_DIR -mtime 30 -name *.log -deletecrontab里配置每5分钟执行一次。等某天出了性能问题你就能翻出事情发生那一刻的进程快照谁在吃CPU、有没有僵尸、进程数量是不是突然暴涨一目了然。这个习惯在排查“凌晨3点CPU飙高”这类问题时尤其能救命。根据我个人的操作习惯我还会在上面脚本里额外记录系统load average和内存占用两个指标这样快照里的信息更完整排查时能少跳好几个来回。既然快照是定时采集的建议同一台机器上的采集时间错开整点比如用1、6、11、16分这样的随机偏移避免多台机器同时采集造成监控数据采集端的短时压力。这篇文章写到这里进程和计划任务这两块的核心玩法也算铺完了。跟很多东西一样知道命令容易难的是组合起来形成自己的一套习惯。我觉得最好的练习方式不是去背各种参数而是真的找一台机器故意把crond停掉一次故意写一个带%号的计划任务再故意用一个不带权限的脚本放进去然后按日志一步步把问题揪出来。栽过跟头之后这些东西就是你的肌肉记忆了。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进