如何避免 Cronjob 重复运行

原文地址: https://www.tony-yin.site/201...

cronjob

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

解决方案

方案1:进程数

这是笔者第一时间本身想的方式,经过进程数来判断当前定时脚本同时执行的数量,好比执行的脚本名为/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,若是定时间隔完毕后又刷新了一轮,总进程数则会变成3code

因此咱们能够在/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

方案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

方案5:solo 程序

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的风险;第三种方案存在锁文件被意外删除和新进程占用相同进程号的问题;第四种方案仍是存在乎外删除锁文件的问题;第五种方案则不须要担忧锁文件被删除致使任务重复运行的问题。

目前看起来第五种方案是最优的,不存在缺陷。不过仍是得看具体场景,笔者认为第三种、第四种、第五种方案都是有可取之处的,你们仍是根据各自的场景选择最适合本身的方案吧。

Refer

相关文章
相关标签/搜索