天天看點

異曲同工的租約

有這樣一個需求:wordpress中有一個叫做wp-cron.php的檔案,它負責做一些定時任務,例如定時發送博文,定時清理垃圾回複等。因為wordpress是運作在web php環境下,不借助第三方工具,實作定時任務有一定的困難。它的思路是,每當部落格有點選時,就總觸發一次cron,為了不阻塞客戶的正常通路,用到了fopensocket 發起一個異步cron請求,當cron頁面收到這個請求的時候,就開始檢查各種執行條件,如果條件滿足,則從資料庫中擷取cron任務并執行。

這裡的問題是,如果有兩個使用者同時點選了頁面,同時觸發了cron任務,如何保證隻起一個cron?如何保證起了一個cron後,它不會退出,常駐背景?如何保證萬一cron退出了,會有後備的cron能起來?

對于每一個cron請求,按照下面的順序執行: 

- select擷取一個任務 

- 把這個任務從資料表中删除 

- 檢查删除是否成功 

- -如果删除成功,則開始執行select到的任務 

- -否則直接退出(有另外一個cron請求也在執行這個任務)

這種方法簡單粗暴,很能解決問題,但是它有這樣幾個問題:

-順序問題:select取任務的順序必須都一緻。cron1取a、b;cron2取b、a,則可能兩個cron都會先後主動退出,導緻背景沒有cron了。 

-開銷問題:每次都會嘗試起cron,意味着每次都會發起一次内部的http連接配接

順序問題:這個保證每次select都是按照主鍵順序取即可,或者按照某個行值唯一的列順序執行即可。

開銷問題:引入lease(租約)機制,每個cron job一旦啟動,就會持有一個lease(3秒),每次執行完一個任務,就續一下自己的lease。任何cron希望啟動的時候,必須先看一下lease是否過期,如果lease過期,則立即擷取lease,并啟動自己。

由于沒有加鎖,可能兩個cron都搶到了lease,沒關系,當他們處理任務的時候,會有一個主動放棄(見上面的流程說明)。這種情況比較罕見,不會影響性能。

上面的方案,在特殊情況下還是有一些小缺陷: 

1. cron job中途異常退出後的3秒内,新的任務無法被執行。如果cron job異常退出3秒後,不再有新的請求到來,那麼任務隊列中堆積的任務将無人處理。

如果cron job異常退出的可能性比較低,則這不是一個很大的問題。如果需要確定任務總能被及時執行,可以考慮使用linux系統自帶的crontab,來定時觸發php的cron job。

分布式系統中,lease的概念被廣泛采用。當我們無法确切了解到彼此的行為時,我們可以依賴一套約定,來規範和預測彼此的行為,以保障系統處于一個一緻的狀态。所謂“一緻的狀态”,就是我們覺得正确、可以了解的狀态。上文中,兩個cron請求無法知道彼此的存在,通過lease的方式,很好地達成了一緻,不會出現兩個cron job同時運作的窘境。

php腳本的執行時間,是有限制的,即使在腳本執行之初調用了 ignore_user_abort 方法。該方法的語義是設定用戶端斷開連接配接時是否中斷腳本的執行,并不能改變php腳本最長。控制php最大執行時長的,需要修改php-fpm、nginx等的配置,詳細參考 這裡和這裡 。不過,在腳本中,也是可以改變php的最大執行時間的,相關函數請參考set_time_limit(), ini_set(“max_execution_time”, “45”),這裡還有一篇小結。根據php官方文檔,希望在腳本中設定最大執行時間的時候,必須保證php.ini配置中safe_mode=off。一般預設改選項都是off,是以你可以在腳本中設定一個無限長的腳本運作時間。不過,安全起見,不建議運作無限長的時間,而是應該在ini_get(“max_execution_time”)的基礎上減去若幹秒來運作,然後主動釋放lease。