Azure Cosmos DB入門(8)

本コンテンツは「Azure Cosmos DB入門」の(8)です

ryuichi111std.hatenablog.com

8 Cosmos DBをもっと知りたい

これまでの「Azure Cosmos DB入門(1)~(7)」では、Azure Cosmos DBの概要から各データモデル(APIモデル)毎のプログラミング手法について説明してきました。
今回は「Azure Cosmos DB入門」の最終回として、Cosmos DBにおけるいくつかのトピックにスポットを当てて、少しだけ技術的に踏み込んでみたいと思います。

8.1 一貫性レベル(Consistency Level)

データベースにおいては「データの一貫性」は非常に重要な要素です。
書き込んだデータは、当然の如く「書き込んだ値」にならなければなりません。
しかし、話はそれほど単純ではなく、このあたりの考え方には「CAP定理」というものが関係してきます。

CAP定理

CAP定理とは・・・その頭文字である「Consistency(一貫性 )・Availability(可用性)・Partition-tolerance(分断耐性 )」の3つの要素は同時に保証することは出来ない、という定理です。
C / A / Pのうち2つは保証することができるが、3つを同時に保証することはできないというものです。
一般的に、RDBにおいてはCとAを保証する設計となっています。分散型NoSQLデータベースにおいては、AとPを保証する設計となっています。
つまり、分散型NoSQLデータベースシステムである Cosmos DB も AとP の保証を優先しています。
ただし、2者を取り1者を完全に捨てるわけではなく、バランスとして2つを優先し1つの優先度を落とす、というのが実際のところの落としどころとなります。
CAP定理と、それに対する RDB / 分散型NoSQL のアプローチの考え方については、ちょっとした1冊の本としても成り立つくらいの内容なので、ここでは省略させていただきます。
ググるとたくさん情報が出てくるので、そちらをご参照ください。

多くの分散型NoSQLのアプローチ

多くの分散型NoSQLデータベースでは「Eventual」および「Strong」という一貫性レベルを提供することが多いです。
この二者択一に限らず、いくつかの細かな一貫性レベルの指定が可能なデータベースは存在しますが、分散型NoSQLデータベースにおける代表的な一貫性レベルは、この2つになります。

Eventualとは「結果整合性」の一貫性レベルを表します。更新されたデータは、最終的に一貫した値に収束するという考え方です。
下図のように、更新されたデータは一定の時間を経ると収束し、一貫性を保ちます。逆に言うと、収束するまでの間は、データは最新のデータを読み取れることもあれば、少し古いデータが読み取られることもあります。

f:id:daigo-knowlbo:20170709163231p:plain

一方 Strongでは、必ず最新のデータを読み取ることができます。

f:id:daigo-knowlbo:20170709163416p:plain

処理としては必ず最新のデータが読み取れた方が、シンプルであり、その方がうれしいでしょう。ただし、分散型NoSQLデータベースにおいては、Strong一貫性レベルによる読み取りは負荷の高い処理になります。RDBとは異なるアプローチにより、スケーラビリティ・アベイラビリティ・ローレイテンシーを目指す分散型NoSQLにおいては Strong は良い選択肢ではありません。
SNSのタイムライン表示などでは結果整合性を採用しても大きな害は発生しません。しかし、銀行システムの預金口座の表示などで、リロードしたタイミングで通帳の残高状況が変わってしまうのは大きな問題があります。
それぞれの業務要件を満たすために、NoSQLデータベース機能による一貫性の管理、および、アプリケーションロジックによる業務要件の担保、といった設計上の考慮が必要になるのが従来のRDBとCosmos DBのような分散型NoSQLの違いでもあります。

※ Cosmos DBにおいてStrong一貫性レベルを使用した場合、クエリー レイテンシーSLA保証されている為、SLA範囲を超える速度の劣化ではなく 消費RUの増加 という影響が現れます。
※ 一貫性という言葉に関してAzureポータルでは「整合性」と記述している箇所があります。これ以降、「一貫性」「整合性」の2つの言葉は同意とします。

8.1.1 Cosmos DBの提供する一貫性レベル(Consistency Level)

Cosmos DBにおいては 5つ の一貫性レベルが提供されます。
強い一貫性レベルから順に記述したのが、以下のリストになります。

  • Strong
  • Bounded Staleness
  • Session
  • Consistent Prefix
  • Eventual

(1) Strong(強固)

Linearizabile(線形化可能性)を保証する最も強い一貫性レベルです。
あるプロセスにより書き込みが行われると、別のどのプロセスから読み取っても最新のデータを読み取ることができます。
消費RUは高くなります。
また、Strong一貫性レベルでは、グローバルレプリケーションを行うことが出来ません(単一リージョンによる運用に限定されます)。

(2) Bounded Staleness(有界整合性制約)

Bounded Stalenessは K個のバージョンの書き込み もしくは t時間の間隔 だけ読み取りが遅れる可能性があります(つまり古いデータを読み取る可能性があります)。
Azureポータル上での設定画面は以下になります(データベースアカウント → 既定の整合性)。

f:id:daigo-knowlbo:20170709152430p:plain

最大ラグ操作(K個)および最大ラグ時間(t間隔)の設定可能範囲はグローバルレプリケーションの設定有無により異なります。

グローバルレプリケーション 最大ラグ操作数(K) 最大ラグ時間(t)
あり(レプリカあり) 100,000~1,000,000 5分~1日
なし(レプリカなし) 10~1,000,000 5秒~1日

