做Linux运维这些年,我几乎每天都要跟Crond定时任务打交道。服务器上那些“到点自己干活”的活儿——日志轮转、数据库备份、服务健康检查、数据同步——十有八九都是靠Crond这个守护进程撑起来的。标题里写了“网络服务”四个字,严谨一点说,Crond本身是个时间驱动的任务调度服务,跟网络没有直接关系;但在实际服务器维护里,它跟Nginx、MySQL这类网络服务总是绑在一起编排的,比如凌晨定时重载Nginx配置、定时把数据库备份传送到远端存储。这篇东西适合刚接触Linux的人、被crontab搞到怀疑人生的新手、以及想把手头脚本规范化的运维同行。读完你能拿到完整的配置格式、三个拿来就能用的生产级脚本模板,还有我踩了好多次才绕开的那些坑。
1. 先搞清楚Crond是什么,以及为什么非用它不可
1.1 从“闹钟+便利贴”说起:Crond到底干了什么事
想象一下你每天早上要看三份报表:第一份是昨天服务器的错误日志,第二份是数据库的备份结果,第三份是几个核心进程的健康状态。人不可能每天凌晨三点爬起来盯着,这时候就需要一个“能定时帮你跑腿”的东西。Crond就是Linux里干这个的:它是一个常驻后台的守护进程,名字叫crond,启动之后每分钟醒来一次,翻一遍“任务清单”,看看当前时间有没有任务需要执行。有,就fork一个子进程去跑;没有,就接着睡下,等下个周期再醒。
这个“任务清单”就是crontab文件。它可以是系统级的,放在/etc/crontab和/etc/cron.d/目录下面;也可以是用户级的,每个人用crontab -e编辑自己的任务表,实际存放在/var/spool/cron/下面(Debian系是/var/spool/cron/crontabs/)。我经常跟新人打比方:把crond理解成家里的闹钟,把crontab理解成贴在冰箱上的便利贴——闹钟到点就响,响完照着便利贴上的事项干活,干完把结果写进日志。
那为什么说它有时候和网络服务放一起讲?因为很多网络服务的“保养”动作是周期性的。比如Nginx的access_log长得飞快,你不可能手动去做日志切割;MySQL的binlog如果不定期清理,磁盘迟早被撑爆。这些任务挂在某个网络服务身上,但执行引擎是Crond。所以说,懂Crond,基本上等于会给一整批网络服务做“自动保养”。
1.2 不是所有定时任务都得用Crond,选型对照
很多人问我:我写个Python脚本常驻内存,定时函数一调不就完事了吗?干嘛非得用Crond。这是一个好问题,我踩过这个坑。早期我维护过一个数据采集程序,脚本里自己写了time.sleep做循环调度,看起来挺灵活。结果跑了不到一个月,内存就悄悄涨上去了,最后进程被系统杀掉了,数据断了整整两天没人知道。Crond的好处恰恰在于:任务执行完,这个子进程就销毁了,不吃内存、不留状态,不存在“跑久了泄漏”的问题。
还有一类朋友一上来就推荐systemd timer,觉得Crond不够“现代”。systemd timer确实更强,也能做秒级触发,但配置上要写两个unit文件(一个service、一个timer),要熟悉systemd的依赖语义,调试成本高不少。我的个人建议是:绝大多数生产服务器的定时任务,用Crond就够了。它够老、够稳,所有发行版都自带,排错资料全网都是。如果用着用着发现确实需要“上次任务漏了这次补跑”,那再考虑Anacron;如果需要精确到秒的触发或者复杂的资源依赖控制,再切systemd timer不迟。工具选型的核心原则是:先满足需求,再谈优雅,别为了技术新鲜感给自己挖坑。
下面用一张表说清楚这几条路怎么选:
| 方案 | 优势 | 劣势 | 适合场景 |
|---|---|---|---|
| Crond | 简单、老牌、资源占用低、系统统一管理 | 最小粒度1分钟、错过不补跑 | 服务器上的绝大多数周期任务 |
| 常驻脚本 | 灵活、可控度高 | 容易内存泄漏、进程死了没人拉 | 不适合生产长期跑 |
| systemd timer | 秒级触发、可做服务依赖 | 配置繁琐、学习曲线陡 | 需要精确控制或专用依赖的任务 |
| Anacron | 能补跑关机期间错过的任务 | 不适合7×24小时服务器 | 笔记本、虚拟机、经常关机的机器 |
一句话总结:工作环境里,Crond是“默认答案”。我之前在某家公司管过一百多台服务器,每台机器上的crontab平均不超过十行,但就是这十行,替我们省掉了大量重复劳动。你说它不起眼,但它从来不宕、从来不闹脾气。
2. 核心细节解析:格式、配置目录、命令与环境变量
2.1 五段式格式拆解:时间和命令到底怎么对
crontab的配置行看起来很简单,但格式里藏着不少容易搞错的细节。标准用户级crontab是这么写的:
分 时 日 月 周 命令五个时间字段从左到右分别是:分钟(0-59)、小时(0-23)、日期(1-31)、月份(1-12)、星期(0-7,其中0和7都代表周日)。最后接要执行的命令。举个例子:
30 2 * * * /opt/scripts/backup_db.sh意思是每天凌晨2点30分执行backup_db.sh。再来几个常用的写法:
*/5 * * * *:每5分钟执行一次0 0 * * *:每天午夜0点15 3 * * 1:每周一凌晨3点15分0 0 1 * *:每月1号0点0 0 1 1 *:每年1月1日0点
特殊符号要熟:*表示任意值,,表示枚举(比如1,15表示第1分钟和第15分钟),-表示范围(比如1-5表示周一到周五),/表示步长(比如*/10表示每隔10个单位)。组合起来可以写出很多复杂的调度节奏。
但这里有个全网都会讲的“经典陷阱”:日字段和周字段同时设置时,是“或”的关系,不是“与”的关系。比如你写:
0 0 1 * 5你以为的是“每月1号且正好是周五才执行”,实际上它的意思是“每月1号执行一次,并且每个周五也执行一次”。我在生产环境被这个坑过:本来只想每月1号做一次全量数据校验,结果每周五晚上都在跑,硬生生把业务库拖慢了。要表达真正“且”的逻辑,只能把判断逻辑写进脚本里,比如在脚本内加一句“如果不是1号并且不是周五,就退出”。这点务必记住。
2.2 配置文件目录体系与权限控制
crontab的存放位置有好几处,很多人只知道一个crontab -e,难免踩乱。我日常实际接触的是这四块:
/etc/crontab:系统级主配置文件,比用户级多一个“用户”字段(分 时 日 月 周 用户 命令)/etc/cron.d/:存放零散的系统级任务文件,格式和/etc/crontab一样,也要写用户/etc/cron.hourly/、/etc/cron.daily/、/etc/cron.weekly/、/etc/cron.monthly/:放脚本的目录,到时间点系统自动执行里面的脚本(具体调度由/etc/crontab里的run-parts控制)/var/spool/cron/(Debian是/var/spool/cron/crontabs/):用户级crontab的存放目录,不要手动编辑,一律通过crontab -e来改
注意一个细节:/etc/cron.d/目录下的文件名不能包含“.”,否则crond会忽略掉。我第一次在这个目录放了个backup.db.conf,结果死活不执行,查了半天日志才发现是文件名里的点在作祟。这种小细节,文档里写了,但不踩一次真的记不住。
权限控制方面,系统通过/etc/cron.allow和/etc/cron.deny来限制哪些用户可以用crontab。两个文件都存在时,cron.allow优先;如果两个都不存在,在大多数发行版里默认所有用户都能用(具体看发行版)。如果你管理的机器上有开发用的普通账号,建议把cron.allow写好,白名单方式比黑名单省心得多。
2.3 常用命令与一个最大的环境变量坑
日常命令就那么几条:
crontab -e:编辑当前用户的crontabcrontab -l:列出当前用户的crontabcrontab -r:删除当前用户的crontab(慎用,会清空)crontab -u username -e:root编辑指定用户的crontab
系统服务的操作:
systemctl status crond:查看crond服务状态(有的发行版是systemctl status cron)systemctl restart crond:重启crondps -ef | grep crond:确认进程是否活着
关于“改完crontab要不要重启crond”这个问题,我在这儿多说一句:不需要。crond每次醒过来都会重新读取配置文件,你改完保存,下一个周期就会读到新内容。这也是它的设计优点之一。
最大的坑来自环境变量。crontab执行任务时,不会加载用户登录时的完整环境变量,比如~/.bashrc和/etc/profile默认都不加载。可以这么说:crontab里的shell环境是一个“极其精简”的环境——PATH往往只有/usr/bin:/bin,你手动执行好好的脚本,一旦进了crontab就各种“command not found”。
我自己遇到过最惨的一次:脚本里调用mysqldump,手动执行没问题,进了crontab之后报找不到命令,因为mysqldump装在/usr/local/mysql/bin下面,而这个路径根本不在crontab的PATH里。从那以后我定了两条铁规矩:
- 脚本内所有外部命令一律写绝对路径(比如把
mysqldump写成/usr/local/mysql/bin/mysqldump) - 脚本开头加一行
source /etc/profile或者至少export PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:$PATH
这两条规矩救过我太多次,新脚本进crontab之前我都会先检查这两点。
3. 实战过程:三个拿来就能用的生产级脚本模板
3.1 案例一:Web日志按天滚动压缩与过期清理
大多数Nginx或Tomcat的访问日志如果不处理,一个月能吃掉好几个G磁盘。我的做法是每天凌晨把昨天的日志压缩归档,同时把超过90天的压缩包删掉。脚本如下:
#!/bin/bash # /opt/scripts/clean_logs.sh # 日志压缩与过期清理 LOG_DIR="/var/log/nginx" STORE_DAYS=7 # 保留7天的原始日志 ARCHIVE_DAYS=90 # 压缩包保留90天 YESTERDAY=$(date -d "yesterday" +%Y%m%d) # 1. 把昨天的日志压缩归档 find ${LOG_DIR} -maxdepth 1 -name "access.log" -type f -mtime 1 | while read f; do tar czf ${LOG_DIR}/access_${YESTERDAY}.tar.gz -C ${LOG_DIR} access.log > ${LOG_DIR}/access.log # 压缩后清空原文件而不是删除 done # 2. 删除超过保留时间的压缩包 find ${LOG_DIR} -name "access_*.tar.gz" -mtime +${ARCHIVE_DAYS} -delete # 3. 写一条执行日志 echo "$(date '+%F %T') clean_logs.sh finished" >> /var/log/cron_clean.log对应的crontab配置:
30 1 * * * /bin/bash /opt/scripts/clean_logs.sh >> /var/log/cron_clean.log 2>&1这里有三个细节值得讲。第一,压缩完用> 日志文件清空而不是rm,为什么?因为Nginx进程还握着这个文件的句柄,直接删掉文件,Nginx不会自动重新创建,会导致日志写丢。清空文件内容则不影响句柄,Nginx接着往空文件里写,等它下次重新打开文件时再正常滚动。第二,find命令里用-mtime 1表示“昨天的文件”,千万别写成-mtime 0,那是“24小时内的文件”,会把前天某些时刻的文件也算进去。第三,所有输出重定向到日志文件,避免crontab默认把输出塞进用户的mail目录——不给它处理邮件的机会,我们主动接管输出。
3.2 案例二:MySQL全量备份保留最近7天
数据库备份是运维的头等大事。我的备份策略是每天凌晨2点做一次全量备份,保留最近7天,并且用gzip压缩减小磁盘占用。脚本如下:
#!/bin/bash # /opt/scripts/backup_db.sh # MySQL全量备份 BACKUP_DIR="/data/mysql_backup" MYSQL_USER="backup_user" MYSQL_PASSWORD="your_password" MYSQL_HOST="127.0.0.1" MYSQL_PORT="3306" DATABASES="order_db user_db" # 要备份的库,空格分隔 KEEP_DAYS=7 mkdir -p ${BACKUP_DIR} STAMP=$(date +%Y%m%d_%H%M%S) for db in ${DATABASES}; do # 用mysqldump备份单个库,gzip压缩 /usr/local/mysql/bin/mysqldump \ -h${MYSQL_HOST} -P${MYSQL_PORT} \ -u${MYSQL_USER} -p${MYSQL_PASSWORD} \ --single-transaction --routines --triggers ${db} \ | gzip > ${BACKUP_DIR}/${db}_${STAMP}.sql.gz # 检查备份是否成功 if [ $? -eq 0 ]; then echo "$(date '+%F %T') backup ${db} success, size: $(du -h ${BACKUP_DIR}/${db}_${STAMP}.sql.gz | cut -f1)" >> /var/log/cron_backup.log else echo "$(date '+%F %T') backup ${db} FAILED" >> /var/log/cron_backup.log fi done # 清理超过保留天数的备份 find ${BACKUP_DIR} -name "*.sql.gz" -mtime +${KEEP_DAYS} -deletecrontab配置:
0 2 * * * /bin/bash /opt/scripts/backup_db.sh >> /var/log/cron_backup.log 2>&1几个要点。第一,mysqldump和mysql必须写绝对路径,这是我在2.3里说的铁规矩。第二,--single-transaction参数特别重要,它让备份基于InnoDB事务快照,备份过程中不会锁表、不影响业务读写。第三,密码直接写在脚本里有安全隐患,生产环境可以用--login-path或者配置文件的方式管理凭据,这里为简洁起见用了明文,实际部署时一定要收敛权限,至少把脚本的权限设为700,属主设为root。
我之前在一次大促前夜发现备份脚本已经连续失败三天了,原因是磁盘满了,备份文件写不进去,但日志文件自己没有做轮转,也没告警。从那以后我给这类脚本统一加了du -h输出、失败exit code判断,还把失败情况接上告警通道。备份脚本不只要“能跑”,更要“跑不出错还让人知道”。这个东西,宁可多写五行业务日志,也别省。
3.3 案例三:服务进程监控与自动拉起
光监控机器层面的指标还不够,业务的进程如果挂了,越早发现越少损失。我习惯用Crond每5分钟做一次健康检查,发现核心进程不在就立刻拉起,同时写日志、发告警。这里拿Nginx举例:
#!/bin/bash # /opt/scripts/monitor_nginx.sh # Nginx进程存活监控与拉起 SERVICE_NAME="nginx" MARK_LOG="/var/log/cron_monitor.log" CHECK_URL="http://127.0.0.1:8080/healthz" # 方法1:进程检查 if ! pgrep -f "nginx: master process" > /dev/null 2>&1; then echo "$(date '+%F %T') [WARN] ${SERVICE_NAME} process not found, try restart..." >> ${MARK_LOG} systemctl start nginx sleep 2 # 拉起后再确认一次 if pgrep -f "nginx: master process" > /dev/null 2>&1; then echo "$(date '+%F %T') [INFO] ${SERVICE_NAME} restarted OK" >> ${MARK_LOG} else echo "$(date '+%F %T') [ERROR] ${SERVICE_NAME} restart failed, need manual check" >> ${MARK_LOG} fi fi # 方法2:HTTP健康检查(可选) HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 --max-time 10 ${CHECK_URL}) if [ "${HTTP_CODE}" != "200" ]; then echo "$(date '+%F %T') [WARN] ${CHECK_URL} return ${HTTP_CODE}, not 200" >> ${MARK_LOG} ficrontab配置(每5分钟检查):
*/5 * * * * /bin/bash /opt/scripts/monitor_nginx.sh >> /var/log/cron_monitor.log 2>&1这里面有个经验:进程在≠服务健康。Nginx的master进程可能活着,但后端已经502了;Java进程还挂着,但端口不监听了。所以我一般进程检查和HTTP检查一起上,进程检查负责“进程没了拉起来”,HTTP检查负责“进程活着但业务挂了要报警”。两件事的处置策略不一样,前者自动恢复,后者通常得人工介入。
还有个容易忽略的点:告警要分级。进程被拉起来了,写INFO;进程拉都拉不起来,写ERROR,并且接上短信/钉钉/微信告警。你不能让脚本在凌晨三点把所有人都吵醒,但真到起不来的情况,该响就得响。我曾经因为告警阈值设置太松,某业务挂了一整晚,第二天业务方找上门来才发现——这种锅背一次就够了。
3.4 定时策略设计:错峰、时区与“整点风暴”
写crontab时间字段的时候,大多数人的第一反应是整点整分。比如“每天0点跑日志切割”,于是所有服务器都在0点准时干活。这个习惯带来的后果,我在运营多台服务器之后深有体会:整点那一分钟,所有机器的磁盘IO、负载、数据库连接数同时飙高,日志切割脚本互相抢资源,原本1分钟跑完的任务跑到5分钟,甚至互相影响。
正确做法是给任务“错峰”。我的经验是:
- 不同任务之间至少错开5~10分钟
- 多台机器做同一件事(比如多台web节点都做日志清理),每台机器用不同的分钟偏移
- 最重的任务排在凌晨人少时段,但别堆在同一个点
比如三台机器可以分别用30 2 * * *、35 2 * * *、40 2 * * *来错开重活。另外,时区问题容易被忽略。如果你的服务器时区设置和业务时区不一致,crontab里的0 2 * * *到底几点跑,要以系统时区为准。排查“任务怎么提前一小时跑了”这种问题,先看date输出的时区对不对。
还要记住:Crond最小粒度是分钟,它本身不支持秒级任务。实在要每秒执行,就把Crond当“调度器”用,让它每分钟启动一个脚本,脚本内部自己循环60次、每次1秒,但这样做要自己控制好循环内的超时,避免任务重叠。大多数情况下用不到秒级,别给自己加戏。
4. 常见问题与排查技巧实录
4.1 任务就是没执行?按这个顺序排查
crontab任务不执行的排查,我有一套固定的顺序,建议你也照着来,效率高很多。
第一,确认crond服务是活着的:systemctl status crond或ps -ef | grep crond。如果服务挂了你写的所有任务都白搭,但这种概率极低,更多是配置的问题。
第二,看日志。CentOS系日志在/var/log/cron,Ubuntu系则在/var/log/syslog里(搜CRON关键字)。日志会记录每次调度的动作,会显示“是执行了但命令找不着”还是“压根没到调度”。这一步能把问题分成两大类:调度问题(没跑)和执行问题(跑了但失败)。
第三,验证脚本本身。在命令行手动执行一次脚本,看有没有报错。如果手动执行成功但crontab里不行,九成是环境变量和权限问题。检查脚本有没有执行权限,chmod +x别省。检查脚本开头有没有指定正确解释器(#!/bin/bash不能少)。检查脚本内部的所有命令是不是绝对路径。
第四,检查用户的crontab语法。crontab -l看一下当前内容,确认时间字段没写错。有一回一个同事把小时和分钟写反了,任务变成每小时的“某分钟”才执行,看起来像没跑,其实就是跑的点不对。crontab -e保存时一般会有语法提示,但别完全依赖它。
4.2 任务执行了但脚本报错?多半是环境变量的锅
这是最高频的问题。手动执行脚本一切正常,crontab里跑就报“command not found”或者各种奇怪的找不到库、找不到配置文件的错。原因就是我前面说的:crontab的执行环境不会加载你登录shell的环境变量。
遇到这类问题,我第一件事就是在脚本开头加两行:
source /etc/profile source ~/.bash_profile然后重新调度试跑。如果某个工具装在了自定义目录,比如Python装在/usr/local/python3/bin,还需要显式加PATH。有个技巧可以在脚本最前面加一行调试输出,把环境变量打到日志里:
echo "$(date '+%F %T') PATH=$PATH" >> /var/log/cron_env.log跑一次看日志,PATH到底缺了什么一目了然。我见过太多人在这个问题上瞎猜半小时,加一行调试信息就能解决的事,别猜。
4.3 特殊字符与语法陷阱:%、引号与换行
crontab里有个非常隐蔽的特殊字符:%。在crontab中,%会被解释成换行符。比如你写:
0 2 * * * echo "today is $(date +%Y%m%d)" >> /tmp/test.log如果没有转义,crontab会把%Y%m%d里的%当成换行,命令就被“断开”了,后面内容全变成了标准输入内容,导致整个命令行为异常。解决办法有两个:要么转义成\%Y\%m\%d,要么干脆把命令写到shell脚本里,脚本内部正常用%不受影响。我的建议是凡是稍复杂的逻辑,一律写脚本,别直接在crontab里堆长命令,一个是转义问题多,另一个是后续维护根本没法看。
另一个坑是,&符号在crontab里如果直接裸写,可能有解析问题,一般建议用>> logfile 2>&1的方式把所有输出和错误都重定向到文件,顺便把“后台”的需求交给脚本内部去控制。
4.4 常见问题速查表
| 症状 | 可能原因 | 排查/解决办法 |
|---|---|---|
| 任务完全不跑 | crond服务挂了 | systemctl status crond;重启crond |
| 跑了但命令找不到 | PATH不完整,环境变量缺失 | 脚本开头source /etc/profile;所有命令写绝对路径 |
| 脚本手动没问题,crontab失败 | crond精简环境、相对路径错误 | 检查脚本内路径是否绝对;加调试日志输出PATH |
| 报了“Permission denied” | 脚本没有执行权限 | chmod +x 脚本 |
| 时间是整点但没跑 | 时区不对/日周逻辑理解错 | date检查时区;确认日周“或”逻辑 |
| 输出到邮件目录 | 没有重定向输出 | 命令后追加 >> log 2>&1 |
| %导致命令被截断 | crontab将%解释为换行 | 转义%或将逻辑写进脚本 |
| /etc/cron.d下文件不执行 | 文件名带了“.” | 重命名不带点的文件 |
| 任务执行了多次 | 脚本重叠运行 | 脚本内加flock锁,或检查时间字段表达 |
这张表是我在带团队时候整理的,贴在运维wiki里,新人出问题先查表,省得我一遍遍重复讲。排查问题最大的效率来源,是把经验沉淀成可检索的东西。
5. 我在实际项目里最后再补两句
写到这儿,Crond定时任务的核心内容基本都覆盖了。如果要给这段运维经验画个重点,我最想补的是这几点:第一,新写的脚本一定先在命令行手动跑通,再挂到crontab里,这个顺序十次有九次能帮你避开低级错误;第二,每个脚本都必须有自己的日志文件,而且要带时间戳,等到出问题的时候没有日志两眼一抹黑,你会非常被动;第三,给脚本加锁防止重叠,尤其是执行时间可能超过调度周期的任务,一个简单的flock就能避免“上一个还没跑完,下一个又起了一个”的灾难场面。
我还想再多说一句踩过的坑:别因为Crond简单就轻视它。它出问题的症状往往都很隐蔽,比如任务确实执行了但输出全丢了、环境变量不对导致备份成功但数据不对、时区偏移导致凌晨任务变成上午跑。管理大批量服务器、长期维护项目的时候,这些“小问题”带来的影响会被无限放大。建议团队里把每台机器上的crontab都纳入版本管理,脚本和配置统一放git仓库,每次改动都留痕。这样即使哪天某台机器被误操作清了任务,我们也能在几分钟内恢复现场。
Crond这东西,看起来就是一个简单的“定时器”,但用得规范不规范,直接决定你凌晨三点是被电话吵醒还是安安心心睡大觉。从一个“会用”的阶段走到“用好”,靠的正是这些细致到文件路径、环境变量、日志策略的偏执。希望这篇分享能帮你少走几步弯路。