なぜv3が必要だったのか

前章でLangfuseのデータモデルを掘り下げました。本章は「そのデータがどう格納され、どう読まれるか」 — すなわちv3アーキテクチャの解剖です。 LangfuseはOSSローンチ(2023年6月)から2024年末まではPostgres単一構成で動いていました。 しかし月あたり億単位のイベントを捌くユーザーが出始めるにつれ、PostgresのINSERTがボトルネックとなり、UIのダッシュボード集計クエリが数十秒かかる事態が頻発しました。 v3(2024年12月GA) はこれを根本から解決する再設計で、OLTPとOLAPを明示的に分離し、非同期ingestionパイプラインを導入したものです。

v2の限界 — なぜPostgres単独では無理だったか

課題 v2での症状 根本原因
書き込み競合 SDKからの大量INSERTでWAL/index更新がボトルネック化 OLTPは行指向 + トランザクション保証で、append-heavyワークロードに不利
集計の遅さ ダッシュボードで「1週間のtoken集計」に30秒超 行指向で全行スキャン。圧縮もWALも効きにくい
同期ingestion SDK Flushが重くなるとアプリ側レイテンシに影響 API層で直接INSERTしていたため失敗時のリトライも困難
ストレージ肥大 月100M event規模でDBサイズが数TBに到達 JSONB中心で圧縮効率が悪く、履歴パージも複雑
運用の難しさ Pgのレプリカ構成とVACUUMチューニングに専門知識が必要 Postgres単独で分析ワークロードまで捌こうとした無理

v3 全体像 — 2プロセス × 4ストレージ

graph TB
  SDK[SDK / OTel Exporter] -->|HTTP ingestion| WEB[Web Container<br/>Next.js API + UI]
  USER[User Browser] -->|UI| WEB
  WEB -->|生イベントを保存| S3[(S3 / MinIO<br/>Event Payload)]
  WEB -->|キューに投入| REDIS[(Redis<br/>Streams + Cache)]
  WEB <-->|メタデータ<br/>認証・組織・API Key| PG[(Postgres<br/>OLTP)]
  REDIS -->|取り出し| WORKER[Worker Container<br/>Ingestion + Async Jobs]
  WORKER -->|S3から本文読込| S3
  WORKER -->|バッチINSERT| CH[(ClickHouse<br/>OLAP)]
  WORKER -->|Eval実行結果| PG
  WEB -->|集計読み取り| CH
  WEB -->|設定読み取り| PG
Langfuse v3 の全体像。SDKからのイベントはまずWebが受け、S3に生永続化 + Redisキュー投入のみで即returnする。Workerがバッチでキューから引き出し、ClickHouseに書き込む。UIはClickHouse + Postgresの両方を読む

図の要点は3つです。

  1. Web と Worker は完全に別プロセス: どちらも同じコードベースだが環境変数 LANGFUSE_BOOT_STRATEGY で役割を切り替える。水平スケールはそれぞれ独立
  2. ストレージは役割ごとに使い分け: 構造化メタデータはPostgres、時系列/集計はClickHouse、キューイング/キャッシュはRedis、生Payload保管はS3
  3. ingestionは非同期で冪等: S3保存 + Redisキュー投入でWebのレスポンスを早期returnし、Workerが後からClickHouseへ書き込む

ストレージ4層の役割分担

レイヤ 用途 選定理由 代替
Postgres 組織・プロジェクト・API Key・Prompt定義・設定・Evaluator config・認証 リレーショナル整合性が必要。更新頻度は低いが一貫性重要 MySQL等は非推奨(migrationスクリプトがPg前提)
ClickHouse Trace / Observation / Score の本体データ、集計クエリ カラム指向 + 圧縮 + ベクトル化集計で TB規模を秒未満でスキャン v3では選択不可(固定依存)
Redis Ingestion Queue(Streams)、Rate limit、Prompt Cache、Session Queue/Cacheとして最も軽く枯れている KeyDB / DragonflyDB等もコミュニティ動作例あり
S3互換ストレージ 生イベントPayload(JSON)の永続化、メディアファイル、Export CSV 安価で耐久性が高く、Workerが失敗した際の再処理ソースとして使える MinIO / R2 / GCS / Azure Blobも公式サポート

3段ingestionパイプラインの中身

sequenceDiagram
  participant SDK as SDK<br/>(app側)
  participant API as Web API<br/>(/api/public/ingestion)
  participant S3 as S3
  participant Redis as Redis Streams
  participant Worker as Worker
  participant CH as ClickHouse
  SDK->>API: POST batch (最大1000events)
  API->>API: 認証 + 入力検証
  API->>S3: 各eventを個別オブジェクトとして書き込み
  API->>Redis: XADD 'ingestion' stream<br/>(S3キーだけを入れる)
  API-->>SDK: 207 Multi-Status(個別successを返す)
  loop Worker Poll
    Worker->>Redis: XREADGROUP<br/>(コンシューマグループで分散)
    Redis-->>Worker: S3キーのバッチ
    Worker->>S3: GET 生イベント
    Worker->>Worker: マージ/変換<br/>(同じtrace_idを畳み込む)
    Worker->>CH: INSERT INTO traces/observations/scores
    Worker->>Redis: XACK
  end
