最新の記事

データベース

MongoDB Change Streams を使用した Cron スケジュールのホットスワップ 最近のプロジェクトで、アプリケーションを再起動することなく、スケジュールをその場で更新できる動的な cron ジョブシステムを実装する必要がありました。設定ファイルで cron ジョブを定義し、変更を反映させるためにサーバーを再起動するという従来のアプローチは選択肢にありませんでした。そこで役立ったのが MongoDB Change Streams です。 MongoDB Change Streams は、コレクション内のリアルタイムのデータ変更をリッスンする方法を提供します。`schedules` コレクションにチェンジストリームを設定することで、スケジュールが追加、更新、または削除されたときに即座に通知を受け取ることができます。 以下に、実装の簡単な概要を示します。まず、スケジュールのスキーマを定義します。 ```javascript const scheduleSchema = new mongoose.Schema({ name: String, cron: String, // e.g., '0 * * * *' task: String, // Identifier for the task to run enabled: Boolean, }); ``` 次に、cron ジョブを管理する関数を設定します。実行中のジョブを追跡するためにマップを使用します。 ```javascript const runningJobs = new Map(); function updateCronJob(schedule) { // If a job for this schedule is already running, stop it first if (runningJobs.has(schedule.name)) { runningJobs.get(schedule.name).stop(); runningJobs.delete(schedule.name); } // If the schedule is enabled, create and start a new job if (schedule.enabled) { const job = new CronJob(schedule.cron, () => { console.log(`Running task: ${schedule.task}`); // Logic to execute the actual task goes here }); job.start(); runningJobs.set(schedule.name, job); } } ``` このソリューションの中核は、チェンジストリームリスナーです。アプリケーションが起動すると、`schedules` コレクションでチェンジストリームを開きます。 ```javascript function watchSchedules() { const changeStream = Schedule.watch(); changeStream.on('change', (change) => { console.log('Change detected:', change); if (change.operationType === 'insert' || change.operationType === 'update') { // For inserts and updates, we fetch the full document const schedule = change.fullDocument; updateCronJob(schedule); } else if (change.operationType === 'delete') { // For deletes, we need to find the job by some identifier // This part is tricky because the full document is not available. // A common pattern is to use the document key (_id) to manage this. // For simplicity, we'll assume we can stop it by a name we somehow retrieve. // In a real app, you'd need a more robust way to map the deleted _id to a running job. } }); } ``` 変更イベントが発生すると、リスナーはそれに応じて反応します。 - **`insert`** および **`update`** 操作の場合、新規または更新されたスケジュールドキュメントを取得し、`updateCronJob` を呼び出します。この関数は、古いジョブ (存在する場合) を停止し、最新のスケジュール定義に基づいて新しいジョブを開始する処理を行います。 - **`delete`** 操作の場合、対応する cron ジョブを停止するメカニズムが必要になります。削除の変更イベントには完全なドキュメントが含まれていないため、これは少し複雑です。一般的なアプローチは、ドキュメントの `_id` を使用してジョブを追跡および停止することです。 このアーキテクチャにより、完全に動的で回復力のある cron システムが可能になります。管理者は、データベースで直接ジョブスケジュールを追加、削除、または変更でき、アプリケーションはダウンタイムや手動介入なしでリアルタイムにそれらをホットスワップします。

cronをハードコードすると、スケジュールが変更されるたびに再デプロイが必要になります。スケジュールをデータベースに保存し、デーモンがchange streamで編集に反応するようにします。これがその設計と障害処理です。

2分で読めます mongodbchange-streamscrongoscheduling
バックエンド

Redisのキースペース通知とリースを使用したGPUワーカープールの負荷分散

GPUは高価であり、一度に1つの重いジョブを実行するため、ラウンドロビンルーティングはビジーなワーカーによって処理が滞ります。ここでは、Redisのリースとキースペース通知から構築された、ビジーアウェアなスケジューラを紹介します。

1分で読めます gpuredisload-balancingdistributed-systemsgo
バックエンド

シングルランナージョブ向けのCASおよびハートビートによるリースロック

単純なロックは、ホルダーが死ぬと永久にデッドロックします。リースは期限切れになります。ここでは、比較と交換による取得とハートビートを用いてリースを構築する方法、そしてその設計を決定する障害モードについて説明します。

2分で読めます distributed-systemslockingredisgojobs
データベース

従量課金制データベースにおいて、なぜカーソルページネーションがスキップより優れているのか

`skip(N)`は、スキップ対象のすべてのドキュメントを読み取って破棄するため、読み取りごとに課金されるデータベースでは、そのすべてに対して料金が発生します。以下に、その計算方法、それを置き換えるクエリ、そして順序を安定させるためのタイブレーカーを示します。

1分で読めます databasespaginationfirestoreperformancecost
データベース

FirestoreのMongoDB互換性における`retryWrites=false`の罠

