Skip to content
Published on

[Prometheus] PromQLエンジン内部構造:パーサーから実行エンジンまで

Authors

1. 概要

PromQL(Prometheus Query Language)はPrometheusのコアクエリ言語であり、時系列データの選択と集約のための関数型式言語です。この記事ではPromQLエンジンの内部構造をソースコードレベルで分析します。

PromQLエンジンのコードはpromql/パッケージに位置し、パーサー(parser)、AST(Abstract Syntax Tree)、評価エンジン(engine)で構成されます。

2. クエリ処理パイプライン全体

PromQL文字列
    |
    v
+----------+
| Lexer    |  (トークン化)
+----------+
    |
    v
+----------+
| Parser   |  (構文解析)
+----------+
    |
    v
+----------+
| AST      |  (抽象構文木)
+----------+
    |
    v
+----------+
| Analyzer |  (最適化/検証)
+----------+
    |
    v
+----------+
| Engine   |  (評価/実行)
+----------+
    |
    v
結果(Matrix/Vector/Scalar)

3. レキサー(Lexer)

3.1 トークン化過程

レキサーはPromQL文字列をトークンストリームに変換します:

入力: rate(http_requests_total[5m]) > 10

トークンストリーム:
  IDENTIFIER("rate")
  LEFT_PAREN
  IDENTIFIER("http_requests_total")
  LEFT_BRACKET
  DURATION("5m")
  RIGHT_BRACKET
  RIGHT_PAREN
  GTR
  NUMBER(10)

3.2 トークンタイプ

PromQLレキサーが認識する主要トークンタイプ:

リテラル:
  NUMBER      - 整数/浮動小数点(例: 42, 3.14)
  STRING      - 文字列リテラル(例: "hello")
  DURATION    - 時間範囲(例: 5m, 1h, 30s)

識別子:
  IDENTIFIER  - メトリクス名、ラベル名
  METRIC_IDENTIFIER - メトリクス識別子

演算子:
  ADD, SUB, MUL, DIV, MOD, POW
  EQL, NEQ, LSS, GTR, LTE, GTE
  AND, OR, UNLESS

キーワード:
  BY, WITHOUT, ON, IGNORING
  GROUP_LEFT, GROUP_RIGHT
  OFFSET, BOOL
  SUM, AVG, MIN, MAX, COUNT, ...

3.3 レキサー実装の特性

レキサーはGoの手動ステートマシン方式で実装されています。重要な特性:

  • 1文字先読み: 現在の文字と次の文字を確認してトークン境界を判定
  • durationパース: 数字の後に時間単位(ms, s, m, h, d, w, y)が続くとDURATIONトークンとして認識
  • 文字列エスケープ: シングルクォート、ダブルクォート、バッククォート文字列リテラルをサポート
  • コメント非対応: PromQLにはコメント構文がない

4. パーサー(Parser)

4.1 パーサー構造

PromQLパーサーはPrattパーサー(Top-Down Operator Precedence)方式を使用します:

パーサー動作フロー:
1. トークンストリームから次のトークンを読み取り
2. prefix解析関数呼び出し(リテラル、単項演算、括弧など)
3. 現在の優先度より高いinfix演算子があればinfix解析
4. ASTノードを返す

4.2 演算子優先順位

優先順位(低い順):
1. OR
2. AND, UNLESS
3. ==, !=, <, >, <=, >=(比較)
4. +, -(加減算)
5. *, /, %(乗除算)
6. ^(べき乗、右結合)
7. 単項 +, -

5. AST(Abstract Syntax Tree)

5.1 ノードタイプ

ASTは以下のノードタイプで構成されます:

Expr(式インターフェース)
  |
  +-- NumberLiteral        (数値リテラル)
  +-- StringLiteral        (文字列リテラル)
  +-- VectorSelector       (即時ベクターセレクタ)
  +-- MatrixSelector       (範囲ベクターセレクタ)
  +-- AggregateExpr        (集約式)
  +-- BinaryExpr           (二項演算式)
  +-- UnaryExpr            (単項演算式)
  +-- Call                 (関数呼び出し)
  +-- ParenExpr            (括弧式)
  +-- SubqueryExpr         (サブクエリ式)
  +-- StepInvariantExpr    (step不変式)

5.2 AST例

クエリ: sum(rate(http_requests_total[5m])) by (job)

AggregateExpr (sum, by=[job])
  |
  +-- Call (rate)
        |
        +-- MatrixSelector
              |
              +-- VectorSelector (http_requests_total)
              +-- Range: 5m

