【Azure】CosmosDBにおけるパーティション入門ガイド【初心者向け】

こんにちは、サイオステクノロジーの佐藤です。
満を持して2024年アドベントカレンダー3回目の登場です!

今回はCosmosDBのパーティションについてご紹介します。
パーティションは、前回ご紹介したRUと同じくCosmosDBにおいて非常に重要な概念となります。

  • コンテナ作るときにPartitionKey設定するけどこれって何の値?
  • どんな値をパーティションキーに設定すればいいの?
  • 論理パーティションと物理パーティションキーって何?

といった方は、是非最後までご覧ください!

はじめに

今回は、CosmosDBのパーティションについて勉強したことをアウトプットしていきます。

  • パーティションとは何か
  • 論理パーティションと物理パーティションの違い
  • 分割方法はどういったものがあるか
  • 良いパーティションキーの設定とは?

といった点について紹介していきたいと思います。

ただパーティション分割の設計ノウハウまで突っ込めるほどまだ精通できていないので
あくまで概要の紹介にとどまりますが、ぜひ最後までご覧ください!

パーティションとは

そもそもパーティションとはなんでしょうか?

CosmosDBにおけるパーティションについては、以下2つのものが存在します。

  • 論理パーティション(Logical Partition)
  • 物理パーティション(Physical Partition)

これらは似て非なるものであるため、
文脈や会話の中で「パーティション」と出てきた際は、どちらを指しているかを正しく把握しておきましょう。

論理パーティション

論理パーティション(Logical Partition)は、コンテナがパーティションキーによって分割されたサブセットの単位になります。
“論理”という名がついている通り、ハードウェア的に分離されてはいません。
あくまでCosmosDB上でソフトウェア的に分離されているサブセットになります。

ではパーティションキーとはなんでしょうか?
CosmosDBでコンテナを作成する際に、指定が必須の項目でもあります。

これはコンテナを論理パーティションに分割し、水平方向にスケーリングするために使われる値です。

コンテナがどのように論理パーティションに分割されるかを見ていきたいと思います。
例として、以下のようなデータベース・コンテナを考えます。

Property Name
Database Restaurants
Container Foods
Partition Key /country

そして、以下のようなアイテム群がFoodsコンテナに格納されているとします。

 

[
    {
        "foodId": "001",
        "name": "寿司",
        "country": "日本",  //partition key
        "price": "2000",
        "category": "米類"
    },
    {
        "foodId": "002",
        "name": "うどん",
        "country": "日本",  //partition key
        "price": "500",
        "category": "麺類"
    },
    {
        "foodId": "003",
        "name": "パスタ",
        "country": "イタリア",  //partition key
        "price": "1000",
        "category": "麺類"
    },
    {
        "foodId": "004",
        "name": "餃子",
        "country": "中国",  //partition key
        "price": "300",
        "category": "点心類"
    }
]

この場合、以下の図ような形で論理パーティションに分割され、その論理パーティションの中に該当アイテムが格納されます。

なお、この論理パーティションの数には制限はありませんが、ストレージ容量に関しては制限があります。
いち論理パーティションあたり20GBまでのデータしか格納することが出来ません。

そのため膨大なデータを格納する場合は、より論理パーティションを分割するために

  • パーティションキーの値を見直す
  • 合成パーティションキー(後述)を採用する
  • 階層パーティションキー(後述)を採用する

といった対応を検討します。

なお、一度作成した論理パーティションの内容は変更できません。
新規のパーティションキーを設定して、新たにコンテナを作り直し、データを移行する必要があります。

物理パーティション

論理パーティションと双璧をなしているのが物理パーティション(Physical Partition)です。

ただし、CosmosDBを利用するエンジニアがこの物理パーティションの分割を考慮する必要はありません。
そこはCosmosDB側で完全に管理してくれます。

論理パーティションとの関係性を見てみたいと思います。

1つ、または複数の論理パーティションが単一の物理パーティションに割り当てられます。
逆に1つの論理パーティションが、複数の物理パーティションに割り当てられることはありません。

先程の例に照らし合わせると、以下のような感じです。

物理パーティションの容量も50GBといったように制限はありますが、これを超過しそうになった場合でも
CosmosDB側で、物理パーティションを分割して論理パーティションの割り当てを入れ替えることによってこれを回避します。

