Quartz是一個流行的Java應用開源作業調度庫。eBay在自己的很多項目中用它來調度作業。
Quartz在低負載時運行良好,但在高負載時會遇到問題。許多觸發器會失效,導致執行線程無法得到任務,大量作業阻塞在觸發器表中。
所以我們必須進行性能調優。本章描述我們是如何逐步解決問題並優化Quartz的。
問題在哪?
1.Quartz作業不能被調度和執行。
2.許多作業在simple_triggers表中等待執行,但一些作業在fired_triggers中。這些簡單觸發器應當設置REPEAT_INTERVAL,表明它們是重復作業。
TIMES_TRIGGERED表示該作業被觸發的次數

3.日志文件中有大量的“Handling the first 20 triggers that missed their scheduled fire-time …”(處理錯過調度觸發時間點的前20個觸發器)

4.數據庫session增加,許多session等待在“SELECT * FROM qrtz_LOCKS WHERE SCHED_NAME = ‘{SCHED_NAME}‘ AND LOCK_NAME = ‘TRIGGER_ACCESS’ FOR UPDATE”。
什么是觸發器失效?
在我們能理解為什么發生之前,先來看看觸發器失效。這是來自Quartz官方網站的解釋:
觸發器失效發生在一致性觸發器,因為調度器關閉而錯過觸發時間點,或者因為Quartz線程池中沒有可執行作業的線程時。不同的觸發器類型有不同的失效指令。默認地,他們使用一種“智能策略”指令---該指令擁有基於觸發器類型和配置的動態行為。當觸發器開始時,它搜索失效的一致性觸發器,並根據配置的失效指令更新每一個觸發器。當你開始在自己的項目中使用Quartz時,你應該先熟悉那些給定類型定義的失效指令,並在JavaDoc中給予解釋。關於失效指令更詳細的信息將會在每一個觸發器類型的輔導中給出。
例如,有一個10秒間隔的觸發器,以“0秒”為時間戳。在第“0秒”,它通過QuartzSchedulerThread把自己傳遞給ExecuteThread執行,並把NEXT_FIRE_TIME設定為“10秒”。不幸的是它耗時超過60秒,沒有在10秒之內完成,於是錯過了“10秒”,“20秒”,“60秒”等觸發點。
“70秒”后,MisfireHandler發現它失敗了,所以NEXT_FIRE_TIME應該被設置為“80秒”。這就是重復簡單觸發器的“智能策略”指令。

為什么要觸發器訪問鎖?
Quartz支持集群,所以我們可以在集群中配置多實例。它需要使用數據庫鎖來協調在triggers和fire_triggers表中的更新。Quartz使用MySQL中標准行級鎖“SELECT * FROM … FOR UPDATE”。
圖片有助於理解觸發器訪問鎖。
1、如果一個新作業要在triggers表中存儲,它必須在LockOnInsert 為真(默認值)時獲得TRIGGER_ACCESS。
2、QuartzSchedulerThread 也需要在它得到觸發器並觸發后拿到鎖。
3、MisfiredHandler拿到TRIGGER_ACCESS用以恢復失效觸發器並為失效觸發器更新NEXT_FIRE_TIME。

當大量觸發器失效時,系統會惡化
我們在產品中多次目睹這樣的問題。以下是細節:
1.一個實例只有幾個執行的作業。
2.一旦失效發生,減少實例數量可以幫助系統恢復。
基於日志和數據庫信息,我們通過以下幾步在本地復現了問題:
1.在本地安裝MySQL數據庫。
2.我們從Quartz的使用案例中復制MisfireExample
3.我們改變了配置以使Quartz使用MySQL數據庫。
4.我們修改MisfireExample支持多實例,以便於我們可以在本地運行多實例。
5.我們將系統設置為每500毫秒生成一些觸發器重復執行3秒間隔的觸發器5遍。
做了這些更改后運行MisfireExample實例更容易復現問題。下面我們可以看到和產品中一樣的問題。
1.大量觸發器在simple_triggers表中堆積。