6. クエリ評価エンジン

6.1 エンジン構成

Engine:
  +-- queryable      (TSDBストレージインターフェース)
  +-- maxSamples     (最大サンプル数制限)
  +-- timeout        (クエリタイムアウト)
  +-- activeQueries  (アクティブクエリカウンタ)
  +-- maxConcurrency (最大同時クエリ数)

6.2 Instant Query vs Range Query

Instant Query: 単一時点での評価

リクエスト: GET /api/v1/query?query=up&time=1609459200

処理:
1. AST生成
2. time=1609459200で一度評価
3. 結果: Vector(各時系列に対する単一値)

Range Query: 時間範囲でのstep別評価

リクエスト: GET /api/v1/query_range?query=up&start=1609455600&end=1609459200&step=60

処理:
1. AST生成
2. startからendまでstep(60秒)間隔で繰り返し評価
3. 各stepでinstant queryのように評価
4. 結果: Matrix(各時系列に対する時系列値配列)

6.3 Step評価メカニズム

Range Queryの各stepは独立したinstant evaluationです:

query: rate(http_requests_total[5m])
start: T0, end: T0+300s, step: 60s

Step評価:
  T0:      rate(T0-5mからT0までのサンプル)
  T0+60:   rate(T0+60-5mからT0+60までのサンプル)
  T0+120:  rate(T0+120-5mからT0+120までのサンプル)
  T0+180:  rate(T0+180-5mからT0+180までのサンプル)
  T0+240:  rate(T0+240-5mからT0+240までのサンプル)
  T0+300:  rate(T0+300-5mからT0+300までのサンプル)

6.4 Lookback Delta

Lookback Delta(デフォルト5分)はinstant vectorセレクタがサンプルを検索する時間ウィンドウです:

評価時点: T
lookback delta: 5m

サンプル検索範囲: (T - 5m, T]

動作:
1. T以前5分以内の最新サンプルを検索
2. そのサンプルがstale markerでなければ結果に含む
3. 5分以内にサンプルがなければその時系列を除外

重要: 正確にT時点のサンプルがなくてもよい
      lookback delta内の最新サンプルを使用

7. 関数評価

7.1 関数レジストリ

すべてのPromQL関数は中央レジストリに登録されます:

関数登録情報:
  Name:       "rate"
  ArgTypes:   [ValueTypeMatrix]     (引数タイプ)
  ReturnType: ValueTypeVector       (戻り値タイプ)
  Call:       funcRate              (実際の実装関数)
  Variadic:   0                     (可変引数数)

7.2 rate()関数の実装

rate()の内部動作:

rate(counter[5m])の評価:

1. 5分範囲のサンプル抽出: [s1, s2, s3, ..., sN]
2. Counterリセット検知と補正:
   - 値が減少したら前の値を累積に加算
3. 補正された総増加量計算:
   resultValue = (last - first) + counterCorrection
4. 外挿(extrapolation):
   - 範囲開始/終了を超える時間比率計算
5. 秒あたりの増加率:
   result = extrapolatedIncrease / rangeDurationSeconds

7.3 histogram_quantile()の実装

histogram_quantile(phi, buckets)の評価:

1. leラベルでバケットをソート
2. 各バケットの累積カウントを収集
3. phi(例: 0.95) * total_count = target_countを計算
4. target_countを含むバケットを検索
5. バケット境界間で線形補間:
   bucketStart + (bucketEnd - bucketStart) *
   (target - countBelow) / (countInBucket)

7.4 集約関数の評価

sum by (job) (metric)の評価:

1. metricのすべての時系列を収集
2. jobラベル値でグルーピング
3. 各グループ内で値を合算
4. 結果時系列: jobラベルのみ保持

実装:
- ハッシュマップでグループ管理
- ラベルセットのハッシュをキーとして使用
- 衝突時は正確なラベル比較

8. ベクターマッチング(Binary Operations)

8.1 一対一マッチング

metric_a + metric_b

マッチングアルゴリズム:
1. 両方のベクターの時系列をラベルセットでインデックス
2. マッチングラベルセットが同一のペアを検索
3. 演算実行(ここでは値の加算)
4. 結果時系列に左側のラベルセットを使用

パフォーマンス:
- ハッシュジョイン方式でO(n+m)時間計算量
- 小さい方をハッシュテーブルとして構築

8.2 多対一/一対多マッチング