FirestoreはMongoDBのワイヤープロトコルに対応していますが、再試行可能な書き込みは拒否します。お使いのドライバーでこの機能がデフォルトで有効になっている場合、書き込みは一見ランダムな形で失敗します。その理由と修正方法を以下に示します。

1分で読めます firestoremongodbgoidempotencydatabases
バックエンド

バックエンドをRustからGoへ移行した理由、そしてビルド時間が決め手となった経緯 ================================================================ zP1zでは最近、バックエンドをRustからGoへ移行するという決断をしました。これは軽々しく下した決断ではありません。私たちはRustとそのエコシステムの大ファンです。その安全性、パフォーマンス、そして最新のツールはすべて最高水準です。しかし、ある一つの要因が、私たちのチームにとって持続的かつ増大する悩みの種となりました。それはビルド時間です。 ## Rustでの経験 私たちのバックエンドサービスzP2zは、Rustで書かれたモノリスとして始まりました。ユーザー認証からデータ処理まで、あらゆるものを扱っていました。サービスの複雑さとコード行数が増すにつれて、コンパイル時間も長くなっていきました。当初、クリーンビルドには約5分かかっていました。1年後には、CI/CDサーバーでのビルド時間は25分から30分に迫り、開発者のマシンでのインクリメンタルビルドでさえ、しばしば数分を要するようになっていました。 私たちは数多くの最適化手法を試しました: - **`sccache`の使用:** これは助けになりましたが、その恩恵が最も感じられたのはクリーンビルドを繰り返す場合であり、日々のインクリメンタルな変更ではそれほどではありませんでした。 - **より小さなクレートへの分割:** モノリスを、複数のより小さなクレートを持つワークスペースに分割しました。これにより、ある程度の並列処理が可能になりましたが、依存関係や機能の管理が複雑になるという問題も生じました。 - **`Cargo.toml`のチューニング:** `codegen-units`のようなさまざまなプロファイル設定を試しましたが、ビルド時間全体と比較すると、得られる効果はわずかでした。 - **より高速なCIハードウェア:** ランナーをアップグレードしましたが、これは単にコストを時間からお金に移しただけで、問題を根本的に解決するには至りませんでした。 この長いフィードバックループは、開発者の生産性を著しく低下させていました。単純な変更でも、CIですべてのテストに合格するかどうかを確認するために10分待たなければならないこともありました。このコンテキストスイッチはコストが高く、フラストレーションのたまるものでした。 ## Goへの切り替え 私たちは実験を行うことにしました。Rustのワークスペースから、十分に分離された単一のマイクロサービスを取り出し、Goで書き直しました。その結果は、あまりにも対照的でした。 | 指標 | Rustサービス | Goサービス | |---|---|---| | コード行数 | 約8,000 | 約7,500 | | クリーンビルド (CI) | 12分 | 45秒 | | インクリメンタルビルド (開発環境) | 1〜3分 | 2秒未満 | | Dockerイメージサイズ | 150 MB | 25 MB (マルチステージ) | ビルド時間の差は桁違いでした。Goコンパイラの速度はよく知られた特徴であり、その設計に由来します。つまり、よりシンプルな言語、複雑な解決を避ける依存関係モデル、そして速度のために設計されたパーシングです。私たちのチームにとって、これはほぼ瞬時のフィードバックループを意味しました。 ## トレードオフと結論 もちろん、この移行にトレードオフがなかったわけではありません。Goに移行したことで、Rustが提供する強力な型システムとコンパイル時の保証を失いました。`if err != nil`というパターンは効果的ではありますが、Rustの`?`演算子よりも冗長です。また、`nil`ポインタのデリファレンスが起こりうる箇所の扱いに、より規律を求められるようになりました。これはRustコンパイラが完全に防いでくれるものです。 しかし、私たちの特定のユースケース、つまり開発者チームが携わる大規模で進化し続けるバックエンドにおいては、ビルド時間の大幅な改善は、Rustの安全性という利点を上回りました。開発者の生産性と士気の向上は、即時かつ顕著なものでした。私たちのCIパイプラインはより速く、より安価になり、開発者はフラストレーションのたまる待ち時間なしに機能のイテレーションを行うことができるようになりました。 これは、GoがRustより「優れている」という宣言ではありません。言語の選択は、状況に大きく依存するという事実の証です。絶対的なパフォーマンスとメモリ安全性が最重要であるプロジェクト(例:システムプログラミング、ゲームエンジン)にとって、Rustは依然として比類のない選択肢です。しかし、私たちのチームとプロダクトにとって、ボトルネックはCPUのパフォーマンスではなく、開発者のイテレーション時間でした。そして、その指標においては、Goが明白な勝者でした。

Rustは、我々がほとんど必要としなかった安全性と、日々格闘することになったコンパイルループをもたらしました。ここに、あるサービスをGoへ移行させる後押しとなった、数値を交えた率直なトレードオフを示します。

1分で読めます gorustmigrationbuild-timedeveloper-experience