Skip to content
Published on

LangfuseがトレースをClickHouseに置く理由 — 保存層の役割分担

シェア
Authors

はじめに — データストアは一つではない

Langfuseを初めてセルフホストすると、たいてい同じところで手が止まります。データベースが一つではないからです。Postgresも、ClickHouseも、Redisも、S3互換のオブジェクトストレージまで必要です。アプリケーション一つにデータストア四つは過剰に見えます。

ところが第一回のデータモデルを思い出すと理由が見えます。組織とプロジェクトの設定は件数が少なく整合性が重要です。トレースは一日に数百万件が入り、一か月分をなめて集計する必要があります。プロンプトの原文は一件が数メガバイトになり得ます。この三つを同じエンジンに押し込めば、どれもうまくいきません。

構成と設定名は2026-08-15に公式ドキュメントで確認しました。Langfuseはバージョンによってアーキテクチャが変わるため、利用中のバージョンのドキュメントを再確認してください。本記事はセルフホストv4のドキュメントを基準にしており、ClickHouseがトレースの保存先になったのはv3からです。v2当時の構造を説明する記事を読んでいるなら、ここから先が異なります。

四つのデータストアと二つのコンテナ

セルフホスト概要のドキュメントが挙げる構成要素は六つです。アプリケーションコンテナが二つ、データストアが四つです。

構成要素ドキュメントが示す役割
Langfuse WebLangfuse の UI と API を提供する主要なウェブアプリケーション
Langfuse Workerイベントを非同期で処理するワーカー
PostgreSQLトランザクション処理のための主データベース
ClickHousetrace、observation、score を保存する高性能な OLAP データベース
Redis/Valkeyキューとキャッシュに使うインメモリのデータ構造ストア
S3/オブジェクトストレージ受信イベント、マルチモーダル入力、大きなエクスポートを保持する

これにプレイグラウンドと評価用のLLM APIゲートウェイが任意項目として付きます。コンテナのドキュメントによれば、ウェブはlangfuse/langfuse:4イメージで3000番ポートを、ワーカーはlangfuse/langfuse-worker:4イメージで3030番ポートを使います。

各ドキュメントが与えた役割をつなぐと、次の図になります。

                      ┌───────────────────────────────┐
   SDK / OTLP  ─────▶ │  langfuse-web        :3000    │
                      │  コンソールと API               │
                      └──────┬──────────────────┬─────┘
                             │                  │
              原本イベント保存 │                  │ キューへ投入
                             ▼                  ▼
                  ┌────────────────────┐   ┌──────────────┐
                  │ S3 / オブジェクト    │   │ Redis/Valkey │
                  │  原本イベント        │   │  キューと          │
                  │  マルチモーダル入力  │   │  キャッシュ         │
                  │  バッチエクスポート  │   └──────┬───────┘
                  └────────────────────┘          ▼
                                        ┌───────────────────────┐
                                        │ langfuse-worker :3030 │
                                        │ 非同期処理と挿入        │
                                        └──────┬────────┬───────┘
                                               │        │
                                               ▼        ▼
                              ┌────────────────────┐  ┌──────────────────┐
                              │ ClickHouse         │  │ PostgreSQL       │
                              │  traces            │  │  組織とプロジェクト  │
                              │  observations      │  │  データセット       │
                              │  scores            │  │  暗号化された API キー │
                              └────────────────────┘  └──────────────────┘

取り込み経路 — イベントが通る順序

この構造を理解する鍵はキャッシュのドキュメントの一文です。RedisはAPIで新しいイベントを素早く受け取り、その処理と挿入を後回しにするために使われ、それによってリクエストの急増を穏やかにさばけると書かれています。

つまりSDKが送ったイベントは、到着した瞬間にClickHouseへ入るわけではありません。ウェブが受け取ってキューに入れ、ワーカーが取り出して処理してから挿入します。ここから二つの結果が出ます。

  • 画面にトレースが出るまで少し遅れるのは正常です。問題は遅れが増え続ける場合で、そのとき見るのはワーカーの処理量です。
  • ワーカーが落ちていてもAPIは200を返し続けます。データはキューに積み上がります。ワーカーのコンテナを監視していなければ静かに滞留します。

オブジェクトストレージのドキュメントは、原本イベントを保持する理由を再試行ジョブ、リプレイと災害復旧、そして任意のレコード組み立てと述べています。処理が失敗しても原本が残るのでやり直せるという意味であり、その代わりバケット容量を消費し続けます。この点は第五回で改めて見ます。

Redis側には必ず守るべき設定が一つあります。ドキュメントは、すべてのRedisまたはValkeyインスタンスでmaxmemory-policynoevictionにしないとキューのジョブが追い出されると明示しています。既定値のままだと、メモリが逼迫したときにキュー上のイベントが静かに消えます。