v3 ingestion pipeline。SDK→Webは「S3に書く + Redisに積む」だけで即レスポンス。Workerが並列にバッチでClickHouseへ書き込む。同じtrace_idの複数eventはWorker側でマージされ、ReplacingMergeTreeで最終状態が残る

ClickHouseは ReplacingMergeTree

Langfuseは Trace / Observation / Score テーブルで ReplacingMergeTree エンジンを使っています。 これは「同じプライマリキーの行が複数入っても、バックグラウンドマージで最新バージョンだけが残る」エンジンです。 Update相当の処理(Trace本文の追記、Scoreの再評価)は、新しい行をINSERTするだけ で表現されます。

クエリ時は FINAL 修飾子で重複を除去できますが、Langfuseは集計系では argMax(value, event_ts) などで明示的に最新行を取る実装が中心です。 これによりUpdateを避けつつ"結果整合性"を保ちます。

Web と Worker の役割分担

プロセス 主な責務 負荷特性 スケール軸
Web UI配信、REST API、Ingestion受付、認証、Webhook、Prompt取得API リクエスト数 × レイテンシ敏感 HPAでPod数を増やす(CPU/メモリ)
Worker Ingestion消費、Async Eval実行、Prompt Experiment、Batch export, Webhook配信 スループット重視 Worker Pod数 × Redis Consumer Group

ClickHouseスキーマの要点

ClickHouse側の主要テーブルは3つです。すべて project_id, start_time でパーティション/ソートされ、時系列スキャンが高速化されています。

テーブル 粒度 主要カラム MergeKey
traces 1 Trace = 1行(複数更新はマージ) id, project_id, name, user_id, session_id, tags, metadata, input, output, release (project_id, id)
observations 1 Observation = 1行 id, trace_id, parent_id, type (SPAN/GENERATION/EVENT), model, usage_details, cost_details, input, output (project_id, trace_id, id)
scores 1 Score = 1行 id, trace_id, observation_id?, name, value, string_value, source (API/ANNOTATION/EVAL), comment (project_id, trace_id, id)

Materialized Viewsで集計を高速化

v3はさらに Materialized View を活用し、「日次・プロジェクト別・モデル別のtoken/cost集計」などを事前計算しています。 ダッシュボードの「過去30日のコスト推移」クエリが数百msで返るのは、この事前集計のおかげです。 v3.50以降はProject/User/Sessionサマリ用のMVも追加され、UI応答性がさらに改善しています。

スケーリングチューニングの実務

ボトルネック兆候 確認方法 対処
Ingestion Lagが増える Redis Streams の XLEN, consumer group PENDING Worker Pod数を増やす / バッチサイズを上げる(LANGFUSE_INGESTION_BATCH_SIZE
ダッシュボードが遅い ClickHouse system.query_log で該当クエリ特定 ClickHouse nodeを増やす / Materialized Viewをrefresh / UI側の期間を短く
Prompt API がスパイクで詰まる Webの99レイテンシ、Redis HITRATE Prompt Cache TTLを伸ばす / Web Pod増 / SDKにローカルキャッシュ
Postgres CPU が張り付く pg_stat_statements 実行計画確認 / 接続プール(PgBouncer)導入 / 不要migration確認
S3コストが膨らむ オブジェクト数とRequest数 保存期間を短縮 / ライフサイクルルールでGlacier移行 / event sampling

2026年3月の"Simplify for Scale"

v3は強力ですが「入門で4種類のミドルウェアを建てるのは重い」という声がSelf-hostedコミュニティから挙がり、 2026年3月のリリースで Simplify for Scale が導入されました。 これは以下の2方向の改善を同時に行ったものです。

  • Single-container mode: Docker Compose one-linerで Web+Worker を1コンテナに同梱。Postgres/ClickHouse/Redis/MinIO込みで起動できる「all-in-one」プロファイル
  • Operator-level scaleout: Kubernetes Operatorが公式で提供され、HPAポリシー・ClickHouse shard追加・Queue lag監視・自動フェイルオーバーを宣言的に扱える

Langfuse自身のオブザーバビリティ

運用では「Langfuseが自分自身をどう観測するか」も重要です。v3は以下のシグナルを標準で公開しています。

シグナル 取得場所 主なメトリクス
Prometheus metrics /metrics エンドポイント (Web/Worker両方) HTTPレイテンシ、Ingestion成功率、Queue lag、ClickHouse書き込みスループット
OTel traces Langfuseコード自体がOTel自動計装対応 自分自身をLangfuseに送る"self-tracing"も可能
構造化ログ stdout (JSON) request id / project id / trace id / error code

まとめ

次章では「このアーキテクチャにアプリからどう繋ぐか」 — SDKとOpenTelemetryの統合を掘り下げます。 Langfuseが「自前SDKを10言語書く」のではなく「OTelに寄せる」戦略を選んだ理由が、v3の設計と表裏一体であることが見えてきます。

理解度チェック

問題 0 / 50%
Q1

Langfuse v3 で ingestion パイプラインの冪等性を構造的に支えている最重要コンポーネントはどれか?

キーボード: 1〜4 で選択、Enter で回答