cron 配置都对脚本却不执行:一次 Linux 定时任务环境排查复盘

一个数据库备份脚本手动跑完全正常,挂进 crontab 后半个月一个备份都没产出,可 crontab 配置在、时间没写错、crond 服务也活着。排查梳理:cron 不是替你敲命令,它在一个自己准备的极简环境里执行脚本,和你登录终端的环境是两个世界;最致命的差异是 PATH,cron 的 PATH 极简通常只有 /usr/bin:/bin,装在 /usr/local/bin 的命令直接 command not found;cron 还不加载 .bashrc、.bash_profile、/etc/profile,你的自定义环境变量在 cron 里全不存在;最坑的是静默失败,脚本报错被当邮件发掉没人看;解法是命令用绝对路径或脚本里设全 PATH、环境变量显式设、cron 任务行永远带 >> 日志 2>&1、用 env -i /bin/sh -c 模拟 cron 环境验证,以及一套 cron 排查纪律。

2024 年,我写了一个数据库备份脚本 /opt/backup.sh,在终端里跑了好几遍,每次都干净利落地把备份打包好、传走,完美。我很放心,把它挂进了 crontab:0 3 * * * /opt/backup.sh,让它每天凌晨 3 点自动跑。然后我就把这事忘了。半个多月后,一次需要回滚,我去找备份——傻眼了,备份目录从那天起,一个文件都没多。我赶紧 crontab -l 看,那行配置稳稳地在;我又核对时间,0 3 * * *,没写错;我再手动 bash /opt/backup.sh 跑一遍,秒成功。这下我彻底糊涂了:cron 配置在、时间对、脚本本身明明能跑——这三样都没问题,它凭什么就是不执行?我甚至怀疑是不是 cron 服务挂了,systemctl status crond 一看,好好地活着,日志里也清清楚楚写着它每天 3 点【确实在跑】这个脚本。脚本被 cron 调起来了,可它就是没产出任何备份。我盯着这个矛盾想了很久,最后才反应过来:我手动跑能成,和 cron 跑能成,根本不是一回事——cron 把我的脚本扔进去执行的那个"环境",和我自己敲命令时所在的环境,是两个不同的世界。这件事逼着我把 Linux 的 cron 执行环境、PATH、登录 shell 加载这一整套彻底理清了。本文复盘这次实战。

问题背景

环境:CentOS 7,一个 crontab 里的数据库备份脚本 /opt/backup.sh
事故现象:
- crontab 里配了每天 3 点跑备份,半个月一个备份都没产出
- ★ crontab 配置在、时间没写错、脚本手动跑完全正常
- crond 服务也活着,日志显示它每天 3 点确实调起了脚本

现场排查:
# 1. cron 配置在,时间也对
$ crontab -l
0 3 * * * /opt/backup.sh                  # ★ 配置没问题

# 2. crond 服务活着
$ systemctl status crond
   Active: active (running)                # ★ 服务正常

# 3. ★ 看 cron 日志 —— 它确实在跑这个脚本
$ grep CRON /var/log/cron | tail
... CROND[8123]: (root) CMD (/opt/backup.sh)   # ★ 每天 3 点都在调

# 4. 手动跑 —— 完全正常
$ bash /opt/backup.sh
备份完成,已上传                            # ★ 手动跑秒成功

# 5. ★ 让 cron 把脚本的错误信息留下来(改 crontab)
0 3 * * * /opt/backup.sh >> /tmp/backup.log 2>&1
# 第二天看 /tmp/backup.log:
$ cat /tmp/backup.log
/opt/backup.sh: line 6: mysqldump: command not found   # ★ 真相!

根因(后来想清楚的):
1. ★ "我手动跑能成" ≠ "cron 跑能成"。cron 执行脚本时,
   给它的【环境】,和我登录终端敲命令时的环境,
   是【两个不同的世界】。
2. ★ 最致命的差异是 PATH。我登录 shell 的 PATH 很丰富,
   包含 /usr/local/bin 等一堆目录;而 cron 给脚本的
   PATH 极简,通常只有 /usr/bin:/bin。
