分散SQLの要点: YugabyteDBにおけるシャーディングとパーティショニング

分散SQLデータベースを使うと、グローバルに広がるデータベースに対して、対象とする行がどこにあるのかを知らなくてもクエリを受け付けるサービスが提供されます。そして、クラスタのトポロジーを知らなくても、いずれかのノードに接続することが可能です。テーブルに対してクエリを実行するとデータベースは対象となるデータがクライアントから近い、あるいは地理的にも遠い場合でも、最適なアクセス方法を決定します。

高性能、高可用性、地理的な分散を取り扱う上で、コロケーションかパーティションかといったデータ構造の方法は、最も重要なポイントとなります。
YugabyteDBは、これらの課題を解決する機能をいくつか用意しており、次のような3つのレベルの物理的なデータ構造に分けられます。

  • クエリレベル (YSQL): PostgreSQLのシンタックスを用いて、ユーザは論理的なテーブルを、列の値に基づいて複数に分割。いわゆるテーブル・パーティションと呼ばれる方法
  • ストレージレベル (DocDB): プライマリキーとIndex列を使って、複数のタブレット(tablet)と呼ばれる単位に行を分散。この分散方法をシャーディングと呼ぶ。
  • ファイルレベル: 行、あるいはIndexのエントリを、Sorted Sequence Table (SST)内で、キー単位でソート

この記事では、YugabyteDBにおけるシャーディングとパーティショニングに関する用語や、その定義、そして、適切な利用方法を皆さんにご紹介いたします。

データのシャーディングとデータの分散

シャーディング vs 分散配置

パーティショニングは一般的な用語ですが、シャーディングとは、シェアード・ナッシングと呼ばれるアーキテクチャを持つデータベースを、スケールアウトさせる際の水平方向のパーティショニングを指します。しかしながら、これらの用語は別々のアーキテクチャ・コンセプトで使われてきました。YugabyteDBではこの両方を利用するため、適切に用語を使い分ける必要があります。

まず、YugabyteDBのアーキテクチャについて触れる前に、以下の3点について、一般的な定義をご紹介いたします。

アプリケーション・シャーディング

お手持ちのデータを複数のデータベースに分割すること、これをシャーディングと呼びます。

仮に、ヨーロッパの顧客向けに一つ、北アメリカ地域にも一つ、そして、アジア地域にも一つ、データベースがあったとします。アプリケーションは、どれかひとつ、あるいはユーザの国コードに依存した特定の、あるいはそれ以外のデータベースに接続します。これはアプリケーション・シャーディングと呼ばれますが、これは分散データベースではありません。そのために特別なデータベースを用意する必要もなく、ある程度の自動化が出来ていれば管理ができます。

また、単一のデータベースのインスタンスを複数用意することでもシャーディングは実現できますが、そのためには、アプリケーションのコード量が増えますし、管理するための運用項目も増えることになります。

シャードされたデータベース

もしミドルウェアを導入して、この追加分のコード量に相当する内容を処理させるとなれば、シャードされたデータベースを手にしたということになります。
CitusDBは複数のPostgreSQLデータベースを稼働させて、そこに一つのマスターノードを追加します。この方式においては、マスターあるいはアグリゲーターと呼ばれるノードにより、全クエリ、あるいはクエリの一部がワーカーと呼ばれるノードにリダイレクトされます。
Oracle Shardingも同様に、シャードディレクターとコネクションプールを利用して、多数のデータベースへの、データ操作のオフロードを調整します。この技術は、データウェアハウスにて非常によく利用されることが多く、クエリを分割して並行に処理するケースに向いています。しかしながら、リレーショナルなテーブルにおけるOLTPトランザクションでは複数のシャードに対してインデックスと参照整合性を維持する必要があります。

分散SQLデータベース

分散SQLデータベースにおいては、前述のようなマスターやシャードディレクターといったものはなく、全てのノードは同等です。どこのノードに接続してもよく、そこからデータベース全体に対してクエリをかけても構いません。各ノードは、クラスタのトポロジー、データの場所とメタデータ、さらにはトランザクションの状態、そして単一のデータベースであるために、シーケンス操作における次の値(あるいはキャッシュされている値)を把握しております。最も発展したリレーショナルなデータベースが提供するSQL機能を諦めることなく、伸縮自在な構成が可能になります。

YugabyteDBの分散 SQL

ここまでで重要な技術手法の定義を確認してきましたので、いよいよYugabyteDBのデータ構造について探っていきましょう。まずはストレージレイヤーからです。

“SST files”

最も低レベルな構造である、行とインデックス・エントリによるデータの集合はLinuxファイルシステム上のSorted Sequence Table(SST)に置かれます。これは書き込みとコンパクション(データ領域の圧縮)が行われている際に自動的に行われるもので、読み込み時に対象の行を簡単に見つけることが可能となるものです。各SSTはキーとバージョンの順で行を保持し、追加のインデックスとそれらの位置を把握するためのブルームフィルタも保持されます。コロケーションに関しては、行あるいはインデックス・エントリのキーが同じハッシュ値を持つ場合で、非常に狭い範囲の値において、同じSSTファイルに保存されます。コンパクションによりSSTファイルの数は制限されることになります。このレベルにおいて、YugabyteDBは全てを自動的に行います。ユーザは単にファイルシステムのパスを定義すれば良いだけです。