やはりグローバルレプリケーションの有無によりその設定可能数値範囲に大きな相違があることが分かります。
また、消費RUは高くなります(Strongと同等)。

(3) Session(セッション)

読み取るデータは最新である保証はありません。ただし、最終的に書き込まれたデータは反映され一貫性と保った状態に収束します。
一貫性レベルとしては弱いですが、この後説明する「Consistent Prefix」「Eventual」と異なり、クライアント セッションにフォーカスした一貫性レベルとなります。
セッションにおいて monotonic reads / monotonic writes / read your own writes (RYW) が保証されています。
* monotonic reads
当該セッションから読み取れるデータは、読み取る毎にさらに古いデータにはならない。
* monotonic writes
当該セッションからの同一データへの書き込みは、書き込み順序が保証される。
* read your own writes (RYW)
当該セッションからのデータ書き込みは、(当該セッションからの)読み取りに反映される(書き込んだデータを読み取れる。古いデータが読み込まれることはない。)。

また、消費RUはStrong / Bounded Stalenessより低く、Eventualよりも高くなります。

(4) Consistent Prefix(一貫性のあるプレフィックス)

読み取るデータは最新である保証はありません。ただし、最終的に書き込まれたデータは反映され一貫性と保った状態に収束します。
レプリカに反映される書き込み順序は保証されます。
A→B→Cというデータ更新が行われた場合、クライアントから見ると、A、A→B、A→B→Cと書き込みが行われた処理データは読み込まれますが、A→CとかB→A→Cと順序が入れ替わって書き込まれる様子が見えることはありません。

(5) Eventual(最終的)

最も弱い一貫性レベルです。
読み取るデータは最新である保証はありません。ただし、最終的に書き込まれたデータは反映され一貫性を保った状態に収束します。
データを繰り返し読み取る際、1度目に取得したデータより更に古い世代のデータが読み取られることもあります。
読み取りと書き込みの待機時間は最も早くなります。
読み取り操作時の消費RUは最も低くなります。

8.1.2 既定の一貫性レベル

Cosmos DBでは「データベースアカウント」に対して、既定の一貫性レベルを設定することができます。
Azureポータル上で設定することができ、「データベースアカウント → 既定の整合性」メニューで変更することができます。

f:id:daigo-knowlbo:20170709152450p:plain

初期状態では、既定値は「セッション」になっています。

読み取り操作を行う場合、明示的な指定を行わない限り、データベースアカウントに対する既定の一貫性レベルが適用されます。
また、「読み取り要求を行う毎」に個別に一貫性レベルを明示的に指定することができます。
以下が例となります(リスト1)。

リスト1 一貫性レベルの明示的な指定(Session一貫性レベルで読み取る例)

DocumentClient client = 
  new DocumentClient(
    new Uri("https://[your account].documents.azure.com:443/"),
    "[your key]",
    new ConnectionPolicy() { // 実運用時はDirect/Tcp推奨
      ConnectionMode = ConnectionMode.Gateway,
      ConnectionProtocol = Protocol.Https},
    ConsistencyLevel.Session);  // ← 一貫性レベルを明示的に指定

// クエリー実行
var query =
    client.CreateDocumentQuery<RoomReservationInfo>(
        UriFactory.CreateDocumentCollectionUri("GroupwareDB", "ReservationCollections")).
        Where(r => r.Room == "大会議室");
var result = query.ToList();

リスト1実行時に発行されたHTTP RequestをFiddlerで確認した結果は以下です。
HTTP Headerに「x-ms-consistency-level: Session」が追加されています。

POST https://cosmosdoc-japanwest.documents.azure.com/dbs/GroupwareDB/colls/RoomReservations/docs HTTP/1.1
x-ms-continuation: 
x-ms-documentdb-isquery: True
x-ms-documentdb-query-enablecrosspartition: False
x-ms-documentdb-query-iscontinuationexpected: False
x-ms-documentdb-populatequerymetrics: False
x-ms-date: Sun, 09 Jul 2017 03:16:15 GMT
authorization: type%3dmaster%26ver%3d1.0%26sig%3dS7DKHRXurZXz6v6VqkMU8QceYxyDHoWQjAIOOn2N9ao%3d
Cache-Control: no-cache
x-ms-consistency-level: Session
User-Agent: documentdb-dotnet-sdk/1.14.1 Host/32-bit MicrosoftWindowsNT/6.2.9200.0
x-ms-version: 2017-02-22
Accept: application/json
Content-Type: application/query+json
Host: cosmosdoc-japanwest.documents.azure.com
Content-Length: 98
Expect: 100-continue

{"query":"SELECT * FROM root WHERE (root[\"Room\"] = \"スタンディングテ\\u30fcブル\") "}

操作毎の明示的な一貫性レベル指定に関する注意事項として、データベースアカウントに対して設定した既定の一貫性レベルより高いレベルの一貫性レベルを指定することはできません。

8.2 RU(Request Unit)詳細

「Azure Cosmos DB入門(2)」においてもRU(Request Unit)の概要について触れました。
それを踏まえて、また+αの概念も含めて、RUについてのポイントをまとめると、以下のようになります。

  • RUはCosmos DBにおけるクエリーや書き込みなどの「処理の計算量を表す抽象単位」である
    (1RU = 一貫性レベルSessionにおいてキー指定で1KBのデータを1件取得する操作に要する処理量)
  • クエリー処理を行うと、その複雑度・データ量等により消費されるRUが変動する
    (複雑なクエリーほど多くのRUを消費する)
  • RUはコレクションに対して割り当てる
  • 1秒あたり利用可能なRUをコレクションに対して割り当てる
    (Storage Capacity:Fixed(10GB)設定時=400~10,000RU/s。Unlimited設定時=2,500~100,000RU/s)
  • RU/second を超える処理量が一時的に必要になった時のために「RU/m(RU/分)」の設定が可能
  • RUはCosmos DBにおける課金対象の大部分を占める

