服务器定时任务怎么设置

结论: 服务器上设置定时任务首选 crontab:用 crontab -e 编辑当前用户的任务表,按"分 时 日 月 周"五个字段写调度表达式,命令一律用绝对路径并显式重定向输出。需要开机执行、依赖网络或要求可观测性时,改用 systemd timer 更可靠。两者都要用 flock 防止任务重叠执行。

引言:定时任务是服务器自动化的地基

定时任务的典型场景包括:数据库每日备份、日志按天切割、证书自动续期、缓存预热、监控脚本轮询、业务报表生成。它看起来简单,但线上出问题时往往最隐蔽——脚本手动执行好好的,放进 crontab 就失败;备份任务跑了两次互相覆盖;服务器重启后任务再也没执行过。

本文把 crontab 的语法、环境陷阱、日志排查讲透,并给出 systemd timer 的对照方案。示例基于 Ubuntu 24.04 LTS(cron 3.0pl1)与 systemd 255(截至 2026 年主流版本)。

crontab 五个字段的含义

结论:crontab 的调度表达式由 5 个时间字段加 1 条命令组成,理解每个字段的取值范围是写对表达式的前提。

text
# ┌───────── 分钟 (0 - 59)
# │ ┌─────── 小时 (0 - 23)
# │ │ ┌───── 日   (1 - 31)
# │ │ │ ┌─── 月   (1 - 12 或 jan-dec)
# │ │ │ │ ┌─ 星期 (0 - 7,0 和 7 都代表周日,或用 sun-sat)
# │ │ │ │ │
# * * * * *  要执行的命令
字段含义取值范围常用写法示例
第 1 位分钟0~59*/5 每 5 分钟;0,30 每半点
第 2 位小时0~230 整点;9-18 9 点到 18 点
第 3 位日1~311 每月 1 号;*/2 每隔两天
第 4 位月1~12 或 jan~dec* 每月;1,4,7,10 每季度首月
第 5 位星期0~7(0 与 7 均为周日)1-5 工作日;0,6 周末

特殊字符与快捷语法:

写法含义示例说明
*任意值 *每分钟执行一次
,值列表0,15,30,45 每 15 分钟
-范围0 9-18 1-5工作日 9~18 点整点
/步进/10 *每 10 分钟
@reboot开机运行一次@reboot /opt/init.sh系统启动时执行
@hourly每小时@hourly /opt/job.sh等价于 0
@daily每天 00:00@daily /opt/backup.sh等价于 0 0 *
@weekly每周日 00:00@weekly /opt/clean.sh等价于 0 0 0
@monthly每月 1 号 00:00@monthly /opt/report.sh等价于 0 0 1

第一步:crontab 基本操作

结论:crontab 分"用户级"与"系统级"两类。日常脚本放用户级(尤其是专用服务账号),需要指定执行用户的放 /etc/cron.d/。

bash
crontab -e                 # 编辑当前用户的任务表(首次会让你选编辑器)
crontab -l                 # 列出当前任务
crontab -r                 # 删除全部任务(无二次确认,慎用!)
crontab -i -r              # 带确认地删除
crontab -u www-data -e     # 编辑指定用户的任务(需 root)
crontab -u root -l

sudo systemctl status cron           # Debian/Ubuntu 服务名是 cron
sudo systemctl status crond          # RHEL 系服务名是 crond

一份典型的用户级 crontab:

bash
# 显式声明环境,避免 PATH 陷阱(见第四步)
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=ops@example.com

# 每 5 分钟检查一次服务存活
*/5 * * * * /usr/local/bin/check-alive.sh >> /var/log/check-alive.log 2>&1

# 每天 03:30 做数据库备份
30 3 * * * /usr/local/bin/db-backup.sh >> /var/log/db-backup.log 2>&1

# 每周一 04:00 清理 7 天前的日志
0 4 * * 1 /usr/bin/find /var/log/app -name '*.log' -mtime +7 -delete

# 开机后拉起业务脚本
@reboot sleep 30 && /opt/app/start.sh >> /var/log/app-start.log 2>&1

第二步:系统级 cron 与目录型任务

结论:需要为不同用户分派任务、或希望用文件管理(便于纳入 Git 与配置下发)时,用 /etc/cron.d/。注意这里的格式多一个用户字段。

bash
sudo tee /etc/cron.d/app-jobs <<'EOF'
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# 分 时 日 月 周  用户   命令
*/5 * * * *  www-data /usr/bin/php /var/www/nextcloud/cron.php > /dev/null 2>&1
0   2 * * *  root     /usr/local/bin/certbot renew --quiet
EOF

sudo chmod 0644 /etc/cron.d/app-jobs      # 权限必须是 644,否则不生效
sudo chown root:root /etc/cron.d/app-jobs

另外还有四个按周期执行的目录,把可执行脚本丢进去即可(由 run-parts 调用):

  • /etc/cron.hourly/、/etc/cron.daily/、/etc/cron.weekly/、/etc/cron.monthly/