インパーティションクエリとクロスパーティションクエリ

論理パーティションと物理パーティションの概要が分かったところで
適切にパーティション分割が行われることによるメリットについてお話したいと思います。

先程、論理パーティションを分割をすることで20GBのデータ許容量を超えないようにすることが可能であると述べましたが
データ容量の観点の他にも、スループットの面で恩恵を受けることができます。

例えばFoodsコンテナに対して、以下のようなクエリを実行します。

select * from c where c.country = "日本"

このクエリを実行した際、コンテナ全体(全ての物理パーティション)に対して検索がかかるのではなく
日本の論理パーティション内、正確にはその論理パーティションと対応する 物理パーティションA でのみ検索が行われます。

このように検索範囲が絞られることにより検索速度も向上しますし、利用されるRU数も少なく済みます。
このように、ひとつの物理パーティション内でのみ検索が行われるものを、インパーティションクエリと呼びます。

一方で

select * from c where c.category = "麺類"

といったクエリの場合は、論理パーティションを跨いだ検索が行われます。

つまり、該当するアイテムがどこの物理パーティションに存在するかが不明であるため、**全ての物理パーティションごとに**検索が行われます。
これをクロスパーティションクエリと呼び、インパーティションクエリに比ると検索速度が劣化します。

とはいいつつ、実際のところ完全にインパーティションクエリだけでアプリケーションを実装することは不可能に近いと思います。
CosmosDB側もクロスパーティションクエリが行われることを想定しており、このクエリを並列化し、高速化するような最適化の手段がSDKなどには提供されています。

以下のようなケースを除いて、そこまで必死に回避する問題ではないとMSのドキュメントにも書かれていました。

  • 30,000 より多くの RU をプロビジョニングする予定
  • 100 GB を超えるデータを格納する予定

スループットの分散

ここからが重要なポイントなのですが
コンテナーに対してプロビジョニングされたスループットは、物理パーティション間で均等に分割されます。

例えばFoodsコンテナに4000RU/sのスループットが割当たっている場合
上記の図においては、物理パーティションA,Bそれぞれに2000RU/sのスループットが割り当たります。

そして、選択したパーティションキーによってデータとアクセスが均等に分散されるようにパーティション分割が行われている事ことが非常に重要です。
このパーティション分割が正しく行われない場合、ホットパーティションと呼ばれる現象が発生することが懸念されます。

ホットパーティション

ホットパーティションは、特定のパーティションにデータやアクセスが集中していることを指します。

例えば先程のデータベースの例で、以下のような状況だとします。

  • 日本食がとても有名なレストラン
  • 日本食のメニューがとても多い
  • お客さんの大半が日本食を注文する

すると、データベースの状況としては以下のようなことが起こりえます。

先程も述べたように

  • 物理パーティションは与えられたスループットを平等に分割して利用する
  • ひとつの論理パーティションを複数の物理パーティションに分割することはできない

といった事情があるため、このひとつの物理パーティションにデータ・アクセスが集中することは避けられません。
このような状況では

  • 物理パーティションA
    • 1000RU/sのスループットでは処理しきれない
  • 物理パーティションB
    • 1000RU/sのスループットを持て余す(コストの無駄)

といった状況が起こりえます。

また、ストレージの面でも同様のことが言えます。
最初に述べたように、1つの論理パーティションの最大容量は20GBです。
そのため、1つの論理パーティションにデータが集中してしまうと、すぐにデータ許容量を超えてしまいます。

そのため、各物理・論理パーティションにデータやアクセスが分散されるように、パーティション分割を行うことが非常に重要です。
なお、ホットパーティションは開発時はデータが少なくて気づかないことが多く、**本番運用の際に大量のデータが入って初めて発覚するケースも多い**ため注意が必要です。

各パーティションにおける利用率はAzureMonitorにて確認が可能です。

例えば、PartitionKeyRUConsumptionの指標を見ることで、パーティションキーあたりのRU/s使用率を確認できます。
また、PartitionKeyStatisticsの指標を見ることで、各パーティションの容量を確認することができます。

これらの値を参照しながら、適切にデータ・アクセスが分散しているかを確認しましょう。

パーティションキーの設定

適切なパーティション分割を行うために、パーティションキーの設定が重要であることが分かりました。
ここでは、パーティションキー設定に関する方法をご紹介します。

