sql_exporter と Prometheus を活用した Solid Queue の滞留監視と可視化 [DeNA インフラ SRE]
DeNA のインフラ SRE チームが、Solid Queue の滞留監視においてアプリケーション依存を排除し、sql_exporter と Prometheus を活用した独立型の可視化システムを構築・運用する手法を紹介している。
キーポイント
アプリ非依存の監視アーキテクチャ
Worker プロセスがダウンしても検知できないリスクを避けるため、Ruby ライブラリ(Yabeda)に依存せず、データベースから直接状態を読み取る構成を採用した。
sql_exporter によるメトリクス抽出
任意の SQL クエリを実行してキューの状態(ready, claimed, failed など)を抽出し、Prometheus 形式で公開する sql_exporter を採用した。
滞留監視とキャパシティ計画への活用
ジョブの滞留状態を可視化することで異常検知を迅速化し、さらに worker の最適なキャパシティ判断にもデータを活用している。
アプリケーション非依存の監視設計
Worker プロセスの死活にかかわらずキュー状態を監視するため、Yabeda などのプロセス内計装ではなく、データベースから直接読み取る構成を採用した。
既存基盤を活用した低コスト構築
新規ツールの追加運用コストを抑えるため、既に運用実績のある Prometheus と Grafana をベースに監視システムを構築した。
sql_exporter による直接クエリ方式
任意の SQL クエリ結果を Prometheus メトリクスとして公開できる sql_exporter を採用し、データベースからキューの状態を抽出・可視化している。
滞留監視のためのメトリクス設計
各ステータス(ready, claimed など)と優先度ごとに「最古ジョブのタイムスタンプ」を取得し、Prometheus側で現在時刻との差分を計算することで滞留時間を可視化している。
重要な引用
worker プロセス自体がダウン・フリーズした際にメトリクスの出力も途絶えてしまい、肝心の滞留を正しく検知できないリスクがあります
アプリケーション (worker) を介さず、データベースから直接キューの状態を読み取り、Prometheus に渡す方式に決定しました
worker プロセス自体がダウン・フリーズした際にメトリクスの出力も途絶えてしまい、肝心の滞留を正しく検知できないリスクがある
滞留時間を表現する際に Prometheus 側で計算した方がより自然なため、タイムスタンプを取得する方式にしています
完了ジョブを即物理削除しているため、「完了したジョブ数」を直接カウントして dequeue 速度を割り出すことは困難でした
solid_queue_global_io_counts メトリクスから得られる各テーブルの INSERT / DELETE 累積回数を、PromQL の rate() 関数で秒間の INSERT / DELETE 平均回数に変換し、近似的なスループットとして表示しています。
影響分析・編集コメントを表示
影響分析
この記事は、大規模なジョブキューシステムにおける「監視の盲点」を解消するための具体的な実装パターンを示しており、特に Worker の死活状態とメトリクス出力が同期しない課題に対する解決策として参考価値が高いです。DeNA のような大規模インフラ運用において、既存ツール(Prometheus)を最大限活用しつつ、アプリケーション層への侵入を最小化する設計思想は、多くのエンジニアに適用可能な知見となります。
編集コメント
AI 技術そのものの進展ではありませんが、インフラエンジニアリングにおける「監視の堅牢性」を高めるための実務的なベストプラクティスとして非常に価値のある記事です。
はじめに
IT 基盤部第二グループの無津呂です。
現在、私たちが運用を担当するプロダクトにおいて、ジョブキューシステムを Solid Queue への移行を進めています。
キューを安定して運用するためには、スループットの可視化と滞留監視が不可欠です。
本記事では、sql_exporter と Prometheus / Grafana を活用した、キューのスループット可視化および滞留監視の仕組みについて解説します。
Solid Queue の構成と監視の目的
本題に入る前に、Solid Queue の構成と監視の目的を整理しておきましょう。
私たちの環境では、アプリケーションサーバ (app) やバッチ処理 (batch) からキューにジョブを投入し、常駐プロセスである worker がキューからジョブを取り出して処理を行っています。
Solid Queue のバックエンドには MySQL を採用しており、ジョブの実体はデータベース上のレコードとして管理されています。
ジョブには「実行待ち (ready)」や「実行中 (claimed)」、「失敗 (failed)」といったステータスがあり、状況に応じて対応するテーブル間で遷移します。
(その他にも、指定時刻まで待機する scheduled、同時実行数の制限によりブロックされる blocked、定期実行用の recurring などのステータスが用意されています)
この仕組みにおける「キューの滞留」とは、何らかの原因で worker に消費されず、ステータスが変わったままテーブル上に残存し続ける異常な状態を指します。
ジョブの処理遅延はエンドユーザの体験を損ない、サービス全体の詰まりにつながる恐れがあるため、滞留監視は非常に重要です。
また、各ステータスのジョブ数やスループットを常時可視化しておくことで、異常にいち早く気づくだけでなく、worker の最適なキャパシティを判断するための指標としても活用できます。
こうした背景から、私たちはキューの滞留監視および可視化を実現する監視システムの構築に取り組みました。
次章では、具体的な構築要件について整理します。
監視要件と技術選定
監視システムを構築するにあたり、以下の 2 つの要件を特に重視しました。
- アプリケーションから独立した監視システム
Solid Queue のキュー監視では、Yabeda などの Ruby 向け計装ライブラリを用いてプロセス内からメトリクスを出力する手法が一般的です。
しかしこの方法だと、worker プロセス自体がダウンまたはフリーズした際にメトリクスの出力も途絶えてしまい、肝心の滞留を検知できないリスクがあります。
そのため、worker の死活状態にかかわらずキューの状態を監視し続けられるよう、アプリケーション層には一切手を加えない構成としました。
- 既存の監視基盤 (Prometheus / Grafana) の活用
すでに本環境で運用実績のある Prometheus と Grafana をベースにすることで、新たなツールの導入による運用コストの増大を防ぎつつ、アラート設定やダッシュボード構築を迅速に行えるようにしました。
これらの要件を踏まえた最適なアプローチとして、アプリケーション (worker) を介さず、データベースから直接キューの状態を読み取り、Prometheus に渡す方式を採用することにしました。
データベースからキューの状態を読み取るツールとしては、任意の SQL クエリの実行結果を Prometheus 形式のメトリクスとして公開できる sql_exporter を選定しました。
次章では、実際に構築した監視システムの全体像を紹介します。
監視システムの全体像
データベース (MySQL) に対して sql_exporter が直接クエリを発行しメトリクスを抽出。これを Prometheus で収集します。そして Grafana でキューの状態を可視化しつつ、Prometheus を活用してキューの滞留状況を監視します。
以下、sql_exporter によるメトリクス化のプロセス、Prometheus と Grafana を用いた収集・可視化の方法、そして Prometheus による滞留監視の仕組みを順に解説します。
Step 1: sql_exporter によるメトリクス化
収集メトリクスと SQL クエリの設計
キューの状態を把握するために、3 つの主要なメトリクスを設計しました。
各メトリクスの目的と、データベースからの抽出方法を以下に説明します。
(1) 各ステータスのジョブ数
現在のキューの状況を可視化するためには、ステータスごとのジョブ数を把握する必要があります。
Solid Queue ではステータスごとにテーブルが分かれているため、以下の SQL を用いて各テーブルのレコード数をカウントすることで、ジョブ数を取得できます。
より正確には、キュー名やジョブの優先度、ステータスごとに集計を行っていますが、これは後述する滞留監視用のメトリクスと同じ粒度に揃えるための措置です。
Solid Queue の各キューと優先度、ステータスごとのジョブ数を取得する SQL クエリは以下の通りです。
まず solid_queue_scheduled_executions テーブルから、キュー名、優先度、ステータスを「scheduled」としてカウントし、同様に solid_queue_blocked_executions(blocked)、solid_queue_ready_executions(ready)のデータを集計します。これら 3 つの結果を UNION ALL で結合しています。
次に、実行中のジョブ情報を取得するために、solid_queue_claimed_executions と solid_queue_jobs テーブルをジョインし、同様に failed や recurring のステータスも関連付けながら集計します。これにより、すべてのステータス(scheduled, blocked, ready, claimed, failed, recurring)におけるキューごとのジョブ数が取得できます。
上記のクエリ結果は、sql_exporter によって Prometheus が監視できる形式のメトリクスに変換されます。具体的には、キュー名、優先度、ステータスをラベルとして付与した solid_queue_job_count という名の時系列データが生成され、例えば「default キューで優先度 10 の準備中ジョブが 5 件」「優先度 20 の実行中ジョブが 2 件」といった形で出力されます。
ただし、このメトリクスはあくまでその瞬間の値を示すものであり、過度な信頼は禁物です。しかし、キュー全体の滞留状況や負荷傾向を大まかに把握する目的には十分活用できます。
(2) 最古ジョブのタイムスタンプ
今回の主目的である滞留監視を実現するには、各ステータスにおいて「最も長く待たされているジョブが、どれくらいの時間滞留しているか」を把握する必要があります。また、各ジョブには処理優先度が割り当てられており、優先度ごとに滞留監視の閾値を使い分ける運用方針のため、優先度ごとの滞留時間を可視化する必要性も生じます。
そこで今回は、キュー名・ジョブの優先度・ステータスごとの「最古ジョブのタイムスタンプ(Unix 時間)」を取得するメトリクスを設計しました。滞留時間を直接メトリクスとして出力することも可能ですが、Prometheus 側で計算した方がより自然な表現になるため、今回はタイムスタンプを取得する方式を採用しています。
SQL でタイムスタンプを抽出する際は、各ステータスの特性に合わせて以下のようにカラムを使い分けます。
- ready / claimed / failed / blocked : created_at(ジョブが各状態に遷移・作成された時刻)
- scheduled : scheduled_at(ジョブが実行されるべき予定時刻)
- recurring : run_at(定期タスクが実行されるべき予定時刻)
Solid Queue の各キュー状態(スケジュール済み、ブロック済み、準備中、占有中、失敗、定期実行)におけるジョブの最古作成時刻を把握するためには、複数のテーブルからデータを抽出し統合する SQL クエリが必要です。具体的には、solid_queue_scheduled_executions テーブルからは scheduled_at の最小値を、solid_queue_blocked_executions と solid_queue_ready_executions テーブルからは created_at の最小値を取得します。また、solid_queue_claimed_executions や solid_queue_failed_executions といったテーブルでは、ジョブ情報を含む solid_queue_jobs テーブルと結合し、それぞれのキュー名と優先度でグループ化して最古の時刻を算出します。定期実行ジョブについても同様に、solid_queue_recurring_executions とジョブ情報を結合して処理を行います。
これらのクエリ結果は、sql_exporter を経由して Prometheus が監視可能な形式のメトリクスに変換されます。
solid_queue_oldest_job_seconds{queue_name="default",priority="10",status="ready"} 1782864000
solid_queue_oldest_job_seconds{queue_name="default",priority="20",status="claimed"} 1782864010
solid_queue_oldest_job_seconds{queue_name="default",priority="30",status="failed"} 1782864060
...
(3) スループット
キューのスループット、つまり単位時間あたりにエンキュー(enqueue)またはデキュー(dequeue)されたジョブ数を可視化するメトリクスも必要です。
我々の運用環境では、完了したジョブは即座に物理削除されるため、「完了したジョブ数」を直接カウントしてデキュー速度を算出するのは困難でした。エンキュー速度についても同様の理由で計測が難しいことが判明しています。
そこで視点を切り替え、MySQL の performance_schema に注目しました。performance_schema 内の table_io_waits_summary_by_table テーブルには、テーブルごとの累積 INSERT および DELETE 回数が統計データとして格納されています。これを利用することで、キューや優先度レベルでの細分化はできませんが、各ステータス(テーブル)単位の大まかなスループットを把握できるようになりました。
SELECT OBJECT_NAME AS `table`, COUNT_INSERT AS `insert`, COUNT_DELETE AS `delete`
FROM performance_schema.table_io_waits_summary_by_table
WHERE OBJECT_SCHEMA = DATABASE()上記の SQL クエリ結果を、sql_exporter を用いて以下のような Prometheus 形式のメトリクスに変換します。
solid_queue_global_io_counts{table="solid_queue_ready_executions",operation="insert"} 1000
solid_queue_global_io_counts{table="solid_queue_ready_executions",operation="delete"} 800
solid_queue_global_io_counts{table="solid_queue_claimed_executions",operation="insert"} 800
...sql_exporter の設定ファイル
設計した 3 つのメトリクスを、以下のように sql_exporter の設定ファイルに記述します。
jobs セクションで接続先のデータベースを設定し、collectors 内の metrics で先ほど解説した各クエリとラベルのマッピングを定義しています。
global:
scrape_timeout: 10s
scrape_timeout_offset: 500ms
max_connections: 3
max_idle_connections: 3Solid Queue の滞留監視と可視化:SQL Exporter と Prometheus の連携
DeNA インフラ SRE チームが取り組む、Solid Queue の運用監視において、SQL Exporter を活用したアプローチが注目されています。本稿では、Prometheus との連携を通じて、キュー内のジョブ滞留状況をリアルタイムで可視化する具体的な構成と、その効果について解説します。
監視対象となる Solid Queue の設定例を以下に示します。この設定では、solid_queue という名前のジョブを定義し、MySQL データベースからメトリクスを取得する構成となっています。
jobs:
- job_name: solid_queue
collectors: [ solid_queue ]
static_configs:
- targets:
solid_queue_dev: 'mysql://user:${PASSWORD}@host:3306/solid_queue_dev'この設定により、開発環境(solid_queue_dev)のデータベース接続情報を指定し、ユーザー名とパスワードを環境変数やシークレット管理を通じて安全に取得しながら、キューの状態を継続的に収集することが可能になります。これによって、ジョブの滞留時間や処理遅延といった重要な指標を、Prometheus のダッシュボード上で直感的に把握できるようになります。
SQL Exporter を用いることで、アプリケーション側のコードを変更することなく、既存のデータベース構造を活用して監視データを抽出できます。特に、Solid Queue が MySQL をバックエンドとして利用している環境では、この手法が非常に効率的です。パスワードなどの機密情報は ${PASSWORD} といった変数形式で管理することで、設定ファイルへの直接記述を避け、セキュリティリスクを低減しています。
本構成を導入した結果、DeNA インフラチームはジョブの滞留状況を早期に検知し、ボトルネックの特定やリソース調整を迅速に行えるようになりました。可視化されたデータは、運用担当者の判断材料としてだけでなく、システム全体の健全性を維持するための重要な指標としても機能しています。
次回の連載では、収集したメトリクスに基づくアラート設定や、実際のトラブルシューティング事例について詳しく取り上げます。
Solid Queue の滞留監視と可視化
Solid Queue のジョブ状況を把握するために、Prometheus と SQL Exporter を組み合わせた監視構成を採用しています。この設定では、solid_queue_job_count というゲージメトリクスを定義し、キュー名、優先度、ステータスをラベルとして付与することで、詳細な可視化を実現します。
具体的には、データベース内の各テーブルからジョブ数を抽出する SQL クエリを実行しています。solid_queue_scheduled_executions テーブルからはスケジュール済みのジョブを、solid_queue_blocked_executions からブロックされたジョブを、そして solid_queue_ready_executions から実行待ちのジョブをそれぞれカウントします。
さらに、実行中のジョブについては solid_queue_claimed_executions と solid_queue_jobs テーブルを結合して取得し、同様に失敗したジョブや再試行が必要なジョブについても関連テーブルを結合することで正確な数を算出しています。これにより、各キューの状態を包括的に把握することが可能になります。
collectors:
- collector_name: solid_queue
metrics:
- metric_name: solid_queue_job_count
type: gauge
help: 'Current number of jobs'
key_labels: [queue_name, priority, status]
values: [job_count]
query: |
SELECT queue_name, priority, 'scheduled' AS status, COUNT(*) AS job_count FROM solid_queue_scheduled_executions GROUP BY queue_name, priority
UNION ALL
SELECT queue_name, priority, 'blocked' AS status, COUNT(*) AS job_count FROM solid_queue_blocked_executions GROUP BY queue_name, priority
UNION ALL
SELECT queue_name, priority, 'ready' AS status, COUNT(*) AS job_count FROM solid_queue_ready_executions GROUP BY queue_name, priority
UNION ALL
SELECT j.queue_name, j.priority, 'claimed' AS status, COUNT(*) AS job_count FROM solid_queue_claimed_executions ce JOIN solid_queue_jobs j ON ce.job_id = j.id GROUP BY j.queue_name, j.priority
UNION ALL
SELECT j.queue_name, j.priority, 'failed' AS status, COUNT(*) AS job_count FROM solid_queue_failed_executions fe JOIN solid_queue_jobs j ON fe.job_id = j.id GROUP BY j.queue_name, j.priority
UNION ALL
SELECT j.queue_name, j.priority, 'recurring' AS status, COUNT(*) AS job_count FROM solid_queue_recurring_executions re JOIN solid_queue_jobs j ON re.job_id = j.id GROUP BY j.queue_name, j.priority- metric_name: solid_queue_oldest_job_seconds
type: gauge
help: '最も古い未完了ジョブの Unix タイムスタンプ'
key_labels: [queue_name, priority, status]
values: [oldest_ts]
query: |
SELECT queue_name, priority, 'scheduled' AS status, UNIX_TIMESTAMP(MIN(scheduled_at)) AS oldest_ts FROM solid_queue_scheduled_executions GROUP BY queue_name, priority
UNION ALL
SELECT queue_name, priority, 'blocked' AS status, UNIX_TIMESTAMP(MIN(created_at)) AS oldest_ts FROM solid_queue_blocked_executions GROUP BY queue_name, priority
UNION ALL
SELECT queue_name, priority, 'ready' AS status, UNIX_TIMESTAMP(MIN(created_at)) AS oldest_ts FROM solid_queue_ready_executions GROUP BY queue_name, priority
UNION ALL
SELECT j.queue_name, j.priority, 'claimed' AS status, UNIX_TIMESTAMP(MIN(ce.created_at)) AS oldest_ts FROM solid_queue_claimed_executions ce JOIN solid_queue_jobs j ON ce.job_id = j.id GROUP BY j.queue_name, j.priority
UNION ALL
SELECT j.queue_name, j.priority, 'failed' AS status, UNIX_TIMESTAMP(MIN(fe.created_at)) AS oldest_ts FROM solid_queue_failed_executions fe JOIN solid_queue_jobs j ON fe.job_id = j.id GROUP BY j.queue_name, j.priority
UNION ALL
SELECT j.queue_name, j.priority, 'recurring' AS status, UNIX_TIMESTAMP(MIN(re.run_at)) AS oldest_ts FROM solid_queue_recurring_executions re JOIN solid_queue_jobs j ON re.job_id = j.id GROUP BY j.queue_name, j.priority
- metric_name: solid_queue_global_io_counts
type: counter
help: 'テーブルごとの I/O 操作の総数'
key_labels: [table]
value_label: operation
values: [insert, delete]
query: |
SELECT OBJECT_NAME AS table, COUNT_INSERT AS insert, COUNT_DELETE AS delete
FROM performance_schema.table_io_waits_summary_by_table
WHERE OBJECT_SCHEMA = DATABASE()
Step 2: Prometheus と Grafana による収集・可視化
Prometheus では、sql_exporter が提供する /metrics エンドポイントのスクレイプ設定を追加しています。
Grafana では、以下のようなダッシュボードを構築してキューの状態を可視化しています。
ジョブ数と滞留時間のグラフ
default という名前のキューについて、左側にジョブの数、右側に滞留時間をそれぞれ表示しています。

