ScheduledThreadPoolExecutor易出現時鐘漂移問題,不宜使用在UTC、系統時間或使用者互動方面的定期排程,CronScheduler是用於與外部互動的可靠Java排程程式 - Leventov

banq發表於2020-01-30

ScheduledThreadPoolExecutor 容易出現無限的時鐘漂移

最近,我意識到ScheduledThreadPoolExecutor容易出現無限制的時鐘漂移,因此不能用於以UTCUnix時間系統時間 (例如,每小時一次)的特定時間戳或特定速率在長時間如超過幾天的排程任務的場合,通常在後端(除非您必須每天強制重啟伺服器應用程式)以及桌面軟體都是屬於這種長時間排程這種情況。

正是StackOverflow上的這個問題使我開始思考這個問題,有人在使用ScheduledThreadPoolExecutor時觀察到每天〜15分鐘的漂移。

java.util.Timer 堵塞定期任務或在系統時間轉移時導致任務堆積

JDK中有一個髒類:Timer,部分不受時鐘漂移問題的影響(對於週期性任務而言是這樣,但對於將來遠期計劃的單發任務則不適用)。這要歸功於Timer使用System.currentTimeMillis(系統時間)作為時間源,而ScheduledThreadPoolExecutor使用System.nanoTime(CPU時間)作為時間源。

儘管系統時間也可能會相對於協調世界時(UTC)漂移,在某些情況下甚至可能比CPU時間更快,但我們假設計算機定期與NTP伺服器同步,以在較小時糾正漂移。

Timer還有其他缺點:

首先,當系統時間偏移時Timer的行為異常。當系統時間向後移動時,週期性任務在該移動時間內停止執行。當系統時間向前移動時,Timer嘗試通過快速連續觸發許多週期性任務例項來趕上,這可能是不希望的。

儘管這在ScheduledThreadPoolExecutor上很少表現出來,但它也可能需要趕上定期任務。ScheduledThreadPoolExecutor僅當其中一項任務長時間阻塞執行程式的執行緒或由於長時間的GC暫停而使定期任務堆積時,才可能堆積這些任務。這兩種型別的事件通常僅持續幾秒鐘,可能長達一分鐘,而使用者可能會手動將系統時間移動數小時甚至數天,從而導致在Timer上執行的計劃任務大量爆發。

當使用Timer(但不使用ScheduledThreadPoolExecutor)時,TimerTask可以通過scheduledExecutionTime與此處建議的當前時間進行比較來手動檢查執行的延遲,從而規避此問題。但是,顯然,強迫使用者自己編寫這種討厭的解決方法不是很好。

通常,Timer具有一些過時的API。任務不能是lambda,因為它們必須擴充套件TimerTask,它是抽象類,而不是功能介面。Timer的schedule方法不返回Future,這是可用於獲得一次性任務執行結果或取消任務的物件。

根據UTC或壁鐘時間而不是根據計算機的抽象時間(系統時間或CPU時間)進行排程時ScheduledThreadPoolExecutor(以及Timer)的另一個鮮為人知的問題是,ScheduledThreadPoolExecutor並不能在計算PC、膝上型電腦或平板電腦處於休眠模式花費的時間(例如睡眠或休眠)。

例如,如果某個任務提交延遲一小時執行,然後一分鐘後使用者關閉膝上型電腦的蓋子一小時,那麼當使用者繼續使用膝上型電腦時,該任務將不會在59分鐘後再啟動,儘管在某些情況下,在膝上型電腦的機蓋開啟後立即執行任務會是更合理的行為:考慮通知或檢查某些Web服務的更新。

解決方案:CronScheduler

如果您之前沒有討論過時間問題,那麼此時您可能會在協調UTC時間、掛鐘時間(ZonedDateTime又名Java時間),Unix時間、系統時間和CPU時間之間旋轉。好訊息是CronScheduler現在可以為您解決這種複雜性。

CronSchedulercron實用程式命名,是因為它努力在Java程式中儘可能接近地匹配cron的排程精度和可靠性。

CronScheduler類似於單執行緒ScheduledThreadPoolExecutor,它類似於Timer,使用系統時間(通過 System.currentTimeMillis)作為時間源而不是CPU時間。如果有更可靠的時間提供者,也可以為CronScheduler例項配置它。

為了解決時鐘漂移問題,並解決上述的機器暫停問題,CronScheduler定義了一個所謂的同步週期,它是CronScheduler執行緒的強制喚醒週期。當CronScheduler喚醒以執行某些任務時,或者因為它已睡眠了整個同步週期,它會檢查系統時間並根據需要調整計劃任務的剩餘等待時間。這樣,CronScheduler通過其同步週期有效地限制了機器暫停事件後的定期任務的延遲。

必須為每個例項的每個例項選擇同步週期,CronScheduler取決於可容許的時鐘漂移量,是否會發生機器掛起事件和重大的系統時間中斷(通常在消費類計算機和裝置上,但在伺服器環境中),以及這些事情發生時,最大可忍受的任務延遲。