8.2.1 RUの設定方法

コレクションに割り当てるRU/sの値は、Azureポータルおよびプログラムから行うことができます。
「2.4.4 予約RUの設定方法」ですでに説明したのでそちらを参照してください。

Azure Cosmos DB入門(2) - ryuichi111stdの技術日記

8.2.2 消費RUの確認方法

クエリーや更新の結果消費したRUの量の確認方法は、「2.4.5 消費RUの確認方法」ですでに説明したのでそちらを参照してください。

Azure Cosmos DB入門(2) - ryuichi111stdの技術日記

8.2.3 RU/s超過時のレスポンスコード429

RU/sは事前予約制です。
Cosmos DBは使用した分を精算する方式ではなく、事前にどれだけの処理能力(キャパシティ)を用意するかを RU/s の値として設定します。
RU/sは消費しても、しなくても利用料金として精算請求が行われます。
コレクションの作成と同時にRUの割り当てを行います。つまりコレクションを作成した時点で、格納するデータが空であっても、設定したRU分の課金が開始されます。

運用コストを考慮すると「必要最低限の RU/s値 をコレクションに対して割り当てたい」ということになります。
しかし、クライアントからのトラフィックに対して RU/s値 が十分でなかった、つまり足りなかった場合、レスポンスコード429というエラーが発生します。

レスポンスコード429の確認

では実際に レスポンスコード429 の発生を確認してみます。
以下のような、クエリーをforループで10回繰り返す処理を実行してみます(リスト2)。
処理対象のコレクションは、最小構成のRU/s=400としています。

リスト2 負荷をかけて400RU/sを超過させる

DocumentClient client = 
  new DocumentClient(
    new Uri("https://[your account].documents.azure.com:443/"),
    "[your key]",
    new ConnectionPolicy() {
      ConnectionMode = ConnectionMode.Gateway,
      ConnectionProtocol = Protocol.Https});

// クエリーを10回繰り返す
for(int i = 0; i < 10; i++)  {
  var query =
    this.client.CreateDocumentQuery<RoomReservationInfo>(
      UriFactory.CreateDocumentCollectionUri("GroupwareDB", "RoomReservations"))
      .Where(r => r.Room == "スタンディングテーブル");
  var result = query.ToList();
}

リスト2の実行結果のHTTPS通信をFiddlerで監視した結果は以下の画面です。

f:id:daigo-knowlbo:20170709152517p:plain

1度 429 が発生していることを確認できます。

f:id:daigo-knowlbo:20170709152531p:plain

レスポンスボディのJSONには、確かに「Requesr rate is large」とのエラーメッセージを確認することができます。
また、レスポンスヘッダに「x-ms-retry-after-ms: 488」という項目が確認できます。
これは 488ms 待ってから再度リクエストを行ってください、という意味を持ちます。
1秒毎にRUのキャパシティが復活するために、指定のリトライ待機時間を取った上で再度のリトライを行うことを促しています。
forループで10回のクエリーを実行しました。そして1回の429(RU超過)エラーが発生しました。その上で、200で成功したクエリー(~/RoomReservations/docs)は10回確認できます。
つまり、429エラーが発生した際は、ライブラリ側でリトライ処理が行われていることが分かります。
仮に、REST APIを自ら実行する方式でCosmos DBを操作した場合には、429の発生の認識およびリトライを自前で実装する必要があります。

8.2.4 リトライ動作の指定

ライブラリが行うリトライ処理の動作については、ユーザープログラム側で制御することができます。
DocumentClientクラスのコンストラクタ引数「ConnectionPolicyクラス」を利用します。
「ConnectionPolicy.RetryOptionsプロパティ」によってリトライ設定を制御することができ、これは「Microsoft.Azure.Documents.Client.RetryOptions」型となります。
以下の2つがRetryOptionsクラスの主要プロパティです。

  • RetryOptions.MaxRetryAttemptsOnThrottledRequestsプロパティ
    RU超過(429)発生に対して、何回までリトライを繰り返すかを設定します。設定したリトライ回数を超えると例外が発生します。

  • RetryOptions.MaxRetryWaitTimeInSecondsプロパティ
    最初の要求からの累積待ち時間の上限を設定します(単位は秒)。この上限値を超える待ち時間が発生すると例外が発生します。

以下がその利用例です(リスト3)。

リスト3 リトライ制御の明示的指定方法

//リトライ回数5回、累積待ち時間60秒の接続ポリシーを作成
ConnectionPolicy cp = new ConnectionPolicy();
cp.RetryOptions.MaxRetryAttemptsOnThrottledRequests = 5;
cp.RetryOptions.MaxRetryWaitTimeInSeconds = 60; 

// ConnectionPolicyを指定してDocumentClientを作成
DocumentClient client = 
  new DocumentClient(
    new Uri("https://[your account].documents.azure.com:443/"),
    "[your key]",
    cp);

//接続ポリシーが適用されたクエリー処理が実行される
var query =
  client.CreateDocumentQuery<RoomReservationInfo>(
    UriFactory.CreateDocumentCollectionUri("GroupwareDB", "ReservationCollections")).
    Where(r => r.Room == "大会議室");