なぜClickHouseなのか — カラム指向とトレース問い合わせ

ClickHouseのドキュメントは、ClickHouseをLangfuse内でtrace、observation、scoreの各エンティティを担う主要なOLAPストレージと説明し、高い書き込みスループットと高速な分析問い合わせに最適化されていると述べています。

なぜその組み合わせが成立するのかは、ClickHouse側の説明を見ると明確です。性能に関するドキュメントが挙げる根拠は四つです。

  • カラム指向のストレージです。同じ型と分布を持つ値が一緒に置かれるため、圧縮に特によく合います。
  • 主キーインデックスがテーブルデータのソート順を定義します。よく選ばれた主キーは、フィルタを全カラム走査ではなく高速な二分探索で評価させます。
  • ベクトル化実行です。問い合わせ計画の演算子が中間結果を一行ずつではなくまとめて渡すため、CPUキャッシュの利用が良くなりSIMD命令を使えます。
  • MergeTreeのバックグラウンドマージです。追加のデータ変換を問い合わせ時ではなくマージ処理で行い、利用者の問い合わせを大きく速くします。

この四つを第一回の保存形式の話と重ねると図が完成します。Langfuseは概念的に一つのobservationsテーブルを置き、各行にobservationのデータとともにtraceレベル属性の複製を持ちます。

関係モデルの感覚では無駄に見えます。user_idが同じtraceの二十件のobservationに二十回複製されるからです。ところがカラム指向では話が変わります。同じ値が繰り返されるカラムは極端によく圧縮され、user_idで絞り込む問い合わせはそのカラムだけを読めば済みます。結合がないのでシャッフルもありません。非正規化のコストはこのエンジンではほぼ消え、代わりに問い合わせ経路が単純になります。

ClickHouseが持つもの — traces、observations、scores

ドキュメントが挙げる主要なテーブルはtracesobservationsscoresの三つです。加えて、オブジェクトストレージに上がったファイルを追跡するblob_storage_file_logテーブルがあるとオブジェクトストレージのドキュメントが述べています。

アクセスパターンについてもドキュメントは明示的です。トレーシングデータは月単位でパーティショニングされ、アクセスパターンはプロジェクトと時間のフィルタを中心とします。ですからプロジェクトと期間を絞った問い合わせはパーティション枝刈りの恩恵をそのまま受け、期間を開いたまま全体をなめる問い合わせはすべてのパーティションに触れます。ダッシュボードの既定期間を広く取っておくと、この費用を毎回払うことになります。

ユーザー権限もドキュメントに正確に載っています。

-- Langfuse が ClickHouse ユーザーに要求する権限
GRANT INSERT, SELECT, ALTER UPDATE, ALTER DELETE,
      ALTER DROP INDEX, CREATE, DROP TABLE
  ON langfuse.*
  TO langfuse;

ALTER UPDATEALTER DELETEが入っている点に注目してください。読み取り専用の分析ストアではなく、保持ポリシーに沿った削除と更新が起きるストアだという意味であり、第五回の保持期間の話に直結します。

クラスタ構成には制約があります。ドキュメントは、Langfuseが現時点で複数シャードのクラスタに対応しておらずシャード数は1でなければならないと明示し、本番では最低3レプリカを推奨しています。拡張の方向がシャーディングではなく複製と垂直拡張だという意味です。読み取り負荷を分けたい場合はCLICKHOUSE_READ_ONLY_URLがあります。ドキュメントはこれをUIと公開APIの問い合わせに使う読み取り専用エンドポイントと説明しています。

PostgresとRedisが担う分

Postgresのドキュメントが挙げる保存対象は、ユーザー、組織、プロジェクト、データセット、暗号化されたAPIキー、設定です。件数が少なく整合性が重要でトランザクションが必要なものであり、トレースはここにありません。

Redisは前述のキューの役割に加えてキャッシュを担います。ドキュメントが明示するキャッシュ対象はAPIキーとプロンプトで、それぞれ既定300秒の寿命を持ちます。APIキーは平文で保存されず、ハッシュまたは暗号化された形だけがキャッシュされます。

ここでセルフホストをよく躓かせる条件が一つ出てきます。セルフホスト概要のドキュメントは、すべてのインフラ構成要素がUTCのタイムゾーンで動作する必要があり、UTC以外の設定は問い合わせが誤った結果や空の結果を返す原因になると明示しています。PostgresのドキュメントとClickHouseのドキュメントの双方に同じ警告があります。

大きなペイロードはどこへ行くのか

LLMトレーシングが一般的な分散トレーシングと決定的に異なるのがここです。スパン一つに付くデータが大きいのです。長い文脈を入れたプロンプト、検索された二十件の文書の原文、画像入力がすべて一つのobservationにぶら下がります。

