なぜ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
図の要点は3つです。
- Web と Worker は完全に別プロセス: どちらも同じコードベースだが環境変数
LANGFUSE_BOOT_STRATEGYで役割を切り替える。水平スケールはそれぞれ独立 - ストレージは役割ごとに使い分け: 構造化メタデータはPostgres、時系列/集計はClickHouse、キューイング/キャッシュはRedis、生Payload保管はS3
- 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
endClickHouseは 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の設計と表裏一体であることが見えてきます。
理解度チェック
Langfuse v3 で ingestion パイプラインの冪等性を構造的に支えている最重要コンポーネントはどれか?
キーボード: 1〜4 で選択、Enter で回答