まず、大前提としてCosmosDBが顧客(アプリ)によってどのように使われるかを検討することがとても大切です。

全く同じデータを格納する場合でも、使われ方によってパーティションキーの選択は変わってきます。

例えば先程のRestaurantデータベースの例においては

  • 読み取りのアクセスが多いのか、書き込みのアクセスが多いのか
  • 何をフィルターとして検索することが多いのか
    • 値段
    • カテゴリ
    • 国情報

など、まずはどのようにアプリが使われるかをしっかりと把握します。

パーティションキーの設定の基本

アプリの使われ方を把握したら、次にパーティションキーの検討を始めます。
まず、パーティションキーの設定を設定するうえで、以下のことに気を付けます。

  • パーティションキーの値が更新されないこと
  • Stringの型であること
  • パーティションキーの値の種類が広範囲に広がること(=カーディナリが高いこと)
  • 各論理パーティションに対してデータ・アクセスが均等に分割されること
  • パーティションキーの値の長さを最大でも2KB以内とすること

これらの点を踏まて、パーティションキーを設定します。

そのうえで、「アイテム要素1つだけだとうまく均等に分散させれなさそう」といった場合に

  • 合成パーティションキー
  • 階層パーティションキー

といったパーティションキー設定の方法を検討します。

合成パーティションキー

合成パーティションキーは、複数の要素を組み合わせをキーとする方法です。

これまで扱ったデータベースを題材とすると、"country + category"といった形の合成キーを考えます。

コンテナ作成時のPartition keyの値としては/partitionKeyといったものを追加し
アイテムをアップロードする際に、以下のような値を追加します。

{
    "foodId": "001",
    "name": "寿司",
    "country": "日本",
    "price": "2000",
    "category": "米類",
    "partitionKey":"日本-米類" //countryとcategoryを組み合わせた文字列を生成
}

他のアイテムに関しても同様に値を追加します。

{
    ...
    "partitionKey":"日本-麺類"
    ...
},
{
    ...
    "partitionKey":"イタリア-麺類"
    ...
},
{
    ...
    "partitionKey":"中国-点心類"
    ...
}

これにより、論理パーティションが4つ作成されることが想定されます。 論理パーティションが3つ→4つに増えたことにより、先程起きていたホットパーティションを多少回避できそうです。 ただし、合成パーティションキーを実施することによるデメリットも発生します。 先ほどインパーティションクエリの例で挙げた以下のクエリですが

select * from c where c.country = `日本`

合成パーティションキーを使用した場合は、country単体ではパーティションキーは設定していないため、クロスパーティションクエリが発生します。
また、そもそもPartitionKeyを設定するためだけに、無駄な結合字列を追加するのもあまり気持ちいいものではありません。

これらのメリット・デメリットを踏まえて合成パーティションキーの採用を検討する必要がありそうです。

階層パーティションキー

階層パーティションキー(HPK)は、階層的にパーティションキーを設定する方法です。
コンテナ作成時にAdd hierarchical partition keyを選択することで、3階層まで設定が可能です。

例えば、上記の例だと"country/category"といった2階層のパーティションキーを設定しています。

この時、countryのパーティションキーによって論理パーティションに分割され、
更に2階層目categoryによってサブバーティションと呼ばれるものに分割されます。

こちらも先程の合成パーティションキーと同じく、データやアクセスを論理パーティションで分散できるものです。
では合成パーティションキー比べて何が違うというと、

  • そもそも無駄な情報(partitionKey)を追加しなくていい
  • 上位のパーティションキーのみでの検索が可能

といったメリット点があります。

このサブセットを有効活用するようなクエリとしては以下のような形となります。

select * from c where c.country = `日本` and c.category = `麺類`

この場合、サブセットパーティション単位でのクエリとなるため、インパーティションクエリとして実行が行われます。

また、以下のような上位のパーティションキーのみをフィルタに用いたクエリでも効率的に検索が行えます。

select * from c where c.country = `日本`

「以下のクエリでも効率的に検索が行えます。」と曖昧に書いたのですが、理由としてはその仕組みにあり

日本/麺類日本/米類といったサブセットがある場合、これらがそれぞれ別の物理パーティションに割り当てられる可能性があります。
つまり、日本(category)というレベルで見た場合は、別々の物理パーティションに分かれる事があるのです。