オブジェクトストレージのドキュメントは、このストアの用途を原本イベント、マルチモーダルコンテンツ、バッチエクスポートの三つに整理しています。マルチモーダル側にはサイズの上限が設定としてあります。LANGFUSE_S3_MEDIA_MAX_CONTENT_LENGTHの既定値は1,000,000,000バイト、つまり1GBです。ダウンロードURLの有効期間はLANGFUSE_S3_MEDIA_DOWNLOAD_URL_EXPIRY_SECONDSが既定3600秒で決めます。

公式にサポートされるのはAmazon S3、Google Cloud Storage、Azure Blob Storage、MinIO/AIStor、Cloudflare R2で、OCI Object StorageやTigrisをはじめとするS3互換サービスはコミュニティサポートです。必要な最小権限はバケットとオブジェクトの双方に対するs3:PutObjects3:ListBuckets3:GetObjectであり、保持ポリシーまで使うならs3:DeleteObjectが追加で必要です。

スキーマは安定した契約ではない

ClickHouseにデータがあると自然に浮かぶ考えがあります。そのまま直接問い合わせればよいのでは、というものです。ドキュメントはここに明確な警告を付けています。ClickHouseのスキーマは安定したAPI契約ではありません。メジャーアップグレード、バックグラウンドマイグレーション、性能改善の作業がテーブルとカラム、重複排除の挙動、結合の方式を変え得るため、独自の問い合わせを作ったならLangfuseを上げるたびに検証し直す必要があります。

そこで判断はこう分かれます。アプリケーションが依存する指標は公開APIから取得し、ClickHouseへの直接問い合わせは一度きりの調査や社内分析のように壊れても人が直せる場所にだけ使います。第六回で扱うメトリクスAPIが前者の位置づけです。

実際のカラム定義が必要なら、推測せずマイグレーションファイルを読むほうが正確です。ClickHouseのドキュメントは手動マイグレーションの手順を説明する中でリポジトリの./packages/shared/clickhouse/migrations/パスを指しています。以下はスキーマを確認する方法を示す例です。

-- 例: 実際のカラム定義を自分の目で確認する
SHOW TABLES FROM langfuse;
SHOW CREATE TABLE langfuse.observations;

-- パーティションが実際にどう切られているかを見る
SELECT partition, sum(rows) AS rows, formatReadableSize(sum(bytes_on_disk)) AS size
FROM system.parts
WHERE database = 'langfuse' AND table = 'observations' AND active
GROUP BY partition
ORDER BY partition DESC;

二つ目の問い合わせはスキーマを推測しなくても安全です。system.partsはLangfuseではなくClickHouseが提供するシステムテーブルだからです。パーティションごとの行数とディスク使用量が出るので、どの月が費用を食っているかがすぐ見えます。

バージョンが分かれる地点

本シリーズでバージョン表記がもっとも重要な節です。保存層の要件がメジャーバージョンによって異なります。

構成要素Langfuse v3Langfuse v4
ClickHouse24.3 以上25.12 以上、26.4 推奨
PostgreSQL12 以上最低 15、16 推奨
Redis7 以上7.2 推奨
Valkey8 以上を公式サポート

ClickHouseの要求バージョンが上がった理由もドキュメントにあります。v4が使う軽量アップデート、JSON型、全文検索の機能のためです。これらの値は変わり得るので、配備の前に利用中のバージョンのドキュメントで再確認してください。

バージョンポリシーのドキュメントによれば、v2はサポート終了、v3は非推奨、v4は正式提供です。v3からv4への移行では従来のバッチ取り込みがOpenTelemetryに置き換わり、旧来の読み取りAPIが削除され、Observations API v2とメトリクスAPI v2が導入されました。ネットで見つけたLangfuseのアーキテクチャ記事は、どのバージョンの話かをまず確認する必要があります。

おわりに — 保存層を知ると診断が速くなる

役割分担を整理すると、トレース本体はClickHouse、組織と設定はPostgres、キューとキャッシュはRedis、原本イベントと大きなペイロードはオブジェクトストレージです。この地図があると障害時にどこを見るかが変わります。

  • 画面にトレースが遅れて出る → ワーカーとRedisのキュー
  • ログインはできるがトレースが空 → ClickHouseの接続かマイグレーション
  • トレースは見えるが添付が開かない → オブジェクトストレージの権限か署名付きURLの失効
  • 問い合わせが空の結果を返す → タイムゾーン設定

今できる点検はパーティションごとの大きさを出してみることです。どの月がどれだけ占めるかを知らないままでは、保持ポリシーを決める根拠がありません。

試してみる

観測データをClickHouseに入れる設計そのものに関心があるなら、観測データをClickHouseに入れるということがソートキーとTTLの設計をさらに詳しく扱っています。

シリーズ

参考資料