var result = query.ToList();

8.2.5 一貫性レベル(Consistency Level)による消費RUの違い

一貫性レベルの設定が消費RUに影響します。
公式ドキュメントに記された情報からは、以下のような関係性であると考えられます。

f:id:daigo-knowlbo:20170709152556p:plain

※図の吹き出しの通り Session と Consistenc Prefix の消費RUの上下関係は未確認です(詳しい方がおられましたら、コメント or RyuichiDaigo@そら (@ryuichi111std) | Twitter までご指摘いただけたら嬉しいです)。

実際の値を確認するために、先程のリスト1を用いて、ConsistencyLevel値をそれぞれに変更してみました。
テストした結果の消費RU値は以下の通りです(絶対的数値の大きさは取得件数・データ量が影響しているので、各一貫性レベル間の相対値の比較が確認対象です)。

Strong Bounded Staleness Session Consistent Prefix Eventual
70.74 70.74 35.37 35.37 35.37

Strong / Bounded Stalenessは高い値。
Session / Consistent Prefix / Eventualが同一値となりました。Eventualが低くなってほしかったのですが、このクエリー処理においては同様のRU消費量のようでした。

※ 上記は、Strongを試すために単一リージョン構成での実行結果です。ただし、グローバルレプリケーション構成でも、同様の結果が得られました(勿論Strong以外)。

8.2.6 RU/m (RU/分) の設定

前述の通り、設定したRU/sの許容量を超えると レスポンスコード 429 のエラーが発生してしまいます。
ライブラリによるリトライ処理が行われ、クエリー処理は継続されますが、当然処理の遅延につながってしまいます。さらにリトライで追いつかない程のキャパシティ不足となった場合、例外が発生してしまいます(つまり、アプリケーションとしてはユーザーに対してタイムアウトエラーを伝えなければなりません)。

アプリケーションでは、時間帯によるトラフィックの変動や、何らかの原因による一時的なトラフィックの増加が発生することがあります。
RU/sの概念のみでこのような状況に対応しようとした場合、最大負荷状態に合わせたRU/s設定を行う必要があります。しかし、一時的な高負荷時間帯以外の時間帯では、過剰なRU/s設定となってしまいコストが増してしまいます。

そこで RU/m(RU/分) という機能が用意されています。
RU/sと同様に、コレクションに対して RU/m の設定を行います。
Azureポータルでの設定画面は以下になります(データベースアカウント → スケール)。

f:id:daigo-knowlbo:20170709152611p:plain

設定対象のコレクションをドロップダウンで選択し、「RU/分」項目を「オン」に設定して「保存」ボタンをクリックします。

RU/mの設定値

RU/m(RU/分)は「オン / オフ」の設定です。
RU/mは具体的な数値を設定しません。RU/sに対して10倍の値が自動的に設定されます。
コレクションに対するRU/s設定が 400 であれば、RU/mは 4,000 となります。
また、5,000RU/sまでのコレクションに対してのみ RU/m を有効化することができます。
5,001RU/s以上のスループットを設定したコレクションには RU/m を有効化することはできません。

RU/mの消費例

RU/m(RU/分)がオンに設定されている場合、あるタイミングにおいて、1秒あたりの予約RU/sを超えてしまったとします。その場合、RU/sとは別枠のRU/mが消費されデータベース操作が行われます。

以下に具体的な RU/sとRU/mの消費と回復 の例を説明します。
前提条件は「RU/s = 1,000 、 RU/mオン」とします。

f:id:daigo-knowlbo:20170709152627p:plain

①開始点から0.2秒後に200RUを消費する処理が実行されました。
→RU/sの残が800となりました。
②0.4秒後に300RUを消費する処理が実行されました。
→RU/sの残が500となりました。
③0.6秒後に600RUを消費する処理が実行されました。
→RU/sの残が0となりました。RU/sが不足したのでRU/mが消費され、残が9,900となりました。 ④0.9秒後に500RUを消費する処理が実行されました。
→RU/sの残が0なので、RU/mが消費され、RU/mの残が9,400となりました。
⑤1秒後にRU/sが回復しました。
→RU/sの残が1000、RU/mの残はそのまま9,400となりました。
⑥1.2秒後に350RUを消費する処理が実行されました。
→RU/sの残が650となりました。
⑦1.5秒後に800RUを消費する処理が実行されました。
→RU/sの残が0となりました。RU/mが消費され、残が9,250となりました。
⑧1.8秒後に1,500RUを消費する処理が実行されました。
→RU/sの残が0なので、RU/mが消費され、RU/mの残が7,750となりました。
⑨60秒後、RU/mは回復し10,000となります。

8.3 グローバルレプリケーション

Cosmos DBは分散型NoSQLデータベースです。世界各国のAzureリージョンに対してグローバルレプリケーションすることが可能です。
2017/7/9現在、Azureの世界各所 24個所 のリージョンにCosmos DBをレプリケーションすることが可能です。
グローバルレプリケーションを行うことのメリットは、主に以下の2点が挙げられます。

  • 耐障害性の向上
    地理的に離れたリージョンにデータベースをレプリケーションすることで、Disaster Recoveryであったり、リージョン障害時の継続稼働を可能にします。

  • 性能向上
    ユーザーが世界各国に散らばっているようなシステムにおいて、利用者に近いリージョンでデータベースを動作させることで、性能向上を図ります。(ネットワーク遅延の抑制)

8.3.1 書き込みリージョンと読み込みリージョン