2.一些作業在fired_triggers表中觸發。

3.大量失效信息打印在控制台。

4.許多MySQL sessions 等待在“SELECT * FROM qrtz_LOCKS WHERE SCHED_NAME = ‘SCHED_NAME’ AND LOCK_NAME = ‘TRIGGER_ACCESS’ FOR UPDATE”.
5.停止新觸發器的存儲無助於恢復觸發器
6.停止3或4個實例增加了有效觸發器。系統將在執行更多作業時恢復正常。
在第5步中,作業生成器在每個實例中每分鍾只生成兩個觸發器。即使生成頻率很低,系統也沒有恢復。這意味着StoreJobAndTriggers在改場景中不是關鍵因素。問題在於MisfireHandler和QuartzSchedulerThread競爭TRIGGER_ACCESS鎖。每一個實例都有一個MisfireHandler和QuartzSchedulerThread。
你可能也注意到打印的失效信息,約每秒打印一次。這說明它每次更新20行約耗時1秒。
另一個事實是每次QuartzSchedulerThread拿到TRIGGER_ACCESS鎖后獲取一個觸發器。和MisfireHandler的速度比起來,它是一種高速操作。
下圖表明了為什么少實例比多實例在遇到失效問題時更好。
更少的實例意味着QuartzSchedulerThread有更多的機會拿到鎖。

如何優化?

上圖展示了優化結果。我們生成500個enable/disable流量作業,開啟兩個Quartz實例處理。使用原始代碼大約需要270分鍾完成所有作業,而使用Quartz批量模式則只需要36分鍾。
使用批量模式
Quartz支持批量模式。在批量模式中,QuartzSchedulerThread可以獲得基於活躍執行線程數的作業。當我們在這種模式下配置時,觸發器可以更快的執行,有效觸發器數和所有實例的總線程數相等。
這段代碼是創建Quartz調度器的方法。我們可以設置maxBatchSize和batchTimeWindow來影響批量模式。
|
1
2
3
4
5
6
7
8
|
public void createScheduler(String schedulerName,
String schedulerInstanceId, ThreadPool threadPool,
ThreadExecutor threadExecutor,
JobStore jobStore, Map<string, schedulerplugin=
""
> schedulerPluginMap,
String rmiRegistryHost, int rmiRegistryPort,
long idleWaitTime, long dbFailureRetryInterval,
boolean jmxExport, String jmxObjectName, int maxBatchSize, long batchTimeWindow)
throws SchedulerException</string,>
|
我們把maxBatchSize設置成和執行線程數一樣。batchTimeWindow應該基於特定時間段內的任務觸發數。代碼中我們設置成1秒。

改變作業完成順序
讓更新作業數據任務在拿到鎖之前執行。Quartz執行線程需要在一個階段完成后拿到TRIGGER_ACCESS鎖。它在拿到鎖后更新Job Data和觸發器表中的狀態。更新作業數據耗費大量時間因為作業數據需要序列化並存儲到作業明細表中。通常只有一個執行線程更新作業數據。所以不需要鎖。
當我們把“updating job data”這一步搬到自己的代碼中,它降低了鎖的時間。這樣,耗時27分鍾就完成所有的500個作業。如圖

減少上下文切換;盡可能多的執行不同階段
我們的作業有許多階段。一個階段可以獨立的運行在任何實例上。作業數據應該永久存儲在數據庫中。它也需要在每一個階段完成后更新觸發器狀態。在一個執行線程上執行所有階段並降低鎖的使用是一個很好的改良。

摘要
Quartz在集群環境下使用數據庫鎖。常規配置的作業在高負載下堆疊。批量模式可以改善性能,減少鎖次數也會有所幫助。
相關:https://www.cnblogs.com/daxin/category/483763.html
