シャーディングは、1 つのデータベースを複数に分け、それぞれがデータの一部を持つようにします。1 台のマシンでは書き込みをさばけなくなったり、データを保持しきれなくなったりしたときの答えですが、同時にシステムの一生で最も高くつく判断の 1 つでもあります。クエリ、トランザクション、スキーマ変更、運用のすべてが難しくなるからです。
この記事では、シャーディングが割に合うのはいつか、シャードキーの選び方、主なシャーディング方式、クエリが正しいシャードを見つける方法、ダウンタイムなしでリシャーディングする方法を扱います。図は、論理シャードを使ってハッシュでシャーディングした注文データベースを示しています。
まずはほかの手をすべて試す
シャーディングの前に、より安い選択肢をおおむね次の順で確認しましょう。
- クエリとインデックスを直す。 「データベースが遅すぎる」という問題の多くは、インデックスの欠如か、読みすぎるクエリです。
- スケールアップする。 メモリーが多くディスクが速い大きなマシンで、多くのシステムは何年も持ちこたえられます。
- リードレプリカを追加する: 読み取り中心の負荷に有効です。それに伴う遅延については データベースレプリケーションの記事 で扱っています。
- よく読まれるデータをキャッシュする: キャッシュ戦略の記事 で紹介している方法です。
- 機能ごとに分割する: 分析、検索、監査のテーブルを専用のデータベースに移します。
これらの手を打っても書き込み量やデータ量が 1 台のプライマリの限界を超え、しかも悪化する傾向にあるなら、シャーディングしましょう。
シャードキーを選ぶ
シャードキーは、行がどのシャードに置かれるかを決めます。良いキーには 3 つの性質があります。
- よく使うクエリがすべてキーを含む。 注文一覧のクエリが顧客で絞り込むなら、
customer_idを使えばルーターはそのクエリを 1 つのシャードに送れます。キーを含まないクエリは、すべてのシャードに問い合わせる必要があります。 - 負荷を均等に分散する。 異なる値が多く、特定の値がトラフィックの大部分を生み出すことがありません。
- 関連するデータをまとめておける。 ある顧客の注文、明細、支払いがすべて 1 つのシャードにあれば、トランザクションと結合はローカルで済みます。
マルチテナントの SaaS なら、通常はテナント ID がキーになります。一般消費者向けのアプリなら、ユーザー ID か顧客 ID です。良さそうに見えて負荷を集中させるキーには注意しましょう。タイムスタンプ(新しい書き込みがすべて 1 つのシャードに行く)や国コード(1 つの国が大半を占める)がその例です。
範囲、ハッシュ、ディレクトリ
| 方式 | 行の配置方法 | 長所 | 短所 |
|---|---|---|---|
| 範囲 | A-F、G-M のようなキーの範囲ごと | 範囲スキャンが 1 つのシャードで済む | 新しい範囲や人気の範囲がホットスポットになる |
| ハッシュ | キーのハッシュ値ごと | 均等に分散する | 範囲クエリがすべてのシャードに当たる |
| ディレクトリ | ルックアップテーブルが各キーやグループをシャードに対応付ける | 完全に制御でき、移動も簡単 | ディレクトリが重要な依存先になり、高速である必要がある |
よくあるハイブリッド方式では、キーを固定数の 論理シャード(たとえば 1,024 個)にハッシュし、論理シャードを物理ホストに対応付ける小さなディレクトリを持ちます。キーが論理シャード間を移動することはなく、ホスト間を移動するのは論理シャード単位です。Instagram のエンジニアリングチームは 2012 年にこの方法を紹介しており、数千の論理シャードを、はるかに少ない台数のデータベースサーバーに載せていました。
クエリのルーティング
各キーがどこにあるかを、何かが知っている必要があります。
- アプリケーション内: キーをハッシュしてホストを調べるライブラリを使います。シンプルですが、すべてのサービスとすべての言語に同じロジックが必要です。
- プロキシ内: アプリケーションとデータベースの間に置きます。Vitess は MySQL 向けに VTGate プロキシでこれを行い、Citus は Postgres 向けにコーディネーターノードで行います。
どちらの場合も、シャードマップをキャッシュし、変更されたら再読み込みします。移動中に古いマップを使ってしまうのは、リシャーディングの典型的なバグです。切り替え後にあるルーターが古いホストに書き込み、その行が新しいホストから黙って消えてしまいます。マップにバージョンを付け、古いバージョンを持つ書き込みは拒否しましょう。シャードキーのないクエリはすべてのシャードに送られて結果がマージされます。まれな管理作業ならそれで問題ありませんが、通常のアクセス経路としては破綻します。
ダウンタイムなしのリシャーディング
論理シャードを使っていれば、容量の追加は、一部の論理シャードを新しいホストに移すことを意味します。
- 古いホストがサービスを続けている間に、論理シャードのデータを新しいホストにコピーします。
- データベースのレプリケーションか変更データキャプチャ(CDC)を使って、コピーが追いつくまで進行中の変更を流し続けます。
- 対象の論理シャードへの書き込みを短時間止め、最後の変更が届くのを待ち、シャードマップを更新して再開します。
- 検証してから、移動したデータを古いホストから削除します。
Vitess と Citus は、この手順のバリエーションを自動化しています。必要になる前にリハーサルしておきましょう。負荷がかかった状態で初めてリシャーディングする日は、ひどい 1 日になります。
シャードをまたいで使える ID
データベースごとのシーケンスでは、どのシャードでも同じ ID が払い出されます。調整なしで一意になる ID を使いましょう。UUID(バージョン 7 ならおおむね時刻順になり、インデックスにやさしい)や、タイムスタンプ、シャードまたはマシンの番号、カウンターを組み合わせた Snowflake 方式の 64 ビット ID です。ID に論理シャード番号を埋め込むと、注文 ID だけで、ルーターが誰にも問い合わせずにどこを探せばよいかわかるというおまけも付きます。
アーキテクチャ図
ダイアグラムの内容
- Order service
- Shard router
- Shard map
- Host 1: shards 0-255
- Host 2: shards 256-511
- Host 3: shards 512-767
- Host 4: shards 768-1023
- Host 5: shards 896-1023
- Analytics warehouse
- query + key
- cached lookup
- move shards 896-1023
- CDC from all hosts
図の読み方は次のとおりです。
- 左: 注文サービスが、すべてのクエリを顧客 ID 付きでシャードルーターに送ります。
- 中央: ルーターが顧客 ID を 1,024 個の論理シャードのいずれかにハッシュし、キャッシュしたシャードマップ(破線)でホストを見つけます。
- 右: 4 台の Postgres ホストが、それぞれ 256 個の論理シャードを持っています。ホスト 4 は上半分を新しいホスト 5 に移動しており、これが進行中のリシャーディングの手順です。
- 右上: 顧客をまたぐレポートは、変更データキャプチャで供給される分析用データウェアハウスを読むので、シャード全体に問い合わせることはありません。
- Markdown ブロック には、クエリを書く全員が守るべきルールを並べています。
受け入れるコスト
この設計をテナントごとのシャードにした変形版は マルチテナント SaaS のアーキテクチャ図 で紹介しており、そこでは以下のトレードオフがテナントごとに当てはまります。
- シャードをまたぐトランザクション は、分散トランザクションかサーガになります。まれにしか起きないように設計しましょう。
- シャードをまたぐ一意性制約 には、データベースごとのシーケンスではなく、グローバルな ID 方式が必要です。
- スキーマ変更 はすべてのシャードで実行されるので、マイグレーションにはツールが必要で、途中までしか完了しない状態にも耐えなければなりません。
- 運用が倍増します: シャードごとにバックアップ、アップグレード、監視が必要です。
シャーディングの図に描くべきもの
- シャードキー と方式(ハッシュ、範囲、ディレクトリ)。
- 論理シャードと物理シャード: 分けている場合に描きます。
- ルーティングを行う場所: ライブラリ、プロキシ、コーディネーターのいずれか。
- キーを含まないクエリの経路: データウェアハウスや検索インデックスなどです。
- リシャーディングの状態: 移動が進行中の場合に描きます。
Archboard で作る
- Google または GitHub でサインインし、図を作成します。
- C4 モデルパックから、サービスとシャードルーター用に コンテナ ノードを追加し、シャードマップ用に データベース を追加します。
- データパックからホスト用に Postgres ノードを追加し、それぞれの論理シャードの範囲をラベルに書きます。新しいホスト用にもう 1 つ追加します。
- データパックからレポート用に データウェアハウス を追加し、点線の CDC エッジでデータを供給します。
- リシャーディングによる移動を破線のエッジで描き、シャーディングのルールを書いた基本パックの Markdown ブロックを追加します。
この例で使うダイアグラムノードは 10 個で、Free プランの範囲に収まります。
よくある質問
シャーディングとパーティショニングの違いは何ですか? パーティショニングは通常、1 台のデータベースサーバーの中でテーブルを分割することを指します。シャーディングはデータを複数のサーバーに分散させ、各サーバーがデータの一部だけを持つようにします。
シャードキーはどう選べばよいですか? よく使うクエリがすべて絞り込みに使い、関連するデータをまとめておける属性で、異なる値が多く負荷が均等なものを選びます。テナント ID や顧客 ID などです。
論理シャードとは何ですか? 固定された多数のハッシュバケットで、それより少ない数の物理サーバーに対応付けられます。論理シャードを丸ごとサーバー間で移動させることで、容量を追加するたびにすべてのキーを再ハッシュせずに済みます。
すべてのシャードにまたがるクエリはどう実行しますか? 頻繁に使う経路では避けます。レポートにはデータウェアハウスにデータを複製し、まれな管理作業ならルーター経由で全シャードに問い合わせるクエリでも構いません。
いつシャーディングすべきですか? インデックス、スケールアップ、レプリカ、キャッシュ、機能分割を行ったうえで、それでも書き込み量やデータ量が、よく調整された 1 台のプライマリでは扱えない水準を超え、その傾向が続く場合です。