このBlog記事はガオ株式会社による Langfuse GmbH "From Zero to Scale: Langfuse's Infrastructure Evolution" の日本語訳 前半となります。原文はこちらをご確認ください。
ゼロからスケールへ:Langfuseの
インフラストラクチャの進化
Langfuseのインフラストラクチャをシンプルなプロトタイプからスケーラブルな
Observability プラットフォームへと進化させた過程を詳しくご紹介します。
Steffen Schmitz Max Deichmann
オープンソースのLLM Observerbility プラットフォームであるLangfuseは、Y Combinator 2023年Winter バッチから誕生しました。 私たちは、多くのLLMアプリケーションを自分たちで構築し、デモから本番環境への移行が難しいことを実感した後、バッチメイトの数名と緊密に協力し、LLM可視化プラットフォームのv0を迅速に開発しました。
当初は、いくつかのコア機能に的を絞りました。SDKは非同期、Langfuseはトレースをベースとし、すべてのコンポーネントはオープンソースで簡単にセルフホスティングできるものでした。最初のバージョンは、NextJs、Vercel、Postgresで書かれていました。私たちは、この実験が1分あたり数万件のイベントを処理するまでに急速に進化するとは、夢にも思っていませんでした。
Langfuseがすべてのユーザーに対してスケーリングできることを確実にするという点において、私たちの最近のV3リリースは重要なマイルストーンとなりました。私たちはすでにLangfuse Cloudでこれらの変更の多くを試験的に導入しており、v3リリースではオンライン評価、非同期/キューイング取り込み、キャッシュされたプロンプトなど、セルフホスティングユーザーにもそれらを利用できるようにしました。
本記事では、Langfuseの開発中に直面したスケーリングの課題と、私たちの「仮説 - 実験 - フィードバック」のループがLangfuse v3の開発にどのように役立ったかについてご説明します。もし、私たちと一緒に同様の課題の解決に取り組みたいとお考えであれば、ベルリンで人材を募集しています!
Where it all started
当初の私たちのアーキテクチャは、単一のコンテナとPostgresデータベースであり、運用とセルフホスティングは非常にシンプルでしたが、スケーリングが非常に困難な構成でした。
私たちは、アーキテクチャを再考せざるを得ないような、いくつかの重要な課題に直面しました。最も重要な課題は次の通りです。
課題 1: 耐障害性が高く、高スループットを取り込めるパイプラインの構築
目標:
Ingestion API は、予測不可能な負荷パターン下でも、大量のイベントを受け入れ、一貫して低レイテンシを維持する
Langfuse の可視化プラットフォームの中核は、SDK および API による効率的なイベントデータ収集に依存しています。 これらの SDK は、ユーザーのアプリケーションへのパフォーマンスへの影響を最小限に抑えるように設計されていますが、取り込みサーバーコンポーネントは、規模を拡大するにつれて、重大な課題に直面しました。
当初の課題:2023年夏には、急激なトラフィックパターンにより、取り込みAPIのレスポンスタイムが最大50秒まで急上昇しました。
重要な要件:Ingestion API は、SDKからのイベントの円滑なフラッシングを確保するために、常に低レイテンシを維持する必要があります。そうでないと、ユーザーのアプリケーションに悪影響が及ぶ可能性があります。
課題は、大量のデータを処理することだけではありませんでした。予測不可能な負荷パターン下で信頼性を維持しながら、ユーザーのアプリケーションパフォーマンスへの影響を最小限に抑えることでした。この技術的なハードルは、トラフィックの急増をより適切に処理するための取り込みアーキテクチャを再考することを迫る、当社にとって最初の大きなスケーリングの課題となりました。
課題 2:実稼働時のワークロードに合わせたプロンプトの最適化
目標:
Prompt API は常に高い可用性とパフォーマンスを維持する
Langfuse の重要な機能のひとつがプロンプト管理システムであり、ユーザーは UI を通じてプロンプトを定義し、SDK を通じて取得することができます。これにより、プロンプトを変更するためにアプリケーションを再デプロイする必要がなくなります。
Trace は非同期かつノンブロッキングですが、プロンプトはLLMアプリケーションのクリティカルパスとなります。このため、一見単純な機能が複雑なパフォーマンス上の課題となりました。取り込みが集中する時間帯には、プロンプト取得の p95 レイテンシが7秒にまで上昇しました。この状況には、他の操作によるシステム負荷が重い場合でも、一貫した低レイテンシのパフォーマンスを維持できるアーキテクチャ上のソリューションが必要でした。
課題 3:高速な分析読み取り(ダッシュボード、テーブルフィルター)
目標:
大規模な観測データにも対応するダッシュボードとテーブルフィルター
当初のデータベースとしてPostgresを選択したことは、初期の段階ではうまくいきましたが、当社の最大顧客がシステムを通じてより多くのObserverbility データを送信するようになると、重大なパフォーマンスのボトルネックにぶつかりました。クエリを最適化しても、当社のダッシュボードとテーブルフィルターの操作は、企業ユーザーにとっては遅すぎました。LLMの分析データは多くの場合、大きなblobで構成されており、何百万行ものデータをスキャンする際には、行指向のストレージがディスク上で重荷となっていました。皮肉なことに、当社の分析機能を最も必要としているお客様が、最もパフォーマンスの低下を経験していました。この成長に伴う問題は、当初のアーキテクチャが迅速な開発には最適であったものの、企業規模の分析作業負荷に対応するには根本的な再考が必要であることを示していました。
課題4:簡単なセルフホスティング
目標:
簡単にセルフホスティングできるだけでなく、運用上の労力をほとんど必要とせずに拡張できること
Langfuseをオープンソースプロジェクトとして構築することは、意図的な選択でした。私たちのビジョンはシンプルでした。誰もが簡単なdocker-compose upでLangfuseを利用し始めるべきであると同時に、Langfuseは同時に、毎分何百ものユーザーと何千ものLLMのやり取りがあるエンタープライズ規模の展開にも対応できなければなりません。このアプローチは、私たち自身が開発者として好むものを反映しています。私たちは、評価や展開が容易なソリューションを重視しています。
しかし、実稼働環境に耐えうるオープンソースのObserverbility プラットフォームを構築するには、特有の課題があります。
汎用性:当社のインフラは、開発者のLaptopからさまざまなクラウド事業者へのデプロイまで、多様な環境でシームレスに動作する必要があります。
オープンソースへの依存:当社はオープンソースコンポーネントのみを使用することで、無制限のセルフホスティング機能を確保することを約束しています。
ゼロタッチ操作:企業ユーザーは、メンテナンスとアップグレードの自動化を必要としています。手動操作ではエラーが発生しやすく、拡張性にも欠けます。
このシンプルさとエンタープライズ対応のバランスが、当社のアーキテクチャ上の決定を形作り、アクセスしやすく拡張性のあるソリューションの作成を後押ししました。
新しい構成要素
これらの課題に対処するために、私たちはスタックに複数のビルディングブロックを追加しました。本記事では、私たちがどのようにスタックを繰り返し改良していったかをご紹介します。
ビルディングブロック1:取り込みデータの非同期処理
同期処理から非同期処理へ
私たちは当初、APIコールごとに多数のイベントを受信し、それらを繰り返し処理し、各イベントを個別に処理するIngestionパイプラインから始めました。処理中、まず同じIDを持つ履歴行を検索し、LLMコールのプロンプトと補完をトークン化し、コストを計算し、データベース内のイベントをupsertします。しかし私たちのテレメトリを調査したところ、2つの大きなボトルネックがあることが分かりました。PostgresのIOPSの枯渇と、長い文字列をトークン化する際のCPU消費です。これらはどちらも、当社のアプリケーションの稼働時間とレイテンシに影響を及ぼすリスクです。最悪の場合、当社の取り込みAPIでEvent が失われ、HTTP 500エラーが返されることになりました。
ソリューションを検討するにあたり、単にコンテナの数を増やすだけでは効果的ではないことに気づきました。個々のユーザーが大規模なバッチジョブを実行すると、取り込みトラフィックが大幅に急増することがよくあります。その結果、ユーザーからの API トラフィックは非常に予測が難しくなり、コンテナインスタンスではこうした急増に対応するのに十分な速さでスケールすることができません。そこで最終的に、すべての取り込みトラフィックを Redis のメッセージキューにルーティングすることにしました。Kafkaとは異なり、Redisは簡単に自己ホストでき、当社の要件を満たすように拡張できます。そして次に、別のLangfuseコンテナ (Worker) が非同期でこのデータをConsumeし、レート制限を適用して、当社のデータベースの負荷とコンテナのCPU使用率を低減します。この変更により、認証と本文の形式のみを確認する軽量な取り込みエンドポイントを作成しました。このWorkerコンテナは、トークン化やデータベースへの書き込みなどのより集中的なタスクを処理します。
Clickhouseから読み込まずにClickhouseに更新を書き込む
私たちは、APIパフォーマンスを短期的に改善する必要があったため、上記のステップのみを行いました。しかし、作業はまだ終わっていませんでした。Worker コンテナがすべての処理を非同期で行っていたとしても、私たちの取り込みパイプラインのロジックを動作させるには、多くのPostgresのIOPSが引き続き必要でした。この問題についても、セルフホスティングユーザーから問い合わせがありました。同時に、私たちは読み取りクエリのAPIレイテンシを改善するという課題にも直面しており、最終的に、TraceデータをPostgresからClickhouseに移行することを決定しました。Clickhouseは、Observerbility分野において多くの新規参入者が使用しているOLAPデータベースで、Apacheライセンスが適用されており、当社の概念実証(PoC)で卓越したパフォーマンスが確認されていました。列指向のストレージレイアウトは、私たちが期待する分析クエリに適しており、大規模なバイナリ列を持つ単一行の検索でも高いパフォーマンスを発揮します。しかし、本番環境への導入は容易ではありませんでした
LangfuseのSDKは、指定されたオブジェクトIDの更新をバックエンドに送信するように設計されています。Postgresにおける単一の行の読み取りと更新は高速かつ簡単ですが、ClickHouseにおけるすべての行の更新は非常にコストのかかる操作です。そこで私たちは更新を新しい挿入に変換し、ClickHouseのReplacingMergeTreeテーブルエンジンを使用して、最終的にバックグラウンドで行の重複を排除しています。
つまり、常に行の最新の状態を取得し、更新を適用し、それをClickhouseに書き戻す必要があるということです。
私たちはトラフィックを分析し、すべての更新の90%が10秒以内にデータベースに書き込まれることを認識しました。つまりこれは、同時実行性とデータの整合性に気を配らなければならないことを意味します。しかし、Clickhouse から行の最新の状態を取得するのは現実的ではありませんでした。Clickhouse は、クエリ結果を返す前に全てのデータが Clickhouse ノード間で同期されることを保証する、非常にコストの高い "select_sequential_consistency" 設定を使用した場合にのみ、書き込み後の読み取りの一貫性が保たれます。したがって、私たちの規模では、Clickhouse から既存のデータを読み取れる保証はありませんでした。また、同じIDに対する2つのイベントが並行して処理され、競合状態が発生する可能性もありました。
そこでこの問題を緩和するために、承認された全てのイベントを Redis にキャッシュすることにしました。そして、Worker コンテナにイベントを送信し、Worker コンテナはオブジェクト ID に関連する全てのイベントを取得して、Clickhouse から読み込む必要なしに、確実に新しい Clickhouse の行を作成します。私たちは新しい取り込みパイプラインを実装し、Postgres 取り込みパイプラインと並行してイベントの処理を開始しました。
しかし非常に大きなインスタンスを利用しても、AWS ElastiCache のネットワーク容量には限界があるという事実がすぐに判明しました。また、Redis のもう一つの欠点は、インメモリ型のアーキテクチャと、保存されたデータが一時的な性質 (ephemeral) であることです。S3 をイベントのPersistent ストレージとして導入することで、Redis には参照情報のみを保持できるようになりました。この変更により、エラーが発生した場合にイベントを再生することも可能になり、さらに驚くことに、Redis 用にイベントをシリアライズする処理が高コストだったため、Web コンテナの CPU 使用率が大幅に低下しました。Kafka の方が取り込みにはより適していたかもしれませんが、新しいマルチモーダル機能のために、Redis のキャッシュ機能と S3 を活用することで、コンポーネント数を少なく保つことを選択しました。これらの調整により、Clickhouse にデータを一貫して、かつ大規模に挿入することに成功しました。
ความคิดเห็น