以前、同社は既存の単一ノード スケジューリングを分散タスク スケジューリングに変換したいと考え、市場で主流のオープン ソース分散タスク スケジューリング フレームワークを研究しました。使うのが面倒に感じました!特に以前は多くのスケジュールタスクが 1 つのクラスに記述されていたため、変換するのはさらに面倒でした。私は怠け者なので、他の人が書いたツールに多くの変更を加えなければならないと感じると、いつも不安を感じます。 [[282425]] そこで、自分でフレームワークを書きたいと思いました。結局のところ、一般的な企業のタスクスケジューリングでは、大量のタスクを同時にスケジュールし、大きな並行性を持たせることは不可能なので、分散タスクスケジューリングは、すべての分散システムの中で最もシンプルだと思います。分散システムに変換する主な目的は、タスクを複数のノードに分散し、より多くのタスクを同時に処理できるようにすることです。 ある日、会社の受付で荷物を受け取っていたとき、同僚数人(私を含む)が、その荷物が自分たちのものかどうかを最初から最後まで確認しているという現象を目にしました。それが自分たちのものであれば、彼らはそれを奪い、そうでなければ無視するでしょう。すると、私はインスピレーションを受けました。このシナリオを分散配送システムに当てはめると、宅配会社または宅配業者が各荷物を私たちの名前と電話番号に従って仕分けしており、私たちは必要なものだけを取り出せばよいと想定できます。 しかし、別の観点から見ると、私たち一人一人が最初から最後まですべての速達便を見て、一定の合意されたルールに従って、自分の速達便であれば持ち帰り、自分のものでなければ無視して次の速達便に移るということも理解できます。速達をタスクとして考えてみると、たくさんの速達を受け取りにいくと、大勢の人が自分の速達をスムーズに受け取ることができます。では、多数のノードが独自にタスクを取得し、それぞれのタスクを適切に処理できるのでしょうか? 従来の分散タスク スケジューリングには、単一障害点を回避するためにマルチノード クラスターを展開するスケジューリング センターがあり、さらに、スケジューリング センターによって分散されたタスクの実行を担当する多数のエグゼキュータがあります。上記から得たインスピレーションに基づいて、私のアイデアは、集中型のディスパッチ センターを放棄し、各実行ノードが直接パブリックな場所に移動し、合意されたルールに従ってタスクを取得して実行できるようにすることです。設計図は以下のとおりです タスク DB ライブラリに単一点の問題があるのではないかと疑問に思う人もいるかもしれません。逆に質問したいのですが、他の分散タスク スケジューリング フレームワークにはこの問題はないのでしょうか?単一ポイントのデータベースについては、ビジネス ライブラリのような高可用性ソリューションを個別に検討できますが、これはこの記事の焦点ではありません。当然のことながら、私たちの焦点は、単一のタスクが複数のノードによって同時に実行されないように実行ノードの高可用性を確保する方法、実行の途中で単一のノードが突然接続を失った場合にこのタスクをどうするか、その他の複雑な問題にあります。 その後、私たちは、修正されていないコードを使用して、これらの問題を一つずつ解決しました (修正されていないとは、主に、構造を最適化していない実行中のアカウントのコード スタイルを意味します。私を含め、多くの人は、他の人のソース コードを読むときに、迷路にいるかのようにいつも目が回るような感覚を覚えます。非常に骨の折れる作業のようです。おそらく、私はまだそのレベルに達していません)。 集中スケジューリングが省略されているので、タスクスケジューリングと呼ばれている以上、スケジューリングのプロセスが存在する必要があることは明らかです。そうでなければ、複数のノードがタスクを競合する場合に、どのようにして競合を回避できるでしょうか?ここでの私の解決策は、まずタスクのいくつかの状態(実行待ち、実行中、例外あり、完了)を明確にすることです。 各ノードは、すぐに実行される保留中のタスクをチェックするスレッドを開始し、これらのタスクを反復処理して、楽観的ロックを使用してタスクのバージョン番号 (バージョン番号 + 1) とステータス (実行中になる) を更新します。更新が成功した場合、更新はノード自身の遅延キューに配置され、実行を待機します。 各ノードのスレッドはデータベースにアクセスして実行するタスクを確認するため、実行中のタスクは次回他のノードによって照会されないことは明らかです。このノードのステータスが更新される前に見つかった実行対象のタスクについては、楽観的ロックの試行後に更新が失敗し、タスクはスキップされます。これにより、複数のノードによって同時にタスクが繰り返し実行されるのを回避できます。キーコードは次のとおりです。 - パッケージ com.rdpaas.task.scheduler;
-
- com.rdpaas.task.common.* をインポートします。
- com.rdpaas.task.config.EasyJobConfig をインポートします。
- com.rdpaas.task.repository.NodeRepository をインポートします。
- com.rdpaas.task.repository.TaskRepository をインポートします。
- com.rdpaas.task.strategy.Strategy をインポートします。
-
- org.slf4j.Logger をインポートします。
- org.slf4j.LoggerFactory をインポートします。
- org.springframework.beans.factory.annotation.Autowired をインポートします。
- org.springframework.stereotype.Component をインポートします。
-
- javax.annotation.PostConstruct をインポートします。
-
- java.util.Dateをインポートします。
- java.util.List をインポートします。
- インポート java.util.concurrent.*;
-
- /**
- * タスクスケジューラ
- * @著者 ロンディ
- * @日付2019-03-13 21:15
- */
- @成分
- パブリッククラスTaskExecutor {
-
- プライベート静的最終 Logger ロガー = LoggerFactory.getLogger(TaskExecutor.class);
-
- オートワイヤード
- プライベート TaskRepository タスクリポジトリ;
-
- オートワイヤード
- プライベート NodeRepository nodeRepository;
-
- オートワイヤード
- プライベートEasyJobConfig設定;/**
- * タスクの有効期限遅延キューを作成する
- */
- プライベートDelayQueue<DelayItem<Task>> taskQueue = new DelayQueue<>();
-
- /**
- * 最大で2つのスレッドしか実行されないことが明確にわかるので、システムに組み込まれたツールを使用するだけで済みます。
- */
- プライベート ExecutorService bossPool = Executors.newFixedThreadPool(2);
-
- /**
- * ワーカースレッドプールを宣言する
- */
- プライベート ThreadPoolExecutor ワーカープール;
-
-
- @投稿コンストラクト
- パブリックvoid init() {
- /**
- * カスタム スレッド プール、初期スレッド数 corePoolSize、スレッド プール待機キュー サイズ queueSize、初期スレッドにタスクがあり、待機キューがいっぱいの場合
- * スレッド数は最大スレッド数 maxSize まで自動的に拡張され、新しく拡張されたスレッドは 60 秒間アイドル状態になった後に自動的にリサイクルされます。カスタムスレッドプールは、Executorsのスレッドツールによるものです。
- * それぞれに欠点があり、本番環境での使用には適していません
- */
- ワーカープール = 新しい ThreadPoolExecutor(config.getCorePoolSize(), config.getMaxPoolSize(), 60, TimeUnit.SECONDS, 新しい ArrayBlockingQueue<>(config.getQueueSize()));
- /**
- * 保留中のタスク読み込みスレッドを実行する
- */
- bossPool.execute (新しいLoader ());
- /**
- * タスクスケジューリングスレッドを実行する
- */
- bossPool.execute (新しいBoss ());
-
- }
-
- クラス Loader は Runnable を実装します {
-
- @オーバーライド
- パブリックボイド実行(){
- のために(;;) {
- 試す {
- /**
- * 指定した時間(秒単位)に開始されるメインタスクのリストを検索します
- */
- リスト<Task> タスク = taskRepository.listPeddingTasks(config.getFetchDuration());
- タスクがnullの場合、tasks.isEmpty() は次のように記述します。
- 続く;
- }
- (タスク task:tasks)の場合{
-
- task.setStatus(TaskStatus.DOING);
- タスクのNodeIdを設定します。
- /**
- * 楽観的ロックを使用してステータスを更新します。更新が成功した場合、他のノードは正常に更新されません。実行するタスクのリストを照会する場合
- * この期間中に一部のノードがこのタスクを更新したため、バージョンは確認時のバージョンと異なる必要があります。ここで更新してください
- * 必ず0を返します
- */
- int n = taskRepository.updateWithVersion(タスク);
- 日付nextStartTime = task.getNextStartTime();
- if(n == 0 || nextStartTime == null ) {
- 続く;
- }
- /**
- * 遅延オブジェクトにカプセル化し、遅延キューに入れる
- */
- task = taskRepository.get(task.getId());
- DelayItem<タスク> delayItem = new DelayItem<タスク>(nextStartTime.getTime() - new Date ().getTime(), task);
- タスクキュー.offer(遅延項目);
-
- }
- スレッドをスリープ状態にします。
- } 例外 e をキャッチします。
- logger.error( "タスク リストの取得に失敗しました。原因:{}" , e);
- }
- }
- }
-
- }
-
- クラスBossはRunnableを実装します{
- @オーバーライド
- パブリックボイド実行(){
- のために(;;) {
- 試す {
- /**
- * 時間切れになると、タスクオブジェクトは遅延キューから取り出され、実行のためにワーカースレッドプールに渡されます。
- */
- DelayItem<タスク> item = taskQueue.take();
- if(item != null && item.getItem() != null ) {
- タスク task = item.getItem();
- ワーカープールを実行します(新しいワーカー (タスク))。
- }
-
- } キャッチ (例外 e) {
- logger.error( "フェッチタスクが失敗しました。原因:{}" , e);
- }
- }
- }
-
- }
-
- WorkerクラスはRunnableを実装します{
-
- プライベートタスクタスク;
-
- パブリックワーカー(タスクタスク) {
- this.task = タスク;
- }
-
- @オーバーライド
- パブリックボイド実行(){
- logger.info( "タスクの実行を開始します:{}" ,task.getId());
- タスク詳細の詳細 = null ;
- 試す {
- //タスクを開始する
- 詳細 = taskRepository.start(タスク);
- if(detail == null )戻り値;
- //タスクを実行する
- タスクの呼び出し
- //タスクを完了する
- 終了(タスク、詳細);
- logger.info( "タスクの実行が完了しました:{}" ,task.getId());
- } キャッチ (例外 e) {
- logger.error( "タスクを実行:{} エラー、原因:{}" ,task.getId(), e);
- 試す {
- taskRepository.fail(タスク,詳細,e.getCause().getMessage());
- } 例外 e1 をキャッチします。
- logger.error( "タスク失敗:{} エラー、原因:{}" ,task.getId(), e);
- }
- }
- }
-
- }
-
- /**
- * サブタスクを完了します。親タスクが失敗した場合、サブタスクは実行されません。
- * @param タスク
- * @param 詳細
- * @例外をスローします
- */
- private void Finish(Task task,TaskDetail detail) 例外をスローします {
-
- // サブクラスタスクがあるかどうか確認する
- リスト<タスク> childTasks = taskRepository.getChilds(task.getId());
- 子タスクがnullの場合、childTasks.isEmpty() は次のように記述します。
- //子タスクがない場合に親タスクを完了する
- taskRepository.finish(タスク,詳細);
- 戻る;
- }それ以外{
- (タスク子タスク : 子タスク) {
- //タスクを開始する
- タスクの詳細の子の詳細 = null ;
- 試す {
- //サブタスクのステータスを実行中に変更する
- childTask.setStatus(TaskStatus.DOING);
- 子タスクのノード ID を設定します。
- // サブタスクを開始
- childDetail = taskRepository.startChild(childTask,detail);
- // ステータスを更新するには楽観的ロックを使用します。そうしないと、回復スレッドで同時実行の問題が発生する可能性があります。
- int n = taskRepository.updateWithVersion(childTask);
- (n > 0) の場合 {
- //上記の更新後にバージョンが同期されなくなるのを避けるために、データベースから再度取得します
- childTask = taskRepository.get(childTask.getId());
- //サブタスクを実行
- 子タスクの呼び出し
- //サブタスクを完了する
- 終了(子タスク、子の詳細);
- }
- } キャッチ (例外 e) {
- logger.error( "子タスクの実行エラー、原因:{}" , e);
- 試す {
- taskRepository.fail(childTask, childDetail, e.getCause().getMessage());
- } キャッチ (例外 e1) {
- logger.error( "子タスクエラーが失敗しました。原因:{}" , e);
- }
- }
- }
- /**
- * サブタスクがある場合は、親タスクを完了する前にサブタスクを完了してください
- */
- taskRepository.finish(タスク,詳細);
-
- }
-
- }
-
- }
前述のように、タスクは一度に 1 つのノードによってのみスケジュールされ、実行されることが保証されます。このとき、複数のノードを展開すると、一団がフロントデスクに速達便を受け取りに行き、すべての速達便をスムーズに持ち帰ることができるのと同じように、タスク ライブラリ内のすべてのタスクがスムーズに実行されるはずです。結局のところ、すべての速達便はあなたのものか他人のものかのどちらかであり、あなたの速達便が他人のものにはなりません。 ただし、ここでの宅配便の発送と集荷は少し異なります。宅配便を受け取る人は誰でも、どれが自分のものなのかを見分ける方法を知っています。ここでのスケジュールにはそのような概念はまったくありません。タスクのステータスを更新するために楽観的ロックを使用できる幸運なノードに完全に依存します。一般的に言えば、違いは合意されたルールが必要であるということです。宅配業者の名前と携帯電話番号を見れば、その宅配業者が自分の業者かどうかがわかります。また、タスクをそれ自体で実行する必要があるかどうかを判断するルールを作成し、どのタスクをどのノードで実行する必要があるかを明確にして、不要なロック競合を回避することもできます。 ここでは、負荷分散戦略を参照できます。現時点では、以下のルールを実装したいと考えています。 - id_hash: タスクの自己増分 ID に従って、残りのノード数を取得します。剰余値は、現在のノードのリアルタイムシリアル番号と一致します。一致した場合は、それを持ち帰って処刑することができます。それ以外の場合は、このタスクを意識的に無視してください。
- least_count: 最も少ない数のタスクを実行するノードが優先されます
- 重み: ノードの重みに応じてタスクを実行します
- デフォルト: デフォルトで先着順、その他のルールはありません
タスクの負荷分散戦略とも言える上記のルールによれば、デフォルトのルールを除いて、残りのルールはノード実行数、ノードシーケンス番号、ノード重みなどのグローバルノード情報を知る必要があることがわかります。そのため、ノードにハートビートを追加し、ハートビートサイクルごとにその情報をデータベースに報告する必要があります。ハートビートのコアコードは次のとおりです。 - /**
- * ノードハートビート遅延キューを作成する
- */
- プライベートDelayQueue<DelayItem<Node>> heartBeatQueue = new DelayQueue<>();
- /**
- * 最大2つのスレッドしか実行されないことが明確にわかるので、システムの組み込みツールを直接使用できます。
- */
- プライベート ExecutorService bossPool = Executors.newFixedThreadPool(2);
-
-
- @投稿コンストラクト
- パブリックvoid init() {
- /**
- * リカバリスレッドスイッチがオンで、ハートビートスイッチもオンの場合
- */
- config.isRecoverEnable() と config.isHeartBeatEnable() の両方が true の場合、
- /**
- * ノードをハートビートキューに遅延0で初期化して登録する
- */
- heartBeatQueue.offer(新しい DelayItem<>(0、新しい Node(config.getNodeId())));
- /**
- * ハートビートスレッドを実行する
- */
- bossPool.execute (新しいHeartBeat ());
- /**
- * 例外回復スレッドを実行する
- */
- bossPool.execute (新しい回復());
- }
- }
-
- HeartBeatクラスはRunnableを実装します{
- @オーバーライド
- パブリックボイド実行(){
- のために(;;) {
- 試す {
- /**
- * 時間が経過すると、ノード オブジェクトを遅延キューから取り出し、時間とシーケンス番号を更新できます。
- * 最後に、ハートビート周期と同じタイムアウト期間を持つ新しいノードオブジェクトを作成し、それを遅延キューに入れて循環ハートビートを形成します。
- */
- DelayItem<Node> アイテム = heartBeatQueue.take();
- if(item != null && item.getItem() != null ) {
- ノード node = item.getItem();
- handHeartBeat(ノード);
- }
- heartBeatQueue.offer(新しい DelayItem<>(config.getHeartBeatSeconds() * 1000、新しい Node(config.getNodeId())));
- } キャッチ (例外 e) {
- logger.error( "タスクのハートビート エラー、原因:{} " 、e);
- }
- }
- }
- }
-
- /**
- * プロセスノードのハートビート
- * @param ノード
- */
- プライベートvoid handHeartBeat(Nodeノード) {
- if(ノード == null ) {
- 戻る;
- }
- /**
- * まずこのノードがデータベースに存在するかどうかを確認します
- * 存在しない場合: まず次のシーケンス番号を見つけ、それをノードオブジェクトに設定し、最後に挿入します
- * 存在する場合: nodeIdに従って現在のノードのシーケンス番号と時間を直接更新します
- */
- ノード currNode = nodeRepository.getByNodeId(node.getNodeId());
- (currNode == null )の場合{
- ノードの行番号を設定します。
- ノードリポジトリ。挿入(ノード);
- }それ以外{
- nodeRepository.updateHeartBeat(node.getNodeId());
- }
-
- }
データベースにノード情報が保存されたら、さまざまな高度なタスク取得戦略を実装できます。コードは次のとおりです。 - /**
- * 抽象戦略インターフェース
- * @著者 ロンディ
- * @日付2019-03-16 12:36
- */
- パブリックインターフェース戦略{
-
- /**
- * デフォルトポリシー
- */
- 文字列DEFAULT = "default" ;
-
- /**
- * タスクIDハッシュの残りを自分のノード番号と一致させます
- */
- 文字列 ID_HASH = "id_hash" ;
-
- /**
- * 最小実行回数
- */
- 文字列 LEAST_COUNT = "least_count" ;
-
- /**
- * ノードの重みによって
- */
- 文字列 WEIGHT = "重量" ;
-
-
- 公共 静的戦略選択(文字列キー) {
- スイッチ(キー) {
- ケースID_HASH:
- 新しいIdHashStrategy()を返します。
- LEAST_COUNTの場合:
- 新しい LeastCountStrategy()を返します。
- ケース重量:
- 新しいWeightStrategy()を返します。
- デフォルト:
- 新しい DefaultStrategy()を返します。
- }
- }
-
- パブリックブール値 accept(List<Node> ノード、Task タスク、Long myNodeId);
-
- }
〜 - /**
- * タスク ID ハッシュ方式に従って有効なノード数の残りを取得し、その残りに 1 を加えて各ノードのシーケンス番号と一致させます。
- * この方法は、タスクIDが自己増加するため、実際にはポーリングと同等です。
- * @著者 ロンディ
- * @日付2019-03-16
- */
- パブリッククラスIdHashStrategyはStrategyを実装します{
-
- /**
- * ここでのノードコレクションは、外部スケジューラによって決定されるため空であってはならず、ノードIDの昇順でソートされます。
- */
- @オーバーライド
- パブリックブール値 accept(List<Node> ノード、タスクタスク、Long myNodeId) {
- 整数 サイズ= nodes.size () ;
- 長いタスクID = task.getId();
- /**
- * 自分のノードを見つける
- */
- ノード myNode = nodes.stream().filter(node -> node.getNodeId() == myNodeId).findFirst().get();
- myNode == nullを返しますか? false : (taskId %サイズ) + 1 == myNode.getRownum();
- }
-
- }
〜 - /**
- * 最小数のタスクを処理する戦略、つまり、タスクが来るたびに、最小数のタスクを処理したかどうかを確認し、そうであれば、このタスクを消費することができます。
- * @著者 ロンディ
- * @日付2019-03-16 21:56
- */
- パブリッククラスLeastCountStrategyはStrategyを実装します{
-
- @オーバーライド
- パブリックブール値 accept(List<Node> ノード、タスクタスク、Long myNodeId) {
-
- /**
- * 回数が最も少ないノードを取得します。ここでは、カウントの昇順で並べ替えてから最初の要素を取得するのと比較することができます。
- * その後、 trueを返します
- */
- オプション<Node> min = nodes.stream()。 min ((o1, o2) -> o1.getCounts().compareTo(o2.getCounts()));
-
- 戻る 最小.isPresent()? min .get().getNodeId() == myNodeId : false ;
- }
-
- }
〜 - /**
- * 加重配分戦略によると、計画は次のようになります。
- * ノード番号 1、2、3、4
- * ノードの重み 2,3,3,2
- * 次に余り 0,1 を取ります | 2,3,4 | 5,6,7 | 8,9
- * シーケンス番号1は、2未満の重みを法として合計を消費することができます。
- * シリアル番号2は、2以上2+3未満の重みを法として合計を消費できます。
- * シリアル番号3は、2+3以上2+3+3未満の重みを法として合計を消費できます。
- * シリアル番号3は、重みを法とした合計を消費することができ、これは2+3+3以上2+3+3+2未満である。
- * 概要: このノードが消費できる範囲は、前のノードの重みの合計以上で、自身の重みの合計より小さい重みの合計を法とする重みの合計です。
- * もっと良いアルゴリズムのアイデアを持っている偉大な神はいるのだろうか
- * @著者 ロンディ
- * @日付2019-03-16 23:16
- */
- パブリッククラス WeightStrategy は Strategy を実装します {
-
- @オーバーライド
- パブリックブール値 accept(List<Node> ノード、タスクタスク、Long myNodeId) {
- ノード myNode = nodes.stream().filter(node -> node.getNodeId() == myNodeId).findFirst().get();
- myNode がnullの場合
- 戻る 間違い;
- }
- /**
- * このノードの前のノードの重みと値を計算する
- */
- int preWeightSum = nodes.stream().filter(node -> node.getRownum() < myNode.getRownum()).collect(Collectors.summingInt(Node::getWeight));
- /**
- * すべての重みの合計を計算する
- */
- 重みの合計を nodes.stream() で収集します (Collectors.summingInt(Node::getWeight))。
- /**
- * 加重合計の残りを計算する
- */
- int残り = ( int )(task.getId() % weightSum);
- 残り >= preWeightSum && 残り < preWeightSum + myNode.getWeight()を返します。
- }
-
- }
次にスケジューリングクラスを変換します - /**
- * タスクの戦略を取得する
- */
- プライベート戦略戦略;
-
-
- @投稿コンストラクト
- パブリックvoid init() {
- /**
- * 構成に基づいてタスク戦略を取得するノードを選択します
- */
- 戦略 = Strategy.choose(config.getNodeStrategy());
- /**
- * カスタム スレッド プール、初期スレッド数 corePoolSize、スレッド プール待機キュー サイズ queueSize、初期スレッドにタスクがあり、待機キューがいっぱいの場合
- * スレッド数は最大スレッド数 maxSize まで自動的に拡張され、新しく拡張されたスレッドは 60 秒間アイドル状態になった後に自動的にリサイクルされます。カスタムスレッドプールは、Executorsのスレッドツールによるものです。
- * それぞれに欠点があり、本番環境での使用には適していません
- */
- ワーカープール = 新しい ThreadPoolExecutor(config.getCorePoolSize(), config.getMaxPoolSize(), 60, TimeUnit.SECONDS, 新しい ArrayBlockingQueue<>(config.getQueueSize()));
- /**
- * 保留中のタスク読み込みスレッドを実行する
- */
- bossPool.execute (新しいLoader ());
- /**
- * タスクスケジューリングスレッドを実行する
- */
- bossPool.execute (新しいBoss ());
-
- }
-
- クラス Loader は Runnable を実装します {
-
- @オーバーライド
- パブリックボイド実行(){
- のために(;;) {
- 試す {
- /**
- * まず利用可能なノードのリストを取得します
- */
- リスト<Node> ノード = nodeRepository.getEnableNodes(config.getHeartBeatSeconds() * 2);
- ノードがnullの場合、ノードは空です。
- 続く;
- }
- /**
- * 指定した時間(秒単位)に開始されるメインタスクのリストを検索します
- */
- リスト<Task> タスク = taskRepository.listPeddingTasks(config.getFetchDuration());
- タスクがnullの場合、tasks.isEmpty() を実行します。
- 続く;
- }
- (タスク task:tasks)の場合{
-
- ブール値の accept = strategy.accept(ノード、タスク、config.getNodeId());
- /**
- * 取るべきではないものを取ってはいけない
- */
- 受け入れる場合
- 続く;
- }
- task.setStatus(TaskStatus.DOING);
- タスクのNodeIdを設定します。
- /**
- * 楽観的ロックを使用してステータスを更新します。更新が成功した場合、他のノードは正常に更新されません。実行するタスクのリストを照会する場合
- * この期間中に一部のノードがこのタスクを更新したため、バージョンは確認時のバージョンと異なる必要があります。ここで更新してください
- * 必ず0を返します
- */
- int n = taskRepository.updateWithVersion(タスク);
- 日付nextStartTime = task.getNextStartTime();
- if(n == 0 || nextStartTime == null ) {
- 続く;
- }
- /**
- * 遅延オブジェクトにカプセル化し、遅延キューに入れる
- */
- task = taskRepository.get(task.getId());
- DelayItem<タスク> delayItem = new DelayItem<タスク>(nextStartTime.getTime() - new Date ().getTime(), task);
- タスクキュー.offer(遅延項目);
-
- }
- スレッドをスリープ状態にします。
- } 例外 e をキャッチします。
- logger.error( "タスク リストの取得に失敗しました。原因:{}" , e);
- }
- }
- }
-
- }
前述のように、さまざまな負荷分散戦略を使用して各ノードが取得するタスクのバランスをとることができ、同じタスクに対するノード間の競合も大幅に軽減されます。 しかし、まだ問題が残っています。ノードがタスクを取得して実行中に更新し、実行が途中まで進んでいるが完了しておらず、例外も発生していない場合、さまざまな理由でこのノードが突然ハングすると、このタスクが再度実行される機会はなくなります。これは、ウンチもせずにトイレを占拠するという伝説の行為です。 この問題は、最終的に一貫性のあるシステムの一般的な方法、つまり異常回復スレッドを使用することで解決できます。このシナリオでは、指定されたハートビート タイムアウト期間 (たとえば、デフォルトの 3 つのハートビート サイクル) 内にハートビート時間を更新していないノードに属する未完了のタスクをチェックし、これらのタスクのステータスを実行保留中に復元し、次の実行時間を現在の時間に変更するだけで済みます。コアコードは次のとおりです。 - クラスRecoverはRunnableを実装します{
- @オーバーライド
- パブリックボイド実行(){
- のために(;;) {
- 試す {
- /**
- * 復元する必要があるタスクを見つけます。ここで、復元する必要があるタスクは、完了しておらず、実行ノードが 3 つ以上あるタスクとして定義されます。
- * ハートビート周期はハートビート時間を更新しません。これらのタスクはノードの実行により時間内に完了しなかったため、
- * タスクを再度スケジュールするには、ステータスを実行保留に戻し、次の実行時間を現在の時間に変更するだけです。
- */
- リスト<タスク> タスク = taskRepository.listRecoverTasks(config.getHeartBeatSeconds() * 3);
- タスクがnullの場合、tasks.isEmpty() を実行します。
- 戻る;
- }
- /**
- * まず利用可能なノードのリストを取得します
- */
- リスト<Node> ノード = nodeRepository.getEnableNodes(config.getHeartBeatSeconds() * 2);
- ノードがnullの場合、ノードは空です。
- 戻る;
- }
- 長い maxNodeId = nodes.get( nodes.size () - 1).getNodeId();
- for (タスク タスク: タスク) {
- /**
- * 各ノードにはリカバリ スレッドがあります。不要な競合を避けるために、利用可能なノードからタスクが属するノードに最も近いノードを検索します。
- */
- 長い currNodeId = chooseNodeId(ノード、maxNodeId、task.getNodeId());
- 長い myNodeId = config.getNodeId();
- /**
- * 現在のノードを処理する必要がない場合は、直接スキップします
- */
- currNodeId != myNodeId の場合 {
- 続く;
- }
- /**
- * タスクのステータスを保留に、ノードを現在のノードに直接変更します
- */
- task.setStatus(TaskStatus.PENDING);
- タスクに次の開始時刻を設定します(新しい日付());
- タスクのNodeIdを設定します。
- taskRepository.updateWithVersion(タスク);
- }
- スレッドスリープ(config.getRecoverSeconds() * 1000);
- } キャッチ (例外 e) {
- logger.error( "次のタスクの取得に失敗しました。原因:{}" , e);
- }
- }
- }
-
- }
- /**
- * 次のノードを選択
- * @param ノード
- * @param 最大ノードID
- * @param ノードID
- * @戻る
- */
- プライベート long chooseNodeId(List<Node> ノード、long maxNodeId、long nodeId) {
- ノードIDがmaxNodeIdより大きい場合
- nodes.get(0).getNodeId()を返します。
- }
- 戻り値: nodes.stream().filter(node -> node.getNodeId() > nodeId).findFirst().get().getNodeId();
- }
前述のように、同じタスクに対する各ノードの異常回復スレッド間の不要な競合を回避するために、各異常タスクは、そのタスクが属するノード ID の次の正常ノードによってのみ回復できます。これにより、タスクが完了する前にノードがクラッシュした場合でも、一定期間後に自動的に復元できるようになります。 一般的に、上記は最適化を考慮せずに、優れたタスク スケジューリング フレームワークとして機能するはずです。もしそれが全てだと思ったら、ごめんなさいとしか言えません。まだありますよ、ハハハ!先ほど、特に Spring アノテーションを使用してスケジュールを記述することに慣れている人にとって、他のタスク スケジューリングを使用することの不便さが嫌いだと述べました。 1 つのクラスに @Scheduled アノテーション付きの n 個のスケジューリング メソッドを記述する可能性があり、これにより変換がさらに面倒になります。分散タスク スケジューリングに直接統合するために、次の方法を実現したいと考えています。 - /**
- * テストスケジュール機能
- * @著者 ロンディ
- * @日付2019-03-17 16:54
- */
- @成分
- パブリッククラス SchedulerTest {
-
- @Scheduled(cron = "0/10 * * * * ?" )
- パブリックvoid test1()はInterruptedExceptionをスローします{
- SimpleDateFormat sdf = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" );
- スレッド.sleep(2000);
- System.out.println ( "現在の時刻1:" +sdf.format(new Date ())) ;
- }
-
- @Scheduled(cron = "0/20 * * * * ?" 、親 = "test1" )
- パブリックvoid test2()はInterruptedExceptionをスローします{
- SimpleDateFormat sdf = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" );
- スレッド.sleep(2000);
- System.out.println ( "現在の時刻2:" +sdf.format(new Date ())) ;
- }
-
- @Scheduled(cron = "0/10 * * * * ?" 、親 = "test2" )
- パブリックvoid test3()はInterruptedExceptionをスローします{
- SimpleDateFormat sdf = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" );
- スレッド.sleep(2000);
- System.out.println ( "現在の時刻 3:" +sdf.format(new Date ())) ;
- }
-
- @Scheduled(cron = "0/10 * * * * ?" 、親 = "test3" )
- パブリックvoid test4() は InterruptedException をスローします {
- SimpleDateFormat sdf = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" );
- スレッド.sleep(2000);
- System.out.println ( "現在の時刻4:" + sdf.format(new Date ()));
- }
-
- }
上記の目標を達成するには、Spring の起動後にカスタム アノテーション (spring と同じ名前) もロードする必要があります。コードは次のとおりです - /**
- * スプリングコンテナが起動したら、カスタムアノテーションをロードします
- * @著者 ロンディ
- * @日付2019-03-15 21:07
- */
- @成分
- パブリッククラスContextRefreshedListenerはApplicationListener<ContextRefreshedEvent>を実装します。
-
- オートワイヤード
- プライベート タスク エグゼキュータ タスク エグゼキュータ;
-
- /**
- * タスクが挿入された後にメソッド名/タスク名とデータベースID間のマッピングを保存し、サブタスクの追加を処理するために使用されます。
- */
- プライベート Map<String,Long> taskIdMap = 新しい HashMap<>();
-
- @オーバーライド
- パブリックvoid onApplicationEvent(ContextRefreshedEventイベント) {
- /**
- * ルート コンテナーが Spring コンテナーであるかどうかを判別して、2 回の呼び出しを防止します (MVC の読み込みでも 1 回トリガーされます)
- */
- イベントのgetApplicationContext().getParent()がnullの場合
- /**
- * スケジュールスイッチがオンになっているかどうかを確認する
- * 有効にした場合: スケジュール注釈を読み込み、スケジュール管理にスケジュールを追加します
- */
- アプリケーションコンテキストコンテキスト = event.getApplicationContext();
- Map<String,Object> beans = context.getBeansWithAnnotation(org.springframework.scheduling.annotation.EnableScheduling.class);
- (豆がnullの場合){
- 戻る;
- }
- /**
- * メソッド名とスケジュールアノテーションによって変更されたメソッド間のマッピングを保存するために使用されます
- */
- Map<String,Method> methodMap = new HashMap<>();
- /**
- * Service、ControllerなどはすべてComponentを含んでいるため、直接的または間接的にComponentで注釈が付けられたすべてのクラスを検索します。
- * スプリングコンテナによって管理されるクラスは、コンポーネントによって直接的または間接的に変更される必要があります。
- */
- マップ <String,Object> allBeans = context.getBeansWithAnnotation(org.springframework.stereotype.Component.class);
- <Map.Entry<String,Object>>を設定します。entries = allBeans.entrySet();
- /**
- * Beanと内部のメソッドを走査して、Scheduledアノテーションによって変更されたメソッドを見つけ、タスクをタスクスケジューラに配置します。
- */
- (Map.Entryエントリ:エントリ){
- オブジェクト obj = entry.getValue();
- クラス clazz = obj.getClass();
- メソッド[] methods = clazz.getMethods();
- for (メソッド m: メソッド) {
- if(m.isAnnotationPresent(Scheduled.class)) {
- メソッドMap.put(clazz.getName() + Delimiters.DOT + m.getName(),m);
- }
- }
- }
- /**
- * スケジュールされた注釈を処理する
- */
- handleSheduledAnn(メソッドマップ);
- /**
- * taskIdMapはSpringの起動後に一度だけ使用されるため、ここで直接クリアできます。
- */
- タスクIDマップをクリアします。
- }
- }
-
- /**
- * メソッドマップ内のすべてのメソッドをループする
- * @param メソッドマップ
- */
- プライベート void handleSheduledAnn(Map<String,Method> methodMap) {
- メソッドマップがnullの場合|| メソッドマップが空の場合 () {
- 戻る;
- }
- <Map.Entry<String,Method>>を設定します。entries = methodMap.entrySet();
- /**
- * Beanと内部のメソッドを走査して、Scheduledアノテーションによって変更されたメソッドを見つけ、タスクをタスクスケジューラに配置します。
- */
- ( Map.Entry<String,Method> エントリ:エントリ){
- メソッド m = entry.getValue();
- 試す {
- メソッドマップのmを処理します。
- } キャッチ (例外 e) {
- e.printStackTrace();
- 続く;
- }
- }
- }
-
- /**
- * 親タスクと子タスクを再帰的に追加する
- * @param メソッドマップ
- * @param m
- * @例外をスローします
- */
- private void handleSheduledAnn(Map<String,Method> methodMap,Method m) は例外をスローします {
- クラス<?> clazz = m.getDeclaringClass();
- 文字列名= m.getName();
- スケジュールされた sAnn = m.getAnnotation(Scheduled.class);
- 文字列 cron = sAnn.cron();
- 文字列親 = sAnn.parent();
- /**
- * 親が空の場合、このメソッドによって表されるタスクはルートタスクであり、タスクスケジューラに追加され、グローバルマップに保存されることを意味します。
- * 親が空でない場合はサブタスクであることを意味し、サブタスクは親タスクのIDを知る必要があります。
- * まず、親によって表されるフルネームまたはメソッド名を使用します(親タスクメソッドと子タスクメソッドは、同じクラス内のメソッド名で直接使用できます。
- * それ以外の場合は、クラスのフルネームを指定する必要があります) taskIdMapから親タスクIDを取得します
- * 親タスクIDが見つからない場合は、まず親メソッドのフルネームに従ってmethodMapで親タスクのメソッドオブジェクトを見つけ、このメソッドを再帰的に呼び出します。
- * 親タスクIDが見つかった場合は、サブタスクを追加します
- */
- if(StringUtils.isEmpty(親)) {
- if(!taskIdMap.containsKey(clazz.getName() + Delimiters.DOT +名前)) {
- Long taskId = taskExecutor.addTask( name 、 cron 、 new Invocation(clazz、 name 、 new Class[]{}、 new Object[]{}));
- taskIdMap.put(clazz.getName() + Delimiters.DOT +名前、taskId);
- }
- }それ以外{
- 文字列 parentMethodName = parent.lastIndexOf(Delimiters.DOT) == -1 ? clazz.getName() + Delimiters.DOT + 親 : 親;
- 長い親タスク ID = taskIdMap.get(親メソッド名);
- 親タスクIDがnullの場合
- メソッド parentMethod = methodMap.get(parentMethodName);
- handleSheduledAnn(メソッドマップ、親メソッド);
- /**
- * 再帰的に戻るときは必ず親タスクIDを更新してください
- */
- 親タスク ID = taskIdMap.get(親メソッド名);
- }
- if(parentTaskId != null && !taskIdMap.containsKey(clazz.getName() + Delimiters.DOT + name )) {
- Long taskId = taskExecutor.addChildTask(parentTaskId、 name 、cron、新しいInvocation(clazz、 name 、新しいClass[]{}、新しいObject[]{}));
- taskIdMap.put(clazz.getName() + Delimiters.DOT +名前、taskId);
- }
-
- }
-
-
- }
- }
上記のコードは、Spring の初期化が完了した後に独自のカスタム タスク スケジューリングをロードするアノテーションを完了し、Spring のスケジューリング スイッチ @EnableScheduling によっても制御され、Spring または Springboot へのシームレスな統合を実現し、私のような怠け者の要件を満たします。 実は、このフレームワークを書くのに余暇を 5 日ほど費やしました。隠れた落とし穴がいくつかあると思いますが、明らかな落とし穴は自分で解決しました。オープンソース化の目的は、議論を刺激し、大多数のプログラマーに新しいアイデアを提供することです。皆様のお役に立てれば幸いです。同時に、皆さんがバグを見つけて一緒に改善してくれることを願っています。無視してください、マスター。 |