1 分間に 100 万ページビュー、1 台のサーバーで:Statnive Live のスケール設計
Go バイナリ、ClickHouse ロールアップ、687 バイトのトラッカーが、8 コアのサーバー 1 台で 1 分間に 100 万ページビューを処理する仕組みを解説します。
アクセス解析のパフォーマンスはサイト速度の問題
アクセス解析のパフォーマンスに関する記事の多くは、バックエンド——サーバーが 1 秒あたり何件のイベントを処理できるか——に焦点を当てています。しかし、それは指標として間違っています。サイト運営者が実際に支払うコストは、アクセス解析スクリプトが訪問者のページ読み込み時間に与える影響——そしてその先にある Core Web Vitals、コンバージョン率、SEO への影響——です。
Google の Core Web Vitals(INP は 2024 年 3 月 12 日に FID を置き換えました)——LCP、INP、CLS——はランキングシグナルです。モバイルでの JavaScript パースは、デスクトップより2〜5 倍遅いとされており、デスクトップで 50 KB のアクセス解析スクリプトがスマートフォンでは 200 KB 相当のパースコストになる可能性があります。レンダリングをブロックするアクセス解析スクリプトは、このジャンルで最大のパフォーマンス上の問題です。
Statnive Live はその非対称性を念頭に設計されました。1 日 2 億イベント/ノード、687 バイトのトラッカー、p99 クエリレイテンシ 500 ms 未満——これらの数値はすべて、「アクセス解析レイヤーがチェックアウトを遅くする理由になってはならない」という 1 つの目標のためにあります。この記事では、その仕組みをファイルパスとともに解説します。すべての主張を自分で検証できます。
この記事は、4 部構成の statnive.live プレローンチシリーズの最終回です。測定可能な主張には、それを証明するファイルまたはコマンドを示しています。
687 バイトのトラッカー
Statnive Live のトラッカーは、2026 年 4 月 28 日時点で 1,394 バイト(minified)/687 バイト(gzipped) と計測されました。これは目標値ではなく、Go の go:embed ディレクティブでバイナリに埋め込まれた実際のバイト数です。リポジトリをクローンすれば再現できます。
$ wc -c internal/tracker/dist/tracker.js
1394 internal/tracker/dist/tracker.js
$ gzip -9 -c internal/tracker/dist/tracker.js | wc -c
687
ファイルは go:embed でバイナリに埋め込まれているため、リビルドなしに別のトラッカーを配布することはできません。また、気づかないうちに肥大化することもありません。internal/tracker/tracker_test.go の Go テストが 1,500 バイト(minified)/700 バイト(gzipped) という上限を CI で強制しており、いずれかのしきい値を超えるとビルドが失敗します。
const (
maxMinifiedBytes = 1500
maxGzippedBytes = 700
)
同じテストは、非自明なトラッカーの構造全体——XMLHttpRequest、localStorage、sessionStorage、indexedDB、document.cookie、平文 URL、CDN インポート——を埋め込みバンドルへの文字列 grep で禁止しています。リファクタリングで大きなトランスポートライブラリが混入したり、新機能が localStorage を使おうとしたりすると、CI が PR を拒否します。
比較として、GA4 の gtag.js スクリプトは圧縮後で約 110 KB、Plausible の公開値は同スクリプトで 135 KB(gzipped)です。どちらと比べても、Statnive Live のトラッカーは 2 桁小さく——GA4 比で 50 倍以上軽量です。
トランスポートは sendBeacon と fetch keepalive の組み合わせ——どちらも fire-and-forget でメインスレッドをブロックしません。構造はバニラ JS の IIFE であり、1,394 バイトにフレームワークは入りません。トラッカーは go:embed でファーストパーティ配信されます。外部 CDN なし、オペレーターのドメイン外への DNS ルックアップなし、サードパーティのタグマネージャーなし。CI のエアギャップバリデーターは、外部参照を再導入するトラッカーの変更を拒否します。
インジェストパス——fire-and-forget、WAL ファースト
トラッカーに対するサイト運営者の要件は「ページをブロックしないこと」です。サーバーのトラッカーに対する要件は「イベントを失わないこと」です。Statnive Live のインジェストパイプラインは、この両方を安価に実現するよう設計されています。
すべてのインジェストリクエストは、ハンドラーが 202 を返す前に Write-Ahead Log(WAL)を通ります。ハンドラーは fsync を待ちますが、イベントごとではなく 100 ms のグループコミットティッカーで処理します。イベントごとの fsync ではコモディティディスクでスループットが ~100 EPS に制限され、SaaS フロアで必要な ~7K EPS の継続スループットを達成できないためです。WAL は tidwall/wal(MIT ライセンス、vendored)で、NoSync: true で開かれ、100 ms ティッカーが耐久性を担保します。ハンドラーは 202 ack を送信する前に AppendAndWait で待機します。sync に失敗した場合はプロセスが終了します——アクセス解析は履歴をサイレントに破損する場所ではありません。
ハンドラーは Go の http.MaxBytesReader でリクエストボディを 8 KB に制限します。
const (
maxBodyBytes = 8 * 1024 // 8 KB MaxBytesReader
maxArrayItems = 10 // batch at most 10 events per request
uaMinLen = 16
uaMaxLen = 500
)
WAL の前段に高速リジェクトゲートがあり、明らかなジャンクを HTTP 204 で排除します——User-Agent の長さが 16〜500 の範囲外、非 ASCII の UA、IP-as-UA、UUID-as-UA、プリフェッチヘッダー(X-Purpose、X-Moz)。これらのリクエストはエンリッチメント、WAL、ロールアップに到達しません。ClickHouse の async-insert は存在しますが、別エンドポイント /ingest-fallback のみです——ホットな /api/event パスには使用しません。
レート制限は CGNAT 対応です。モバイルキャリアの ASN からのリクエストは (ip, site_id) の複合キーで 1K req/s 持続/2K バーストを許可し、それ以外は IP あたり 100 req/s にフォールバックします。site_id ごとのグローバル上限は 25K req/s で、1 つのカスタマーがホストを飽和させることを防ぎます。CGNAT への対応が重要なのは、携帯電話ネットワークのゲートウェイが単一 IP の後ろに存在する場合、単純な IP ごとの制限で同一キャリアの数千の正規訪問者をブロックしてしまうためです。
生 IP は絶対に永続化しません。GeoIP ルックアップにのみ使用され、バッチライターが行を見る前に廃棄されます。監査ログも設計上 IP フリーです——レートリミッターは制限判定に IP をキーとして使用しますが、監査ログのシリアライズ時に失われます。gdpr-code-review ルールが CI でこれを強制しています。
クエリパス——3 つのロールアップ+HyperLogLog
ダッシュボードは生イベントを直接クエリしません。すべてのダッシュボード読み取りはロールアップテーブルから行われます——これはアーキテクチャルール 1 であり、CI のチョークポイントで強制されています。生の events_raw テーブルは書き込み専用です(windowFunnel() を使用するファネルウィンドウは時間キャッシュ付き結果で呼び出す場合を除く)。
3 つの v1 ロールアップはすべて AggregatingMergeTree ビューで、site_id を最初のキーとしています。
hourly_visitors—ENGINE = AggregatingMergeTree() PARTITION BY toYYYYMM(hour) ORDER BY (site_id, hour)daily_pages—ORDER BY (site_id, day, pathname)daily_sources—ORDER BY (site_id, day, channel, referrer_name, utm_source, utm_medium)
訪問者のカーディナリティは AggregateFunction(uniqCombined64, FixedString(16)) による HyperLogLog を使用——誤差約 0.5%、メモリはサブリニアです。FixedString(16) は BLAKE3-128 ハッシュを 16 バイトに切り詰めたもので、同一性は BLAKE3(daily_salt || identity_input) で決まります。daily salt は HMAC(master_secret, site_id || YYYY-MM-DD) として導出され、毎日ローテーションされ、永続化されません。同じ訪問者でも日が変われば異なるハッシュになり、ロールアップにはハッシュ状態のみが保持され、入力は含まれません。
すべてのダッシュボードクエリは 1 つのヘルパーを経由します。
// whereTimeAndTenant emits the WHERE clause every read query MUST start
// with: site_id = ? AND <timeColumn> >= ? AND <timeColumn> < ?.
// site_id is the first WHERE term so the (site_id, …) ORDER BY prefix
// can prune partitions cleanly.
func whereTimeAndTenant(f *Filter, timeColumn string) (string, []any) {
clause := fmt.Sprintf("WHERE site_id = ? AND %s >= ? AND %s < ?",
timeColumn, timeColumn)
return clause, []any{f.SiteID, f.From, f.To}
}
whereTimeAndTenant をバイパスするクエリや WHERE site_id = ? で始まらないクエリは CI で拒否されます。細かく見えるかもしれませんが、実際にはパーティションがきれいに枝刈りされるか、マルチテナント ClickHouse がすべてのダッシュボードレンダリングで全員のデータをスキャンするかの差です。
アナリティクスカラムでは Nullable(...) を禁止しています——集計での測定コストは 10〜200%(プロジェクトドキュメント 20 では Nullable(Int8) で 2 倍を計測)。ロールアップは代わりに DEFAULT '' と DEFAULT 0 を使用し、書き込みとマージパスの両方を高速に保ちます。
数値
/live ページの公開プルーフストリップには 4 つの数値が示されています。
- 600 B gzipped トラッカー(687 B のマーケティング上の丸め値)
- 1 日 2 億イベント/ノード
- p99 500 ms 未満
- EU/EEA 限定データ
それぞれの正直な注釈:
- トラッカー: 2026 年 4 月 28 日に 1,394 B min/687 B gz と計測。上限は 1,500 B/700 B gz で CI で検証済み。
- 1 日 2 億イベント: 測定された本番値ではなく設計上の上限です。 出典:プロジェクトドキュメント 19 の Hetzner クラスエンベロープ。SaaS フロアは Hetzner AX42(8 コア/64 GB)でヘッドルームあり。2 億 /日 ≈ ~2,300 EPS 持続、ClickHouse の公開スループットエンベロープの範囲内(Cloudflare は 36 ノードで 1 秒あたり 1,100 万行のインジェストを運用。Plausible は 1 日 ~100 万イベントを超えると ClickHouse が必須として PostgreSQL から移行)。
- p99 500 ms 未満: 測定された本番値ではなく設計上の上限です。 Phase-11a 本番 p99 は公開サインアップ後に公開予定。ProofStrip の主張は計測値ではなく卒業ゲートのしきい値です。
- EU/EEA 限定データ: ドイツ・ニュルンベルクの Netcup VPS 2000 G12 NUE で処理——
iptables -P OUTPUT DROPでバイナリを実行し、必要な外部通信がないことを証明するインテグレーションテストにより「計測済み」。
ダッシュボードは 16 KB(gzipped)初期 JS バジェットの範囲内に収まっており、ビルド済み index-*.js チャンクに対して size-limit で検証されています。遅延チャートチャンクは 25 KB、遅延パネルチャンクは 10 KB、CSS は 5 KB/3 KB に制限されています。ローカルでゲートを再実行できます。
$ npm --prefix web run bundle-gate
テストゲートが強制するアクセス解析インバリアント SLO:
- イベントロス ≤ 0.05%(サーバー)/≤ 0.5%(クライアント)
- 重複 ≤ 0.1%
- アトリビューション正確性 ≥ 99.5%
- 同意/PII 漏洩 = 0
- TTFB オーバーヘッド ≤ +10% / +25 ms
すべてのしきい値はリリースブロッキングで、CI によってすべての PR で検証され、本番カットオーバー前の 72 時間ソーク+6 シナリオカオスマトリックスでも追加検証されます。次の大きなトラフィックスパイクがどのようなものであれ、これらのゲートをクリアしなければリリースできません。
正直なトレードオフ——1 時間の遅延
1 時間の遅延は Statnive Live の中で一部の読者が好まないかもしれない部分です。正直に書きます。アーキテクチャルール 3:
1 時間の遅延、リアルタイムではない——クエリコストを 98% 削減。5 分リアルタイムは構築しない。
「98%」という数値は、同じスタックでの仮想的な 5 分パイプラインに対する根拠です——ロールアップ書き込みを安価に保ち、サイトごとのロールアップフットプリントを 1 日あたり 100 KB/サイト未満(v1 で 3 つのロールアップ、v1.1 で最大 6 つ)に維持し、ホットテーブルをスキャンする代わりにコンパクトな集計からダッシュボードクエリを提供します。アクセス解析を 1 時間ごとまたは 1 日ごとに確認するなら、1 時間の遅延は見えません。ライブイベントのスパイクモニター用にサブ分単位のフィードバックが必要なら、Live は適切なツールではありません——リアルタイムのアクセス解析製品を選び、~50 倍高いクエリコストを受け入れてください。
リアルタイムパネルは引き続き存在し、他のすべてが読み取る hourly_visitors ロールアップから 直近 1 時間のアクティブ を表示します。その後ろに別の 5 分パイプラインはありません——意図的な設計です。このトレードオフはアーキテクチャの核心であり、隠れたコストではありません。
サイトへの影響
上記のアーキテクチャが、サイト運営者にとっての「地味さ」を実現します。
トラッカーはチェックアウトをブロックしません。 sendBeacon と fetch keepalive は fire-and-forget です——アクセス解析のオリジンがオフラインであっても、ページは遷移し、顧客は支払いを完了します。アクセス解析エンドポイントを停止してページが正常動作することで確認できます。
Core Web Vitals への影響は 687 バイトとインライン IIFE に限定されます。 このジャンルで「レンダリングブロッキング」とされるしきい値を大きく下回ります。WordPress プラグインのトラッカーの LCP 影響は別の記事でベンチマークしました。Live トラッカーの測定済み LCP デルタはまだ公開していません——計測していないものは主張しません。
サーバーサイドのオーバーヘッドは別のオリジンで処理されます。 トラッカーはウェブアプリではなく Statnive Live のエンドポイントに POST します。100 ms WAL fsync ティッカーにより SaaS フロアで ~7K EPS の継続スループットを実現——これはアプリケーションの PHP、Node、Rails のリクエストバジェットと競合しません。
よくある質問
1 日 1,000 万ページビューにスケールできますか?
はい。1 日 1,000 万 PV は約 115 EPS の継続スループット——単一の 8 コア/32 GB ボックスの設計上の上限(~2,300 EPS)を大きく下回ります。1 ノードを超えた場合、マイグレーションはすでに {{if .Cluster}} Go テンプレートを使用しているため、シングルノードから Distributed への移行は設定の変更のみで、再プラットフォームは不要です。
共有ホスティングで動かせますか?
いいえ。ClickHouse には本物のサーバー(最低 8 コア/32 GB)が必要です。共有ホスティングには WordPress プラグインが正解です——既存の MySQL/MariaDB に保存し、新たな運用負荷はゼロです。
GA4 の 110 KB スクリプトと比べてどうですか?
GA4 の gtag.js はペイロードバージョンによって 110 KB 圧縮後(Stape)から 135 KB gzipped(Plausible)の範囲です。Statnive Live のトラッカーは 687 B(gzipped)。どちらの GA4 の数値と比べても 50 倍以上小さいです。モバイルのパース時間の差が支配的で、ミッドレンジの Android スマートフォンではトラッカーはノイズの中に消えます。
SaaS プランはどのハードウェアで動作しますか?
公開している SaaS フロアは Hetzner AX42(8 コア/64 GB)です。現在の SaaS 本番 VPS はドイツ・ニュルンベルクの Netcup VPS 2000 G12 NUE——EU/EEA 限定処理、第 5 章の転送なし。第 3 記事では契約面を、第 2 記事では規制面を扱っています。
サイズバジェットはどのように強制されますか?
すべての PR で 2 つの CI ゲートが実行されます。(a) go test ./internal/tracker/... がトラッカーの 1,500 B/700 B gz バジェットと禁止トークン拒否を強制します。(b) npm --prefix web run bundle-gate が web/.size-limit.json の 5 つのダッシュボードエントリすべてに対して size-limit を実行します。どちらも make ci-local の一部であり、GitHub Actions ワークフローが実際の ClickHouse に対して 8〜12 分でエンドツーエンド実行します。
証拠を示す
上記のすべての主張は statnive-live のクローンから再現できます。
# Tracker size budget — 1,500 B min / 700 B gz, asserted by Go test
$ wc -c internal/tracker/dist/tracker.js
1394 internal/tracker/dist/tracker.js
$ gzip -9 -c internal/tracker/dist/tracker.js | wc -c
687
$ go test ./internal/tracker/...
ok github.com/statnive/statnive.live/internal/tracker 0.32s
# Dashboard bundle budget — five size-limit entries
$ npm --prefix web run bundle-gate
# Whole gate — ClickHouse + integration + smoke + e2e (~8–12 min)
$ make ci-local
同じコマンドが GitHub Actions ですべての PR で実行されます。「リリースベンチマーク」は別途存在しません——PR がバジェットを破ればマージされません。リリースが 72 時間ソーク中に SLO を破れば出荷されません。1 分間に 100 万ページビューを処理するための設計は、実際には地味です。ほとんどは CI ゲート、高速リジェクトフィルター、ロールアップテーブルで構成されており、英雄的な要素はほとんどありません。
まとめ
2026 年に出荷するアクセス解析スタックは、サイトに対して「何をするか」ではなく、サイトに「何をしてしまうか」によって評価されます。Statnive Live の設計上の選択はトレードオフを明示しています。687 バイトのファーストパーティトラッカー、リアルタイムが要求するクエリコストの 98% を節約する 1 時間遅延ロールアップパイプライン、リリース前にブロックする CI 検証済みの SLO。まだ出荷していない p99 の本番計測値は主張せず、まだベンチマークしていない LCP デルタも主張しません——しかし上記のすべての数値は検証可能なファイルパスに対応しています。
Statnive Live は ja.statnive.com/live でまもなく公開予定です。 この 4 部構成シリーズはスローなイントロダクションです。WP プラグイン vs Statnive Live で意思決定フロー、2026 年の GDPR 対応アクセス解析 で規制面、アクセス解析データを自分で所有する でデプロイメント形態、そしてこの記事でエンジニアリング面を扱っています。機能ページ はワンページャーです。この記事の数値に誤りがあれば、ご連絡ください——すべての主張にはファイルまたはコマンドが紐づいており、磨かれた半分の真実より誤りを訂正することを選びます。