3. mysqldump 装在 /usr/local/bin 下。手动跑时 PATH 里
   有这个目录,找得到;★ cron 跑时 PATH 里没有,
   于是 "mysqldump: command not found" —— 脚本第 6 行
   就挂了,后面的备份/上传根本没执行。
4. ★ 而且 cron【不会】加载 .bashrc / .bash_profile /
   /etc/profile —— 我在那些文件里设的环境变量、扩的
   PATH,cron 一概没有。
5. 最坑的是它"静默失败":脚本报的错被 cron 当邮件
   发掉了,我从来没看过那个邮箱,所以表面上"什么都
   没发生" —— 配置在、服务在、就是没产出。
cron 不是"替我敲命令",它是在一个极简环境里跑脚本。

修复 1:手动跑能成、cron 跑不成——环境是两个世界

# === ★ 先纠正最核心的误解:cron 不是"替我敲命令" ===

# === 我以为的 cron vs 真实的 cron ===
# 我以为:cron 到点了,就【像我本人坐在终端前】一样,
#   把那条命令敲一遍。我能跑成,它就能跑成。
# ★ 真相:cron 是一个【后台守护进程】。它到点时,
#   是在一个【它自己准备的、极简的环境】里执行你的
#   脚本 —— 这个环境和你登录终端时的环境,差很多。

# === ★ 差在哪:cron 的执行环境有这些不同 ===
# 1. PATH 不同 —— cron 的 PATH 极简(最致命,见修复 2)
# 2. ★ 不加载你的 shell 配置 —— .bashrc / .bash_profile /
#    /etc/profile 一概不读(见修复 3)
# 3. 工作目录不同 —— cron 跑脚本时,当前目录通常是
#    用户的家目录,不是你以为的那个目录
# 4. shell 可能不同 —— cron 默认用 /bin/sh,不是 bash
# 5. 没有终端 —— 任何需要交互、需要 tty 的命令都会出问题

# === 一个实验:看清 cron 环境到底长什么样 ===
# 临时加一行 cron,把 cron 环境打印出来:
$ crontab -e
* * * * * env > /tmp/cron_env.txt           # 每分钟导出一次环境
# 一分钟后:
$ cat /tmp/cron_env.txt
PATH=/usr/bin:/bin                           # ★ PATH 极简!
HOME=/root
SHELL=/bin/sh                                # ★ 是 sh 不是 bash
# ★ 把它和你自己终端里的 env 一对比,差异一目了然。

# === 和你登录 shell 的环境对比 ===
$ env | grep PATH
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/bin
# ★ 你的 PATH 有 7 个目录,cron 的只有 2 个 ——
#   差的那几个目录里的命令,cron 全都找不到。

# === ★ 于是怪现象解释通了 ===
# "脚本手动能跑" 证明的只是:在【我登录 shell 的环境】里,
#   它能跑。它【没有】证明在【cron 的极简环境】里能跑。
# ★ 这是两个独立的命题。我一直拿前者去担保后者,
#   所以才会对着"配置都对却不执行"百思不得其解。

修复 2:cron 的 PATH 极简——命令"找不到"

# === ★ cron 故障的头号原因:PATH 太短,命令找不到 ===

# === 为什么 PATH 这么关键 ===
# 你在脚本里写 mysqldump,shell 怎么知道它在哪?
#   它【按 PATH 里的目录,一个个去找】这个命令。
# ★ PATH 里没有那个目录,就报 "command not found"。

# === cron 的 PATH 有多短 ===
# cron 给脚本的默认 PATH,通常就是:
#   /usr/bin:/bin
# 而很多东西装在这两个目录【之外】:
#  - /usr/local/bin :★ 自己编译/装的,mysqldump 常在这
#  - /usr/sbin /sbin:管理类命令
#  - 各种语言/工具自带的 bin 目录
# ★ 这些目录里的命令,在 cron 里【一律找不到】。

