Cron作业仅运行一次
1. 概述
cron守护进程允许任务的调度。它允许我们控制任务何时开始,但仅此而已。
特别是,cron无法**防止同一任务的两次执行重叠。**如果我们想避免并发执行我们的任务,我们需要在任务脚本中自己实现它。
在本教程中,我们将学习两种基于进程检测和使用*.pid*文件来防止任务重叠的方法。
2. 工作示例
出于本教程的目的,我们将使用bash脚本作为任务。我们将构建它,一次一种技术,因此它将阻止第二个实例运行。
2.1. 任务脚本
首先,让我们准备脚本:
#!/usr/bin/env bash
DURATION=$1
do_the_action () {
date +"PID: $$ Action started at %H:%M:%S, ETA: $DURATION seconds"
sleep $DURATION
date +"PID: $$ Action finished at %H:%M:%S"
}
do_the_action
这里我们使用sleep 命令来模拟任务持续时间。第一个参数——$1——将允许我们定义任务执行的时间。
为了帮助理解执行期间发生了什么,date 命令输出带有时间戳的消息。为了帮助理解哪个进程是哪个,我们从 bash 变量 $$ 输出进程PID 。
2.2. 安排任务
现在,让我们设置一个cron任务来每分钟运行一次该脚本,持续时间为 55 秒。任务应该在下一个实例出现之前结束。
我们将脚本放在*/tmp/action1.sh*中,然后创建一个 crontab 记录来运行我们的任务并将输出记录到日志文件中:
$ echo '*/1 * * * * /tmp/action1.sh 55 >> /tmp/action1.log' | crontab
我们应该观察到crontab 命令的管道替换了当前用户的**整个 crontab 。**这对我们的教程来说不是问题,但可能不适合在生产中使用。
让这个任务运行几分钟后,我们可以检查*/tmp/action1.log*文件并注意没有重叠:
$ tail -f /temp/action1.log
PID: 21764 Action started at 13:41:01, ETA: 55 seconds
PID: 21764 Action finished at 13:41:56
PID: 21770 Action started at 13:42:01, ETA: 55 seconds
PID: 21770 Action finished at 13:42:56
2.3. 模拟重叠任务
现在让我们将持续时间增加到 70 秒来创建我们想要修复的场景:
echo '*/1 * * * * /tmp/action1.sh <strong>70</strong> >> /tmp/action1.log' | crontab
这次我们可以清楚地看到问题所在,下一个任务在上一个任务完成之前开始:
$ tail -f /temp/action1.log
PID: 21881 Action started at 13:43:01, ETA: 70 seconds
PID: 21886 Action started at 13:44:01, ETA: 70 seconds
PID: 21881 Action finished at 13:44:11
为了防止这种冲突,我们需要实现代码来检测先前的实例并中止脚本的重叠调用。
3. 按进程检测运行实例
3.1. 识别任务进程
在我们的示例中,脚本使用bash。当它运行时,我们可以使用pgrep 找到它:
$ pgrep --list-full bash
19125 bash
21172 bash /tmp/action1.sh 70
21187 bash /some/other/script
在这里,我们使用*–list-full*选项来获取所有 bash 进程的列表及其命令行。
从上面的输出中,我们可以看到进程21772似乎是我们任务的一个实例,因为它的命令行包含我们脚本的名称。
我们可以使用带有确切脚本名称的额外grep来缩小搜索范围:
$ pgrep --list-full bash | grep '/tmp/action1.sh'
21172 bash /tmp/action1.sh
在将这些方法添加到我们的脚本之前,在命令行中尝试这些方法会很有帮助。
但是,如果我们将此方法添加到我们的脚本中,它将检测脚本的每次调用,包括当前实例。我们需要从结果中排除这个实例。这就是使用grep实用程序 是正确的地方: *grep -v “^$$”*将过滤掉以当前 PID 开头的所有行并过滤掉当前调用。
3.2. 实施准则
现在,让我们将检测器添加到脚本中的一个函数中:
previous_instance_active () {
pgrep -a bash | grep -v "^$$ " | grep --quiet '/tmp/action1.sh'
}
我们应该注意到在最后一个grep命令中添加了*–quiet*选项。这可以防止检测输出出现在日志文件中。
接下来,我们修改任务脚本以在检测到先前的调用时退出:
if previous_instance_active
then
date +'PID: $$ Previous instance is still active at %H:%M:%S, aborting ... '
else
do_the_action
fi
3.3. 测试结果
应用这些更改后,我们可以再次检查*/tmp/action1.log*文件以查看检测是否按预期进行:
$ tail -f /tmp/action1.log
...
PID: 11529 Action started at 14:18:01, ETA: 70 seconds
PID: 11531 Previous instance is still active at 14:19:01, aborting ...
PID: 11529 Action finished at 14:19:11
PID: 11545 Action started at 14:20:01, ETA: 70 seconds
尽管此方法似乎运行良好,但对于每个用例而言可能都不够可靠。
实际上,脚本可能不知道它在文件系统上的唯一路径,因此可能必须搜索包含其名称的子字符串。因此,如果 bash 进程的另一个实例恰好包含相同的子字符串,那么就会出现误报。我们可能需要更强大的解决方案。
4. 通过*.pid*文件检测正在运行的实例
4.1. 使用文件来传达状态
通过这种方法,我们利用了.pid文件技术 。这样,下一个任务实例**可以检测到前一个化身留下的文件,以确定另一个实例是否正在运行。*我们将介绍一个PIDFILE变量和两个处理.pid*文件的过程:
PIDFILE="/tmp/action1.pid"
create_pidfile () {
echo $$ > "$PIDFILE"
}
remove_pidfile () {
[ -f "$PIDFILE" ] && rm "$PIDFILE"
}
我们应该从执行操作的代码周围调用它们(在实例已经运行时中止的条件逻辑中):
create_pidfile
do_the_action
remove_pidfile
4.2. 从上一个实例中读取消息
让我们重写我们的previous_instance_active函数来使用这个方法:
previous_instance_active () {
local prevpid
if [ -f "$PIDFILE" ]; then
prevpid=$(cat "$PIDFILE")
kill -0 $prevpid
else
false
fi
}
通过在此处使用*kill -0*,我们通过额外检查强制执行*.pid*文件技术。如果失败,那么我们知道在文件中检测到的先前实例 PID不再是正在运行的进程。
4.3. 避免旧的*.pid*文件
基于文件的检测比使用pgrep可靠得多,但它可能仍然不够健壮。
我们考虑一下任务执行中途出现故障的 情况。任务进程消失,但*.pid*文件将保留。尽管我们正在检查该 PID 是否处于活动状态,但系统可以将 PID 重新用于其他进程。这可能会导致错误检测。
因此,我们希望增加进程退出时删除*.pid*文件的机会。我们可以使用trap 指令来做到这一点。trap命令将代码的执行绑定到系统信号。**我们可以使用特殊的 EXIT 信号使清理代码在脚本退出时运行,**无论退出原因是什么。
要添加此退出时删除功能,我们只需修改最后几行:
trap remove_pidfile EXIT
create_pidfile
do_the_action
我们不再显式调用remove_pidfile过程,而是让bash在脚本终止时执行该调用。