Skip to content

Latest commit

 

History

History
367 lines (203 loc) · 15.7 KB

README.md

File metadata and controls

367 lines (203 loc) · 15.7 KB

sample-cosmosdb-fbook

CosmosDBのスキーマー設計は、RMDBと異なる部分があります。 RMDBの感覚でスキーマー設計をすると、RUを大きく消費したり、性能がでないケースがあります。

本入門では、Facebook的なアプリをサンプルにCosmosDBらしいスキーマ設計とは何かを学んでいただけます。

本資料は、MicrosoftのCosmosDBチームのGithubで公開されているCosmosDB-DataModeling.pptxと、アメリカで開催されたセミナーの内容をもとに書き起こし&意訳&追記したものになります。

Facebook的アプリとは

多くのユーザーがいて、ユーザーには友達がいて、記事を投稿できて、そこにLikeやコメントができ、ユーザーのトップページは記事一覧になっているアプリです。 このアプリをまずはRMDB的なスキーマー設計で作成し、それぞれの機能でのRU消費量とレスポンスを確認します。そこから各クエリとコマンドの結果を改善するには、どうスキーマーを書き換えて、どういうストアドプロシージャを作成するといいのかを見ていき、最後には同じアプリなのに設計(スキーマー、ストアドプロシージャ)が変わることで、消費RUが大幅に改善することを理解できます。

アプリケーションの要件

ユーザー は、 投稿 を作成できる。 ユーザーは、投稿に、 Like ができ、 コメント をつけられる。 トップページを表示すると最近の投稿一覧が表示される。 特定ユーザーのすべての投稿を一覧表示できる。投稿のすべてのコメントを一覧表示でき、Likeを押したユーザーを見れる。 投稿は、投稿者の名前とコメント数とLike数が表示される。コメントは、コメントの投稿者の名前も表示される。 一覧が表示されたとき、投稿は概要だけが表示される。

RMDB的な観点で設計した場合の例

この要件をみたすアプリケーションをモデルリングすると、下記のようなスキーマー構成にするかと思います。

しかし、CosmosDBでは、このモデリングでは性能が十分にでなかったり、課金額が高くなってしまいます。 この資料では、CosmosDBとしては、どうモデリングをしたらいいのかを考えていきます。

アクセスパターンの整理

このアプリケーションを実現するには、どのようなリクエストがあるでしょうか。 大きく2つに分類できます。

  • Command (書き込み処理)
  • Query (読み取り専用)

この分類に基づいて、アプリで必要な処理を設計してみます。

  • <C1> ユーザー情報の作成と編集
  • <Q1> ユーザー情報の検索
  • <C2> 投稿の作成と編集
  • <Q2> 投稿の検索
  • <Q3> ユーザー投稿の一覧
  • <C3> コメントの作成
  • <Q4> 投稿のコメントの一覧
  • <C4> 投稿へのLike
  • <Q5> 投稿のLike一覧
  • <Q6> 直近X件の投稿一覧

こういった処理がCosmosDBに対して実行されることになります。

CosmosDB のデータ格納

CosmosDBデータベースでは、ドキュメント をコンテナー に保存します。 Likeはテーブルに行を保存するのでしょか。

このように、ドキュメントとコンテナーを1対1で紐づけることを考えるかもしれません。

CosmosDBでは、こちらのほうがいいでしょう。

性能

予測性能は、プロビジョニングに依存します。 プロビジョニングは、秒間リクエスト単位・Request Units per second (RU/s)で表現できます。 CPU、メモリ、I/Oのリクエストコストを代替しています。

性能はプロビジョニングができます。プロビジョニング単位は、データベースレベルとコンテナーレベルで制御できます。プロビジョニング性能は、API経由でプログラムで変更できます。

この性能については、コスト削減には重要な話です。

パーティショニング

CosmosDBは、データをパーティショニング分割して保存することで、水平スケーラビリティがあります。 コンテナーのパーティションキーに基づいて、データを論理的にパーティショングループに分けます。

素晴らしいパーティションキーは、ストレージの観点やスループットなどからとてもバランスが取れたパーティションのときです。 読み取りクエリは、一つのパーティションからすべての結果を取得すべきです。

