crontab定时任务设置超时、互斥

线上业务经常会用到邮件系统,假设在没有使用异步消息队列(最常用的方式)处理的情况下,使用php脚本+crontab定时任务(每分钟启动一次)来发送邮件,都需要考虑什么情况呢?

定时任务执行超时

crontab定时任务设为每分钟启动一次,如下:

1
* * * * * php /root/php/cron.php

问题:如果这脚本出现异常,进程僵死怎么办?
假设由于未知因素,cron.php 脚本一直执行,没有退出。极端情况,进入一个死循环。原来说好的一分钟执行一次,现在后面的脚本也不能跑了。

解决办法:使用timeout设置脚本执行超时时间

1
* * * * * timeout 120 php /root/php/cron.php >> /root/php/logs.log 2>&1

上面定时任务脚本最大执行时间120秒,超过120秒之后进程就会退出,这样就可以解决僵死的情况。

定时任务进程互斥

问题:
由于设置的定时任务是每分钟启动一次,在未来的某个时刻就会出现多个脚本进程在执行相同任务,此刻可能就会造成数据的不一致性等问题。那么,该怎么避免呢?

解决办法:
使用 flock 设置文件锁来进行互斥控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
用法:
flock [选项] <文件|目录> <命令> [<参数>...]
flock [选项] <文件|目录> -c <命令>
flock [选项] <文件描述符号码>

参数选项:
-s, --shared 获取共享锁
-x, --exclusive 获取排他锁(默认)
-u, --unlock 移除锁
-n, --nonblock 被其他服务锁住的时候返回失败而非等待
-w, --timeout <秒> 等待限定的时间
-E, --conflict-exit-code <数字> 冲突或超时后的退出代码
-o, --close 运行命令前关闭文件描述符
-c, --command <命令> 通过 shell 运行单个命令字符串
-F, --no-fork 执行命令时不 fork
--verbose 增加详尽程度
-h, --help display this help
-V, --version display version

文件锁互斥参数为 flock -xn, 同时设置互斥文件 /tmp/cron1.lock。

1
* * * * * flock -xn /tmp/cron1.lock -c "timeout 120 php /root/php/cron.php"

为了方便排查,把输出数据追加到指定日志文件中。

1
* * * * * flock -xn /tmp/cron1.lock -c "timeout 120 php /root/php/cron.php >> /root/php/logs.log 2>&1"

定时任务执行频率提升

问题:
觉得一分钟启动一次频率太低,想10s启动一次怎么办?

解决办法:
添加多个相同定时任务,每个定时任务sleep指定时间之后执行。

1
2
3
4
5
6
* * * * * php /home/app/email.php >> /home/log/test.log 2>&1
* * * * * ( sleep 10 ; php /root/php/cron.php >> /root/php/logs.log 2>&1 )
* * * * * ( sleep 20 ; php /root/php/cron.php >> /root/php/logs.log 2>&1 )
* * * * * ( sleep 30 ; php /root/php/cron.php >> /root/php/logs.log 2>&1 )
* * * * * ( sleep 40 ; php /root/php/cron.php >> /root/php/logs.log 2>&1 )
* * * * * ( sleep 50 ; php /root/php/cron.php >> /root/php/logs.log 2>&1 )

定时任务互斥及超时验证

用于测试的php脚本

1
2
3
4
5
6
7
<?php

$i = 10000;
while ($i > 0) {
echo --$i . \PHP_EOL;
sleep(1);
}

添加crontab定时任务

1
* * * * * flock -xn /tmp/cron1.lock -c "timeout 120 php /root/php/cron.php >> /root/php/logs.log 2>&1"

查看日志文件/root/php/logs.log发现会有数值不停输出,120秒左右之后数值暂停输出。此时使用ps -aux | grep php查看没有发现定时任务相关的进程信息,说明timeout 120起到了作用。然后,等待若干秒,等下一分钟的第一秒到达时,会发现数值又开始输出,也可以看到对应的定时任务进程信息,如此往复。

1
2
3
4
5
6
9999
9998
9997
9996
9995
....

定时任务执行期间,若再打开另外一个终端,手动在命令行执行:

1
flock -xn /tmp/cron1.lock -c "timeout 120 php /root/php/cron.php >> /root/php/logs.log 2>&1"

结果:
① 命令行执行命令由于没有获得锁,直接退出(因为 flock 互斥)
② 定时任务执行120 秒后自动由于超时退出进程(因为 timeout 120),此时再手动在命令行执行则可以成功