Cosmos DBでは、複数のリージョンにデータベースをレプリケーションすることが可能です。
この場合、書き込みリージョンは1つ、それ以外は読み取りリージョンとなります。
つまりレプリケーションを行うことによる地理的性能向上は、読み取りに対して有効なもので、書き込みに関しては、世界で1つの書き込みリージョンに対して行うことになります。
書き込みリージョンに対して書き込まれたデータが、Cosmos DBのバックエンド処理によりレプリケーション構成された各リージョンにコピーされる仕組みとなります。

8.3.2 レプリケーションの作成

レプリケーションは、Azureポータルでマウス操作のみで簡単に行うことができます。
レプリケーションは簡単に作成できますが、レプリケーションしたリージョン毎に 割り当てRU の課金が行われますので注意してください(料金プランについては今後変更の可能性もあるので、公式情報をご確認ください)。
例えば 1,000RU/s のコレクション1つを含むCosmos DBアカウントにおいて、レプリケーションを1つ追加した場合、1,000RU/s × 2リージョン = 2,000RU/sの料金 が発生します。

Azureポータルでレプリケーションを作成する手順は、以下の通りです。

(1) Azureポータルを表示

Azureポータルにログインし、対象のCosmos DBアカウントを選択。さらに「データをグローバルにレプリケートする」メニューを選択します。
世界地図とリージョンを表すマークが表示されます。

f:id:daigo-knowlbo:20170709152645p:plain

(2) レプリケーションするリージョンを選択

世界地図上のマークの意味は以下の通りです。

いくつかのリージョンを選択して「保存」ボタンをクリックすると、レプリケーションが開始されます。
小さなデータベースでも、1つレプリケーションを増やすのに数分はかかると思います。ただし、レプリケーション構成中も、データベースは停止することなく、アクティブに動作し続けています。
また公式ドキュメントの記述では「100TBまでのデータであれば30分」でレプリケーション可能であるとのことです。

8.3.3 アプリケーション配置 と Cosmos DB レプリケーション

Cosmos DBはデータベースシステムなので、ユーザーに対して単体で提供するものではありません。
1つの代表的な(そしてもっとも単純な)利用想定は、以下のような形です。

f:id:daigo-knowlbo:20170709152700p:plain

Azure App ServiceでWebアプリケーションをホストします。
バックエンドのデータベースとしてCosmos DBを利用します。

そして、ユーザーが世界各国に散らばっていた場合、負荷分散と地理的なネットワーク遅延の問題回避のために以下のような構成をとります。

f:id:daigo-knowlbo:20170709152713p:plain

Azure App Serviceを世界のリージョンに分散配置すると共に、Cosmos DBも併せてApp Serviceと同一リージョン(もしくは構成によっては近くのリージョン)に配置(レプリケーション)します。

8.3.4 読み取りリージョンの明示的指定

グローバル レプリケーション設定は、Azureポータル上の操作で簡単に行えました。
しかし、プログラム側でも「どのリージョンから読み取るか」という指定をする必要があります。これを行わないと、データベース自体はグローバルレプリケーションされているが、実際にアプリケーションがアクセスするのは、マスターの書き込みリージョンのみになってしまいます。

具体的なプログラムでの設定方法は以下の通りです。

DocumentClientオブジェクトを生成する際に指定可能な、ConnectionPolicyオブジェクトの「PreferredLocationsプロパティ」を利用します。
ConnectionPolicy.PreferredLocationsプロパティは、「Collection型」で複数のリージョンを優先順位順に設定することができます。
以下のリスト4は「WestEurope」「BrazilSouth」の優先順位を指定してクエリーを行います。
WestEuropeが読み取りできない状態であった場合、BrazilSouthkから読み取りが行われます。WestEurope / BrazilSouthのどちらのリージョンからも読み取れない状況であった場合は、書き込みリージョン(マスターリージョン)からの読み取りが行われます。

リスト4 リージョンを指定して読み取り

// リージョン名はstring型で指定しますが「LocationNames列挙体」が用意されています。
ConnectionPolicy connectionPolicy = new ConnectionPolicy();
connectionPolicy.PreferredLocations.Add(LocationNames.WestEurope);
connectionPolicy.PreferredLocations.Add(LocationNames.BrazilSouth);
connectionPolicy.ConnectionMode = ConnectionMode.Gateway;
connectionPolicy.ConnectionProtocol = Protocol.Https;

DocumentClient client = 
  new DocumentClient(
    new Uri("https://[your account].documents.azure.com:443/"),
    "[your key]",
    connectionPolicy);

var query =
  this.client.CreateDocumentQuery<RoomReservationInfo>(
    UriFactory.CreateDocumentCollectionUri("GroupwareDB", "RoomReservations"))
    .Where(r => r.Room == "大会議室");
var result = query.ToList();

読み取りリージョンを確認する

論理的なお話よりも実際の動きを検証した方が、しっくりと頭に入ってくると思います。
ということで、PreferredLocations指定時のHTTP通信内容をFiddlerで確認してみたいと思います。
前提条件は以下とします。

  • 書き込みリージョン(マスター)は Japan West です
  • レプリケーションされた読み取りリージョンは「WestEurope」と「BrazilSouth」です

実行するコードは、先程のリスト4です。
Fiddlerを確認すると、3つのHTTPS通信が行われています。

f:id:daigo-knowlbo:20170709152731p:plain