スループットのグラフ
solid_queue_global_io_counts メトリクスから得られる各テーブルの INSERT / DELETE 累積回数を、PromQL の rate() 関数を使って秒間の平均回数に変換し、近似的なスループットとして表示しています。

enqueue / dequeue の速度を確認したい場合は、主に以下のポイントに着目するのが良いでしょう。
- enqueue 速度:ready テーブルの INSERT 速度
- dequeue 速度(処理開始時点):claimed テーブルの INSERT 速度、または ready テーブルの DELETE 速度
- dequeue 速度(ジョブ完了時点):jobs テーブル(全ジョブが共通して格納されるテーブル)の DELETE 速度
なお、claimed テーブルに記録されたジョブは、対応する jobs テーブル内のレコード削除に伴ってカスケード削除されるようになっています。そのため claimed テーブルの delete 速度は表面上ゼロに見え、dequeue 速度の指標としては利用できません。
滞留監視の閾値
移行前のキューシステムでは、ジョブの種類ごとに個別に閾値を設定して監視を行っていました。細かく調整できる反面、十数種類あるジョブそれぞれで閾値を管理する必要があり、運用負荷が課題となっていました。
Solid Queue ではこの個別管理を廃止し、ジョブの優先度に基づく汎用的な閾値運用へ移行しました。優先度は「High」「Normal」「Low」の 3 段階に分類され、ステータスと組み合わせることで滞留監視の閾値を設定します(表の数値は当社の環境・要件に基づいた一例です)。
| ステータス | High (priority=10) | Normal (priority=20) | Low (priority=30) |
|---|---|---|---|
| blocked / ready / claimed | 1 分 | 15 分 | 6 時間 |
| scheduled | 1 分 | 1 分 | 1 分 |
| failed / recurring | 10 分 | 10 分 | 10 分 |
アラートルール実装
Prometheus のアラートルールでは、time() 関数と solid_queue_oldest_job_seconds メトリクスの差分を計算することで、ジョブの滞留時間(秒)を評価します。
例えば、「ステータスが ready で優先度が High(閾値 1 分)」の場合のアラートルールは以下のようになります。
rules:
- alert: SolidQueueReadyJobWait_PriorityHigh
expr: |
time() - solid_queue_oldest_job_seconds{status="ready", priority="10"}
> 60
for: 1m
labels:
severity: criticalまとめ
アプリケーション側への改修は行わず、独立した監視構成として sql_exporter から直接データベースを参照する方式を採用しました。これにより、Solid Queue のスループット可視化と滞留監視を実現しています。
本構成での運用はまだ始まったばかりですが、実際の運用状況に合わせて閾値やメトリクスの調整は十分可能です。この記事が、Solid Queue の監視体制を検討されている方々の参考になれば幸いです。
原文を表示
はじめに
IT 基盤部 第二グループの無津呂です。
私たちのチームがインフラ運用を担っているプロダクトにおいて、ジョブキューシステムを Solid Queue に移行しています。
キューの安定運用のためにはスループット可視化や滞留監視が不可欠です。
本記事では、sql_exporter と Prometheus / Grafana を用いたキューのスループット可視化および滞留監視の仕組みについてご紹介します。
Solid Queue の構成と監視の目的
本題に入る前に、Solid Queue の構成と監視の目的について整理します。
我々の環境では、アプリケーションサーバ (app) やバッチ処理 (batch) からキューにジョブを投入し、常駐プロセス (worker) がキューからジョブを取り出して処理しています。
Solid Queue のバックエンドとして MySQL を使用しており、ジョブの実体はデータベース上のレコードです。
ジョブは実行待ち (ready) や実行中 (claimed)、失敗 (failed) などのステータスを持ち、ステータスに応じたテーブル群を遷移する仕組みになっています。
(ステータスは他にも、指定時刻になるまで待機する scheduled、同時実行数の制御によってブロックされている blocked、定期実行用の recurring などが存在します)
このような仕組みにおいて「キューの滞留」とは、ジョブが何らかの要因で worker に消費されず、ステータスが変化しないままテーブルに残り続けている異常な状態を指します。
ジョブの処理遅延はエンドユーザの体験を損ねたり、サービス全体の詰まりにつながるため、滞留監視は重要です。
また、各ステータスのジョブの数やスループットを常に可視化しておくことで、異常にいち早く気付けるだけでなく、worker の最適なキャパシティを判断するための情報としても活用できます。
こうした背景から、キューの滞留監視および可視化を実現する監視システムの構築に取り組みました。
次章では、具体的な構築要件について整理します。
監視要件と技術選定
監視システムを構築するにあたり、以下の 2 つの要件を重視しました。
- アプリケーションから独立した監視システム
Solid Queue のキュー監視では、Yabeda などの Ruby 向け計装ライブラリを用いてプロセス内からメトリクスを出力する手法が一般的です。
しかし、この手法では worker プロセス自体がダウン・フリーズした際にメトリクスの出力も途絶えてしまい、肝心の滞留を正しく検知できないリスクがあります。
そのため、worker の死活にかかわらずキューの状態を監視し続けられるよう、アプリケーション層には一切手を入れない構成としました。
- 既存の監視基盤 (Prometheus / Grafana) の活用
すでに本環境で運用実績のある Prometheus と Grafana をベースにすることで、新たなツールの運用コストを増やさず、アラート設定やダッシュボード構築を行えるようにしました。
これらを踏まえた最適なアプローチとして、アプリケーション (worker) を介さず、データベースから直接キューの状態を読み取り、Prometheus に渡す方式に決定しました。
データベースからキューの状態を読み取るためのツールとしては、任意の SQL クエリの実行結果を Prometheus 形式のメトリクスとして公開できる
を採用しました。
次章では、構築した監視システムの全体像を紹介します。
監視システムの全体像
データベース (MySQL) に対し、sql_exporter から直接クエリを発行してメトリクスを抽出し、Prometheus で収集します。
そして Grafana でキューの状態を可視化しつつ、Prometheus でキューの滞留監視を行います。
以下、sql_exporter によるメトリクス化、Prometheus と Grafana による収集・可視化、Prometheus による滞留監視の方法を順に説明します。
Step 1: sql_exporter によるメトリクス化
収集メトリクスと SQL クエリの設計
キューの状態を把握するために、3 つのメトリクスを設計しました。
各メトリクスの目的とデータベースからの抽出方法を説明します。
(1) 各ステータスのジョブ数
現在のキューの状況を可視化するために、ステータスごとのジョブ数を知る必要があります。
Solid Queue はステータスごとにテーブルが分かれているため、以下のような SQL で各テーブルのレコード数をカウントすることでジョブ数を取得できます。
より正確には、キュー名・ジョブ優先度・ステータスごとにジョブ数を集計していますが、これは後述する滞留監視用のメトリクスと同じ粒度に揃えるためです。
SELECT queue_name, priority, 'scheduled' AS status, COUNT(*) AS job_count FROM solid_queue_scheduled_executions GROUP BY queue_name, priority
UNION ALL
SELECT queue_name, priority, 'blocked' AS status, COUNT(*) AS job_count FROM solid_queue_blocked_executions GROUP BY queue_name, priority
UNION ALL
SELECT queue_name, priority, 'ready' AS status, COUNT(*) AS job_count FROM solid_queue_ready_executions GROUP BY queue_name, priority
UNION ALL
SELECT j.queue_name, j.priority, 'claimed' AS status, COUNT(*) AS job_count FROM solid_queue_claimed_executions ce JOIN solid_queue_jobs j ON ce.job_id = j.id GROUP BY j.queue_name, j.priority
UNION ALL
SELECT j.queue_name, j.priority, 'failed' AS status, COUNT(*) AS job_count FROM solid_queue_failed_executions fe JOIN solid_queue_jobs j ON fe.job_id = j.id GROUP BY j.queue_name, j.priority
UNION ALL
SELECT j.queue_name, j.priority, 'recurring' AS status, COUNT(*) AS job_count FROM solid_queue_recurring_executions re JOIN solid_queue_jobs j ON re.job_id = j.id GROUP BY j.queue_name, j.priority
上記のクエリの結果を、sql_exporter によって以下のような Prometheus 形式のメトリクスに変換します。
solid_queue_job_count{queue_name="default",priority="10",status="ready"} 5
solid_queue_job_count{queue_name="default",priority="20",status="claimed"} 2
solid_queue_job_count{queue_name="default",priority="30",status="failed"} 1
...このメトリクスは瞬間的な個数しか示さないため過度な信頼は禁物ですが、キューの大まかな状況を把握するのには使えます。
(2) 最古ジョブのタイムスタンプ
今回の主目的である滞留監視のためには、各ステータスにおいて「もっとも古くから待たされているジョブが、どれくらいの時間滞留しているか」を計測する必要があります。
さらに、各ジョブには処理の優先度を割り当てており、優先度ごとに滞留監視の閾値を変える運用にしたいため、優先度ごとに滞留時間を把握する必要もあります。
ここでは、キュー名・ジョブ優先度・ステータスごとの「最古ジョブのタイムスタンプ(Unix 時間)」を取得するメトリクスを設計しました。
(滞留時間を直接メトリクス化することも可能ですが、滞留時間を表現する際に Prometheus 側で計算した方がより自然なため、タイムスタンプを取得する方式にしています)
SQL でタイムスタンプを抽出する際は、各ステータスの特性に合わせて以下のようにカラムを使い分けます。
- ready / claimed / failed / blocked : created_at(ジョブが各状態に遷移・作成された時刻)
- scheduled : scheduled_at(ジョブが実行されるべき予定時刻)
- recurring : run_at(定期タスクが実行されるべき予定時刻)
SELECT queue_name, priority, 'scheduled' AS status, UNIX_TIMESTAMP(MIN(scheduled_at)) AS oldest_ts FROM solid_queue_scheduled_executions GROUP BY queue_name, priority
UNION ALL
SELECT queue_name, priority, 'blocked' AS status, UNIX_TIMESTAMP(MIN(created_at)) AS oldest_ts FROM solid_queue_blocked_executions GROUP BY queue_name, priority
UNION ALL
SELECT queue_name, priority, 'ready' AS status, UNIX_TIMESTAMP(MIN(created_at)) AS oldest_ts FROM solid_queue_ready_executions GROUP BY queue_name, priority
UNION ALL
SELECT j.queue_name, j.priority, 'claimed' AS status, UNIX_TIMESTAMP(MIN(ce.created_at)) AS oldest_ts FROM solid_queue_claimed_executions ce JOIN solid_queue_jobs j ON ce.job_id = j.id GROUP BY j.queue_name, j.priority
UNION ALL
SELECT j.queue_name, j.priority, 'failed' AS status, UNIX_TIMESTAMP(MIN(fe.created_at)) AS oldest_ts FROM solid_queue_failed_executions fe JOIN solid_queue_jobs j ON fe.job_id = j.id GROUP BY j.queue_name, j.priority
UNION ALL
SELECT j.queue_name, j.priority, 'recurring' AS status, UNIX_TIMESTAMP(MIN(re.run_at)) AS oldest_ts FROM solid_queue_recurring_executions re JOIN solid_queue_jobs j ON re.job_id = j.id GROUP BY j.queue_name, j.priority
上記のクエリの結果を、sql_exporter によって以下のような Prometheus 形式のメトリクスに変換します。
solid_queue_oldest_job_seconds{queue_name="default",priority="10",status="ready"} 1782864000
solid_queue_oldest_job_seconds{queue_name="default",priority="20",status="claimed"} 1782864010
solid_queue_oldest_job_seconds{queue_name="default",priority="30",status="failed"} 1782864060
...(3) スループット
キューのスループット(単位時間あたりの enqueue / dequeue されたジョブ数)を可視化するためのメトリクスも必要です。
我々の運用では完了ジョブを即物理削除しているため、「完了したジョブ数」を直接カウントして dequeue 速度を割り出すことは困難でした。
enqueue 速度についても、同様の理由から算出が難しいことがわかりました。
そこで視点を変え、MySQL の performance_schema に着目しました。
performance_schema の table_io_waits_summary_by_table にはテーブルごとの累積 INSERT / DELETE 回数が統計データとして格納されています。
これを取得することで、キューや優先度別の細分化はできないものの、各ステータス(テーブル)単位での大まかなスループットを把握できるようにしました。
SELECT OBJECT_NAME AS `table`, COUNT_INSERT AS `insert`, COUNT_DELETE AS `delete`
FROM performance_schema.table_io_waits_summary_by_table
WHERE OBJECT_SCHEMA = DATABASE()
上記のクエリの結果を、sql_exporter によって以下のような Prometheus 形式のメトリクスに変換します。
solid_queue_global_io_counts{table="solid_queue_ready_executions",operation="insert"} 1000
solid_queue_global_io_counts{table="solid_queue_ready_executions",operation="delete"} 800
solid_queue_global_io_counts{table="solid_queue_claimed_executions",operation="insert"} 800
...sql_exporter の設定ファイル
設計した 3 つのメトリクスを、以下のように sql_exporter の設定ファイルに落とし込みます。
jobs で接続先データベースを設定し、collectors 内の metrics で先ほど解説した各クエリとラベルのマッピングを定義しています。
global:
scrape_timeout: 10s
scrape_timeout_offset: 500ms
max_connections: 3
max_idle_connections: 3
jobs:
- job_name: solid_queue
collectors: [ solid_queue ]
static_configs:
- targets:
solid_queue_dev: 'mysql://user:${PASSWORD}@host:3306/solid_queue_dev'
collectors:
- collector_name: solid_queue
metrics:
- metric_name: solid_queue_job_count
type: gauge
help: 'Current number of jobs'
key_labels: [queue_name, priority, status]
values: [job_count]
query: |
SELECT queue_name, priority, 'scheduled' AS status, COUNT(*) AS job_count FROM solid_queue_scheduled_executions GROUP BY queue_name, priority
UNION ALL
SELECT queue_name, priority, 'blocked' AS status, COUNT(*) AS job_count FROM solid_queue_blocked_executions GROUP BY queue_name, priority
UNION ALL
SELECT queue_name, priority, 'ready' AS status, COUNT(*) AS job_count FROM solid_queue_ready_executions GROUP BY queue_name, priority
UNION ALL
SELECT j.queue_name, j.priority, 'claimed' AS status, COUNT(*) AS job_count FROM solid_queue_claimed_executions ce JOIN solid_queue_jobs j ON ce.job_id = j.id GROUP BY j.queue_name, j.priority
UNION ALL
SELECT j.queue_name, j.priority, 'failed' AS status, COUNT(*) AS job_count FROM solid_queue_failed_executions fe JOIN solid_queue_jobs j ON fe.job_id = j.id GROUP BY j.queue_name, j.priority
UNION ALL
SELECT j.queue_name, j.priority, 'recurring' AS status, COUNT(*) AS job_count FROM solid_queue_recurring_executions re JOIN solid_queue_jobs j ON re.job_id = j.id GROUP BY j.queue_name, j.priority
- metric_name: solid_queue_oldest_job_seconds
type: gauge
help: 'Unix timestamp of the oldest unfinished job'
key_labels: [queue_name, priority, status]
values: [oldest_ts]
query: |
SELECT queue_name, priority, 'scheduled' AS status, UNIX_TIMESTAMP(MIN(scheduled_at)) AS oldest_ts FROM solid_queue_scheduled_executions GROUP BY queue_name, priority
UNION ALL
SELECT queue_name, priority, 'blocked' AS status, UNIX_TIMESTAMP(MIN(created_at)) AS oldest_ts FROM solid_queue_blocked_executions GROUP BY queue_name, priority
UNION ALL
SELECT queue_name, priority, 'ready' AS status, UNIX_TIMESTAMP(MIN(created_at)) AS oldest_ts FROM solid_queue_ready_executions GROUP BY queue_name, priority
UNION ALL
SELECT j.queue_name, j.priority, 'claimed' AS status, UNIX_TIMESTAMP(MIN(ce.created_at)) AS oldest_ts FROM solid_queue_claimed_executions ce JOIN solid_queue_jobs j ON ce.job_id = j.id GROUP BY j.queue_name, j.priority
UNION ALL
SELECT j.queue_name, j.priority, 'failed' AS status, UNIX_TIMESTAMP(MIN(fe.created_at)) AS oldest_ts FROM solid_queue_failed_executions fe JOIN solid_queue_jobs j ON fe.job_id = j.id GROUP BY j.queue_name, j.priority
UNION ALL
SELECT j.queue_name, j.priority, 'recurring' AS status, UNIX_TIMESTAMP(MIN(re.run_at)) AS oldest_ts FROM solid_queue_recurring_executions re JOIN solid_queue_jobs j ON re.job_id = j.id GROUP BY j.queue_name, j.priority
- metric_name: solid_queue_global_io_counts
type: counter
help: 'Total number of table I/O operations'
key_labels: [table]
value_label: operation
values: [insert, delete]
query: |
SELECT OBJECT_NAME AS `table`, COUNT_INSERT AS `insert`, COUNT_DELETE AS `delete`
FROM performance_schema.table_io_waits_summary_by_table
WHERE OBJECT_SCHEMA = DATABASE()
Step 2: Prometheus と Grafana による収集・可視化
Prometheus 側では、sql_exporter の /metrics エンドポイントを scrape する設定を追加しています。
Grafana では以下のようなダッシュボードでキューの状態を可視化しています。
ジョブ数と滞留時間のグラフ
default という名前のキューについて、ジョブの個数(左)と滞留時間(右)をそれぞれ表示しています。

