Upgrade to Pro — share decks privately, control downloads, hide ads and more …

分散型タスクスケジューリングシステム

 分散型タスクスケジューリングシステム

More Decks by NearMeの技術発表資料です

Other Decks in Programming

Transcript

  1. 0
    分散型タスクスケジューリングシステム
    2023-11-17 第68回NearMe技術勉強会
    @yujiosaka

    View full-size slide

  2. 2
    タスクスケジューリングの悩み
    今決まった時間に実行している
    バッチ処理を、データベースに
    登録された値に応じて、動的に
    タスクの実行間隔を制御したい
    タスクをスケジューリング
    するためだけにプロセスを
    常駐させるのは面倒だし、
    乗り換え作業も大掛かりだわ

    View full-size slide

  3. 3
    やりたいことのイメージ
    const jobInterval = await dokkaKaraMottekuru();
    // > “0 * * * *” — cron expression in string
    // > 1000 * 60 * 60 — milliseconds in number
    // > { “hours”: 1 } — date-fns Duration object
    // > etc.
    await schedule({ jobInterval }, async () => {
    });
    await nannkaOmoiShori();

    View full-size slide

  4. 4
    気をつけないといけないこと
    • どのタイミングでタスクの実⾏間隔(jobInterval)を更新するのか
    • どこにタスクの実⾏履歴を保存するのか
    • タスクをスケジューリングするためのプロセスを常駐させるのか
    → 最初に登録された実⾏間隔でタスクが実⾏され続けないようにしたい
    → プロセスを再起動した時に、実⾏間隔がリセットされないようにしたい
    → プロセスを常駐させるなら、そのプロセスをモニタリングしないといけない

    View full-size slide

  5. 5
    node-cron
    https://github.com/kelektiv/node-cron
    5

    View full-size slide

  6. 6
    API
    cron.schedule(“0 * * * *”, () => {
    nannkaOmoiShori();
    })

    View full-size slide

  7. 7
    やってること
    (概念的には)Cron Expressionをミリ秒に置き換えてsetIntervalを実⾏しているだけ
    (実際にはsetIntervalではなくてsetTimeoutを再起的に実⾏している)
    cron.schedule(“0 * * * *”, () => {
    nannkaOmoiShori();
    })
    setInterval(() => {
    nannkaOmoiShori();
    }, 1000 * 60 * 60);

    View full-size slide

  8. 8
    嬉しいこと
    • 直感的に使えてシンプルなAPI
    • 依存関係がなく、インストールするだけですぐに使える
    困ったこと
    • プロセスが再起動すると実⾏間隔がリセットされてしまう
    • タスクの実⾏間隔(jobInterval)をいつ更新するべきかが悩ましい
    • プロセスを常駐させるため、タスクが実⾏されていない間のリソースが無駄
    • プロセスの⽣存を常にモニタリングしなければならない
    • Cron型からプロセス常駐型への移⾏に⼿間が少しかかる

    View full-size slide

  9. 9
    Agenda
    https://github.com/agenda/agenda
    9

    View full-size slide

  10. 10
    API
    const agenda = new Agenda({ db: { address: process.env.MONGO_URI } });
    await agenda.start();
    // worker
    agenda.define(“nannka omoi shori”, async job => {
    await nannkaOmoiShori();
    });
    // scheduler
    await agenda.every(“0 * * * *”, “nannka omoi shori”);

    View full-size slide

  11. 11
    やってること
    MongoDBをジョブキューとしてタスク処理を分散化
    MongoDB
    Task
    Task
    Task
    Worker
    Worker
    Worker
    Scheduler
    Task

    View full-size slide

  12. 12
    嬉しいこと
    • 複数のプロセスでタスク処理を分散することができる
    • 並列度やタスクの優先度を細かく設定することができる
    • タスクスケジューリング以外にもキューとして使⽤できる
    困ったこと
    • スケジューラーが再起動するたびにスケジュールがリセットされてしまう
    • タスクの実⾏間隔(jobInterval)をいつ更新するべきかが悩ましい
    • スケジューラーは冗⻑化させることができず、モニタリングは相変わらず必要
    • Cron型からキュー型への移⾏はとても⼤変
    • MongoDB縛り(同様にbullはRedis縛り)

    View full-size slide

  13. 13
    Cronのシンプルさとモダンなタスクスケジューラーの
    柔軟性を持ち合わせたライブラリを作れないだろうか

    View full-size slide

  14. 14
    Cronyx
    https://github.com/yujiosaka/Cronyx

    View full-size slide

  15. 15
    API
    await cronyx.requestJobExec(
    {
    jobName: "hourly-job",
    jobInterval: "0 * * * *",
    },
    async (job) => {
    await nannkaOmoiShori();
    },
    );

    View full-size slide

  16. 16
    やってること
    • 実際はタスクスケジューラーというよりタスクガードとして振る舞う
    • 直前のタスクの実⾏時間を保存し、条件を満たした時にだけタスクが実⾏される
    • 条件を満たしていない場合タスクを実⾏せず、すぐにPromiseをResolveする
    Database
    Lock
    Lock
    Lock
    ロック要求
    ロック要求

    View full-size slide

  17. 17
    嬉しいこと
    • プロセスを常駐させる必要がないため、リソースが無駄にならずモニタリングも簡単
    • タスクの実⾏間隔(jobInterval)の更新に強く、常に最新の設定が反映される
    • プロセスに障害が発⽣しても実⾏間隔がリセットされることがない
    • Cronを使い続けることができるので、乗り換えが楽
    困ったこと
    • 「タスクスケジューラー」ではなく「タスクガード」なので、少し混乱するかも

    View full-size slide

  18. 18
    その他の機能
    • 依存関係の解決
    • タスクの実⾏に遅延や障害が発⽣しても、⾃動で空⽩期間を埋めて復旧する
    • MongoDB、Redis、MySQL、Postgresの4つをデータソースとしてサポート
    • トランザクションやアトミックな処理等を使って安全なタスク分散を実現
    • それ以外のデータソースも⾃分でプラグインを作成して利⽤できる

    View full-size slide

  19. 19
    依存関係の解決
    await cronyx.requestJobExec(
    {
    jobName: "child-job",
    jobInterval: "*/30 * * * *",
    },
    async (job) => {
    console.log(job.intervalStartedAt);
    console.log(job.intervalEndedAt);
    },
    );
    await cronyx.requestJobExec(
    {
    jobName: "parent-job",
    jobInterval: "0 * * * *",
    },
    async (job) => {
    console.log(job.intervalStartedAt);
    console.log(job.intervalEndedAt);
    },
    );
    child-job.ts parent-job.ts
    requiredJobNames: ["child-job"],

    View full-size slide

  20. 20
    その他の機能
    • 依存関係の解決
    • タスクの実⾏に遅延や障害が発⽣しても、⾃動で空⽩期間を埋めて復旧する
    • MongoDB、Redis、MySQL、Postgresの4つをデータソースとしてサポート
    • トランザクションやアトミックな処理等を使って安全なタスク分散を実現
    • それ以外のデータソースも⾃分でプラグインを作成して利⽤できる

    View full-size slide

  21. 21
    MongoDB
    export const mongodbJobLockSchema = new Schema({
    jobName: { type: String, required: true },
    jobInterval: { type: Number, required: true, default: 0 },
    jobIntervalEndedAt: { type: Date, required: true },
    isActive: { type: Boolean, required: true, default: true },
    createdAt: { type: Date, required: true, default: Date.now },
    updatedAt: { type: Date, required: true, default: Date.now },
    }).index({ jobName: 1, jobIntervalEndedAt: 1 }, { unique: true })

    View full-size slide

  22. 22
    MongoDB
    try {
    return await this.#model.findOneAndUpdate(
    { jobName, jobIntervalEndedAt, isActive: true },
    { jobInterval, updatedAt: new Date() },
    { setDefaultsOnInsert: true, new: true },
    );
    } catch (error) {
    throw error;
    }
    if (error instanceof MongoError && error.code === 11000) {
    return null;
    }
    , upsert: true

    View full-size slide

  23. 23
    Cronからの移行手順
    23

    View full-size slide

  24. 24
    ステップ①
    cronyx.requestJobExecで元の処理を囲む
    await nannkaOmoiShori();
    await cronyx.requestJobExec({
    jobName: "job",
    jobInterval: "0 * * * *",
    }, nannkaOmoisShori);

    View full-size slide

  25. 25
    ステップ②
    Cronの実行時間をjobIntervalよりも短い間隔で設定する。
    → こうすることで、遅延が発生しても自動で復旧できるようになる
    0 * * * * ./nannka-omoi-shori.ts */10 * * * * ./nannka-omoi-shori.ts

    View full-size slide

  26. 26
    これからやること
    • あらゆるサービスから利⽤できるように、HTTPサーバーを提供する
    • 様々な⾔語から利⽤できるように、クライアントを提供する
    やったこと

    View full-size slide

  27. 27
    CronyxServer
    https://github.com/yujiosaka/CronyxServer

    View full-size slide

  28. 28
    CronyxClient.js
    https://github.com/yujiosaka/CronyxClient.js

    View full-size slide

  29. 29
    CronyxClient.py
    https://github.com/yujiosaka/CronyxClient.py

    View full-size slide

  30. 30
    おまけ
    • CronyxServerはBun + Elysiaで実装
    • その他のプロジェクトもNodeで動作するがBunファーストで開発(今度発表します)

    View full-size slide

  31. 31
    解説記事
    https://medium.com/@yujiisobe/cronyx-bridging-the-gap-between-cron-jobs-and-task-scheduling-790b9f709224

    View full-size slide