# === ★ 解法 1:脚本里用命令的【绝对路径】===
# 先查出命令的真实位置:
$ which mysqldump
/usr/local/bin/mysqldump
# 然后脚本里【写全路径】,不依赖 PATH:
$ vi /opt/backup.sh
/usr/local/bin/mysqldump -u... > backup.sql      # ★ 写绝对路径
# ★ 最稳妥 —— 绝对路径不依赖 PATH,在哪都能找到。

# === ★ 解法 2:在脚本开头,自己把 PATH 设全 ===
$ vi /opt/backup.sh
#!/bin/bash
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# ... 后面的命令就能正常找到了
# ★ 在脚本第一行(shebang)之后就设好 PATH,
#   这样脚本内所有命令都有完整的 PATH 可用。

# === ★ 解法 3:在 crontab 顶部统一设 PATH ===
$ crontab -e
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# ↑ 在所有任务行【上面】写一行 PATH=...
0 3 * * * /opt/backup.sh
# ★ crontab 文件里可以直接写环境变量赋值,
#   写在任务行上面,对下面所有任务都生效。

# === 验证某命令在 cron 的 PATH 下能不能找到 ===
$ env -i /usr/bin/sh -c 'mysqldump --version'
# ★ env -i 清空环境模拟 cron 的"干净"状态,
#   这样跑能成,cron 里大概率也能成。

修复 3:cron 不读 .bashrc——环境变量全没了

# === ★ 第二个大坑:cron 不加载你的 shell 配置文件 ===

# === 你的环境变量是哪儿来的 ===
# 你登录终端时,系统会【自动加载】一串配置文件:
#  - /etc/profile、/etc/profile.d/*.sh
#  - ~/.bash_profile、~/.bashrc
# 你平时设的 JAVA_HOME、扩的 PATH、各种自定义变量,
#   基本都写在这些文件里 —— 一登录,它们就生效了。

# === ★ 关键:cron 一个都不读 ===
# cron 执行脚本时,★【完全不会】去加载上面那些文件。
# 所以你在 .bashrc 里 export 的一切:
#  - JAVA_HOME、自定义的 PATH 追加
#  - 数据库连接串、各种业务环境变量
#  - 你自己定义的 alias、函数
# ★ 在 cron 里【统统不存在】。脚本里一旦用到它们,
#   要么是空值,要么直接报错。

# === 一个典型翻车 ===
# 脚本里写: $JAVA_HOME/bin/java -jar app.jar
# 手动跑:JAVA_HOME 有值(.bashrc 设过),正常。
# ★ cron 跑:JAVA_HOME 是【空】的 -> 实际执行变成
#   /bin/java -jar ... -> command not found。

# === ★ 解法 1:脚本里需要什么变量,自己显式设 ===
$ vi /opt/backup.sh
#!/bin/bash
export JAVA_HOME=/usr/local/jdk
export PATH=$JAVA_HOME/bin:/usr/local/bin:/usr/bin:/bin
export DB_HOST=10.0.0.5
# ★ 别指望继承,脚本要用的,就在脚本里自己设清楚。

# === ★ 解法 2:让脚本主动 source 配置文件 ===
$ vi /opt/backup.sh
#!/bin/bash
source /etc/profile                          # 主动加载系统级配置
source /root/.bashrc                          # 主动加载用户配置
# ... 之后就能用到那些文件里定义的变量了
# ★ cron 不帮你 source,那就在脚本里【自己 source】。
#   注意:.bashrc 里若有"非交互式就 return"的判断,
#   可能 source 了也不全生效 —— 解法 1 更可靠。

# === ★ 解法 3:把变量写进 crontab(对该任务生效)===
$ crontab -e
JAVA_HOME=/usr/local/jdk
DB_HOST=10.0.0.5
0 3 * * * /opt/backup.sh
# ★ crontab 顶部的变量赋值,对下面的任务都可见。

# === 原则 ===
# ★ 不要假设 cron 里有任何"想当然该有"的环境变量。
#   脚本要用的每一个变量,都要有明确的来源:
#   要么脚本里自己设,要么 source,要么写进 crontab。

修复 4:cron 怎么调试——让它"开口说话"

# === ★ cron 最坑的是"静默失败",先让它开口 ===