①「/」へのGET
まず、「https://cosmosdoc.documents.azure.com/」へのGETリクエストを行っています。
特定リージョンではなく「https://cosmosdoc.documents.azure.com/」に対してのGETとなります。
以下にRequest / Reponseのトレースをつらつらと張りつけますが、Response Body内のJSONで「writableLocations / readableLocations」という項目があります。
それぞれ「書き込みリージョン情報」および「読み取りリージョン情報」となります。
これによりDocumentDBライブラリは接続先データベースのマスター及びレプリケーションリージョンの構成を把握します。

--- 1つ目のHTTP Request(/) ---

GET https://cosmosdoc.documents.azure.com/ HTTP/1.1
x-ms-version: 2017-02-22
User-Agent: documentdb-dotnet-sdk/1.14.1 Host/32-bit MicrosoftWindowsNT/6.2.9200.0
x-ms-date: Sat, 08 Jul 2017 13:51:10 GMT
authorization: type%3dmaster%26ver%3d1.0%26sig%3dEYpxSo1lqVjtGwbUoMVFp2eZLPlR8OeC06L5WcsTtEg%3d
Host: cosmosdoc.documents.azure.com
Connection: Keep-Alive


--- 1つ目のHTTP Request(/)に対するResponse ---
HTTP/1.1 200 Ok
Cache-Control: no-store, no-cache
Pragma: no-cache
Transfer-Encoding: chunked
Content-Type: application/json
Content-Location: https://cosmosdoc.documents.azure.com/
Server: Microsoft-HTTPAPI/2.0
x-ms-max-media-storage-usage-mb: 2048
x-ms-media-storage-usage-mb: 0
x-ms-databaseaccount-consumed-mb: 0
x-ms-databaseaccount-reserved-mb: 0
x-ms-databaseaccount-provisioned-mb: 0
Strict-Transport-Security: max-age=31536000
x-ms-gatewayversion: version=1.14.33.2
Date: Sat, 08 Jul 2017 13:51:09 GMT

5B7
{  
  "_self":"",
  "id":"cosmosdoc",
  "_rid":"cosmosdoc.documents.azure.com",
  "media":"//media/",
  "addresses":"//addresses/",
  "_dbs":"//dbs/",
  "writableLocations":[  
    {  
      "name":"Japan West",
      "databaseAccountEndpoint":"https://cosmosdoc-japanwest.documents.azure.com:443/"
    }
  ],
  "readableLocations":[  
    {  
      "name":"Japan West",
      "databaseAccountEndpoint":"https://cosmosdoc-japanwest.documents.azure.com:443/"
    },
    {  
      "name":"Brazil South",
      "databaseAccountEndpoint":"https://cosmosdoc-brazilsouth.documents.azure.com:443/"
    },
    {  
      "name":"West Europe",
      "databaseAccountEndpoint":"https://cosmosdoc-westeurope.documents.azure.com:443/"
    }
  ],
  "userReplicationPolicy":{  
    "asyncReplication":false,
    "minReplicaSetSize":3,
    "maxReplicasetSize":4
  },
  "userConsistencyPolicy":{  
    "defaultConsistencyLevel":"BoundedStaleness"
  },
  "systemReplicationPolicy":{  
    "minReplicaSetSize":3,
    "maxReplicasetSize":4
  },
  "readPolicy":{  
    "primaryReadCoefficient":1,
    "secondaryReadCoefficient":1
  },
  "queryEngineConfiguration":"{\"maxSqlQueryInputLength\":30720,\"maxJoinsPerSqlQuery\":5,\"maxLogicalAndPerSqlQuery\":500,\"maxLogicalOrPerSqlQuery\":500,\"maxUdfRefPerSqlQuery\":2,\"maxInExpressionItemsCount\":8000,\"queryMaxInMemorySortDocumentCount\":500,\"maxQueryRequestTimeoutFraction\":0.9,\"sqlAllowNonFiniteNumbers\":false,\"sqlAllowAggregateFunctions\":true,\"sqlAllowSubQuery\":false,\"allowNewKeywords\":true,\"sqlAllowLike\":false,\"maxSpatialQueryCells\":12,\"spatialMaxGeometryPointCount\":256,\"sqlAllowTop\":true,\"enableSpatialIndexing\":true}"
}