ベンチマークするデータ量

  • 100,000ユーザー
  • 5-50 投稿/ユーザー
  • 0-20 コメント/投稿
  • 0-100 like/投稿

始める前のデータ保存状況

ユーザーコンテナー(users)に、IDをパーティションキーにしてユーザードキュメントを保存します。

投稿コンテナー(posts)に、それぞれドキュメントを保存します。例えば投稿に関するドキュメントを保存した場合。

こちらは、コメントに関するドキュメントを保存した場合。

そして、Likeに関するドキュメントを保存した場合。

初めの段階での実装方法

<C1> ユーザー情報の作成と編集 7ms/5.71RU

userドキュメントをusesコンテナーに格納します。これは特に問題ありません。

<Q1> ユーザー情報の検索 3ms/2.90RU

usersコンテナーからパーティションキーのidで検索してuserを取得しています。これは特に問題ありません。

<C2> 投稿の作成と編集 9ms/8.76RU

投稿をpostsコンテナーに登録します。これも特に問題ありません。

<Q2> 投稿の検索 9ms/19.54RU

投稿者のIDで、usersコンテナーからPKであるidでフィルターして、ユーザー名を取得します。これは特に問題ありません。

投稿とコメント数とLike数を取得するために、postsコンテナーからPKであるpostIdでフィルターして、情報を取得します。これも特に問題ありません

<Q3> ユーザー投稿の一覧 130ms/619.41RU

Facebookのトップページの用にフォローしているユーザーの投稿一覧と各投稿のコメント数とLike数を取得します。 各投稿の投稿者名とコメント数、Like数を取得するために、投稿数分繰り返しクエリを実行します。 これは読み取り数が多くなり性能問題につながります。

投稿を取得するために、postsコンテナーからPKではないuerIDでフィルタリングをしています。これは全件アクセスする必要があり、非常にコストがかかります。

<C3> コメントの作成 7ms/8.57RU

コメントをpostsコンテナーに登録します。これは特に問題ありません。

<Q4> 投稿のコメントの一覧 23ms/27.72TU

特定の投稿につけられたコメント一覧を取得するために、postsコンテナーからpkのpostIDでフィルタリングして情報を取得しています。これは特に問題ありません。

コメント者名を取得するために、コメント数分usersコンテナーからpkでフィルタリングして結果を取得しています。これはN+1になるのでコストがかかり問題があります。

<C4> 投稿へのLike 6ms/7.05RU

Likeをpostsコンテナーに登録します。これは特に問題ありません。

<Q5> 投稿のLike一覧 59ms/58.92RU

特定の投稿に紐づいたLikeをpostsコンテナーからPKのpostIdでフィルタリングして結果を取得しています。これは特に問題ありません。

Like者名を取得するために、Like数分usersコンテナーからpkでフィルタリングして結果を取得しています。これはN+1になるのでコストがかかり問題があります。

<Q6> 直近X件の投稿一覧 306ms/2063.54RU

各投稿の投稿者名とコメント数、Like数を取得するために、投稿数分繰り返しクエリを実行します。 これは読み取り数が多くなり性能問題につながります。

直近の投稿を取得するために、postsコンテナーからpkではないtypeでフィルタリングして結果を取得しています。これは非常にコストがかかります。

改善をしていく

一回のリクエストで複数のクエリを実行する問題がありました。また、パーティションキーではない条件で絞り込む、パーティションスキャンを引き起こす問題のあるクエリもありました。 それでは、それぞれ改修していきましょう。

非正規化

改修をするには非正規化を活用します。 ストアドプロシージャーを使うことで、同じロジカルパーティション内で非正規化ができます。

  • Javascriptで書く
  • 一つの論理パーティションを対象とする
  • アトミックトランザクションとして実行する

<Q3> ユーザー投稿の一覧 130ms/619.41RU → 28ms/201.54RU

Facebookのトップページの用にフォローしているユーザーの投稿一覧と各投稿のコメント数とLike数を取得します。

そして問題となったのは下記でした。

各投稿の投稿者名とコメント数、Like数を取得するために、投稿数分繰り返しクエリを実行します。 これは読み取り数が多くなり性能問題につながります。