metric_a * on(job) group_left metric_b

処理:
1. on(job)でマッチングキーを決定
2. metric_b(one側)をjob別ハッシュマップにインデックス
3. metric_a(many側)の各時系列に対して:
   a. jobラベルでmetric_bのマッチング項目を検索
   b. 演算実行
   c. metric_aのすべてのラベルを保持(group_left)

9. サブクエリ

9.1 サブクエリ処理

max_over_time(rate(http_requests_total[5m])[30m:1m])

評価過程:
1. 外部関数(max_over_time)がrange vectorを必要
2. サブクエリ部分: rate(http_requests_total[5m])[30m:1m]
3. 内部評価:
   - 現在時点(T)の30分前からTまで
   - 1分間隔でrate(http_requests_total[5m])を評価
   - 30個のinstant vector結果を生成
4. range vectorに変換
5. max_over_timeが各時系列の最大値を計算

9.2 サブクエリ最適化

サブクエリ評価の主要最適化:
1. 内部クエリのrange vectorを再利用(同一時系列)
2. stepアラインメント:サブクエリのstepを外部stepに整列
3. resolutionが指定されなければglobal evaluation_intervalを使用

10. メモリ管理

10.1 サンプルカウンティング

--query.max-samples(デフォルト50,000,000)

カウント方式:
- 各stepでロード/生成されたサンプル数を累積
- range vectorの場合は範囲内のすべてのサンプルをカウント
- 制限超過時にクエリを即座に中断

10.2 クエリ同時実行制御

--query.max-concurrency(デフォルト20)

実装:
- セマフォで同時クエリ数を制限
- 新クエリがセマフォ取得を待機
- タイムアウト内に取得失敗時にエラーを返す

10.3 ポイントプール

PromQLエンジンはメモリ割り当てを最小化するためにポイントプールを使用します:

ポイントプール動作:
1. クエリ開始時にポイントスライスをプールから割り当て
2. step評価間で再利用
3. クエリ完了時にプールに返却
4. sync.Poolを使用してGC負担を軽減

11. Native Histogram処理

Native Histogramは既存のfloatサンプルと異なる処理パス:
1. ヒストグラムスキーマ(resolution)の整列
2. バケット単位でのマージ
3. 集約時にスキーマ変換が必要な場合は自動実行

sum(native_histogram_metric)の評価:
1. 各時系列のヒストグラムを収集
2. スキーマが異なる場合はより低い解像度に統一
3. バケット別カウントを合算
4. sumとcountフィールドを合算

12. パフォーマンス最適化技法

12.1 Series Set マージ

複数ブロックにまたがる同一時系列が存在する場合:
1. 各ブロックで時系列集合を検索
2. マージソートで統合
3. チャンクレベルで時間範囲フィルタリング
4. 重複排除

12.2 StepInvariantExpr最適化

stepに関係なく同一結果を返す式を検知:
- 数値リテラル
- offsetのないスカラー関数
- 特定の集約結果

検知された場合:
- 一度だけ評価してすべてのstepに結果をコピー
- 不要な繰り返し評価を排除

12.3 クエリ前処理

最適化パス:
1. 定数畳み込み(constant folding): 2 + 3 -> 5
2. ラベルマッチング最適化:インデックスヒント生成
3. 不要なラベル演算の除去
4. 空の結果の早期リターン

13. エラー処理と制限事項

13.1 一般的なエラー状況

1. "query processing would load too many samples"
   - max-samples制限超過
   - 解決:クエリ範囲の縮小または制限の増加

2. "query timed out in expression evaluation"
   - query.timeout超過
   - 解決:クエリ最適化またはタイムアウトの増加

3. "found duplicate series for the match group"
   - ベクターマッチングで多対一関係にgroup_left/right未指定
   - 解決:group_leftまたはgroup_rightの追加

4. "many-to-many matching not allowed"
   - 両側とも多数のマッチングはサポートされない
   - 解決:集約で片側を一(one)に縮小

14. まとめ

PromQLエンジンは時系列データに特化した関数型クエリ言語を効率的に処理するシステムです。Prattパーサーによるクリーンな構文解析、stepベースのrange query評価、lookback deltaによる柔軟なサンプル検索、ポイントプールとStepInvariantExpr最適化によるメモリ効率がコアです。

次の記事ではPrometheusのサービスディスカバリメカニズムを分析します。kubernetes_sdのwatchメカニズム、リラベリングの動作原理、ターゲットライフサイクルを解説する予定です。