引子
正常情况下脚本执行时间是很短的,但是一旦遇到IO阻塞等问题,会出现多个任务同时运行的情况,这种情况往往不是我们所期望的,可能会导致意想不到的问题。
即使不是秒级的定时任务,只要任务执行时间超过定时间隔都会出现重复运行的问题,比如每分钟运行的定时任务,而其执行时间需要三分钟等等
解决方案
方案1:进程数
这是笔者第一时间自己想的方式,通过进程数来判断当前定时脚本同时执行的数量,比如执行的脚本名为/opt/test.sh,当有一个任务在运行的时候:
[root@server ~]# ps -ef | grep /opt/test.sh
root 1107 25880 0 23:26 pts/0 00:00:00 /usr/bin/bash /opt/test.sh
root 1305 1175 0 23:27 pts/5 00:00:00 grep --color=auto /opt/test.sh
此时通过 ps -ef | grep /opt/test.sh | wc -l
得到的数量应该是2,如果定时间隔完毕后又刷新了一轮,总进程数则会变成3。
所以我们可以在/opt/test.sh中加入进程数的判断,如果进程数大于2,就说明存在已有任务在运行,此时应该退出执行
count=$(ps -ef | grep /opt/test.sh | wc -l)
if [ $count -gt 2 ]; then
echo "Exist job running!"
exit 1
fi
do something
但是事与愿违,当我们在/opt/test.sh中通过ps命令获取定时任务运行数量的时候发现,如果只存在当前的任务运行时,得到的进程数是3,如果有其他一个已在运行,则进程数是4,以此类推。这是为什么呢?
经过一番研究发现,当只存在当前任务运行时,如果脚本里面是直接运行ps命令,得到的进程数是2,如下所示:
ps -ef | grep /opt/test.sh | wc -l
不难看出这是$()的原因,它在shell中起了一个子shell,所以在子shell执行ps的同时多了一个当前脚本任务运行的进程,所以比正常进程数多1,所以上面代码我们需要改为:
count=$(ps -ef | grep /opt/test.sh | wc -l)
if [ $count -gt 3 ]; then
echo "Exist job running!"
exit 1
fi
do something
方案2:普通文件锁
可以通过一个文件来标识当前是否存在任务在运行,具体做法为当运行任务时,先检查是否存在文件锁,如果存在则表示上个任务还没有运行结束,则退出;如果不存在文件锁,则新创建一个文件锁,然后执行任务,最后执行完毕后删除文件锁。
具体代码如下:
file_lock=/opt/test.lock
if [ -f file_lock ]; then
echo "Exist job running!"
exit 1
fi
touch file_lock
do something
rm -f file_lock
方案3:进程号文件锁
所谓进程号文件锁,相比于方案2的普通文件锁不同的地方就是会把当前运行任务对应的进程号写入锁文件中,其优势在于除了可以通过检查文件是否存在来判断是否存在已经运行的任务,还可以再通过锁文件里面的进程号来做第二次确认。
也许有人会问这个二次确认有啥用?你还别说,这个还真有用,很多时候进程意外终止或者被手动杀掉后,文件锁依然存在,那么使用普通文件锁的结果就是其实并没有正在运行的任务,但是由于存在文件锁,之后所有的任务都不会再运行。而进程号文件锁则可以在文件锁判断之外,再对锁文件中的进程号进行判断是否还在运行,具体代码如下:
#!/bin/bash
PIDFILE=/opt/test.pid
if [ -f $PIDFILE ]
then
PID=$(cat $PIDFILE)
ps -p $PID > /dev/null 2>&1
if [ $? -eq 0 ]
then
echo "Exist job running!"
exit 1
else
echo $$ > $PIDFILE
if [ $? -ne 0 ]
then
echo "Could not create PID file!"
exit 1
fi
fi
else
echo $$ > $PIDFILE
if [ $? -ne 0 ]
then
echo "Could not create PID file!"
exit 1
fi
fi
do something
rm $PIDFILE
虽然此方案看起来很完美,但是还是有一个场景没有考虑到,那就是如果正在运行任务的进程被kill掉,然后另一个进程使用了和被kill进程相同的pid,这样也会导致其实任务并没有在运行,由于存在锁文件和对应进程号的进程在运行,之后所有的任务不再运行。虽然这种场景很极端,但是也是有可能出现的,不过没关系,下面的方案会帮你解决这个问题。
方案4:flock 锁
linux flock锁有区别于一般的锁,它不仅仅是检查文件是否存在,它会一直存在直到进程结束,所以可以直接地知道进程是否真的执行结束了。
格式:
flock [-sxun][-w #] fd#
flock [-sxon][-w #] file [-c] command
选项:
-s, --shared: 获得一个共享锁
-x, --exclusive: 获得一个独占锁
-u, --unlock: 移除一个锁,脚本执行完会自动丢弃锁
-n, --nonblock: 如果没有立即获得锁,直接失败而不是等待
-w, --timeout: 如果没有立即获得锁,等待指定时间
-o, --close: 在运行命令前关闭文件的描述符号。用于如果命令产生子进程时会不受锁的管控
-c, --command: 在shell中运行一个单独的命令
-h, --help 显示帮助
-V, --version: 显示版本
锁类型:
共享锁:多个进程可以使用同一把锁,常被用作读共享锁
独占锁:同时只允许一个进程使用,又称排他锁,写锁。
这里由于我们只允许同时存在一个任务运行,所以选择独占锁,然后需要在脚本执行完丢弃锁:
* * * * * flock -xn /opt/test.lock -c /opt/test.sh