- Reploは、Shopifyマーチャントが運用するライブページ、オファー、A/Bテストに対して、製品内でリアルタイム分析を提供するためにClickHouseを活用しています。
- 同チームは複数のデータモデルを試行錯誤しながら、事前計算、重複排除、再計算の境界がスケールにおけるリアルタイム分析にどう影響するかを学んできました。
- 現在、ClickHouseは毎秒3,000〜5,000イベントの取り込みと、1,000億件を超えるイベントの分析を、ダッシュボードの高速かつレスポンシブな状態を維持しながら支えています。
まとめ
昨年9月、Replo のエンジニアである Ryan Voris は、サンフランシスコで開催された ClickHouse のミートアップに参加しました。多くのミートアップと同様、満足度の高い顧客が自社のデータ運用をどのように ClickHouse でスケールさせたかを語る場でした。しかし Ryan にとっては、少し物足りなさが残るものでした。
「みんなが ClickHouse の素晴らしさを語り、見事なユースケースを次々と紹介していたんです」と彼は振り返ります。「でも、『こんな風に失敗しました』とか『こうやるとうまくいきませんでした』『これはやめておいた方がいい』といった話は誰もしていなかったんですよ。」
そこで Ryan は、前回のサンフランシスコ・ミートアップ に登壇した際、これまでとは違うことをしようと決めました。きれいに整えられたケーススタディの代わりに、4,000社を超える Shopify マーチャントから信頼される AI 搭載のページビルダーである Replo が、自社のアナリティクス製品 を ClickHouse 上に構築するまでの実話を、失敗や移行、アーキテクチャの仕切り直しまで含めて率直に語ったのです。
その結果として生まれたのは、1,000億件を超えるイベントを処理・分析しながら、ダッシュボードを高速に保ち、アトリビューションを正確に維持し、オンライン小売業者にとって使えるアナリティクスを提供できるシステムです。
ライブキャンペーンのためのリアルタイム分析 #
Replo Analytics は製品の中に直接組み込まれています。顧客はこれを使ってキャンペーン全体のセッション、購入、コンバージョン率、平均注文額、A/B テストの成績を追跡します。新しいページや広告を立ち上げたブランドは、数時間後ではなく即座にリアルタイム分析 が見えることを期待します。
裏側では、これは秒間 3,000~5,000 件のペースで ClickHouse へと流れ込むフロントエンドイベントの絶え間ないストリームを意味します。「クリック、ページビュー、購入などを追跡しています」と Ryan は説明します。「ページ上で購入があるたびに、そのイベントと購入額を記録し、それをもとに平均注文額、セッションあたりの売上、コンバージョン率などの情報を算出しています。」
Replo のトラフィックパターンは予測可能なリズムに従い、北米の業務時間帯にピークを迎え、夜間には落ち着きます。しかし、期待値が変わることはありません。ダッシュボードは応答性を保たなければなりません。アトリビューションは正確でなければなりません。そして、アナリティクスがライブキャンペーンを不透明あるいは信頼できないものに感じさせるような遅延を持ち込むことは許されません。
最初から(Ryan が入社する以前から)、Replo はこのシステムを ClickHouse 上に構築していました。ClickHouse がボリュームを処理できるかどうかについて疑念はほとんどありませんでした。本当の問題は、Replo Analytics が成長し、利用が拡大し、要件が進化する中で、いかに高速性を保ちながら分析をモデル化・計算するかでした。
1テーブルから事前計算へ #
Replo の最初のアナリティクスパイプラインはシンプルなものでした。すべてのイベントが単一のテーブルに流れ込み、ダッシュボードのクエリは毎回その場でメトリクスを再計算していました。
これは数ヶ月間うまく機能しましたが、利用が拡大するにつれ問題が現れ始めました。直近1時間のものでも半年前のものでも、すべてのクエリが同じテーブルにヒットし、同じ計算を繰り返していたのです。セッション単位のメトリクスは、そのセッションに変化がなくても何度も再計算されていました。
スキーマ自体も助けにはなりませんでした。Ryan が「変な感じのネストされたペイロード」と表現するものに、他に入りきらないものが詰め込まれていました。「あまり効果的とは言えませんでした」と彼は語ります。「結局これは破綻し、新しいテーブルが必要だと気付きました。」
次のバージョンでは、顧客固有の名前空間に始まり、時間とセッション識別子が続くという、より明確な構造が導入されました。イベントを顧客とセッションでグルーピングすることで、クエリのスコープをより厳密に絞り込み、マーチャントが実際にデータとどう対話しているかを反映できるようになりました。これは、アナリティクスを延々と再計算するものではなく、一度計算して再利用するものとして扱うことへの一歩でした。
そこからチームは、事前計算された結果を保持するための2つ目のテーブルを導入しました。総購入額や AOV のようなメトリクスをあらかじめ計算し、結果を保存しておくことで、クエリのコストを下げるという発想です。ほとんどのイベントは計算をまったく必要としませんでしたが、Ryan が説明するように、「計算が必要なセッション(つまり購入)については少しだけ興味深い話になります。」
これを管理するため、チームは「マーク/アンマーク戦略」を実装しました。SummingMergeTree を使って、処理が必要なセッションを追跡し、計算が完了したらクリアするという仕組みです。裏側では、リフレッシュ可能なマテリアライズドビュー が一定間隔で実行され、巨大な計算関数をトリガーして、マークされた各セッションのすべてのイベントを集め、集計を行い、最終結果を ClickHouse に書き戻していました。
複雑ではありましたが、機能していました。クエリは明らかに高速化されました。「これまでうちのシステムを使えなかった顧客が、実際に使えるようになりました」と Ryan は言います。書き込み時の遅延は多少ありましたが、彼の言葉を借りれば「それでもリアルタイムに感じられた」とのこと。パフォーマンスが制御下に入り、数百億件のイベントが移行されると、チームの自信は高まりました。Ryan は当時を振り返ってこう言います。「プロダクトオーナーたちは『これは素晴らしい、もっと機能を追加しよう』と思ったんです。」
リアルタイムと結果整合性の衝突 #
次の大きな機能要望のひとつが、フラクショナル(分割)アトリビューションでした。購入を単一のページにフルで帰属させるのではなく、顧客がセッション中に接触したすべてのページに価値を分配したい、というものです。誰かが5ページを訪問して5ドル使ったら、各ページに1ドルずつ割り当てる、というわけです。理屈は単純に聞こえますが、実際にやってみると数字が合いませんでした。「何かおかしいと気付きました」と Ryan は言います。「計算が合わなかったんです。」
下層のロジックは正しそうに見えました。システムはどのイベントがどのセッションに属するかをすでに把握しており、チームはイベントが書き込まれれば、クエリされたデータはそれらのセッションのクリーンに重複排除されたビューを反映するはずだと考えていました。「そこで、マージツリーはいずれ重複排除されるものであって、即座に重複排除されるものではないと気付きました」と Ryan は語ります。「私たちはリアルタイムの情報を使い、意図的に重複データをデータベースに書き込んでいたため、自ら問題を引き起こしていたのです。」
幸いなことに、ClickHouse には DISTINCT や FINAL のような句があり、クエリ時に重複排除を強制できます。これらを適用すると結果は即座にクリーンになりました。これらの変更によって、テストデータは期待通りの挙動を示し始めました。数十万行程度のローカルデータセットでは、すべて辻褄が合っていました。「クエリはまだサクサク動き、移行も簡単でした」と Ryan は振り返ります。「うまくいっているように見えたんです。でもデプロイした途端、ほぼ即座にすべてが遅くなり始めました。」
今回、チームが詳しく見てみると、同じセッションが何度も繰り返し再処理されていることが分かりました。何ひとつクリアされていなかったのです。「どうもセッションがアンマークされていないようです」と、バックログが膨らみ続ける中、Ryan は Slack でチームにそう書きました。
犯人は、リアルタイム再計算と、大規模なテーブル全体での重複排除の組み合わせでした。テストでは安価だったクエリが、巨大なデータセットをスキャンする必要が生じ、再計算を駆動するマテリアライズドビューの1分間のリフレッシュウィンドウより長い時間がかかるようになりました。前のサイクルが終わる前に、次のサイクルが起動してしまうのです。
Ryan の言葉を借りると、「自分で自分の足を撃ち、システム全体に暴走列車を引き起こしてしまった」状態でした。その時点で、彼は言います。「問いはひとつだけでした。『どうやってこれを直すか?』」
最後のアプローチ:データを小さく保つ #
最終的な解決策に簡単にたどり着いたわけではありません。「これが次に試したアプローチだと言ったら嘘になります」と Ryan は認めます。「『マーク/アンマーク』方式を必要以上に長く動作させようとしました。それから、あらゆる重複排除や異なる TTL を使った数十個の CTE も試しました。」
最終的に問題を解決したのは、時間に対する考え方のリセットでした。アナリティクスを再計算する必要があるのは、購入がたった今発生したからに他なりません。Ryan が指摘するように、「顧客が6ヶ月前に購入することは絶対にあり得ません。だから6ヶ月前のデータについては気にする必要がないんです。」
この気付きから、ライブセッションイベント専用に設計された新しいテーブルが生まれました。履歴データセット全体をスキャンする代わりに、このテーブルは直近40分間のアクティビティだけを、しかも購入関連のイベントだけを追跡します。「これは私たちが持つ1,000億件のレコードと比べれば、ごくわずかなデータです」と Ryan は言います。クリックやページビューは他の場所に存在しますが、それらが再計算を支配することはなくなりました。
スコープが絞られたことで、処理ロジックも簡素化できました。チームは深くネストされた CTE から離れ、単一のマテリアライズドビューの中で結合(JOIN)を中心とした計算へと作り直しました。出来上がった flusher ビューはよりコンパクトで、理解しやすく、暴走的な再計算に陥る可能性もはるかに低くなりました。
今度はシステムが持ちこたえました。クエリは高速なままでした。書き込み遅延は1分前後で安定しました。履歴データの移行にはさらに労力がかかり、古いイベントをバックフィルするためのスクリプトが必要でしたが、そのトレードオフは意図的なものでした。Ryan の言葉を借りると、「これは扱いやすく、限定された問題です。移行を処理するスクリプトを書けばよく、ライブシステムに対して、はるか過去に起きたことを自動で自己修復するほどのフォールトトレラント性を求める必要はないんです。」
次に向けたスケーラブルな基盤 #
4ヶ月経った今でも、Replo のアナリティクスアーキテクチャは健在です。「すべてが以前と変わらずサクサク動いています」と Ryan は言います。
コアパイプラインが安定したことで、チームはアナリティクスデータのモデリングとクエリ手法の改良を継続しています。それには、LowCardinality カラムの導入、JSON ペイロードから頻繁にアクセスされるフィールドのマテリアライズ、そして Nullable 型から離れてよりシンプルなデフォルト値を採用することなどが含まれます。
Ryan はこれがゴールではないと明言しています。「もっと多くのパフォーマンスを引き出せると考えています」と彼は語ります。さらに、探求すべき ClickHouse のクエリ最適化 は山ほどあります。「やりたいことはまだまだたくさんあります。ただ、まだそこまで手が回っていないだけなんです。」
結局のところ、Replo の ClickHouse ストーリーは、特定のひとつの最適化や機能に集約されるものではありません。それは、リアルタイムシステムがどこでシンプルさから恩恵を受けるか、そしてトレードオフを明確にすることが、いかに理解しやすく、運用しやすく、スケールしやすいシステムにつながるかを学ぶ物語なのです。