如果CronScheduler在某個時間點檢測到系統時間已向後移,它還會檢查所有計劃的定期任務,以檢視它們現在是否需要比預期的更快開始。它可以防止因系統時間倒退而導致定期任務凍結(至少不超過CronScheduler的同步時間)。

在壁鐘時間安排定期任務

CronScheduler具有ScheduledExecutorService除了的所有方法的等效項scheduleWithFixedDelay。

另一方面,CronScheduler提供了其他scheduleAtRoundTimesInDay方法來安排一天中某個時段的定期任務(例如,在每個3小時週期的開始:00:00、03:00、06 :00等)。 )在給定的時區中,要處理計算初始觸發時間並考慮夏令時變化的複雜性。

無論何時,如果堅持夏令時更改(或永久性時區偏移更改),則始終堅持指定時區的壁鐘時間,這意味著在物理時間或系統時間方面,任務的最佳週期性執行可能會受到干擾在時鐘改變的時刻。使用scheduleAtRoundTimesInDay方法之前,請確保考慮此折衷。

跳至最新的定期任務執行

CronScheduler還提供了等效scheduleAtFixedRate,以及scheduleAtRoundTimesInDay,考慮系統時間可以前移,並跳過所有中斷,在時間偏移發生時“任務連發執行”的問題在Timer中是很容易出現的。

建議:何時使用哪個排程程式?

  1. 僅ScheduledThreadPoolExecutor,用於與Java流程的內部業務有關的任何事情。但是,請仔細觀察,程式內互動在語義上並未與某些外部互動相關聯,並且它不會以某種微妙的方式影響高層系統(機器或叢集)的動態。例如,如果存在一個查詢,並且使用者指定了5秒鐘的超時,則在流程內安排協調中斷,實際上並不是純粹的內部問題。(這並不是說ScheduledThreadPoolExecutor在這種情況下不應該使用,它仍然在此列表的下一個專案中涵蓋。)
  2. 使用ScheduledThreadPoolExecutor用於單觸發超時,到期,驅逐,延遲的重試,清理,殺死,通知,或任何其它類似的動作,在機器內或遠端的,只要該延遲是相對短的(比方說,不到一天的時間)和機器不應進入掛起模式,即在伺服器上。考慮CronScheduler是否滿足以下條件之一,即是否以周為單位計算延遲(示例:身份驗證令牌或cookie過期),或者使用者的計算機或裝置可能進入睡眠狀態。
  3. 採用ScheduledThreadPoolExecutor定期清理,沖洗,重新整理,配置重新裝載,轉儲,日誌旋轉,心跳,健康檢查,狀態檢查,或其他任何類似的動作,機器內或遠離,只要時間不是語義參與行動和行動是冪等的。
  4. 如果機器內或分散式系統中的週期性動作與時間概念有關,請考慮CronScheduler。一個示例是Java程式每分鐘一次將指標傳送到某個外部監視系統。如果使用ScheduledThreadPoolExecutor,則過程和監視系統不能簡單地假設每個傳送都對應於下一分鐘:時鐘漂移最終將使度量儀表板誤導相關的分散式系統上不同節點上的事件。或者,您可以將當前的系統時間(每分鐘的截斷時間)附加到分鐘,但是,缺席的時間或兩次傳送將很常見。使用CronScheduler會更簡單,更可靠,併產生更平滑的指標。其他例子可能會糾纏時間成分的週期性操作包括備份,預寫日誌迴圈,複製,節點間同步和檢查點。
  5. 為了在機器內或分散式系統中按優先順序生成時間推移事件,排程資料處理作業或定期執行資料保留規則(業務規則,法律策略)(如果我們僅考慮排程精度和可靠性),使用:—您的雲提供商可以使用的計劃功能;- systemd或cron實用程式—可從您的群集管理或執行框架(如Kubernetes或Mesos)使用的排程工具;—通過使用無GC或低中斷GC(例如C ++,Rust或Go)語言編寫的程式進行計劃;— CronScheduler,最好在具有低暫停GC的JVM中執行,例如Shenandoah GC或ZGC。這些事件總是根據UTC,Unix時間或系統時間來定義的,因此您永遠不應將其ScheduledThreadPoolExecutor用於這些目的。
  6. 對於與人的任何互動(例如警報,通知,計時器或任務管理),以及使用者計算機或裝置與遠端服務之間的互動(例如檢查新電子郵件或訊息,小部件更新或軟體更新):—開Android,請使用Android專用的API。檢視此帖子以獲取更多詳細資訊。— CronScheduler,如果您正在編寫香草Java應用程式。
  7. 永遠不要使用Timer:所有有效的用例都被ScheduledThreadPoolExecutor或取代CronScheduler。

實碼

以下是Apache Druid的生產程式碼庫中的兩個具體示例,ScheduledThreadPoolExecutor應該將其替換為CronScheduler(兩個都包含在第4條建議中):

我在哪裡可以得到CronScheduler以及如何開始?

請參閱Github上的自述檔案。

 

相關文章