# === 为什么 cron 故障这么难发现 ===
# cron 跑脚本,脚本的输出(stdout)和报错(stderr)
#   ★ 默认是被 cron 收集起来,以【邮件】发给用户的。
# 而绝大多数服务器,没人会去看那个本地邮箱。
# ★ 结果:脚本明明报了错,错误信息躺在没人看的邮箱里,
#   表面上"风平浪静、什么都没发生" —— 静默失败。

# === ★ 第一招:把输出重定向到日志文件 ===
$ crontab -e
0 3 * * * /opt/backup.sh >> /tmp/backup.log 2>&1
# 拆开看这个重定向:
#  - >> /tmp/backup.log :把标准输出【追加】到日志文件
#  - 2>&1               :★ 把标准错误也【并到】标准输出
#                          一起进日志 —— 报错才不会丢
# ★ 这一行是排查 cron 的命门。加上它,脚本说的每句话
#   (包括报错)都落到 /tmp/backup.log,翻日志就能看见。

# === 第二招:确认 cron 到底有没有调起脚本 ===
$ grep CRON /var/log/cron | tail -20
... CROND[8123]: (root) CMD (/opt/backup.sh)
# ★ /var/log/cron 记录了 cron 每次调度。
#  - 日志里【有】这行 = cron 调起了,问题在脚本/环境内
#  - 日志里【没有】这行 = cron 根本没调,查时间格式/服务

# === 第三招:模拟 cron 的极简环境,手动复现 ===
$ env -i /bin/sh -c '/opt/backup.sh'
# ★ env -i 把环境清空,/bin/sh 模拟 cron 的 shell ——
#   用这个跑,基本就还原了 cron 的执行现场。
#   它要是失败,你就当场看到了 cron 里失败的原因。

# === 第四招:在脚本里加足够的日志 ===
$ vi /opt/backup.sh
#!/bin/bash
echo "[$(date)] 备份开始, PATH=$PATH"          # 把关键信息打出来
set -x                                          # ★ 打印每一条执行的命令
# ★ set -x 让脚本逐行回显它在执行什么 ——
#   配合重定向到日志,脚本卡在哪一行一目了然。

# === 第五招:也看看 cron 的邮件(错误其实在这)===
$ cat /var/mail/root        # 或 mail 命令
# ★ 在你加重定向之前,所有报错其实都堆在这里。

修复 5:写一个"在 cron 里也能跑"的脚本

# === ★ 把经验固化成"cron 友好"的脚本模板 ===

# === 一个健壮的 cron 脚本,长这样 ===
$ cat /opt/backup.sh
#!/bin/bash
# ① ★ 显式声明用 bash,且开启严格模式
set -euo pipefail
#   -e:命令一失败就退出   -u:用未定义变量就报错
#   -o pipefail:管道里任何一环失败,整条算失败

# ② ★ 自己把 PATH 设全,不依赖 cron 的极简 PATH
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# ③ ★ 脚本要用的环境变量,显式设清楚
export JAVA_HOME=/usr/local/jdk
export DB_HOST=10.0.0.5

# ④ ★ 不依赖"当前目录",一律用绝对路径
cd /opt/backup_work || exit 1
BACKUP_DIR=/data/backups

# ⑤ ★ 关键命令也尽量用绝对路径
/usr/local/bin/mysqldump -h "$DB_HOST" ... > "$BACKUP_DIR/db.sql"

# ⑥ ★ 自己打日志,带时间戳
echo "[$(date '+%F %T')] backup done" >> /var/log/backup.log

# === crontab 那一行,也写得规范点 ===
$ crontab -e
# 顶部统一设 PATH(双保险)
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# ★ 任务行带上输出重定向,永远不要"裸跑"
0 3 * * * /opt/backup.sh >> /var/log/backup_cron.log 2>&1

# === ★ 上线前,务必用 cron 的方式验证一遍 ===
# 别只 bash 脚本.sh 就完事 —— 那是你的环境。
# 用这个模拟 cron 环境跑:
$ env -i /bin/sh -c '/opt/backup.sh >> /tmp/t.log 2>&1' ; cat /tmp/t.log
# ★ 这样跑能成功,挂到 cron 上才真的放心。