そのため、どこの物理パーティションにアイテムが存在しているか分からず、全ての物理パーティションに対して検索がかかるのかな…
と思ったのですが、実際のところはそうではないみたいです。

ここはMSのドキュメントを引用しますが、どうやらいい感じにルーティングを行ってくれるようです。

完全またはプレフィックスのサブパーティション分割されたパーティション キー パスを指定することで、完全なファンアウト クエリを効率的に回避できます。 たとえば、コンテナーに 1000 個の物理パーティションがあり、特定の TenantId 値がそのうちの 5 つにしかない場合、クエリは少数の関連する物理パーティションにのみルーティングされます。

※ここで書かれているTenantIdは上記最上位のPartitionKeyを指しており、上記の例でいうcountryと同列。
※ファンアウトクエリ=全ての物理パーティションごとにクエリを実行すること。

ということで、詳細までつかめていないので「効率的に検索が行えます」といった曖昧な書き方にとどめました。
このあたりしっかり分かったらまた追記修正します。

それで話は少し戻るのですが
今回、country/categoryのサブパーティションの単位でストレージ20GBの制限が設けられます。

つまり、countryのレベルで見た場合、20GB以上のデータを保持することが可能になるのです。
これが単一のcountryをパーティションキーとした比較した場合のメリットになります。

これまでの情報をまとめると、階層パーティションキーを利用することで

  • データ・アクセスを分散させることが出来る
  • 上位のパーティションキーだけでも20GB以上のデータを保持できる
  • 上位のパーティションキーだけでも効率的な検索が行える

といったメリットがあることがうかがえます。

ただ、この効率の良い検索もあくまで上位パーティションキーのみが対象で

select * from c where c.category = `麺類`

といった、下位のパーティションキーで検索をかけた場合はクロスパーティションクエリとして扱われるので注意が必要です。

パーティションキーにアイテムIDを設定する

最後に、パーティションキーの値にアイテムID(=”/id)”を採用することについてご紹介します。

この”/id”とはCosmosDBにアイテムを追加した場合に自動で割り当てられる値です。

例えば以下のようなデータを追加した場合でも

このような値が登録され、idというGUIDのような値が割当たっています。

(もしくはidの値を自分でも設定することが可能ですが、ここでは「アイテムごとに異なる値である」という前提で話を進めます。)

この値をパーティションキーとすることで、全データを全て異なる論理パーティションに分散させることができます。
分散の極みであり、ホットパーティションとは無縁ですね。

なおかつ、このid値が分かっている場合は非常に高速で、低いRUで検索が行うことができます。

「え、そんなに何十万、何百万っていう個数の論理パーティション作っていいの…?」
と不安になる方もいるかもしれませんが、心配ありません。

MSのドキュメントにも以下のように書かれていました。

ID をパーティション キーにした場合、顧客の数だけ論理パーティションが存在し、各論理パーティションに 1 つのドキュメントしか格納されていない状態になるのでは、と不安になるかもしれません。 何百万人もの顧客がいれば、何百万個もの論理パーティションができることになります。
ただし、これはまったく問題ありません。 論理パーティションは仮想的な概念であり、持てる論理パーティション数に制限はありません。 Azure Cosmos DB により、同じ物理パーティション上に複数の論理パーティションが併置されます。 論理パーティションの数またはサイズが大きくなると、Cosmos DB により、必要に応じて新しい物理パーティションに移動されます。

参考:https://learn.microsoft.com/ja-jp/training/modules/implement-non-relational-data-model/6-choose-partition-key

そのため、遠慮なく必要であればidの値をパーティションキーとして設定しましょう。

まとめ

今回はCosmosDBのパーティションについてまとめてみました。
論理パーティションと物理パーティションの違いや、パーティションキーの設定についてなんとなく分かっていただけたのではないでしょうか?
パーティションキーの設定に関しては色々と奥が深そうなので、また触っていくうちにアウトプットしていきたいと思います。

引き続きCosmosDBの学習を続けていきたいと思うので、また読んでいただけると幸いです。
ではまた!

参考

おまけ

前回の記事でも紹介しましたが、2024年のMicrosoft Ignite でCosmosDBのベーシックな内容が紹介されていました。
Partition Keyの設定に関しても触れられていたので、是非こちらも見ていただければと思います。

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

1人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です