Tablet シャーディング

先のSSTファイルでは、テーブル(行)とインデックスに対するキーバリューペアを保持していました。ここでは、シャーディングが適切な表現となります。というのも、各tabletは(RocksDBをベースにした)独自の実装をもったデータベースだからです。前述したシャードされたデータベースのような特徴を持ちますが、tablet自体はSQLデータベースではなく、キーバリュー型のドキュメントストアです。信頼性のあるデータストアとして全ての必要な機能を備えており、トランザクションと強い一貫性に対応します。しかしながら、SQLレイヤーが上段に配備されているため、tablet自体を複数のデータベースとして管理をするための大きな負担はありません。ジョインやセカンダリインデックスといった処理はこのレベルでは行われません。というのも、シャード間のトランザクションが妨げられるからです。

シャーディングは自動的に、かつSQLのデータモデルで指定されたオプションをベースに処理されます。:

  • コロケートされた属性、もしくはテーブルグループにて、どのSQLテーブル(例えば、インデックスやパーティション、後述でも記載)がシャードされて、複数の専用のtabletsに配備されるかを定義。またどれがコロケートされており、レプリケートされているか、あるいはシャードがされてないかを定義。
  • プライマリキー、あるいはインデックスの列を定義することで、シャーディングの方法を定義、その際はハッシュあるいはレンジ(昇順、あるいは降順)のシャーディングかを指定。ロードバランスをさせることとホットポイントを防ぐためには、ハッシュカラムにてデータを分散させる。また、複数行を昇順あるいは降順でまとめることで、レンジスキャン(例えば、‘<‘, ’>’, ‘BETWEEN’ などの演算子を使うこと)が可能。
  • あるいはオプションで、tabletの数を指定するのに、システムレベルで
    --ysql_num_tablesを呼ぶか、 SPLIT AT/INTOのテーブル処理においてインデックスレベルでの定義が可能

シャーディングにより、メタデータレベルでのデータモデルの分散方法を定義することが可能となります。しかしながら、データレベルにおいては、これらは自動的に行われ、分散ストレージレイヤーにて、行、もしくはインデックス・エントリがどこに書き込まれるか、あるいは読み込まれるかを自ら管理します。ほとんどのアプリケーションにとって、このアプローチで十分なものとなります。

  • メモリのフットプリントを減らすために、小さなテーブルをコロケートさせる
  • 大きい、あるいは肥大しているテーブルは複数のtabletにシャードされる。ノードには一つかそれ以上が配備され、より多くのノードが追加された場合は、リバランスされスケールする
  • シャードされたテーブルにおいては、レンジ演算子を使って複数行が同時にクエリがなされた場合、レンジスキャンによりどのカラムがアクセスされるかを想定する必要がある

テーブルパーティショニング

前述したシャーディングのアプローチは、全てのノードが同等と捉えていました。同じデータセンター、あるいは複数のアベイラビリティ・ゾーン(AZ)に配備されているという想定です。全てのコンピュートとストレージエンジンに負荷分散をさせたい、そしてスケールアウトも必要に応じてさせたい、と思うかもしれません。データ構造はテーブルとインデックス毎に決定されます。しかしながら、実際の値をベースに、行レベルでの制御をしたいと考えるかもしれません。その場合は、SQLレベルで、列の値を元に、HASH関数や、RANGE、あるいは値のLISTなどを使ってテーブルをパーティションする事が可能です。これは、PostgreSQLの機能で、宣言型パーティショニングと呼ばれているもので、YugabyteDBが、PostgreSQLとコードレベルで互換性があるため、この機能の利用も可能となってます。

パーティション、さらにパーティションプルーニング(条件に合うパーティションのみを検索対象とする)をする理由は、情報のライフサイクル管理にあります。月毎、あるいは年毎にパーティションをするのであれば、ある特定のパーティションを削除するといった具合に、古いデータを削除することが簡単に出来ます。これがスケーラブルに行える唯一の方法です。

別の理由は、分散SQLデータベースにおいて、特定のデータと特定の地域に限定したい場合です、例えば

  • 法的な理由で、特定の地域の特定の国にデータを保存する必要がある
  • レスポンスタイムを想定内に抑えるために、ユーザに近い場所のデータを使う
  • コストの関係で、AZ間、リージョン間、あるいはクラウドプロバイダー間のネットワークトラフィックを抑えたい

最後のオプションとしては、パーティションをテーブルスペースにマップさせることです。テーブルスペースはPostgreSQLで利用されているもので、特定のファイルシステムにパーティションを保持するものです。YugabyteDBにおいては、テーブルスペースをクラスタートポロジーにマップさせることによって、AZやリージョン、クラウドプロバイダーといった配備情報に紐づける事が可能となります。