目录型任务的优点是零配置,缺点是无法精确到具体分钟,且脚本必须去掉扩展名(不能有 .sh)、具备可执行权限、且不以 . 开头。

第三步:环境变量陷阱(cron 最经典的坑)

结论:cron 执行环境的 PATH 只有 /usr/bin:/bin,没有登录 Shell 的环境变量、没有 ~/.bashrc、当前目录是用户的 home。脚本手动能跑、cron 里失败,九成是这个原因。

常见表现与处理:

  • command not found —— 命令不在精简 PATH 中。解决:写绝对路径(/usr/local/bin/node),或在 crontab 顶部声明 PATH=...。
  • python: command not found 但手动能跑 —— 你用的是 pyenv/conda/venv 里的解释器。解决:脚本里写完整路径,如 /opt/venv/bin/python。
  • 中文乱码或 locale 报错 —— 加 LANG=zh_CN.UTF-8 或 LC_ALL=C。
  • 脚本依赖当前目录 —— cron 的工作目录是 HOME,脚本里用 cd /path && ... 显式切换。
  • docker: command not found 或权限不足 —— 用 /usr/bin/docker 绝对路径,并确认用户在 docker 组。

调试技巧:先把当前环境导出对比,再在 crontab 中补齐:

bash
# 查看 cron 实际拿到的环境(临时任务)
* * * * * env > /tmp/cron-env.txt 2>&1

# 对比你的登录环境
env | sort > /tmp/shell-env.txt
diff /tmp/shell-env.txt /tmp/cron-env.txt

另外两个语法细节:

  • 命令中的 % 是换行符,必须转义为 \%,否则后面的内容会被当作标准输入。写日期时尤其容易踩:date +\%Y\%m\%d。
  • 输出默认会通过邮件发给任务所属用户,未配置 MTA 时会堆在 /var/spool/mail/。生产任务一律显式重定向到日志文件,不关心的加 > /dev/null 2>&1。

第四步:日志与排错

结论:cron 本身只记录"有没有执行",不记录脚本的输出。要定位问题,先看 cron 日志确认触发,再看脚本自己的日志看报错。

bash
# Debian / Ubuntu:cron 日志混在 syslog 里
sudo grep CRON /var/log/syslog | tail -30
# RHEL 系
sudo tail -f /var/log/cron

# 用 journalctl 查看(systemd 系统通用)
sudo journalctl -u cron --since "2 hours ago"
sudo journalctl -u cron -f

# 确认服务在跑、配置被加载
sudo systemctl status cron
sudo ls -l /var/spool/cron/crontabs/

排查清单:

  1. cron 服务是否在运行(systemctl status cron)。
  2. 任务是否出现在 crontab -l 输出里(别改错用户)。
  3. cron 日志里有没有 CMD (...) 记录 —— 有说明已触发,问题在脚本内部。
  4. 脚本是否有可执行权限、shebang 是否正确(#!/bin/bash)。
  5. 脚本日志里是否有 command not found / Permission denied。
  6. /etc/cron.allow / /etc/cron.deny 是否限制了用户。

第五步:systemd timer —— 更现代的替代方案

结论:cron 简单但"哑"——没有依赖管理、错过执行不会补跑、日志分散。systemd timer 支持依赖网络就绪、错过时间点后补执行、随机延迟打散,日志统一进 journald,是生产关键任务的更好选择。

创建服务单元 /etc/systemd/system/db-backup.service:

ini
[Unit]
Description=Daily database backup
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
User=root
ExecStart=/usr/local/bin/db-backup.sh
StandardOutput=journal
StandardError=journal

创建定时器单元 /etc/systemd/system/db-backup.timer:

ini
[Unit]
Description=Run database backup every day at 03:30

[Timer]
OnCalendar=*-*-* 03:30:00
RandomizedDelaySec=300
Persistent=true
AccuracySec=1min
Unit=db-backup.service

[Install]
WantedBy=timers.target

启用与验证:

bash
sudo systemctl daemon-reload
sudo systemctl enable --now db-backup.timer

systemctl list-timers --all            # 查看所有定时器及下次触发时间
systemctl status db-backup.timer
journalctl -u db-backup.service -f     # 看任务输出

# 校验时间表达式是否符合预期(非常实用)
systemd-analyze calendar '*-*-* 03:30:00'
systemd-analyze calendar 'Mon *-*-* 04:00:00'

cron 与 systemd timer 的对照:

对比项cronsystemd timer
语法难度简单需要写两个单元文件
最小粒度分钟秒(AccuracySec 控制)
错过补跑不支持Persistent=true 支持
依赖管理无支持 After=network-online.target
日志需自行重定向自动进 journald
随机打散需自己写 sleepRandomizedDelaySec 内置
适用场景简单周期脚本关键业务、备份、需依赖的任务