スループットのグラフ
solid_queue_global_io_counts メトリクスから得られる各テーブルの INSERT / DELETE 累積回数を、PromQL の rate() 関数で秒間の INSERT / DELETE 平均回数に変換し、近似的なスループットとして表示しています。

enqueue / dequeue 速度を知りたいときは、主に以下に着目するのが良いでしょう。
- enqueue 速度: ready テーブルの INSERT 速度
- dequeue 速度(処理開始時点): claimed テーブルの INSERT 速度、または ready テーブルの DELETE 速度
- dequeue 速度(ジョブ完了時点): jobs テーブル(全ジョブが共通で入っているテーブル)の DELETE 速度
なお、claimed テーブルのジョブは、対応する jobs テーブル内のジョブの削除に伴ってカスケード削除されるようになっています。
そのため claimed テーブルの delete 速度は表面上はゼロに見え、dequeue 速度の指標としては利用できません。
Step 3: Prometheus による滞留監視
滞留監視の閾値
移行前のキューシステムでは、ジョブの種類ごとに個別の閾値を決めて滞留監視を行っていました。
個別の閾値管理は細かな調整が効く反面、十数個あるジョブの種類数だけ閾値を管理せねばならず、多少の運用負荷がかかるという課題がありました。
Solid Queue では個別の閾値管理を廃止し、ジョブの優先度に基づく汎用的な閾値運用にしました。
ジョブの優先度は High / Normal / Low の 3 段階で、優先度とステータスに応じて滞留監視の閾値を以下のように設定します。
(表内の数値はあくまで我々の環境・要件に合わせた一例です)
ステータス
High (priority=10)
Normal (priority=20)
Low (priority=30)
blocked / ready / claimed
1 分
15 分
6 時間
scheduled
1 分
1 分
1 分
failed / recurring
10 分
10 分
10 分
アラートルール実装
Prometheus のアラートルールでは、PromQL の time() 関数と solid_queue_oldest_job_seconds メトリクスの差分を用いてジョブの滞留時間(秒)を評価します。
たとえば、ステータス ready, 優先度 High(閾値 1 分)についてのルールはこのように書きます。
rules:
- alert: SolidQueueReadyJobWait_PriorityHigh
expr: |
time() - solid_queue_oldest_job_seconds{status="ready", priority="10"}
> 60
for: 1m
labels:
severity: critical
まとめ
アプリケーション側には変更を加えず、独立した監視構成として sql_exporter から直接データベースを参照する方式で、Solid Queue のスループット可視化および滞留監視を実装しました。
本構成での運用は始まったばかりで、今後実態に合わせて閾値やメトリクスを調整していく余地は大いにありますが、この記事が Solid Queue の監視方式を検討している方にとって参考になれば幸いです。
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み