グローバル・インデックス

データ分散について、いくつかのレベルをご紹介しました。ここで、なぜこれらが必要かを改めて説明しましょう。特にOLTPにおいては、SQLは複雑になります。大抵の場合、複数のキー、通常であれば、一つのサロゲートキーと一つ以上のナチュラルキーを持っています。そして、ユニークなインデックスも必要となります。テーブルは一つのキーのみでパーティションされるため、その他のキーについては、グローバルなインデックスが必要となります。さらにグローバルなインデックスはパーティション可能で、かつテーブルのキーと同じであってはなりません。

YugabyteDBにおいて、tabletは前述した通り、テーブルの行とインデックスのエントリをキーバリューペアとして保存します。これにより、行レベルでの分散による高性能と、同期レプリケーションによる高可用性がもたらされます。

シャードされたデータベースに対して、前述したように(ドキュメントストアとして構成されているデータベースであり)、このレベルにSQL機能を追加すると、非常に限定的な機能となります。SQL機能は単一のシャードに限定されてしまうからです。これが意味するのは、インデックスはローカルのみであり、外部キーはインターリーブ上のテーブルのみに限定されてしまいます。そして、トランザクションは複数のシャードからなる行を操作できません。そのため、プライマリキーのみでテーブルにアクセスしなければならない、階層型データベースに戻ることになります。

OLTPトランザクションにおいては、イミュータブルなIDを取得するためのシーケンス、あるいはUUIDから生成されたサロゲートキーである、プライマリキーがあります。しかしながら、ユニークな制約を持ったナチュラルキーもあります。ここが、我々がSQLデータベースを利用するポイントです、つまり、複数のアクセスパスと、整合性の制約により、我々のデータをより少ないアプリケーションコードの量で管理する事ができます。データウェアハウスにおいては、トランザクションではないグローバルインデックスを受け付けることができますが、OLTPでは好ましくありません。ユニークな制約を強要するのも、障害後あるいは論理的レプリケーションにおける二重書き込みなどの、データ不整合を防ぐためです。

簡単な例を考えましょう: ある国の国民のテーブルがあるとします。ナチュラルキーは社会保障ナンバーとします。これはユニーク制約が設定されます。プライマリキーは生成された数にて定義され、不変で、外部キーにて参照されます。

シャードされたデータベースの場合、例えばCitusDBあるいはそれ以外のForeign Data Wrapperをベースにしたデータベースの場合、このようなことはできません。というのも、各シャードはモノリシックなデータベースとなり、複数のデータベースに対して、単一性を持たせることが出来ないためです。もしハッシュあるいはサロゲートキーにてシャードできれば、ユニーク制約や、外部キーにより、同じシャードのみに限定することが可能となります。しかし、ユニークなナチュラルキーというのは保証できません。SQL機能を各シャードに持つことはできますが、グローバルレベルではありません。

このテーブルに対して必要なものは、プライマリーキーでのテーブルのシャーディング、外部キーによるジョイン、そして、ビジネスバリューによるクエリのためのナチュラルキーでのインデックスのシャーディングです。これがグローバルインデックスが必要となる箇所です。

先の例だと、国ごとにパーティションされて、各大陸を物理的な地域に分離することができます。そして、目的は配置場所によるパーティションであり、データの分散ではないため、ローカルのインデックスのみで十分となります。

まとめ

YugabyteDBのような分散SQLデータベースにおいて、テーブルをシャーディングをする目的は、低レベルでのスケールアウトにあります。その上で、ユニーク制約や、セカンダリインデックス、外部キー制約、そして、トランザクションを定義することができます。パーティションとテーブルスペースは、物理的な位置情報に基づいた管理がしやすくなります。分散SQLデーターベースはその両方を必要とし、どのノードにも透過的に接続可能な一つのデータベースアプリケーションとして扱う事ができます。シャーディングはtablet上でデータ分散のために動作し、ハッシュもしくはレンジ関数を行とグローバルインデックスエントリに適用します。パーティショニングは、データ配備のためにテーブルパーティション上にて動作し、レンジあるいはリストをローカルインデックスを用いてテーブルに適用します。Tabletシャーディングは、YCQLとYSQLの両方に適用されますが、パーティショニングはYSQLのみの機能です。より詳細は以下のドキュメントサイト(英語版)をご参照ください。:

YSQLのコンセプトとシンタックスは、PostgreSQLのCREATE TABLECREATE INDEXステートメントの拡張版です。YCQLにおいては、一般的な”パーティション”がtabletの自動的なシャーディングを表す言葉として使われています。これは、Cassandraにおける名称であり、tableレベルで宣言されるパーティションがないためです。

本記事は、The Distributed SQL Blogsにて2021年11月19日に公開されたDistributed SQL Essentials: Sharding and Partitioning in YugabyteDBを翻訳および一部訳注を追加しております。最新情報は英語版の記事を参照してください。

--

--