このコンテンツは自動機械翻訳サービスによる翻訳版であり、皆さまの便宜のために提供しています。原本の英語版と異なる誤り、省略、解釈の微妙な違いが含まれる場合があります。ご不明な点がある場合は、英語版原本をご確認ください。
Cloudflareでは、オープンソースのオンライン分析処理(OLAP)データベースであるClickHouseのヘビーユーザーです。Cloudflare製品の使用に対するユーザーの課金方法を決めるために、Cloudflareは毎日何百万ものClickHouseに問い合わせます。迅速にこれらの作業を終了させなければ、請求書の調整は非常に困難になります。
このパイプラインは、数億ドルの使用料、詐欺システムなどを支えているため、遅延が下流に大きな影響を与えます。
このような理由から、Cloudflareの請求額を処理するクリックハウスの日次集計ジョブが、移行後に遅くなった時に、大きな問題となりました。I/O、メモリ、スキャンされた行、読み取り部分など、通常の疑わしいものはすべてクリーンに見えました。ClickHouseのクエリーが遅い時に通常チェックするはずのすべてが、正常なようです。
これは、ClickHouseの内部に深く潜む隠れたボトルネックを発見した経緯と、それを修正するために書いた3つのパッチについてです。
当社は、数十のクラスターにわたって100ペタバイトを超えるデータを保存するためにClickHouseを使用します。多くの社内チームのオンボーディングを簡素化するために、2022年初頭、「Ready-Analytics」と呼ばれるシステムを構築しました。
前提はシンプルです。新しいテーブルを設計する代わりに、チームは単一の巨大なテーブルにデータをストリーミングできます。データセットは名前空間によって識別され、各レコードは標準スキーマ(例:20個の浮動小数点フィールド、20個の文字列型フィールド、タイムスタンプ、およびインデックスID)を使用します。
ClickHouseでは、クエリパフォーマンスを最適化するために、データのソート方法が重要です。そこで登場するのがindexIDです。これは文字列フィールドであり、プライマリキーの一部を形成します。つまり、個々のネームスペースは、そのネームスペースの所有者が実行していると予想されるクエリに最適な方法でデータをソートできます。( namespace,indexID, timestamp )このようなプライマリキーになります。
このシステムは人気があり、何百ものアプリケーションが使っています。2024年12月までに、すでに2PiBを超えるデータ、および毎秒数百万行の取り込み速度にまで成長しました。しかし、Cloudflareには重大な欠陥がひとつありました。それは、保持ポリシーです。
Cloudflareは、ネイティブの有効期限(TTL)機能が実装される以前から、何年もClickHouseを使用しています。その結果、パーティショニングに基づく独自のリテンションシステムを構築したのです。Ready-Analyticsテーブルは日単位でパーティション分割され、保持ジョブは31日より古いパーティションを削除するだけです。
この「画一的」な31日間の保持期間は大きな制限でした。法的または契約上の義務により、何年もデータを保存する必要があるチームもあれば、わずか数日でデータを保存する必要があるチームもありました。この制限により、これらのユースケースはReady-Analyticsを使用できず、オンボーディングプロセスがはるかに複雑な従来のセットアップを選択せざるを得なくなりました。
当社では、ネームスペースごとの保持を可能にする新しいシステムが必要でした。
当社は主要なアプローチとして2つ検討しました。
ネームスペースごとのテーブル:これにより保持の問題は当然解決されますが、何千ものテーブルをオンデマンドで管理するには大幅な新しい自動化が必要になります。
新しい分割キー: 分割キーは(日)から(ネームスペース、日)に変更できます。
私たちは2つ目の選択肢を選びました。これにより、既存の保持システムは引き続きパーティションを管理できますが、ネームスペースごとに粒度の細かい管理が可能になります。
これによって、テーブル内のデータコンポーネントの総数が増えることはわかっていましたが、重要な仮定がありました。すべてのクエリは特定のネームスペースによってフィルタリングされているため、単一のクエリで読み込まれる部分の数は変化しないはずです。これなら、パフォーマンスに影響を与えないと私たちは考えました。
これは、パーティショニングを変更し、単一のネームスペースのデータを安価にドロップできる方法を示しています。
この新しいシステムにより、高度なストレージ管理レイヤーも構築できました。 最大最小公平性アルゴリズムを使用することで、目標ディスク使用率(例:90%)を設定し、利用可能なスペースを自動的に「共有」できます。公正なシェアを下回るネームスペースは、使われていない容量を、より多く必要なネームスペースに振り分けることになります。これにより、クラスターを90%の稼働率で自信を持って稼働できるようになりました。
2025年1月に移行を開始しました。ClickHouseのMergeテーブル機能を使用して、古いテーブルと新しいテーブルを結合し、古いデータが順次削除されるようにしながら、すべての新しいデータを新しいパーティションテーブルに書き込みました。
それから2か月後、2025年3月下旬、請求チームから日々の集計業務が遅くなっているという報告がありました。これらの仕事はタイムクリティカルです。終了しないと、請求書は発生しません。作業は徐々に遅くなり、期限に近づいていました。
調査をしましたが、通常の疑わしい人は全員責任を負うものではありませんでした。I/Oは問題ありませんでした。メモリは問題ありませんでした。個々のクエリの指標は、以前よりも多くのデータや多くの部分を読み取っていないことを示しました。最初の仮定は正しいように見えましたが、システムは急停止に追い込まれていました。
理論的な話が出るまでに数日かかりました。最後に、クラスター内の合計パート数に対するクエリ時間をプロットしました。この相関関係は否定できません。
Ready Analytics ClickHouseクラスターの平均SELECTクエリ時間。段階的なパフォーマンス低下を示しています。
新しい(ネームスペース、日)分割スキームに従って、テーブルレプリカごとの総データ部数が直線的に増加します。
でも、なぜでしょうか?余分な部分を読んでいないのに、なぜ余分な部分があるだけの状態だったのでしょうか?
そこで、ClickHouseに組み込まれたtrace_logを使用してフレームグラフを作成しました。これは、実行中のClickHouseサーバーからのトレースを記録する組み込みテーブルです。つまり、実行されているコードのトレースだけでなく、これらを特定のユーザー、クエリID、その他のメタデータに関連付けることができるため、必要に応じて正確なイベントセットまでフィルタリングできます。今回の場合、特にリーフSELECTクエリに注目したいと思いました。このテーブルにメタデータがあったため、これは簡単でした。
最初のCPUベースのフレームグラフは、クエリの計画に膨大な時間が費やされているという疑念をすぐに裏付けるものです。これは、ClickHouseがどの部分を読むかを決定する、実行前のフェーズです。
フレームグラフ:リフレクションクエリーのCPU時間の45%が、パーティションIDに基づいたパーツのベクトルのフィルタリングに費やされていることを示すフレームワーク
火災グラフは明白で、サンプリングされたCPU時間の45%が、filterPartsByPartitionと呼ばれる単一の関数に費やされていることがわかりました。
私たちが最初に修正を試みたのは、この正確なコードパスへの小さなパッチでした。プランナーはヒューリスティックを評価してパーツを剪定しますが、私たちのテーブルにとって最適な順序で評価されていないと考えたのです。当社のパッチは順序を変え、5%小さな改善をもたらしました。私たちは正しい道に進めていましたが、本当の問題を見逃していました。
それまでは、アクティブなスレッドのみをサンプリングする「CPU」トレースを生成していました。非アクティブまたは待機中のスレッドを含む、すべてのスレッドをサンプリングする"Real"トレースに切り替えました。新しいフレームグラフは啓示を与えてくれました。
フレームグラフ:リフレクション攻撃時間の半分以上がアクティブ部のリストを保護するミュートを待つことに費やされていることを示すフレームワーク
問題はCPUに縛られる作業ではなく、大規模なロック競合でした。クエリ時間の半分以上は、テーブルの部分リストを保護する単一のミューテックス(MergeTreeData)の取得を待つのに費やされていました。クエリを計画するには、すべてのスレッドは次の必要があります:
このミュートの専用ロックを取得します。
表内のすべての部品のリストを完全にコピーします。
ロックを解除します。
関連部分までフィルタリングします。
何万ものコンポーネント、何百もの同時クエリーがあり、それらはすべて単一ファイルの行に並んでいるのです。
このインサイトは、これらのホットスポットを軽減するための一連の最適化を計画するのに役立ちました。ClickHouseに作るすべてのパッチと同様に、汎用的なものにして、最終的にはアップストリームのコードベースに貢献させることを試みます。そうすることで、フォークを維持するのがより簡単になり、私たちの変更はコミュニティにも利益をもたらします。
クエリプランナーは、パーツリストを変更しません。単に読み取るだけです。専用ロックを使うケースはありませんでした。
修正: 代わりに、共有ロック (std::shared_lock) を取得するようにコードを変更しました。これにより、すべてのクエリプラン担当者が同時にクリティカルセクションに入ることができました。
結果:クエリー時間が大幅に短縮されました。ロックコンテンツが消失しました。
共有ロック最適化(最適化1)の平均SELECTクエリ時間への影響の即時分析、ロックコンテンツの解決を示す。
パフォーマンスは大幅に改善されましたが、まだベースラインには戻りませんでした。トレースログに戻って、別の「リアル」なフレームグラフを作成しました。
フレームグラフは、リフレクションクエリー期間の4分の1ですべての部品のベクトルをコピーし、別の四半期でフィルタリング(再度コピー)していることを示しています。
新しいフレアグラフは、ボトルネックが単に移動しただけであることを示しました。今では、共有ロックがあるにもかかわらず、巨大なパーツのベクトルをコピーするのに時間が費やされていました。直感的に、ベクトルのコピーは安価に聞こえますが、数万の要素が含まれており、1秒間に数百回行うと、コストが積み上がります。
修正版:コピーの作成を完全に遅らせました。当社は、部品リストの「共有コピー」を作成しました。読み取り専用操作(クエリ計画など)は、このコピーから読み取るだけです。パーツのセットを変更する操作(新規挿入など)は、キャッシュを再生成します。プランナーは、実際に必要な部品のフィルター済みリストのみをコピーするようになりました。
結果:もう1つ、大幅なパフォーマンス改善が見られます。
ベクトルコピーの最適化(最適化2)を展開した後のさらなるパフォーマンス改善。
このように大幅なコスト削減を社内で確認した後、私たちはこの変化をコミュニティにも提供することにしました。ClickHouse Inc.のメンテナーとのいくつかの小さなデザインの反復の後、変更はPR #85535の下でマージされました。ClickHouseのバージョン25.11から利用可能です。
これで終わりではありません。部品の数が増加しても、パフォーマンスはそれでもずっとゆっくりと低下します。部数との相関関係は依然ありました。数か月後に戻ると、新しいフレームグラフ(図3と同じ)は、フィルタリングのコードパス(私たちが最初に修正しようとしたもの)で費やされている時間を示しています。このコードは、すべてのパーツに対して線形スキャンを実行し、それぞれに対する述語を評価します。数か月かけて、最適化前からの期間選択に戻りました。
しかし、このコンポーネントのリストは分割キーでソートされることがわかっています。パーティションキーの最初の列は名前空間であることを覚えておいてください。名前空間はクエリの大部分でフィルタリングされ、「テナント」を識別するためです。これをどのように活用できるか?
修正方法:パーティションIDのネームスペースの部分に基づいてバイナリ検索を実装しました。これが機能するのは、ベクトルがソートされているため、実際にそれらを見なくても多くのエントリをフィルタリングできるからです。ネームスペースはソートキーの最初の部分であるため、これは特に効果的です。このバイナリ検索の最初の通過後、調査する必要のある部分の範囲ははるかに小さくなり、それらについては、以前と同じロジックを適用して他の条件に基づいて部分を除外します。
結果: 2026年3月にこのパッチをデプロイした後、クエリ時間が50%短縮されました(図8を参照)。さらに重要なのは、これによりクエリ期間と部数の相関関係が解消されることです。残念ながら、このソリューションは任意のクエリー条件(例:namespace in (5,10) のような条件。部分フィルタリングをカバーするために、クエリ条件キャッシュを拡張するなど、より一般的なアプローチを検討しています。
バイナリ検索のパート削除(最適化3)の実装後の持続的な遅延削減。
こうした最適化により、請求システムに関する差し迫った問題は解決されました。しかし、今回の訪問で、分割を選択した場合の目に見えない深刻なコストが明らかになりました。
その他の問題も残っています。このブログ記事では、選択した期間でパートカウントが増加する問題についてのみ述べてきましたが、それはClickHouse内のすべてのパートのメタデータを追跡するZooKeeperにとっても問題になりました。おそらく、いつかという日が、100ギガバイトのZooKeeperクラスターの話をすることになるでしょう。
私たちは自分たちに大きな余裕をもたらしましたが、根本的な疑問は残っています。この分割スキームが長期的に正しい選択だったのでしょうか?あるいは、最終的には中断させて、別のアーキテクチャに移行する必要があるのでしょうか?今現在はパッチが適用されている状態ですが、この経験は、十分に計画された変更であっても、誤った思い込みにつながる可能性があることを示す明確な例でした。
請求チームがこの問題を最初に報告したとき、当社ではレプリカ1つあたり30,000件の部品がありました。パート率は増加を続けることがなく、1年後はレプリカあたり16万パートに達しましたが、ここで行った最適化のおかげでクエリ時間は安定しています。
Cloudflareでは、複雑なエンジニアリング問題を大規模に解決します。ここで説明したデバッグと最適化が、あなたが求めている課題のように思われる場合は、当社が募集しているいくつかのオープンな職種をご確認ください。