# === 几条"cron 友好"准则 ===
# 1. 所有外部命令,要么绝对路径,要么脚本里设全 PATH
# 2. 所有环境变量,脚本里显式设,别指望继承
# 3. 用绝对路径,不依赖当前工作目录
# 4. shebang 写清 #!/bin/bash,别让它默认走 sh
# 5. crontab 任务行永远带 >> 日志 2>&1
# 6. 上线前用 env -i 模拟 cron 环境验证

修复 6:cron 排查纪律

# === 这次事故暴露的认知盲区,定几条纪律 ===

# === 1. ★ "手动能跑"不等于"cron 能跑",是两个环境 ===
# 别拿手动测试的成功,去担保 cron 里也能成功。

# === 2. ★ cron 的 PATH 极简,通常只有 /usr/bin:/bin ===
# 命令要么写绝对路径,要么在脚本/crontab 里设全 PATH。

# === 3. ★ cron 不读 .bashrc/.bash_profile/profile ===
# 你那些环境变量在 cron 里都不存在,要在脚本里自己设。

# === 4. ★ cron 任务行永远带输出重定向 ===
$ 任务  >> /var/log/xxx.log 2>&1
# 不加它,报错被当邮件发掉,故障静默,你永远发现不了。

# === 5. 看 cron 有没有调起任务:/var/log/cron ===
$ grep CRON /var/log/cron | tail
# 有 CMD 记录 = 调起了,查脚本;没有 = 查时间/服务。

# === 6. 模拟 cron 环境复现:env -i /bin/sh -c '脚本' ===

# === 7. 脚本里 set -x 打印执行轨迹,定位卡在哪一行 ===

# === 8. 排查"cron 不执行/执行失败"的步骤链 ===
$ crontab -l                            # ① 配置和时间对不对
$ systemctl status crond                # ② cron 服务活着吗
$ grep CRON /var/log/cron               # ③ cron 调起任务了吗
$ 给任务加 >> 日志 2>&1                  # ④ ★ 让它开口说话
$ cat 日志                               # ⑤ 看脚本到底报什么错
$ env -i /bin/sh -c '脚本'               # ⑥ 模拟 cron 环境复现
# 按这个顺序,cron 问题基本能定位。

命令速查

需求                        命令
=============================================================
看当前用户的 cron 任务      crontab -l
编辑 cron 任务              crontab -e
看 cron 有没有调起任务      grep CRON /var/log/cron | tail
看 cron 服务状态            systemctl status crond
导出 cron 的环境看看        cron 里加: * * * * * env > /tmp/cron_env.txt
查命令的绝对路径            which 命令名
任务输出重定向到日志        任务 >> /var/log/xxx.log 2>&1
模拟 cron 极简环境跑脚本     env -i /bin/sh -c '/path/script.sh'
看 cron 发的报错邮件        cat /var/mail/用户名
脚本里打印执行轨迹          脚本里加 set -x

口诀:手动能跑不等于 cron 能跑,cron 环境极简 PATH 短还不读 .bashrc
      cron 任务永远带 >> 日志 2>&1,命令用绝对路径,变量脚本里自己设

避坑清单

  1. cron 不是替你敲命令,它在一个自己准备的极简环境里执行脚本,和登录环境不同
  2. 手动能跑只证明在你的登录环境能跑,不能担保在 cron 的极简环境也能跑
  3. cron 的 PATH 极简通常只有 /usr/bin:/bin,装在 /usr/local/bin 等处的命令找不到
  4. 解决命令找不到,要么脚本里写绝对路径,要么在脚本或 crontab 顶部设全 PATH
  5. cron 不加载 .bashrc、.bash_profile、/etc/profile,你的自定义环境变量全不存在
  6. 脚本要用的环境变量必须显式设或主动 source,不要指望从登录环境继承
  7. cron 默认把脚本输出当邮件发,没人看邮箱就成了静默失败,故障极难发现
  8. cron 任务行永远带 >> 日志 2>&1,把标准输出和标准错误都落到文件才看得见报错
  9. 查 /var/log/cron 有 CMD 记录说明 cron 调起了任务,没有则查时间格式或服务
  10. 用 env -i /bin/sh -c 脚本 模拟 cron 极简环境,上线前这样验证才真正放心

