Quartz叢集增強版_01.叢集及缺火處理(ClusterMisfireHandler)

funnyZpC發表於2024-11-12

Quartz叢集增強版_01.叢集及缺火處理(ClusterMisfireHandler)

轉載請著名出處 https://www.cnblogs.com/funnyzpc/p/18542452

主要目的

  • 應用(app)與節點(node)狀態同步

    不管是 node 還是 app,都可以透過對應 state 來控制節點及整個應用的啟停,這是很重要的功能,同時對於叢集/缺火的鎖操作也是基於 app 來做的,同時附加在 app 上的這個鎖是控制所有應用及叢集之間的併發操作,同樣也是很重要的~

  • 任務狀態與執行狀態更新

    因為任務掃描主要操作的是執行時間項(execute)資訊,同時變更的也是執行項的狀態(state),故此需要更新任務(job)狀態

  • 熄火任務恢復執行

    任務掃描排程的過程可能存在 GCDB斷連 的情況,需要及時修正 next_fire_time 以保證在異常恢復後能正常被掃到並被執行

  • 清理歷史記錄

    清理的執行頻度很低,如果可以的話建議是後管接入 click sdk 手動操作,這裡的自動清理是兜底方案,基於資料庫鎖的任務併發在表資料越少時效能理論上就越好~ ,自動清理有兩大任務:

    • 1.清理執行無效應用及非執行節點
    • 2.清理任務及執行配置
  • 建立應用及執行節點

這是必要的操作,預建立節點及應用方便後續管理,同時執行排程也依賴於節點及應用的狀態

前置處理

前置處理指的是 Quartz 啟動時必做的維護,主要包含三部分主要內容:

  • 01.寫入應用(app) 及 節點(node) ,這是很重要的
  • 02.恢復/更新應用狀態
  • 將執行中或異常的 job 拿出來並檢查其關聯的執行項,透過執行項(execute)的狀態更新任務(job)狀態,如果
    多執行項存在多個狀態,狀態的優先順序為(從高到低):ERROR->EXECUTING->PAUSED->COMPLETE
    程式碼表象為 :
 List<QrtzExecute> executes = getDelegate().getExecuteByJobId(conn,job.getId());
      boolean hasExecuting = false;
      boolean hasPaused = false;
      boolean hasError = false;
      boolean hasComplete = false;
      for( QrtzExecute execute:executes ){
          final String state = execute.getState();
          if("EXECUTING".equals(state)){
              hasExecuting=true;
          }else if("PAUSED".equals(state)){
              hasPaused=true;
          }else if("ERROR".equals(state)){
              hasError=true;
          }else if("COMPLETE".equals(state)){
              hasComplete=true;
          }else{
              continue; // 這裡一般是INIT
          }
      }
      // 如果所有狀態都有則按以下優先順序來
      String beforeState = job.getState();
      if(hasError){
          job.setState("ERROR");
      }else if(hasExecuting){
          job.setState("EXECUTING");
      }else if(hasPaused){
          job.setState("PAUSED");
      }else if(hasComplete){
          job.setState("COMPLETE");
      }else{
          continue; // 這裡對應上面的INIT狀態,不做處理
      }
      // 不做無謂的更新...
      if(!job.getState().equals(beforeState)){
          job.setUpdateTime(now);
          getDelegate().updateRecoverJob(conn,job);
      }
  • 03.恢復/更新執行狀態

獲取當前應用下的所有執行中或異常的任務(job),並逐步恢復任務下所有執行中(EXECUTING)或異常(ERROR)的任務,主要是重新計算 next_fire_time

後置處理

  • 01.後置處理的內容是包含所有前置處理,同時對叢集併發做了加鎖 (這個很重要,後一段會講到)
  • 02.同步節點狀態與應用狀態不一致的問題
  • 03.更新 check 標誌,這個 check 標誌主要方便於後續清理之使用,同時 app 上的 check (time_next) 是作為鎖定週期的判斷依據

?關於併發鎖的處理

這個問題可以詳細說明一下,一般一個loop(迴圈)是 15s(TIME_CHECK_INTERVAL) ,在叢集環境中同時存在多個節點的併發問題,所以對叢集及缺火的處理就存在重複執行
一開始我的思考是按照樂觀鎖的思路來做,程式碼大概是這樣的:

    int ct = getDelegate().updateQrtzAppByApp(conn,app);
    // 5.獲取app鎖的才可執行 clear 清理以及 recover 恢復,以減少讀寫
    if( ct>0 ){
      // 獲取到鎖後的處理
    }

但是這樣存在重複執行的情況,具體情況先看圖:

上圖中node1node2 的開始時間相差5s,所以造成了他們獲取鎖的時間存在5s的時間差異,因為有這5s的存在,多個節點幾乎都可以執行這個update語句以獲取鎖,這樣往下的邏輯必然存在重複執行!
任務排程掃描(QuartzSchedulerThread)是統一等到 next_fire_time 的那一刻來競爭鎖,而叢集/缺火處理(ClusterMisfireHandler)在一個 while 的大迴圈內 這個迴圈每次是15s,所以每個節點的所執行的週期是15s(TIME_CHECK_INTERVAL),而鎖的競爭卻是在執行 update 的那一刻
如果借用 任務掃描(QuartzSchedulerThread )的處理思路就是 再加一個 while 或者 sleep 等待到下一個 check_time(time_next),程式碼將如下:

    long t=0;
    // 這裡的 check_time 就是應用的check時間,loop_time則是當前迴圈開始時間 
    if( (t=check_time-loop_time)> 0 ){
      Thread.sleep(t);
    }
    int ct = getDelegate().updateQrtzAppByApp(conn,app);
    // 5.獲取app鎖的才可執行 clear 清理以及 recover 恢復,以減少讀寫
    if( ct>0 ){
      // 獲取到鎖後的處理
    }

以上這樣就可以可以基本保證多個node在同一時間競爭同一把鎖了... ,這樣做還有一個好處,就是基本保證了各個節點的 ClusterMisfireHandler迴圈時間基本一致,同時透過sleep可以隨機打散迴圈時間(新增偏移量)將
ClusterMisfireHandler 的迴圈處理打散在其他節點執行 。

但是,但是哦,如果使用 sleep + update 的方式 也可能導致同一時間加鎖(update)競爭的開銷,所以,我借鑑了 shedlock 開源專案的啟發,就是思考能不能在競爭鎖之前判斷鎖定時間,獲取到鎖之後加一個鎖定時間😂
鎖定時間內的不再去競爭鎖,鎖定時間外的則可以,大致如圖:

看圖,如果我們假定 node1 是先於 node2 執行, 當 node1 在 14:15 成功獲取鎖後 他的下一次執行時間預期就是 14:30 ,同時如果加一個10s鎖定時間(圖中藍線),就是在 15:25 及之前是不可以去競爭鎖,這樣當
node2 在 14:20 去嘗試獲取鎖之前發現最近一個鎖定時間點是 14:25 (及之後) ,此時 node2 會自動放棄競爭鎖(執行update),同時進入下一時間點 14:35 並再次判斷鎖定時間點兒,當然這並不是沒有代價的,各位自行領悟吧😂

經過改造後的程式碼如下:

   // TIME_CHECK_INTERVAL 是迴圈週期,固定為15秒
   long tw = TIME_CHECK_INTERVAL/10*3;  // 70% 減少併發
   if( (app.getTimeNext()-_start)>tw ){
       continue;
   }

   // 5.獲取app鎖的才可執行 clear 清理以及 recover 恢復,以減少讀寫
   if( ct>0 ){
       // 獲取到鎖後的處理
   }

相關文章