我干运维这么多年,有个特别深的体会:很多线上事故不是被什么高深架构击垮的,而是挂在最基础的进程管理和定时任务上。 进程杀不掉、脚本没执行、任务重复跑、日志把磁盘塞满……这些事儿几乎每个用 Linux 的人都遇到过。进程管理和计划任务调度,就像是 Linux 系统的“交通警察”和“日程管家”,一个管着系统里所有正在运行的程序,一个管着哪些任务在什么时间点自动执行。这篇文章不扯虚的,就把我这些年排查问题、写脚本、调服务器积累下来的实操经验完整盘一遍,告诉你进程到底怎么查、怎么管、怎么让它听话,定时任务怎么写才能稳如老狗。
1. 进程管理的基础认知:PID、父进程与生命周期
很多新手一上来就敲 ps -ef,看到一堆输出直接懵了。其实进程管理这事儿,只要先弄明白三个最基本的概念,后面全部是顺势而为。
1.1 进程不是程序,PID 才是身份证
程序是放在磁盘上的静态文件,比如你 /usr/bin/nginx 那个文件,它躺在硬盘里不占 CPU 不占内存。进程是程序跑起来之后的动态实体,是程序的一次执行过程。
每个进程都有唯一的 PID(Process ID,进程标识号),这是它在系统里的身份证。系统启动后第一个进程是 PID 为 1 的 systemd(老一点的发行版是 init),它是所有进程的祖师爷,负责拉起整个用户空间的进程树。
我见过不少人分不清“程序和进程”,导致排查问题时找错对象。比如你要重启 Nginx,光 kill 掉 master 进程没用,因为 master 会重新拉起 worker;你要是把 master 杀了,worker 也会跟着退出。所以操作之前先搞清楚你面对的是哪个进程,它在整个进程树里的位置。
1.2 进程状态切换:从运行到阻塞再到僵尸
Linux 的进程不是一直“活着”的,它会在几种状态之间反复横跳。你可以用 ps aux 查看进程状态列(STAT),常见的有:
| 状态 | 含义 | 出现场景 |
|---|---|---|
| R | 运行中或可运行 | 正在占用 CPU 或排队等待 CPU |
| S | 可中断睡眠 | 等待 IO、等待网络请求,随时能被信号唤醒 |
| D | 不可中断睡眠 | 等待磁盘 IO 等内核态操作,kill 都杀不掉 |
| T | 已停止 | 被 Ctrl+Z 挂起,或用 kill -STOP 暂停 |
| Z | 僵尸进程 | 子进程已结束,但父进程还没回收它的退出状态 |
| I | 空闲内核线程 | 内核线程特有状态,正常存在 |
这里我必须重点说一下 D 状态和 Z 状态,这俩是运维面试题常客,也是实际故障里的“钉子户”。
D 状态意味着进程正在内核态做不可中断的操作,通常是等磁盘 IO。这时候你 kill 它没用,因为内核正忙着处理 IO,压根不理会用户态的信号。遇到 D 状态的进程,只能等 IO 恢复,或者直接重启机器,没有第三条路。我之前遇到过一台数据库服务器,底层存储卡死,一堆进程全部变 D,那画面真是叫天天不应。
Z 状态是僵尸进程。子进程死了之后,会向父进程发一个 SIGCHLD 信号,然后等着父进程调用 wait() 系统调用回收它的退出状态码。如果父进程没写回收逻辑,或者父进程自己卡住了,子进程就会一直处于 Z 状态,像僵尸一样赖在进程表里。
僵尸进程不占 CPU 不占内存,但它占 PID 号和进程表项。 如果 PID 耗尽,系统就真的“死给你看”了。处理僵尸进程的办法不是 kill(因为它已经死了),而是杀掉它的父进程,让 PID 1 的 systemd 去领养并回收。不过如果父进程是 PID 1,那就比较麻烦,一般只能重启。
1.3 孤儿进程为什么反而成了好事
孤儿进程是父进程先挂了,子进程被系统重新挂到 systemd 或 init 名下。很多人觉得“孤儿”听起来很惨,但在 Linux 里这其实是保护机制。
举个例子:你用 nohup 启动一个后台任务,然后终端退出。这时候 shell 终端进程先结束,你的任务进程就成了孤儿,但它不会被杀掉,而是被 systemd 收养,继续在后台运行。这就是 nohup 命令能实现“关掉终端程序不退出”的底层原理。
理解了这些基本概念,就有资格往下聊真正的实操命令了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 进程查看实战:ps 参数组合、top 交互操作与精准定位
查看进程是运维的日常,但很多人只会一个 ps -ef 和 top,遇到复杂问题就抓瞎了。这个章节我把查看进程的几套打法完整捋一遍。
2.1 ps 命令的几种经典参数组合,别再用错了
ps 命令的诡异之处在于它兼容三种风格:Unix 风格(一个横杠)、BSD 风格(无横杠)、GNU 风格(两个横杠)。同一个功能,不同写法参数名还不一样,新手经常混着用。
我平时用的最多的三套组合:
bash复制# 查看所有进程完整信息,System V 风格
ps -ef
# 查看所有进程,BSD 风格,带 CPU 和内存占用
ps aux
# 自定义查看需要的字段
ps -eo pid,ppid,user,stat,%cpu,%mem,cmd --sort=-%cpu
ps -ef 输出的是 UID PID PPID C STIME TTY TIME CMD,没有 CPU 内存占用百分比。ps aux 输出里多了 %CPU %MEM VSZ RSS STAT START,更有诊断价值。所以日常排查 CPU 异常时,我几乎不用 ps -ef,直接用 ps aux --sort=-%cpu | head -20 把 CPU 占用最高的前 20 个进程拉出来。
再说一个容易被忽略的参数:ps -T 可以查看某个进程下的线程。排查多线程应用(比如 Java 应用)CPU 飙高时,先 ps -L -p <PID> 查看这个进程下所有线程的 PID,再配合 top -Hp <PID>,能精确定位到底哪个线程在“发疯”。
2.2 top 命令的交互式操作:比表面看起来强得多
top 是实时刷新的进程监控工具,但大部分人只会看一眼 CPU 和内存,然后按 q 退出。其实 top 的交互模式很强大,掌握下面几个快捷键就够用了:
bash复制top # 进入实时监控
# 按 1:查看每个 CPU 核的负载情况;再次按 1 切回汇总
# 按 P:按 CPU 占用排序(注意大写)
# 按 M:按内存占用排序
# 按 T:按运行时间排序
# 按 u:输入用户名,只看某个用户的进程
# 按 k:输入 PID,直接对进程发送信号(默认是 SIGTERM,建议先 SIGTERM 再考虑 SIGKILL)
# 按 r:输入 PID,重新设置进程优先级(nice值)
# 按 z:高亮当前排序的列
我特别要提一下 top -d 1 这个用法,它可以设置刷新间隔为 1 秒。默认的 3 秒刷新在有些场景下太慢了,比如你在压测或者抓瞬时性能问题,1 秒间隔能让你看到 CPU 的瞬时波动。
还有个容易误解的点:top 里的 load average(平均负载)。很多人以为负载高就是 CPU 忙,这是不准确的。平均负载是 R 状态进程数 + D 状态进程数的总和。 如果负载高但 CPU 使用率低,说明进程在等 IO(D 状态多),可能是磁盘慢、存储卡,甚至网络文件系统有问题。
2.3 按需精准定位进程:pgrep、pidof、lsof
有时候你知道进程名,想知道它的 PID;有时候你知道端口,想知道谁在占用。这时候再用 ps 去大海捞针就太笨了。
bash复制# 按名字查找 PID
pgrep -a nginx
# 按完整进程名查找 PID(精确匹配)
pidof nginx
# 查看 80 端口被哪个进程占用
lsof -i :80
ss -lntp | grep 80
lsof 这个命令是排查故障的神器,它不仅可以查端口,还可以查看某个文件被哪些进程占用。比如你想卸载一个目录却发现 device is busy,用 lsof +D /目录名 就能看到是哪个进程“霸占”着它。
2.4 实时性能监控:htop 和 dstat 的对比
top 虽然够用,但如果你用习惯了 htop,你会回不去。htop 的优势是彩色显示、可以直接鼠标操作、按 F6 选择排序字段、按 F9 直接选信号杀进程。在最小化安装的服务器上没装 htop 的话,记得 yum install htop 或 apt install htop 装一个。
对于性能调优场景,我推荐 dstat,它可以同时显示 CPU、内存、磁盘 IO、网络流量,一屏看全。它的优势在于持续采集时能看出瓶颈在哪个环节。
3. 进程控制实战:前后台切换、信号管理与 nohup 保活
会看进程只是第一步,真正考验功力的是“管”。进程的启动、暂停、终止、前后台调度,这里面有很多细节和坑。
3.1 前后台任务切换:Ctrl+Z、fg、bg、jobs 的配合
跑一个前台命令,突然发现要临时处理别的事,你会怎么做?直接 Ctrl+C 杀掉?如果这个命令已经跑了两小时,杀了等于白干。正确操作是:
bash复制# 1. 按 Ctrl+Z 把当前前台进程挂起(发送 SIGTSTP 信号)
# 2. 查看后台任务列表
jobs -l
# 3. 把挂起的任务放到后台继续跑
bg %1
# 4. 如果又要调回前台
fg %1
注意这里的 %1 是任务编号,不是 PID。jobs 命令输出里的 + 表示当前默认任务,- 表示上一个任务。不带任务编号执行 fg 默认操作带 + 的那个任务。
这里有个细节:Ctrl+Z 挂起后,进程是 T 状态,不占 CPU 但占用内存。如果你临时挂起了很多任务,内存依旧被占着,要留意剩余内存。另外,进程被挂起后如果终端退出,任务很可能会被 SIGHUP 信号杀掉。想避免这个情况,就得用到后面要讲的 nohup 或 setsid。
3.2 信号机制与 kill 命令:别一上来就 kill -9
信号(signal)是 Linux 进程间通信的一种方式,也是控制进程的主要手段。kill 命令的本质是发送信号,而不是“杀死”的意思。默认不带参数发送的是 SIGTERM(15),它请求进程正常退出,进程可以捕获这个信号做清理工作。
我见过太多人一遇到杀不掉的进程就 kill -9,这是非常危险的习惯。SIGKILL(9)是直接让内核强制终止进程,进程没有任何机会保存数据、清理临时文件、释放锁。跑数据库的服务器如果被 kill -9,很可能直接导致数据文件损坏。
常用信号整理如下:
| 信号 | 数字 | 用途 | 能否捕获 |
|---|---|---|---|
| SIGHUP | 1 | 终端挂断,常用于通知守护进程重读配置 | 可捕获,nginx reload 就是用它 |
| SIGINT | 2 | 终端中断,Ctrl+C 触发 | 可捕获 |
| SIGQUIT | 3 | 终端退出,Ctrl+\ 触发,会生成 core 文件 | 可捕获 |
| SIGKILL | 9 | 强制杀死,内核直接终止 | 不可捕获 |
| SIGTERM | 15 | 请求终止,默认终止,可捕获清理 | 可捕获 |
| SIGCONT | 18 | 继续运行已停止的进程 | 可捕获 |
| SIGSTOP | 19 | 暂停进程,Ctrl+Z 等价发送 SIGTSTP | 不可捕获 |
| SIGTSTP | 20 | 终端暂停,可捕获 | 可捕获 |
实际操作中,我的杀进程优先级是:先 kill <PID>(SIGTERM),等 5 秒看看进程还在不在,不行再 kill -9 <PID>。对于 Nginx,优雅重载配置用 kill -HUP $(cat /var/run/nginx.pid),这个信号动作是让 Nginx 重新读配置文件并平滑重启 worker 进程,不影响线上服务。
3.3 nohup 与 setsid:让进程真正脱离终端
用 & 把命令放后台执行,这只是暂时的。只要终端一关,后台进程就会收到 SIGHUP 信号退出。要解决这个问题,有几种方案:
bash复制# 方案一:nohup + & (最常用)
nohup python app.py > app.log 2>&1 &
# 方案二:setsid 让进程完全脱离会话
setsid python app.py > app.log 2>&1 &
# 方案三:在脚本里用 disown
python app.py > app.log 2>&1 &
disown
nohup 的作用是忽略 SIGHUP 信号,但进程仍然在同一个会话里。setsid 更彻底,它直接让进程成为一个新会话的领头进程,完全脱离控制终端。disown 是把任务从 shell 的任务表中移除,之后 shell 退出时不会向它发 SIGHUP。
我自己的习惯是:临时跑脚本用 nohup + &,长期后台服务型任务直接用 systemd 管理服务,而不是 nohup。因为 nohup 没有开机自启、没有崩溃自动拉起、没有资源限制,出了问题也不会被系统自动恢复。
3.4 调整进程优先级:nice 与 renice
Linux 的 CPU 调度按照优先级分配时间片。优先级通过 nice 值调节,范围从 -20(最高优先级)到 19(最低优先级),默认是 0。
bash复制# 启动时指定优先级(nice 值 10,较低优先级)
nice -n 10 ./backup.sh
# 对已运行的进程调整优先级
renice -n 5 -p 12345
# 查看进程的 nice 值
ps -eo pid,ni,cmd
对于一般用户,只能调高 nice 值(降低优先级);只有 root 才能调低 nice 值(提高优先级)。 这个限制很合理,防止普通用户给自己的进程加塞导致系统卡死。实际运用场景比较典型的是:备份任务在业务高峰期跑,可以 renice 到 15 左右,避免和线上业务抢 CPU;而关键的数据库实例,可以启动时设置 nice -5 保证响应速度。
4. 计划任务调度纵深:一次性任务 at/batch 与周期性任务 cron
说完进程管理,来到计划任务调度。Linux 下的定时任务主要分两大类:一次性任务(只跑一次,比如凌晨 2 点重启一个服务)和周期性任务(每隔多久跑一次,比如每分钟采集监控数据)。一次性任务用 at,周期性任务用 cron。
4.1 at 一次性任务:实际应用场景比想象中多
很多教程讲 at 只是一笔带过,但实际工作中 at 的用处真的不少。比如你想在 5 分钟后执行一次磁盘清理脚本,或者想在业务低峰期执行一次数据迁移,用 at 最合适。
bash复制# 安装 at 服务
yum install at -y # CentOS/RHEL
apt install at -y # Debian/Ubuntu
# 启动 atd 服务
systemctl enable --now atd
# 在 14:30 执行一次
at 14:30
> /opt/scripts/cleanup.sh
> 按 Ctrl+D 结束输入
# 今天 23:00 执行
at 23:00 <<< "/opt/scripts/backup.sh"
# 5 分钟后执行
at now + 5 minutes <<< "echo 'hello' >> /tmp/test.log"
# 查看待执行任务队列
atq
# 删除某个待执行任务(编号是 atq 里的编号)
atrm 3
at 支持的完整时间格式很灵活:at noon(中午 12 点)、at midnight(凌晨 0 点)、at teatime(下午 4 点)、at 3:00pm tomorrow(明天下午 3 点)都可以。
有一个坑大家注意:at 默认只允许 /etc/at.allow 文件里的用户使用。如果 /etc/at.allow 不存在,则允许所有用户(除了 /etc/at.deny 里的用户)。服务器安全加固时,建议创建 /etc/at.allow 并只写入需要使用的用户名,避免被滥用。
其实 batch 命令也是 at 家族的一员,只不过它是在系统平均负载低于 0.8 时才执行任务,适合跑那些“不着急但别影响当前负载”的批处理任务。现在用 batch 的人少了,但写脚本时如果对时间不敏感,batch 反而比 at 更智能。
4.2 cron 的五个时间字段:彻底搞懂分时日月周
cron 是计划任务的核心,配置文件在 /etc/crontab,而每个用户的任务通过 crontab -e 编辑。新手最容易搞混的就是五个时间字段的顺序和含义。
crontab 里每一行任务分为六个部分:
code复制分钟 小时 日 月 星期 命令
* * * * * /path/to/command
- 分钟:0-59
- 小时:0-23
- 日:1-31
- 月:1-12
- 星期:0-7(0 和 7 都表示周日)
最常见的误区是“日”和“星期”的关系。cron 的规则是:当“日”和“星期”都设置了具体值时,它们是“或”的关系,满足任意一个就会执行。 比如你写 0 2 1 * 1 /opt/backup.sh,这表示“每月 1 日执行”和“每周一执行”两个条件都会被触发,而不是“每月 1 日且恰好是周一才执行”。这个特性坑了不知道多少人。
举几个实际例子加深印象:
bash复制# 每天早上 6:30 执行
30 6 * * * /opt/scripts/sync_data.sh
# 每 5 分钟执行一次
*/5 * * * * /opt/scripts/monitor.sh
# 每小时的第 15 分钟执行
15 * * * * /opt/scripts/check_health.sh
# 每周一至周五 9:00 执行
0 9 * * 1-5 /opt/scripts/report.sh
# 每月 1 日和 15 日执行
0 3 1,15 * * /opt/scripts/clean_logs.sh
# 每天 0-6 点之间每 30 分钟执行一次
*/30 0-6 * * * /opt/scripts/sync_cache.sh
顺便说一下,crontab -e 编辑的是当前用户的 crontab,而 /etc/crontab 是系统级任务,区别在于系统级需要多一个“用户名”字段:
code复制# /etc/crontab 里写任务时第 6 位是用户名
0 2 * * * root /opt/scripts/backup.sh
另外有个冷知识点:cron 守护进程是 crond。想确认它是否在运行,用 systemctl status crond。没启动 cron 的话,所有 crontab 任务都会静默失败,而且没有任何告警,这是最坑的。
4.3 系统级定时任务目录:cron.d 与 cron.hourly
除了 crontab -e 和 /etc/crontab,还有几个目录值得关注。我平时更推荐把任务脚本放到这些预设目录里,比直接编辑 crontab 更清晰、更好管理。
bash复制# 让脚本每小时、每天、每周、每月自动运行
/etc/cron.hourly/ # 每小时执行一次
/etc/cron.daily/ # 每天执行一次
/etc/cron.weekly/ # 每周执行一次
/etc/cron.monthly/ # 每月执行一次
# /etc/cron.d/ 目录下也可以放 crontab 格式的文件
ls /etc/cron.d/
使用这些目录时,脚本必须具有可执行权限,而且脚本开头要写 shebang(#!/bin/bash)。我踩过最大的坑就是脚本没有执行权限,cron 直接跳过,但在日志里根本看不到明确的错误提示,排查了很久。
/etc/cron.d/ 目录适合放特定应用程序的定时任务,比如安装某些软件包时会自动在 /etc/cron.d/ 下生成任务文件。运维人员不建议在这里手动添加任务,避免和包管理器冲突。
4.4 crontab 环境变量与 PATH 的坑
这是计划任务排错中最容易翻车的点,值得单独拿出来说。
cron 执行脚本时的最小环境变量集,和交互式 shell 完全不同。 尤其是 PATH 环境变量,你交互式终端里能直接执行 docker、mysql、python3,但 cron 里可能就是 command not found。
我自己遇到过最典型的例子:写了个 crontab 任务执行 Python 脚本,交互式终端手动执行一切正常,放到 cron 里就是找不到 Python 模块。原因就是 cron 环境里的 PATH 只有 /usr/bin:/bin,没有 /usr/local/bin,而 Python 装在 /usr/local/bin/python3。
解决办法,在 crontab 顶部显式设置环境变量:
bash复制# 在 crontab -e 里第一行设置 PATH
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# 在脚本内部开头重新声明环境,这是更推荐的做法
#!/bin/bash
source /etc/profile
export PATH="/usr/local/bin:$PATH"
用“脚本内 source /etc/profile”比在 crontab 里写 PATH 更稳妥,因为 crontab 的顶部只对 crontab 自身有效,脚本里如果调用了别的脚本,还得靠自己的环境变量。
4.5 定时任务日志:排错全靠它
当你的定时任务没有按预期执行,第一反应不是怀疑脚本代码,而是先看 cron 的执行日志。
bash复制# CentOS/RHEL 的 cron 日志
tail -f /var/log/cron
# Debian/Ubuntu 的 cron 日志可能在 syslog 里
grep CRON /var/log/syslog
日志里出现 CRON (root) CMD (xxxx) 说明任务被正常执行。如果看到 Permission denied 说明脚本无执行权限;如果看到 No such file or directory 说明脚本路径不对;如果看到 (root) MAIL (mailed 1 byte of output) 说明命令有 stdout/stderr 输出,系统默认会通过邮件发给你(如果没有配置邮件服务,这个提示会忽略)。
我在实际操作中几乎都会把 cron 任务的标准输出重定向到文件,避免 cron 内部的邮件通知机制产生隐患:
bash复制30 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
这样既能看到脚本输出,又不会因为大量输出导致系统邮件服务压力过大。
5. 计划任务的高阶技巧与排错实战
这章才是真正的干货精华。计划任务不是“定时跑一下”这么简单,生产环境里涉及并发控制、幂等性、锁机制、依赖管理等大量细节问题。
5.1 定时任务重复执行的并发问题:flock 防重入
如果脚本执行时间长于定时任务的间隔,第二次任务会在第一次还没跑完时再次启动,这就会产生并发冲突。比如一个数据库备份脚本需要 2 小时,但你设置每小时跑一次,两次任务同时跑,轻则重复备份浪费资源,重则两个进程同时写同一个备份文件,把文件搞坏。
解决并发问题最简单可靠的办法是 flock(文件锁):
bash复制*/5 * * * * /usr/bin/flock -xn /tmp/backup.lock -c "/opt/scripts/backup.sh"
参数说明:-x 表示排他锁,-n 表示拿不到锁时直接失败退出,-c 是要执行的命令。
如果任务在锁文件被占用时直接退出,你可能想知道它失败了。可以把失败信息写日志:
bash复制*/5 * * * * /usr/bin/flock -xn /tmp/backup.lock -c "/opt/scripts/backup.sh" || echo "备份任务被跳过:已有实例在运行" >> /var/log/backup_skip.log
用 flock 比自己在脚本里做判断更可靠。因为 flock 是内核级别的文件锁机制,程序崩溃后锁会自动释放,不用担心“死锁”。
5.2 百分号在 crontab 里的特殊含义:% 是换行符
crontab 里有个用法很多人不知道:% 在 crontab 中有特殊含义,会被当作换行符处理。
看这个例子:
bash复制# 错误的写法:date 命令里的 %Y%m%d 会被 cron 解析成换行符
30 2 * * * /opt/scripts/backup.sh /data/backup.$(date +%Y%m%d).tar.gz
# 正确的写法:必须在 crontab 里转义百分号
30 2 * * * /opt/scripts/backup.sh /data/backup.$(date +\%Y\%m\%d).tar.gz
如果不想转义那么多百分号,更推荐的方式是把所有复杂逻辑都封装进脚本,crontab 只负责调用:
bash复制30 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
脚本内的 % 不用转义,这样就避免了 crontab 转义的坑。
5.3 计划任务里依赖其他服务的问题:等待与重试
很多定时任务的前置条件是某个服务必须处于运行状态。比如每天凌晨的同步任务,假设依赖 MySQL 已启动,如果在 MySQL 还没起来的时候同步脚本去连数据库,就会报错。
处理这类问题有几种思路,我按照推荐顺序排列:
第一种,脚本内等待重试:
bash复制#!/bin/bash
for i in {1..10}; do
if mysqladmin ping --silent 2>/dev/null; then
break
fi
sleep 5
done
# 继续执行主逻辑
第二种,使用 systemd 定时器替代 crontab。 systemd timer 支持 OnUnitActiveSec、OnBootSec 等触发条件,并且支持 After=mysql.service 这种依赖关系。如果你的服务器是纯 systemd 环境,可以逐步把复杂定时任务迁移到 systemd timer,管理起来优雅很多。
第三种,控制业务开启时间。 如果你把同步任务定在每天 1 点,而数据库每天 2 点才启动,那任务必然失败。在写 crontab 之前,先确认所有依赖服务的启动顺序和大致启动时间,能省去很多不必要的重试逻辑。
5.4 日志轮转与磁盘空间:定时任务最大的隐性风险
定时任务每 5 分钟跑一次,每次写 1MB 日志,一天就是 288MB,一个月就 8GB。如果服务器的 /var 分区不够大,磁盘被日志塞满,会把整个系统拖垮。
处理日志增长的常规做法是配置 logrotate:
bash复制cat > /etc/logrotate.d/myapp << EOF
/var/log/myapp/*.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
create 0640 root root
}
EOF
参数含义:daily 每天轮转一次,rotate 30 保留最近 30 份,compress 用 gzip 压缩旧日志,delaycompress 推迟一天再压缩以免影响正在写入的进程,missingok 日志文件不存在时不报错,notifempty 空文件不轮转。
logrotate 通常由 cron.daily 触发,所以它本身也要依赖 cron 正常运行。
5.5 crontab 丢失事故:备份与恢复
最后分享一个刻骨铭心的教训。很多人在线上服务器直接改 crontab,但 crontab 没有自动版本管理,一旦误操作或者被人覆盖,所有任务全部丢光,而且没有后悔药。
我现在的习惯是每次修改 crontab 之前先备份:
bash复制# 备份当前 crontab
crontab -l > /backup/crontab_$(date +%Y%m%d_%H%M%S).bak
# 导入新的 crontab 文件(覆盖式)
crontab /path/to/new_crontab_file
# 恢复备份
crontab /backup/crontab_20240101_120000.bak
另外,强烈建议把整个 crontab 作为配置文件纳入版本管理(比如 Git 仓库),每次修改提交一次 commit,这样即使服务器故障重建,也能快速恢复所有定时任务。
6. 进程与计划任务联动的实战案例:一个自动化日志清理器
理论知识讲得再多,都不如一个完整的实战案例价值大。这章用我线上环境的真实需求,把进程管理和计划任务串起来做一个日志清理器,顺便验证前面学过的所有知识点。
6.1 需求场景还原
线上有一组 Java 服务,日志文件按天滚动,每天产生约 1.5GB 日志,需要保留 30 天,但 /data/logs 分区只有 80GB 空间。结合业务访问量,我需要每天凌晨 2:30 执行一次日志清理任务,要求:
- 清理 30 天前的旧日志文件,保留最近 30 天
- 如果磁盘使用率超过 80%,清理时额外压缩 7 天前的日志
- 脚本运行期间不能和手动运维操作冲突
- 每次运行必须记录结果到独立日志,并防止重复执行
6.2 清理脚本的完整实现
bash复制#!/bin/bash
# /opt/scripts/cleanup_logs.sh
# 脚本开始时间
START_TIME=$(date +"%Y-%m-%d %H:%M:%S")
LOG_FILE="/var/log/log_cleanup.log"
# 日志清理主目录
LOG_DIR="/data/logs"
# 目录不存在则退出
if [ ! -d "$LOG_DIR" ]; then
echo "[$START_TIME][ERROR] 日志目录 $LOG_DIR 不存在" >> "$LOG_FILE"
exit 1
fi
# 1. 删除 30 天前的日志文件
find "$LOG_DIR" -type f -name "*.log" -mtime +30 -delete
# 2. 检查磁盘使用率
DISK_USAGE=$(df -h "$LOG_DIR" | awk 'NR==2 {print $5}' | sed 's/%//')
if [ "$DISK_USAGE" -gt 80 ]; then
echo "[$START_TIME][WARN] 磁盘使用率已达 ${DISK_USAGE}%,开始压缩 7 天前的日志" >> "$LOG_FILE"
find "$LOG_DIR" -type f -name "*.log" -mtime +7 -exec gzip {} \;
fi
# 3. 输出最终磁盘使用率
echo "[$START_TIME][INFO] 清理完成,当前磁盘使用率: $(df -h "$LOG_DIR" | awk 'NR==2 {print $5}')" >> "$LOG_FILE"
脚本里有个值得说明的点:为什么先删 30 天前的日志,再检查磁盘使用率?因为删除后磁盘可能已经降到 80% 以下,就没必要压缩 7 天前的日志,避免不必要的 CPU 消耗。逻辑顺序对性能影响很大。
6.3 配置 crontab 与 flock 防重入
我把 crontab 配置和 flock 结合,防止极端情况下脚本执行时间过长导致重入:
bash复制# crontab -e
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# 每天凌晨 2:30 执行日志清理,flock 防止重复运行
30 2 * * * /usr/bin/flock -xn /tmp/log_cleanup.lock -c "/opt/scripts/cleanup_logs.sh" 2>&1 >> /var/log/log_cleanup.log
PATH 写在 crontab 开头,确保 find、gzip、df 等命令可以被正确找到。同时用 flock 给脚本加锁,如果上一次任务还没跑完,第二次触发会直接失败跳过,避免两个清理进程同时扫同一个目录。
6.4 验证脚本执行结果
任务配置好之后,不能干等着第二天看结果,而是先手动执行一遍验证:
bash复制# 手动执行脚本(注意当前工作目录)
cd /opt/scripts
bash cleanup_logs.sh
# 查看清理日志
cat /var/log/log_cleanup.log
# 确认 crontab 语法正确
crontab -l
手动验证完,还可以把 cron 执行时间临时改到几分钟后,等 cron 正常触发一次,确认日志里有 CRON (root) CMD 记录,再改回正式时间。这套流程能让你在任务上线前发现绝大多数问题。
7. 综合排查思路:当进程管理和定时任务同时出问题时怎么办
最后这部分分享一套综合排查思路,适合在生产环境里实际应用。因为很多时候不是单一问题,而是进程管理和定时任务交叉作用导致的复杂故障。比如:定时任务里启动的 Python 进程没有正确退出,变成僵尸进程,导致内存越来越高,最终系统 OOM 触发定时任务大面积失败。
7.1 从负载异常定位到僵尸进程的完整过程
我先模拟一个真实场景:收到服务器负载告警,登录后 uptime 看到 load average 飙到 30 以上,但 CPU 使用率只有 20%。
第一步排查:
bash复制# 查看负载和 CPU 的对应关系
top -d 1
# 观察进程状态,有没有大量 D 状态或 Z 状态
ps -eo pid,stat,cmd | grep -E '^.* [DZ] '
输出里看到大量 Z 状态进程,父进程是一个 Python 脚本的 PID。这个 Python 脚本是定时任务 5 分钟前启动的,它 spawn 了一堆子进程,但脚本本身不回收子进程,导致子进程退出了变成一个个僵尸。
第二步处理:
bash复制# 查看父进程 PID
ps -o pid,ppid,stat,cmd -p 6213
# 杀掉父进程,让 systemd 回收所有僵尸子进程
kill 6213
# 再次确认僵尸进程已经被清理
ps -eo pid,stat,cmd | grep -E '^.* Z '
如果你运气不好,杀掉的父进程还在持续被 crontab 拉起,那就需要临时注释掉对应的 crontab 任务,或者把 crontab 里的命令改成 flock 加锁的版本,确保旧进程退出前新进程不会再次启动。
7.2 排查顺序建议
当生产环境出问题时,我的排查顺序是固定的,分享给大家参考:
- 看负载和总体状态:
uptime、free -h、df -h、top - 查进程异常:
ps aux --sort=-%cpu | head、ps -eo pid,ppid,stat,cmd | grep 'Z\|D' - 查定时任务执行日志:
tail -50 /var/log/cron - 查看应用自身日志:确认应用有没有报错,有没有 OOM 记录(
dmesg | tail -50可以查看内核日志) - 确认磁盘空间:
df -h、du -sh /var/log/* - 最后再考虑网络层:
ss -lntp、netstat -an
这个顺序的依据是:先把“系统层面已经发生的事实”弄清楚(负载、内存、磁盘、进程状态),再去看“应用层面为什么会导致这些事实”,往往十分钟内就能定位问题。相反,一上来就翻应用日志,容易被大量日志带偏方向。
7.3 我平时积累的几个排查小习惯
这里分享几个小习惯,都是从实战里踩坑总结出来的:
习惯一:写脚本时统一加锁开头。 不管脚本是否可能并发,写脚本时顺手加上 flock 或者 pid 文件判断,已经成了肌肉记忆。很多偶发的线上问题,最终都归因到“同一个脚本被 cron 和手动同时执行”。
习惯二:crontab 里重定向输出全部到位。 每条任务都加 >> 日志文件 2>&1,宁可多几个日志文件,也比任务失败后无迹可寻强。cron 的输出默认是发邮件的,服务器没配邮件系统时输出直接丢失,等于没有日志。
习惯三:每个关键脚本必须做幂等控制。 幂等指的是同一操作反复执行,结果保持不变。删除类的操作要小心,压缩类的操作要判断目标文件是否已存在。比如 gzip 已经压缩过的文件再 gzip 一次,会把文件搞成假扩展名,下次脚本就识别不出来了。
习惯四:定期重建一台验证机。 配置 cron 任务时,先用验证机跑一遍完整流程,包括 30 天后才会出现的边界情况。有些代码逻辑 30 天内没问题,30 天后因为日志文件太多导致 find 命令太慢,这种问题只有经验能预防。
8. 几个必须养成的好习惯,以及最后的忠告
写到这里,进程管理和计划任务调度的核心内容已经全部覆盖了。最后说几个我从入行到现在,真正觉得值得反复强调的习惯。
进程管理层面,第一是尽量不要用 kill -9,给进程一个优雅退出的机会;第二是杀进程前先确认 PID 和 PPID,搞清楚它的父子关系,别杀错;第三是管理长期运行的后台任务,优先用 systemd 而不是 nohup 加裸脚本,systemd 能给你更完整的生命周期管理、日志采集和自愈能力。
计划任务层面,第一是每次修改 crontab 前备份,这个习惯救过我很多次;第二是任务里尽量写绝对路径,不要依赖相对路径和环境变量;第三是把复杂逻辑全部封装进脚本,crontab 只做一行调用,避免在 crontab 里写复杂命令导致转义问题;第四是给每个任务配置输出重定向和文件锁,这两步能消灭 80% 以上的定时任务疑难杂症。
最后一条忠告:再好的工具和命令,都比不上你对系统状态的持续观察。定时任务出了问题,第一反应该是“去查日志,看事实”,而不是“凭感觉猜原因”。顺着我们前面说的排查链路一步步走,绝大多数问题都能在十分钟内定位。熟练了之后,你就会发现进程管理和计划任务调度不过就是这么一回事,剩下的全是经验积累。希望这篇文章对你有实实在在的帮助,踩过的坑你们就不用再踩一遍了。