总结

这次"cron 配置明明都对、备份却半个月颗粒无收"的事故,纠正了我一个关于 cron 的、最根本的想象。在我的脑子里,cron 一直是一个"代我之手"的角色——我把一条命令、一个时间交给它,它就像一个忠实的助手,到了点,替我坐到这张终端前,把那条命令一字不差地敲下去。既然它敲的命令和我敲的一模一样,那么逻辑上,我能跑成的,它就一定能跑成。正是这个"代我之手"的想象,构成了我整个排查的思维定式:我反复确认 crontab 那行配置对不对、时间格式错没错、脚本本身能不能跑——我所有的注意力,都死死盯在"命令和时间"这两样东西上,因为在我的模型里,只要这两样对了,cron 就该和我本人一样把事办成。所以当配置、时间、脚本三样全都验证无误、它却依然什么都没产出时,我陷入了一种近乎荒谬的困惑:我已经把所有该对的都对了,它凭什么还不干活?复盘到根上,我才终于明白,我漏掉了一整个维度。cron 执行一个脚本,真正决定成败的,从来不只是"命令是什么",还有一个我从未正眼看过的东西——"这条命令是在什么样的环境里被执行的"。而恰恰在这个维度上,cron 和我,身处两个截然不同的世界。我坐在终端前敲命令时,我其实是站在一个被精心布置过的舞台上:登录的那一刻,系统已经默默替我加载了一长串配置文件,把我的 PATH 扩得很宽很全,把我需要的各种环境变量一一就位。我以为是"我"能跑成这个脚本,其实是"我和我脚下这个被布置好的舞台"一起,才跑成了它。而 cron 呢?它到点叫起我的脚本时,是把它扔进了一个空荡荡的房间——PATH 短得只剩两个目录,我那些写在 .bashrc 里的环境变量一个都没有,连 shell 都换成了更简陋的那个。我的脚本在第六行就摔倒了,因为它伸手去够一个叫 mysqldump 的命令,而在 cron 那个极简的世界里,这个命令所在的目录,根本不在它的搜索范围内。它不是没被执行,它是被执行了、然后立刻就失败了——而失败的哭喊,又被 cron 默默塞进了一个没人会去看的邮箱。于是在我这边,呈现出来的就是那个最折磨人的样子:风平浪静,仿佛什么都没发生过。这次最大的收获,是我意识到,"一段代码能不能跑通"这个问题,从来都不是代码自己能单独回答的。代码不是在真空里运行的,它运行在一个由 PATH、环境变量、工作目录、shell 类型共同构成的"上下文"里。同一段脚本,在一个上下文里是完美的,换一个上下文,可能寸步难行。我过去做测试时,总是不假思索地在自己的登录终端里跑一遍,跑通了就以为"它能跑了"——可我验证的,自始至终只是"它在我这个特定的、丰饶的环境里能跑"。我从来没有验证过它在那个真正会执行它的、贫瘠的环境里行不行。所以下一次,在我把任何一段脚本托付给 cron、托付给 systemd、托付给任何一个"不是我本人"的执行者之前,我会先停下来问一句:它将要被运行的那个环境,和我此刻测试它的环境,是同一个吗?如果不是,那我就得想办法,先到那个真实的环境里,把它验证一遍。让测试发生的地方,和让代码最终运行的地方,是同一个世界——这件听起来理所当然的事,恰恰是我这次,用半个月空空如也的备份目录,才换来的教训。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
Linux教程

一行 fstab 写错整台服务器开不了机:一次 Linux 开机挂载排查复盘

2026-5-20 20:55:37

Linux教程

服务报 Cannot assign requested address:一次 Linux TIME_WAIT 端口耗尽排查复盘

2026-5-20 21:04:17

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索