②コレクション情報取得
PreferredLocationsで第1優先先として指定した「WestEurope」に対してのGETリクエストとなります(https://cosmosdoc-westeurope.documents.azure.com/dbs/GroupwareDB/colls/RoomReservations)。

--- 2つ目のHTTP Request ---

GET https://cosmosdoc-westeurope.documents.azure.com/dbs/GroupwareDB/colls/RoomReservations HTTP/1.1
x-ms-date: Sat, 08 Jul 2017 13:51:10 GMT
authorization: type%3dmaster%26ver%3d1.0%26sig%3dkTjmw8Dl1qxV8Boc6tjAEgxvV5K32FIYxENUN7CHdOQ%3d
Cache-Control: no-cache
x-ms-consistency-level: BoundedStaleness
User-Agent: documentdb-dotnet-sdk/1.14.1 Host/32-bit MicrosoftWindowsNT/6.2.9200.0
x-ms-version: 2017-02-22
Accept: application/json
Host: cosmosdoc-westeurope.documents.azure.com



--- 2つ目のHTTP Requestに対するResponse ---

HTTP/1.1 200 Ok
Cache-Control: no-store, no-cache
Pragma: no-cache
Transfer-Encoding: chunked
Content-Type: application/json
Content-Location: https://cosmosdoc-westeurope.documents.azure.com/dbs/GroupwareDB/colls/RoomReservations
Server: Microsoft-HTTPAPI/2.0
Strict-Transport-Security: max-age=31536000
x-ms-last-state-change-utc: Tue, 04 Jul 2017 00:10:13.508 GMT
etag: "00007800-0000-0000-0000-5960908e0000"
collection-partition-index: 0
collection-service-index: 0
x-ms-schemaversion: 1.3
x-ms-alt-content-path: dbs/GroupwareDB
x-ms-content-path: -2kXAA==
x-ms-xp-role: 1
x-ms-request-charge: 2
x-ms-serviceversion: version=1.14.33.2
x-ms-activity-id: 4bdac467-4bfd-4710-a3c8-457a154e746a
x-ms-session-token: 0:1539
x-ms-documentdb-collection-index-transformation-progress: -1
x-ms-gatewayversion: version=1.14.33.2
Date: Sat, 08 Jul 2017 13:51:12 GMT

225
{
  "id":"RoomReservations",
  "indexingPolicy":{
    "indexingMode":"consistent",
    "automatic":true,
    "includedPaths":
    [
      {
        "path":"\/*",
        "indexes":[
          {"kind":"Range","dataType":"Number","precision":-1},
          {"kind":"Hash","dataType":"String","precision":3}
        ]
      }
    ],
    "excludedPaths":[]
  },
  "partitionKey":{"paths":["\/Room"],"kind":"Hash"},
  "_rid":"-2kXAO4IMQA=",
  "_ts":1499500658,
  "_self":"dbs\/-2kXAA==\/colls\/-2kXAO4IMQA=\/",
  "_etag":"\"00007800-0000-0000-0000-5960908e0000\"",
  "_docs":"docs\/",
  "_sprocs":"sprocs\/",
  "_triggers":"triggers\/",
  "_udfs":"udfs\/",
  "_conflicts":"conflicts\/"
}
0

③ WestEuropeリージョンに対してクエリー処理が行われます。
https://cosmosdoc-westeurope.documents.azure.com/dbs/GroupwareDB/colls/RoomReservations/docs

--- 3つ目のHTTP Request ---

POST https://cosmosdoc-westeurope.documents.azure.com/dbs/GroupwareDB/colls/RoomReservations/docs HTTP/1.1
x-ms-continuation: 
x-ms-documentdb-isquery: True
x-ms-documentdb-query-enablecrosspartition: False
x-ms-documentdb-query-iscontinuationexpected: False
x-ms-documentdb-populatequerymetrics: False
x-ms-date: Sat, 08 Jul 2017 13:51:12 GMT
authorization: type%3dmaster%26ver%3d1.0%26sig%3dod2h8xICdwZh0fqdvSt1Dl6OUZW19p9qHRZVXlOTwp8%3d
Cache-Control: no-cache
x-ms-consistency-level: BoundedStaleness
User-Agent: documentdb-dotnet-sdk/1.14.1 Host/32-bit MicrosoftWindowsNT/6.2.9200.0
x-ms-version: 2017-02-22
Accept: application/json
Content-Type: application/query+json
Host: cosmosdoc-westeurope.documents.azure.com
Content-Length: 98
Expect: 100-continue

{"query":"SELECT * FROM root WHERE (root[\"Room\"] = \"スタンディングテ\\u30fcブル\") "}


--- 3つ目のHTTP Requestに対するResponse ---

HTTP/1.1 200 Ok
Cache-Control: no-store, no-cache
Pragma: no-cache
Transfer-Encoding: chunked
Content-Type: application/json
Server: Microsoft-HTTPAPI/2.0
Strict-Transport-Security: max-age=31536000
x-ms-last-state-change-utc: Tue, 04 Jul 2017 00:10:13.881 GMT
x-ms-resource-quota: documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;
x-ms-resource-usage: documentSize=1;documentsSize=859;documentsCount=1164;collectionSize=1152;
x-ms-item-count: 97
x-ms-schemaversion: 1.3
x-ms-alt-content-path: dbs/GroupwareDB/colls/RoomReservations
x-ms-content-path: -2kXAO4IMQA=
x-ms-xp-role: 2
x-ms-request-charge: 70.74
x-ms-serviceversion: version=1.14.33.2
x-ms-activity-id: 25dd501b-ff38-49d5-aea4-e8946e204445
x-ms-session-token: 0:1539
x-ms-gatewayversion: version=1.14.33.2
Date: Sat, 08 Jul 2017 13:51:14 GMT

E093
{"_rid":"-2kXAO4IMQA=","Documents":[{"id":"0000000004","Room":"スタンディングテーブル","Title":"アーキテクチャ社内勉強会(06\/02回)","ReservedUserId":"kubota","ReservedUserName":"久保田元也","Start":"2017-06-02T13:00:00.0000000",
...省略...
"_attachments":"attachments\/","_ts":1499500709}],"_count":97}
0

上記トレース情報から、ライブラリがリージョンを確認し、その上で優先指定されたリージョンに対してクエリー操作を発行している様子を確認することができます。

【補足】

つい最近 三宅@ZEN さんも、この点についての投稿を行われているので、そちらも参考になります。
k-miyake.github.io

8.4 障害復旧およびフェールオーバー

グローバルレプリケーションを設定していると、特定リージョンで障害が発生した際にフェールオーバーによる障害復旧を行うことが出来ます。
もしくは、障害とは関係なく手動でのフェールオーバーも可能です。
フェールオーバーには「自動」と「手動」の2つの方法が用意されています。

8.4.1 自動フェールオーバー

自動フェールオーバーについて、「読み取りリージョンが停止した場合」「書き込みリージョンが停止した場合」の、2つのケースについて以下に説明します。

(1) 読み取りリージョンが停止した場合

書き込み(マスター)リージョンは正常稼働状態で、読み取りリージョンが停止した場合の動作について説明します。
データベース操作ロジックにおいて、PreferredLocationsで指定された優先リージョンのうち、有効なリージョンに自動的に接続します。PreferredLocationsで指定されたリージョンがすべて停止している場合は、書き込みリージョンにアクセスします。

具体的な例は以下の通りです。
前提は・・・

  • JapanEastが書き込みリージョン
  • JapanWestおよびBrazilSouthがレプリケーションされた読み取りリージョン
  • 操作ロジックのPreferredLocations設定はリスト5の通り

この時、BrazilSouthが停止した場合には、JapanWestから読み取りが行われます。
BrazilSouthに加えてJapanEastも停止してしまっていた場合には、書き込みリージョンであるJapanEastから読み取りが行われます。

リスト5 
ConnectionPolicy connectionPolicy = new ConnectionPolicy();
connectionPolicy.PreferredLocations.Add(LocationNames.BrazilSouth);
connectionPolicy.PreferredLocations.Add(LocationNames.JapanWest);

(2) 書き込みリージョンが停止した場合

書き込みリージョンが停止した場合の自動フェールオーバーは、「自動フェールオーバーの有効化」が、オンになっている必要があります。
Azureポータル上で「データをグローバルにレプリケートする」→「自動フェールオーバー」で設定を行うことができます。

f:id:daigo-knowlbo:20170709152756p:plain

f:id:daigo-knowlbo:20170709152808p:plain

書き込みリージョンが停止した場合、書き込みリージョンのデータベースは、自動的にオフラインモードになります。
更に優先度上位の読み込みリージョンが、書き込みリージョンに昇格します。
元々の書き込みリージョンが復旧したら、データのマージ及び競合の解消を行い、手動フェールオーバーにより書き込みリージョンに再度昇格させます。

※書き込みリージョンの障害からのフェールオーバーについては、私自身実際の操作経験がない為、公式ドキュメントからの受け売りですm(_ _)m

8.4.2 手動フェールオーバー

手動フェールオーバーは、Azureポータルもしくはプログラムから行うことができます。
そして、書き込みリージョンの変更(フェールオーバー)処理において、「データ ロス 0」「アベイラビリティ ロス 0」が保証されています。

Azureポータルでの操作手順

Azureポータルを表示し、「データをグローバルにレプリケートする」メニューを選択します。 「手動フェールオーバー」メニューを選択します。

f:id:daigo-knowlbo:20170709152832p:plain

書き込みリージョンに変更(昇格)する読み取りリージョンを選択する画面が表示されます。
書き込みリージョンに昇格させる読み取りリージョンを選択してOKをクリックします。

f:id:daigo-knowlbo:20170709152843p:plain

フェールオーバー処理にしばらくの時間がかかりますが、以上で手動による「書き込みリージョンのフェールオーバー」が完了です。前述の通り、稼働状態でこの作業を行うことが可能です。
以下が手動フェールオーバーが完了した画面です。選択したWest Europeが書き込みリージョンに昇格しています(私の環境では、ブラウザをリロードしないと表示が反映されませんでした)。

f:id:daigo-knowlbo:20170709152857p:plain

Cosmos DB公式サイトにおいて、手動フェールオーバーの利用目的の1つとして以下のようなことが挙げられていました。

Follow the clock model: If your applications have predictable traffic patterns based on the time of the day, you can periodically change the write status to the most active geographic region based on time of the day.

つまり・・・
1日の中で時間帯による書き込みトラフィックのパターンが予測できる場合、時間毎に最もアクティブなリージョンに書き込みリージョンをフェールオーバーする。

個人的には、何かあった時、計画されたアプリケーションアップデートの際、に使用するものかと思っていましたが、上記のような運用上の仕組みとして書き込みリージョンの手動フェールオーバーを行っても良いようです。
実運用適用には、いろいろ試してから適用したいですね・・・。

8.5 まとめ

「Cosmos DB入門」と題して全8回にわたって投稿を行いました。
Cosmos DB、つまりNoSQLは、すべてのリレーショナルデータベースに置き換わるものではありません。
SQL ServerSQL Database(PaaS)は引き続き利用され続けるでしょうし、それは正しいことです。一方、クラウドネイティブなシステムにおいて、Cosmos DBの適用が、よりスケーラブル で よりローレイテンシー を実現する肝となることもあると考えます。
本格的にNoSQLを導入する場合、トランザクションやデータの一貫性といった点だけでも、従来のRDBの感覚とは全く異なる、NoSQLならではのコツが必要になってきます。むしろ多くの苦労を要する事も多々あることでしょう。同時に、それと引き換えに得られるメリットは十分にあるとも考えます。
本連載では、やはり「入門」というレベルに留まった内容であり、本番運用のシステムに適用するにはより多くの知識が必要になると考えています。
まずは、Cosmos DBの概要から設定・操作・コーディング手法の基本を学ぶきっかけになっていただけたならうれしく思います。
私個人としては、今後も本連載以外のトピックや、私自身の知識とノウハウの積み重ねにより更に踏み込んだ面白い投稿をしていけたらと思っています。
ということで、長文投稿ばかりの「Cosmos DB入門」を読んでいただいた皆様、ありがとうございました!