原文地址: 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
的風險;第三種方案存在鎖文件被意外刪除和新進程佔用相同進程號的問題;第四種方案仍是存在乎外刪除鎖文件的問題;第五種方案則不須要擔憂鎖文件被刪除致使任務重複運行的問題。
目前看起來第五種方案是最優的,不存在缺陷。不過仍是得看具體場景,筆者認爲第三種、第四種、第五種方案都是有可取之處的,你們仍是根據各自的場景選擇最適合本身的方案吧。