問題の本質的には、各投稿がコメント数とLike数を保持していないために、そのデータを取得するのに追加でクエリを実行しなければいけないからです。言い換えれば、各投稿にコメント数とLike数があれば、クエリ発行数を減らすことができます。

この問題を解決するために、非正規化をして、各刀投稿にコメント数、Like数を持たせます。

コメントドキュメントに、コメント者名を持たせます。

Likeドキュメントに、Likeした人の名前を持たせます。

ストアドプロシージャーを追加する

非正規化をした後は、Azure Functionで、userデータの更新をトリガーにして、postsのドキュメントも更新するようにします。ユーザー名が変更された場合は、Functionで各投稿のユーザー名を反映させます。

改修結果

改修前がこれでした。

改修をすると、Postsコンテナーから情報を一度取得すればいいだけになり、性能改善が実現できました。

130ms/619.41RU から、 28ms/201.54RU に改善しました。

<Q4> 投稿のコメントの一覧 23ms/27.72TU → 4ms/7.72RU

これには次の問題がありました。

コメント者名を取得するために、コメント数分usersコンテナーからpkでフィルタリングして結果を取得しています。これはN+1になるのでコストがかかり問題があります。

改修前はこのような実装でした。

改修後はシンプルな実装となりました。

23ms/27.72RU から、 4ms/7.72RU に改善しました。

<Q5> 投稿のLike一覧 59ms/58.92RU → 4ms/8.92RU

これには次の問題がありました。

Like者名を取得するために、Like数分usersコンテナーからpkでフィルタリングして結果を取得しています。これはN+1になるのでコストがかかり問題があります。

改修前はこのような実装でした。

改修後はシンプルな実装となりました。

59ms/58.92RU から、 4ms/8.92RU に改善しました。

<Q6> 直近X件の投稿一覧 306ms/2063.54RU → 83ms/532.33RU

各投稿の投稿者名とコメント数、Like数を取得するために、投稿数分繰り返しクエリを実行します。 これは読み取り数が多くなり性能問題につながります。

改修前はこのような実装でした。

改修後はシンプルな実装となりました。

306ms/2063.54RU から、 83ms/532.33RU に改善しました。

<Q3> ユーザー投稿の一覧 28ms/201.54RU → 4ms/6.46RU

再びQ3を見てみましょう。これには次の問題が残っていました。

投稿を取得するために、postsコンテナーからPKではないuerIDでフィルタリングをしています。これは全件アクセスする必要があり、非常にコストがかかります。

正規化を崩して情報をユーザーコンテナーにも投稿情報をもたせます。

上記のデータを入れるのに合わせて、userドキュメントに項目を追加します。

この対応により、参照先のコンテナーがUsersに変わります。

データ抽出が、UsersコンテナーのパーティションキーuserIdとなるため性能が改善します。

28ms/201.54RU から、 4ms/6.46RU に改善しました。

<Q6> 直近X件の投稿一覧 83ms/532.33RU → 9ms/16.97RU

最後に次の問題がまだ残っています。

直近の投稿を取得するために、postsコンテナーからpkではないtypeでフィルタリングして結果を取得しています。> これは非常にコストがかかります。

ここで必要なデータは、直近で更新されたデータでした。そこで、Change Feedを利用し、パーティションキーをtypeにします。

Azure CosmosDBは、Change Feedを提供しており、変更されたドキュメントは変更された順に並べ替えられた一覧として出力されます。

データが更新されると、feedコンテナーが更新されるようにします。

改修前は次のようでした

この対応により、次のような処理に変わり、パーティションキーによるフィルタリングが聞くようになり性能改善ができました。

83ms/532.33RU から、 9ms/16.97RU に改善しました。

完成

改修した結果、次のようなコンテナーとドキュメントを保存するようになりました。

これを実現させるためにストアドプロシージャーを使用し、データフローは次のようになりました。

これらの対応により、性能はドキュメント数、ユーザー数、投稿数に依存しなくなりました。 10ユーザーでも100万ゆーざーでもクエリレイテンシーはいつでも同じになります。