sharded-counters について
高負荷カウンター設計向けのスキルです。論理カウントを複数の書き込みシャードに分割する判断と、書き込み性能、読み取りコスト、一貫性のバランスを扱います。近似的なユニーク数、時間窓カウント、集計値のキャッシュも解説します。
主な機能
- 01カウンターを分割する基準の評価
- 02ランダムなシャードへの書き込み分散
- 03HyperLogLogによるユニーク数の推定
- 04時間バケットによる期間集計
- 05シャード集計値のキャッシュ
活用シーン
- 01人気コンテンツのいいね数や閲覧数の集計
- 02ライブイベントの閲覧数の追跡
- 03期間ごとのリクエスト率の算出
- 04大規模なユニーク訪問者数の推定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ proyecto26/ system-design-skills --skill sharded-countersインストール後、Claude Code で/sharded-countersを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/deep-dive.mdreferences/providers/aws.mdazure.mdgcp.mdgeneric.md詳細
シャーディングカウンター
単一の行、キー、パーティションでは書き込みを直列化しきれないほど高速に増加する値(「いいね」、閲覧数、投票数、レート集計、在庫の減算など)を数えます。ここで陥りがちなのが、ホットカウンターです。すべての書き込みが1つのレコードで競合するため、箱の性能をどれだけ上げてもレイテンシーは増加し、スループットは頭打ちになります。設計を誤ると、単純な +1 が機能全体のボトルネックになります。
どのようなときに使うか
1つの論理カウントへの同時インクリメントが、単一の行/キーで処理できる量を超える場合(バズっている投稿の「いいね」数、ライブイベントの閲覧カウンター、全体的なレート集計など)に使います。ストアのほかの部分はアイドル状態なのに、1つのレコードで書き込み競合(ロック待ち、CAS の再試行、パーティションのホットスポット化)が発生するのが症状です。これを選ぶということは、問題が 書き込み 側にあり、ミリ秒単位で正確な合計値は必要ないということです。
使わない場合
書き込み頻度が低い場合(単一のアトミックな INCR で毎秒数千回処理できます。誰も頻繁に更新しないカウンターをシャーディングするのは避けましょう。YAGNI)。常にトランザクション上正確で、読み取り直後の一貫性が必要なカウント(銀行口座の残高、完売時の座席在庫など)には使いません。これはトランザクションによる減算です。ファンアウト集計ではなく、consistency-coordination を参照してください。メンバー一覧も必要な項目の 正確な 重複なしカウント(ユニーク訪問者など)には使いません。カウンターではなく、ストア内のセットを使います。書き込みが安価で読み取りが多い場合は、シャーディングではなく、キャッシュされた集計値が必要です。
まず確認すること
- 最も負荷の高い単一カウントへの書き込み頻度 — 集計全体ではなく、1つの 論理カウンターへのピーク時のインクリメント数/秒です(→
back-of-the-envelope)。 - 正確な値か、近似値か — 数回分の誤差が許容されるか、シャード間の不一致がどのくらいの期間許容されるか(結果整合性)を確認します。シャード数と読み取り方法が決まります。
- 発生回数か、重複なしの項目数か — 累積数か、ユニーク数か(「いいね」数かユニーク閲覧者数か)によって、通常のシャードを使うか HyperLogLog を使うかが決まります。
- 読み取り頻度と鮮度 — 合計値がどのくらいの頻度で読み取られるか、返す数値にどの程度の古さが許容されるか(1秒未満か、数分か)を確認します。
- 期間単位か、累積か — 「過去1時間の閲覧数」にはバケット化したキーと有効期限が必要ですが、累積値には不要です。
選択肢
- 単一のアトミックカウンター — アトミックな
INCR/UPDATE +1を使う単一の行/キーです。最も負荷の高いカウントへのピーク書き込み頻度が、1ノードで直列化できる書き込みスループットの範囲内に収まる場合に使います。デフォルトの選択肢です。早まって限界を超えたと判断しないでください。 - 書き込みシャード(ストライプ)カウンター — 1つの論理カウントを N 個の物理シャード(
counter:{id}:shard:{0..N-1})に分割します。書き込みごとにランダムまたはハッシュで選んだシャードをインクリメントし、読み取り時に N 個すべてを合計 します。単一キーの競合がボトルネックで、合計値に結果整合性が許容される場合に使います。 - 重複なし項目数の近似値(HyperLogLog) — ユニークな 項目数を数える固定サイズの確率的スケッチ(約 12 KB)で、誤差は約 2% です。正確なメンバー情報が不要で、大規模なユニーク数を数える場合(ユニーク訪問者数、重複なしの検索語数など)に使います。
- 期間単位(バケット化)カウンター — 時間バケットごとにカウンターのキーを設定し(
views:{id}:2026-06-01T14)、現在のバケットをインクリメントします。読み取り時に直近のバケットを合計し、古いバケットは期限切れにします。「過去 N 分/時間」のレート形式のカウントに使います。 - 読み取り時の集計とキャッシュ済み合計値 — シャードを定期的に合計する(またはロールアップする)ことで、数値をキャッシュし、その値を返します。読み取りが書き込みを大幅に上回り、多少古い合計値が許容される場合に使います(
cachingと併用)。
トレードオフ
| 選択肢 | 解決すること | 悪化すること | 変更するタイミング |
|---|---|---|---|
| 単一のアトミックカウンター | 最もシンプルで正確です。書き込み後の読み取りも簡単です | ホットレコードが1つのため書き込みスループットに上限があり、急増時に競合します | 1つのカウントへのインクリメントが1ノードの処理能力を超えたら、書き込みをシャーディングします |
| 書き込みをシャーディングしたカウンター | 書き込み負荷をN個に分散し、ホットスポットを解消します | 読み取りにN回の検索と合計が必要です。合計値は結果整合性です。Nの値は事前に決める必要があります | N個の合計にかかる読み取りコストが問題になったら、集計値をキャッシュするか、ロールアップします |
| HyperLogLog | 巨大な規模でも、固定されたごく小さなメモリでユニーク数をカウントできます | 誤差は約2%です。メンバーの一覧表示や正確なカウントはできません | 正確なユニーク数やメンバー集合が必要になったら、保存されたセットを使います |
| 時間窓バケット | ローリングカウントやレートカウントを低コストで行えます。古いデータは自動的に期限切れになります | キーの数が増えます。ウィンドウ境界に注意が必要です。複数バケットにまたがる読み取りでは、多数のキーを合計します | 正確な累計値が必要になったら、別途、累計カウンターを保持します |
| 読み取り時の集計+キャッシュ | 書き込みの多いカウントの読み取りを低コストにします | 取得される合計値は、更新間隔の分だけ書き込みより遅れます | 書き込み時点まで最新の読み取りが必要なら、シャードをその場で読み取ります(N個の合計コストを許容します) |
負荷が高い状況での動作
カウンターは小さな仕組みですが、障害時には見た目以上の影響を及ぼします。
- ホットシャードの偏り: 書き込み先のシャードをランダムではなく
hash(userId)で選ぶと、 話題になったユーザーや不適切なハッシュによって、1つのシャードに負荷が集中することがあります。対策: 書き込みごとにシャードをランダムに選び、平均ではなくピーク時の競合に合わせてNを決めます。 - 急増時の読み取り増幅: カウントが急増すると、合計値の読み取りでN個のシャードを合計する処理が、読み取りのファンアウト全体で繰り返され、ストアに過負荷がかかることがあります。
対策: 読み取りごとではなく、一定間隔で集計値をキャッシュして更新します
(→
caching)。 - インクリメントの消失: 結果を待たずに実行したインクリメント(またはバッファー/ライトバック経路でフラッシュ前にクラッシュした場合)は、気付かないうちにカウントを過少にします。対策: ストアのアトミックインクリメントを使い、結果整合性が生じる期間を明示的に許容します。後で正確性が重要になった場合は、信頼できる唯一の情報源と照合します。
- ウィンドウ境界での集中アクセス: 時間バケット型カウンターは、毎時ちょうどに一斉に新しいキーへ切り替わるため、キャッシュが空のバケットへのアクセスと読み取りの集中が同時に起こります。対策: バケットを事前に作成し、ロールアップのタイミングにジッターを加えます。
- 大量の期限切れ: バケット型カウンターの有効期限が同時に切れると、ストアへの負荷が急増することがあります。 対策: TTLの期限を分散させます。
監視項目: シャードごとの書き込み分布(偏り)、1秒あたりのインクリメント数とノードの上限、N個の合計を行う読み取り経路のレイテンシ、スケッチの誤差予算(HLL)、および信頼できる唯一の情報源に対する過少/過剰カウントのずれ。
適用方法
- 入力を明確にします — 最も負荷の高い単一カウントにおけるピーク増分数/秒、厳密値と概算値の許容差、発生数と個別件数のどちらか、読み取りレート、鮮度の許容範囲を確認します(最初に確認することを参照)。単一カウントが過負荷になることを示す数値がなければ、単一のアトミックカウンターを使います(YAGNI)。
- トレードオフ表から選びます — 1つのノードで処理できるなら単一のアトミックカウンター、単一キーの競合が限界なら書き込みをシャーディング、ユニーク数にはHyperLogLog、ローリングウィンドウには時間バケットを使います。組み合わせることもできます(例:シャーディング+集計値のキャッシュ)。
- 主要な設定を決めます — ピーク時の競合状況からN(シャード数)を選び、シャードの選択ルール(ユーザーIDではなくランダム)、読み取り時の集計方法、ウィンドウのバケット粒度とTTL、キャッシュの更新間隔を決めます。
- 負荷テストで選択を検証します — 高負荷時の動作を確認し、シャードの偏り、読み取り増幅、増分の消失、ウィンドウ境界のそれぞれに、実際のトラフィック特性に必要な対策があることを確かめます。
- 数値でサイジングします — N ≥ ピーク増分数/秒 ÷ シャードあたりの書き込み上限です。N個の合計読み取りコストと、HLLの誤差が予算内に収まることを確認します(→
back-of-the-envelope)。 - プロバイダーを選びます — 既定では汎用の手順を使い、ユーザーがクラウドを指定した場合に限り、プロバイダーのファイルを開きます(プロバイダーの選択を参照)。
- 回答でトレードオフを示します — ここで選んだ項目ごとに1行を設けた
Decision | Solves | Worsens | Change it when…テーブルを提示します(例:方式、そのストア、障害時のポリシー)。最後の列には具体的なトリガー(数値または観測可能なシグナル)を示します。その後、最初に何が破綻するかを明記します。
推奨事項と禁止事項
推奨事項
- 単一のアトミックカウンターから始め、数値によって単一カウントがボトルネックだと示された場合にのみシャーディングします。
- アクターに関係なく負荷が均等に分散されるよう、書き込み先のシャードはランダムに選びます。
- 読み取りが多い場合は、合計値をキャッシュし、一定間隔で更新します。
- 大規模なユニーク数にはHyperLogLogを使い、約2%の誤差を既知のコストとして明示します。
- 時間バケットのキーには異なるTTLを設定して期限切れにし、次のバケットを事前に作成します。
- 結果整合性の許容期間を明確に示します。シャーディングでは、スループットと引き換えに即時の厳密性が失われます。
禁止事項
- 単一の
INCRで処理できるカウンターをシャーディングしないでください(早すぎるシャーディングは、無駄な読み取りコストを生みます)。 - シャードをユーザー/エンティティIDでハッシュしないでください。負荷の高いアクターにより、負荷が再集中します。
- 拡散したカウントを読み取るたびにN個のシャードを合計しないでください。集計値をキャッシュします。
- 数値をトランザクション上、厳密に正確にする必要がある場合(金額、在庫切れ管理)に、シャーディング/結果整合性カウンターを使わないでください →
consistency-coordination。 - メンバー一覧や厳密な件数も必要な場合に、HyperLogLogを安易に使わないでください。
重要な数値
判断の決め手となるのは、最も負荷の高い単一カウントのピーク増分数/秒と、ノードの直列書き込み上限との比較です。この比率からNが決まります。HyperLogLogのスケッチは、ユニーク数が数十億でも標準誤差約2%で約12 KBです。これが大規模環境で保存済みセットを上回る理由です。シャーディングされた合計値の読み取りにはN回の参照が必要なため、Nによって書き込みの余裕と読み取りコストのバランスが決まります。QPSレート、ノードあたりの書き込み上限、ストレージのサイジングについては、ここで繰り返さず、back-of-the-envelopeを参照してください。
インターフェースの概要
シャーディングされたカウンターは、単一の値ではなくキーの契約です。
- 書き込み:
INCR counter:{id}:shard:{rand(0..N-1)}(アトミックに、1つのシャードへ送信します)。 - 読み取り:
sum(GET counter:{id}:shard:{0..N-1})— またはT秒ごとに更新されるキャッシュ済み集計値count:{id}を読み取ります。 - 個別件数:
PFADD uniq:{id} {member}の後にPFCOUNT uniq:{id}を実行します(HLLスケッチ)。 - ウィンドウ集計:
INCR views:{id}:{bucket}をTTL付きで実行し、直近のバケットを合計して読み取ります。
N、シャードの選択ルール、集計/更新ポリシー、ウィンドウの粒度をあらかじめ決めてください。これらは実装の詳細ではなく、契約です。
プロバイダーの選択
まずは上記の汎用レシピを使用してください。ユーザーがクラウドを指定した場合は、
references/providers/<provider>.md を読み、マネージドサービスとの対応関係、
クォータ/上限、プロバイダー固有のトレードオフを確認してください。そのプロバイダー用の
ファイルがない場合は、汎用レシピを回答として使用してください。
図
ファンアウトする書き込みパス(writer → random shard)と、読み取り時に集計する合計(read → N shards → cached total)を
可視化するには、プラグイン内の
architecture-diagram スキルを使用してください。シャードにはストアと同じ色を使い、読み取りのファンアウトは
破線の合計矢印で示し、キャッシュされた集計値にはキャッシュの色を使います。
関連する構成要素
data-storage— シャードが物理的にどこに格納されるかを扱うため、これに依存します。 シャーディング/パーティショニングの理論とキー設計は、こちらが担当します。caching— キャッシュされた集計値を提供するために、これと組み合わせます。これにより、急増したカウントの 読み取りのたびに N 個のシャードを再集計せずに済みます。consistency-coordination— 正確なカウントと結果整合性のあるカウントのどちらを選ぶかを判断するために、これに依存します。トランザクション/アトミックなセマンティクスとクォーラムは、こちらが担当します。back-of-the-envelope— これに情報を提供します。シャーディングを正当化し、N を設定するための、カウントあたりの書き込みレートとノードあたりの上限を提示します。system-design— オーケストレーターが概念を担当します。推論のループ、トレードオフの検討方法、10 個の障害モードを扱います。
参考資料
references/deep-dive.md— シャード数の計算、ランダム選択とハッシュ選択の比較、 HyperLogLog の仕組みと誤差、時間バケットの構成、ロールアップ/集計 パターン、照合について説明します。カウンターを詳細に設計する際に読んでください。references/providers/{generic,aws,azure,gcp}.md— 各環境のサービス対応関係、 アトミック性/競合の上限、落とし穴について説明します。