原文地址: https://www.tony-yin.site/201...
Cronjob
使用中有不少问题须要注意,前段时间写了一篇文章《为何 Cronjob 不执行》,里面谈到了各类会致使cronjob
不执行的因素和解决方案,而本文就cronjob
重复运行的场景,对技术手段、技术方案、具体代码和相互优劣展开详细讲解。linux
以前写过一篇文章《Ctdb Rados(二):多场景断网高可用》,文中提到支持秒级的定时任务的方法,由于cronjob
自己最小只支持分钟级别的定时任务,因此笔者在cronjob
定时脚本中经过for
循环来达到秒级定时的目的。shell
然而这种定时间隔很短的任务是很容易出现重复运行的问题的。正常状况下脚本执行时间是很短的,可是一旦遇到IO
阻塞等问题,会出现多个任务同时运行的状况,这种状况每每不是咱们所指望的,可能会致使意想不到的问题。bash
即便不是秒级的定时任务,只要任务执行时间超过定时间隔都会出现重复运行的问题,好比每分钟运行的定时任务,而其执行时间须要三分钟等等spa
例子以下:.net
$ ps -elf | grep forever 4 S vagrant 4095 4094 0 80 0 - 1111 wait 21:59 ? 00:00:00 /bin/sh -c /var/tmp/forever.sh 0 S vagrant 4096 4095 0 80 0 - 2779 wait 21:59 ? 00:00:00 /bin/bash /var/tmp/forever.sh 4 S vagrant 4100 4099 0 80 0 - 1111 wait 22:00 ? 00:00:00 /bin/sh -c /var/tmp/forever.sh 0 S vagrant 4101 4100 0 80 0 - 2779 wait 22:00 ? 00:00:00 /bin/bash /var/tmp/forever.sh 4 S vagrant 4130 4129 0 80 0 - 1111 wait 22:01 ? 00:00:00 /bin/sh -c /var/tmp/forever.sh 0 S vagrant 4131 4130 0 80 0 - 2779 wait 22:01 ? 00:00:00 /bin/bash /var/tmp/forever.sh 4 S vagrant 4135 4134 0 80 0 - 1111 wait 22:02 ? 00:00:00 /bin/sh -c /var/tmp/forever.sh 0 S vagrant 4136 4135 0 80 0 - 2779 wait 22:02 ? 00:00:00 /bin/bash /var/tmp/forever.sh
这是笔者第一时间本身想的方式,经过进程数来判断当前定时脚本同时执行的数量,好比执行的脚本名为/opt/test.sh
,当有一个任务在运行的时候:vagrant
[root@tony ~]# 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
。code
因此咱们能够在/opt/test.sh
中加入进程数的判断,若是进程数大于2
,就说明存在已有任务在运行,此时应该退出执行blog
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
,以下所示:rem
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
能够经过一个文件来标识当前是否存在任务在运行,具体作法为当运行任务时,先检查是否存在文件锁,若是存在则表示上个任务尚未运行结束,则退出;若是不存在文件锁,则新建立一个文件锁,而后执行任务,最后执行完毕后删除文件锁。
具体代码以下:
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
所谓进程号文件锁,相比于方案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
,这样也会致使其实任务并无在运行,因为存在锁文件和对应进程号的进程在运行,以后全部的任务再也不运行。虽然这种场景很极端,可是也是有可能出现的,不过不要紧,下面的方案会帮你解决这个问题。
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
Solo是一个Perl
脚本,它的工做原理与flock
相似,但它并不依赖于锁文件,由于Solo
程序是经过绑定端口来实现。
$ ./solo -port=6000 /opt/test.sh & [1] 7503 $ ./solo -port=6000 /opt/test.sh solo(6000): Address already in use
执行solo
时,将绑定指定的端口并执行后面指定的命令。一旦命令完成,就会释放端口,容许任务的下一个调用正常执行。
solo
的优点在于没有人可以经过删除一个文件并意外地致使任务重复运行。即便使用flock
命令,若是锁文件被删除,也能够启动第二个做业。因为solo
绑定了一个端口,因此不可能出现这种状况。
上面提到了五种方案,第一种方案略显粗糙,可是缺陷相对来讲较少;第二种方案存在锁文件被意外删除或者进程被kill
的风险;第三种方案存在锁文件被意外删除和新进程占用相同进程号的问题;第四种方案仍是存在乎外删除锁文件的问题;第五种方案则不须要担忧锁文件被删除致使任务重复运行的问题。
目前看起来第五种方案是最优的,不存在缺陷。不过仍是得看具体场景,笔者认为第三种、第四种、第五种方案都是有可取之处的,你们仍是根据各自的场景选择最适合本身的方案吧。