異曲同工的租約
問題描述
有這樣一個需求: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
分布式係統中,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。
最後更新:2017-04-01 13:37:10