(Durable Functions)「第22回 Azureもくもく会」に参加+LTした話

久しぶりに kingkino@マンダム (@kingkinoko) on Twitterさん主催のAzureもくもく会に参加しました。
で、LTもさせていただきました。

1. 何を もくもく+LT したの?

「Durable Functionsの基礎学習」とその発表でした。
(何かを作り上げた!みたいなものではないのだけれど、昔から技術のバックグラウンドを調べたりするの好きなので)

今年に入ってから、仕事では .NET/Azure を離れ、Java/AWS の世界に移っていましたが、ぽろぽろと趣味で.NET/Azureは やっていました。
で、先日のGlobal Azure Boot Camp 2018に参加してAzure界隈の方々とお話しさせていただいたら「Durable Functions」がホットだということで。
GW終わり辺りからドキュメントを読み、もくもく+LTをさせていただいたという感じです。

2. 発表資料

発表資料は↓↓↓です。

speakerdeck.com

なのですが、「学んだこと」「伝えたかった事」のメインはDemoをさせていただいた部分でした。
加えて、「内容がLT・プレゼン向きじゃない」+「私の説明が決して上手くない」ということで、伝わりにくかったかと思います。

ということでDemoした部分について、ブログらせて頂こうかと・・・

3. 何を学んだのか?何をDemoしたのか?

MS公式のDurable Functionsの資料を読んだ結果、以下のドキュメントがDurable Functionsが Durable である所以的な部分を指しているような気がしました。

docs.microsoft.com

しかし、
「ドキュメントに記述されているルールに従って実装すれば、いい感じにDurable Functionsは動くのだろうけど、何故上手く動くのかが腑に落ちない」
という感覚を持ち、その もやもや を払拭するための技術探索を行いました。
つまり、以下のような内容が、話としては理解するけれど、具体的にそうなる根拠となるバックエンドの知識が欲しい、と。。。

・オーケストレーター関数
 決定論的である必要がある。
 複数回実行されても結果が同じでなければならない。
  →現在日付の取得・GUIDのランダム生成の実行はNG。
  →DateTime.Now ではなく DurableOrchestrationContext.​Current​Utc​Date​Time を使う
・アクティビティ関数
 非決定論的操作が可能。
 ある責務を持ったドメイン ファンクションはここで定義。
  →つまり「DBアクセス」などの処理はアクティビティ関数に実装
・イベントソーシング(パターン)で実装されている。
・Azure Storage(キュー・テーブル)に実行履歴をチェックポイントする事で信頼性を得ている。
・awaitが呼び出されるとオーケストレーター関数をゼロから再実行する。

4. Demo内容

Durable Functionsがどのようなフローで実行されるのか?Azure Storageを使ってどのようにDurableに実行されるのか?(直訳すれば "丈夫に" "恒久的に"ですね)
ほぼVSテンプレが吐き出す以下のコードで検証します(Http TriggerによるDurable Functionsコード)。
カスタムしたのは 19 / 20行目 を追加したぐらいでしょうか。

01: using System;
02: using System.Collections.Generic;
03: using System.Net.Http;
04: using System.Threading.Tasks;
05: using Microsoft.Azure.WebJobs;
06: using Microsoft.Azure.WebJobs.Extensions.Http;
07: using Microsoft.Azure.WebJobs.Host;
08: 
09: namespace FunctionApp2
10: {
11:   public static class Function2
12:   {
13:     [FunctionName("Function2")]
14:     public static async Task<List<string>> RunOrchestrator(
15:       [OrchestrationTrigger] DurableOrchestrationContext context)
16:     {
17:       var outputs = new List<string>();
18: 
19:       var currentUtc = context.CurrentUtcDateTime;
20:       var current = DateTime.Now; // ※本当はオーケストレーター関数で実行しちゃいけないやつ
21: 
22:       outputs.Add(await context.CallActivityAsync<string>("Function2_Hello", "Tokyo"));
23: 
24:       outputs.Add(await context.CallActivityAsync<string>("Function2_Hello", "Seattle"));
25: 
26:       outputs.Add(await context.CallActivityAsync<string>("Function2_Hello", "London"));
27: 
28:       return outputs;
29:     }
30: 
31:     [FunctionName("Function2_Hello")]
32:     public static string SayHello([ActivityTrigger] string name, TraceWriter log)
33:     {
34:       log.Info($"Saying hello to {name}.");
35:       return $"Hello {name}!";
36:     }
37: 
38:     [FunctionName("Function2_HttpStart")]
39:     public static async Task<HttpResponseMessage> HttpStart(
40:       [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")]HttpRequestMessage req,
41:       [OrchestrationClient]DurableOrchestrationClient starter,
42:       TraceWriter log)
43:     {
44:       // Function input comes from the request content.
45:       string instanceId = await starter.StartNewAsync("Function2", null);
46: 
47:       log.Info($"Started orchestration with ID = '{instanceId}'.");
48: 
49:       return starter.CreateCheckStatusResponse(req, instanceId);
50:     }
51:   }

ブレークポイントは張りまくります。

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

テストではローカルAzure Storage(Emu)を使いますが、中身を完全に空にしておきます。

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

4.1. 実行

(1) デバッグ実行

プロジェクトをデバッグ実行します。
以下のような感じでローカルでAzure Functionsが実行されます。

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

ここで、Azure Storage ExplorerでStorageの確認を行います。

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

「Blob Contaners」「Queues」「Tables」に色々作成されています(中身は空)。

※上記Storage項目のそれぞれの説明はここらへんに詳細が書かれています。構成により調整が可能。

(2) HTTPトリガーをキック

HTTPトリガーの受け口である「http://localhost:7071/api/Function2_HttpStart」をポストマンでキックします。

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

当然、FunctionsのHTTPトリガーである「HttpStart()」が呼び出されます。

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

「続行(F5)」を行います。
すると、コードの実装の通り「StartNewAsync("Function2")」でFunction2オーケストレーター関数が呼び出されます。
ということで、以下の画面のように Function2オーケストレーター関数 でブレークします。

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

再び「続行(F5)」して「outputs.Add(await context.CallActivityAsync("Function2_Hello", "Tokyo"));」まで飛んだ状態が以下です。

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

日付取得結果は以下の通りでした。

context.CurrentUtcDateTimeの値 → 2018/5/12 14:30:29
DateTime.Nowの値 → 2018/5/12 23:30:46

今度は「ステップオーバー(F10)」します。
CallActivityAsync()により Function2_Hello が呼び出され、以下の場所でブレークします。

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

この状態でAzure Storage Explorerにて「DurableFunctionsHubHistoryテーブル」を確認すると以下のようレコードが作成されています。

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

※要するに「Function2オーケストレーター関数→Function2_Helloアクティビティ関数の呼び出しの履歴の保存」がAzure Storageに対して行われたということです。

で、「続行(F5)」しちゃいます。
結果、ブレークするのはココ↓↓↓↓↓。

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

そう、次のアクティビティ関数呼び出しの個所ではなく、オーケストレーター関数の頭のブレークに飛びます。

また、「続行(F5)」しちゃいましょう。
こんな感じになりますね↓↓↓。

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

context.CurrentUtcDateTimeの値 → 2018/5/12 14:30:29
DateTime.Nowの値 → 2018/5/12 23:32:31

DateTime.Nowは実行した時間そのものが取得されているのに対し、context.CurrentUtcDateTime値は「初回実行時と同じ値」が取得されています!
何度実行しても(何度再生されても)結果が同じ、つまり「決定論的動作」をしています。オーケストレーター関数の条件を満たしている!

次に「ステップオーバー(F10)」すると、先程1度実行済みの「outputs.Add(await context.CallActivityAsync("Function2_Hello", "Tokyo"));」が実行されるはず・・・
では「ステップオーバー(F10)」してみます。
Function2_Helloアクティビティ関数は呼び出されず、次のアクティビティ関数呼び出し箇所「outputs.Add(await context.CallActivityAsync("Function2_Hello", "Seattle"));」に移りました。

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

さらに「ステップオーバー(F10)」してみます。
今度は、Function2_Helloアクティビティ関数が呼び出されました(引数nameはもちろんSeattle)。

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

ここで再びAzure Storage Explorerを見てみましょう。
レコードが増えましたね。Function2_Helloアクティビティが2回呼ばれた記録が行われています。

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

また、Result列に「Hello Tokyo!」と、1つ目のアクティビティ関数の処理結果が保存されています。

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

ここでちょっといたずら的にTokyoをOsakaにUpdateしてしまいます。

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

再び「続行(F5)」しちゃいましょう。
想像通り、オーケストレーター関数のトップに帰ってきます。

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

F5を連打し、Durable Functionsの処理をすべて終了させます。

結果として処理の履歴はDurableFunctionsHubHistoryテーブルに以下のように出力されました。

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

DurableFunctionsHubInstancesテーブルを見ると、処理結果が保存されています。

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

ポストマンでstatusQueryGetUriをリクエストしてみると・・・

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

先程いたずらでAzure Storageデータを変更した「Osaka」の文字が。
つまり、ロジックをメモリ上で実行して終わりではなく、Azure Storageに処理状態を永続化していることが理解できました。

5. まとめ

長々と分かりにくい検証を行いましたが以下のことが明確に理解できたのではないかと思います。

  • Durable Functionsは、アクティビティ関数の実行履歴を細かくAzure Storageに保存している。
  • Durable Functionsは、オーケストレーター関数で定義されたアクティビティ関数を高い信頼性で実行するためにawaitのタイミングでAzure Storageに状態を保存し、自らをリプレイしてすべてのアクティビティ関数をワークフローとして実行している。

Durable Functionsの実装については以下のgithubで公開されているので、更にソースレベルで探索ができるのではないかと思います。

github.com

また、勉強会等でもkingkino@マンダム (@kingkinoko) on Twitterさんや(「🍖・ω・)「🍺 (@yu_ka1984) on Twitterさん などDurable Functionsマスターがいらっしゃるので、色々伺えるのではないかと思います。

Azure FunctionsでCosmos DBを入力バインドする

以前「Azure FunctionsからCosmos DBに出力バインドする」という記事を書きましたので、それと対になる形で入力バインドについても簡単にまとめておこうと思います。

ryuichi111std.hatenablog.com

今回 実装することは以下の通りです。

  • HttpTriggerを持つAzure Functionsを作成
  • Cosmos DBドキュメントを入力バインドで受け取る
  • Function内の処理で、受け取ったCosmos DBドキュメントに変更を加える

1 開発環境

利用する環境は、以下の通りです。

2 前提条件

以下のような Cosmos DB アカウント を用意している前提とします。

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

  • APIモデルは「DocumentDB」
  • データベース名「CompanyDB」、コレクション名「EmployeeCollection」を作成済
  • ドキュメントが1件登録済み(id=“0000000001")
-- 登録済みドキュメント --
{
  "id": "0000000001",
  "firstname": "ryuichi",
  "lastname": "daigo",
  "_rid": "5T9QALZ5bQABAAAAAAAAAA==",
  "_self": "dbs/5T9QAA==/colls/5T9QALZ5bQA=/docs/5T9QALZ5bQABAAAAAAAAAA==/",
  "_etag": "\"0300aeae-0000-0000-0000-599081c20000\"",
  "_attachments": "attachments/",
  "_ts": 1502642619
}

3 実装

Visual Studio 2017 Preview(15.3)を起動します。

(1) プロジェクト作成

メニュー「ファイル → 新規作成 → プロジェクト」を選択します。

「新しいプロジェクト」ウィンドウが表示されます。
以下の入力を行います。

  • プロジェクトテンプレート:「Visual C# → Cloud → Azure Functions」
  • 名前:適当に・・・ここではデフォルトの FunctionApp1 にしました

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

設定用JSONのみの空っぽのソリューションが出来上がります。

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

(2) Functionsコードの追加

ソリューションエクスプローラで FunctionApp1 をマウス右ボタンクリック。
表示されたメニューから「追加 → 新しい項目」を選択します。

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

「新しい項目の追加」ウィンドウが表示されるので、「Azure Function」アイテムを追加します。
ここでは名前をデフォルトの「Function1.cs」としました。

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

「New Templates - Function1」というファンクションのトリガーを選択するウィンドウが表示されます。

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

「HttpTriggerWithParameters」を選択します。
また、「AccessRights」を「Anonumouse」としました。これにより、トリガーとなるHTTPリクエストを行う際に認証なしとなります(テストの利便性の為ここではそうしていますが、誰でもHTTPトリガーをキックできてしまうということなので、ご利用には気を付けてください)。
「FunctionName」はデフォルトの「HttpTriggerWithParametersCSharp」とします。

自動生成された Function1.cs 配下の通りです(リスト1)。

//リスト1 自動生成されたFunction1.cs

using System.Linq;
using System.Net;
using System.Net.Http;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;

namespace FunctionApp1
{
  public static class Function1
  {
    [FunctionName("HttpTriggerWithParametersCSharp")]

    public static HttpResponseMessage Run(
      [HttpTrigger(
        AuthorizationLevel.Anonymous, 
        "get",
        "post", 
        Route = "HttpTriggerCSharp/name/{name}")]
      HttpRequestMessage req, 
      string name, 
      TraceWriter log)
    {
      log.Info("C# HTTP trigger function processed a request.");

      // Fetching the name from the path parameter in the request URL
      return req.CreateResponse(HttpStatusCode.OK, "Hello " + name);
    }
  }
}

Run()メソッドがFunctionのエントリーポイントです。
第1引数「HttpRequestMessage req」に「[HttpTrigger]属性」が付与されています。HttpTrigger属性のパラメータで以下の設定が宣言されています。

(3) CosmosDB入力バインド実装の追加

まず、CosmosDB入力バインド用エクステンションを NuGetパッケージ より追加します。

ソリューションエクスプローラで FunctionApp1 をマウス右ボタンクリック。
表示されたメニューから「NuGet パッケージの管理」を選択します。

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

「NuGet パッケージの管理」画面が表示されたら、「参照」タブを選択し「Microsoft.Azure.WebJobs.Extensions.DocumentDB」を検索します。

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

Microsoft.Azure.WebJobs.Extensions.DocumentDB」を選択して、インストールボタンをクリックします。

では、 Function1.cs を開き、以下のように編集します。

// リスト2 CosmosDB入力バインド処理を追加したFunction1.cs

using System.Linq;
using System.Net;
using System.Net.Http;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;

namespace FunctionApp1
{
  public static class Function1
  {
    [FunctionName("HttpTriggerWithParametersCSharp")]

    public static HttpResponseMessage Run(
      [HttpTrigger(
        AuthorizationLevel.Anonymous, 
        "get",
        "post", 
        Route = "HttpTriggerCSharp/name/{name}")]
      HttpRequestMessage req,
      [DocumentDB(
        "CompanyDB", 
        "EmployeeCollection",
        ConnectionStringSetting = "cosmosdb_DOCUMENTDB", 
        Id = "{name}")]
      dynamic document,
      string name, 
      TraceWriter log)
    {
      log.Info("C# HTTP trigger function processed a request.");

      // 入力ドキュメントをログ出力
      log.Info("input document info");
      log.Info(" firstname:" + document.firstname);
      log.Info(" lastname:" + document.lastname);

      // 入力ドキュメントに変更を加える
      document.age = 25;
      document.Salary = 15000000;

      // Fetching the name from the path parameter in the request URL
      return req.CreateResponse(HttpStatusCode.OK, "Hello " + name);
    }
  }
}

Function1.csへの修正ポイントは以下の通りです。

  • 引数「document」の追加
    HttpTrigger引数に続けて「document引数」を追加しています。これが CosmosDB入力バインドパラメータになります。
    パラメータには「DocumentDB属性」を付与しています。
    入力バインドの情報として「データベース名」「コレクション名」を指定しています。
    「ConnectionStringSetting」は、Cosmos DBへの接続文字列情報です。接続文字列自体ではなく、接続文字列を設定した「構成情報キー」を指定します(設定方法は後述)。
    「Id」は、入力バインドするCosmos DBドキュメントの ID を表します。ここでは「{name}」としていますが、これはHttpTriggerのパラメータ「{name}」に該当します。「nameパラメータ値 = ドキュメントID」と、意味的に少し変ですが、自動生成コードの修正を最小限にしています。

  • documentのログ出力
    document引数には ID に該当するドキュメント オブジェクトが自動的にバインド設定されて呼び出されます。
    document.firstname / document.lastname をログ出力することで、内容を確認できるようにしています。

  • documentへのプロパティ値追加
    以下のようにdocumentに対して変更を加えています。
    入力バインドしたドキュメントへの変更は、Functionの正常終了後に Cosmos DBドキュメント に反映されます(データ更新が行われます)。

// 入力ドキュメントに変更を加える
document.age = 25;
document.Salary = 15000000;

(4) 公開 & Azureポータルでの構成

では、Azure上に公開します。

ソリューションエクスプローラで FunctionApp1 をマウス右ボタンクリック。
表示されたメニューから「公開」を選択します。

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

「新規作成」を選択して「発行」ボタンをクリックします(既にAzure FunctionsをAzure上に作成済みの場合には、「既存のものを選択」を利用します)。

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

Functionの名前等は適当に・・・ここでは Function Appの名前を InputBindExample として、「リソースグループ」「App Service プラン」「ストレージアカウント」は、それぞれ新規作成しました。

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

「公開」が成功した後の、Azureポータル画面が以下となります。
「Azure Functions(App Service)」「App Service Plan」「ストレージアカウント」の3つのリソースが作成されました。

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

Azure Functions「InputBindExample」を選択します。
「アプリケーション設定」をクリックして、Cosmos DBへの接続文字列項目を追加します。

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

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

(5) 実行

では、Functionを実行してみます。
まず、Azureポータルから実行します。

Function「HttpTriggerWithParametersCSharp」を選択します。
そして右側のテストタブを表示し、パラメータ「name」に「0000000001」を設定します。これは「入力パラメータname = ドキュメントID」として実装しているためです。
実行ボタンをクリックします。

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

出力されたログが以下の通りです。
ID = 0000000001 のドキュメント内容 firstname / lastname が出力されていることを確認することができます。つまり、確かにCosmos DBドキュメントが入力バインドパラメータとして引き渡されました。

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

次にAzureポータルで、Cosmos DBの「データエクスプローラ」を表示します。
age / salary 項目が追加されていることが確認できます。

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

匿名認証OK、HTTP GET許可としている為、ブラウザで「Https://inputbindexample.azurewebsites.net/api/HttpTriggerCSharp/name/0000000001」をアクセスする事でもFunctionをキックすることができます。

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

4 まとめ

ということで、Azure FunctionsへのCosmos DB入力バインドでした。
入力バインドといっても、Functionsの処理内でCosmos DBドキュメントに対して変更を加えることもできました。

Azure Functionsへの入出力バインドはCosmos DB以外にも「Blob storage」だったり、「Queue storage」だったりと、たくさんのバリエーションがあります。これらのバインドを利用すると、従来型の定型的ロジックの記述を省略することができます。
今回のCosmos DBバインドの例では、Cosmos DBへの接続処理の記述、取得・更新処理 の記述といったものが不要となりました。
つまり、煩雑なロジックの記述が Azure上のプラットフォームインフラ に吸収されていることにより、開発者はビジネスロジックの記述に注力することが可能になります。

本投稿では、ローカル実行やデバッグ等、色々端折っておりますが、Azure FunctionsへのCosmos DB入力バインドの一例として参考になればと思います。

「Serverless Meetup Tokyo #4」に参加した話

2017/8/9 「Serverless Meetup Tokyo #4」に参加してきました。

serverless.connpass.com

4回目の開催ということですが、私は初めての参加になりました。
すんごい人気なので、#5も今の時点で満員状態のようです(補欠当選も十分あるので、興味があれば、溢れてても登録しておいて損はないと思います。私も #4 では補欠繰り上げしたので^^)。

Azure勢 と AWS

で、私はこのブログを見てもわかる通り、いわゆる「Azure勢」です。
そして、この「Serverless Meetup Tokyo」は「AWS勢」が主軸です。

基本的には「Azure勢」とか「AWS勢」とか線引きをするべきではないし、したくないのですが、現在の多様化した技術世界においては、Azure / AWS の両方の技術をくまなく把握する人間というのは、ある意味限られた優秀な方々なのだと思っています。私のような凡人は、片方、もしくは両方に触手を伸ばしたとしても、どちらかに偏る事になるのだと思っています。
そして、一方に完全に偏ったとしても「Azureのすべてを知る」「AWSのすべてを知る」これ自体が非常に困難なほどの状況ではないかと思ってもいます。

楽しかったのですよ

そんな中での「Serverless Meetup Tokyo #4」だったのですが・・・

結論としては非常に楽しめました!

あのー、Azure系イベントに参加するよりも、もしかしたら(ある意味)楽しかったかも・・・これは、きちんと説明しないと語弊があるのだけれど・・・

私自身、Azureについては日々勉強を重ねています。
ですので、Azure系イベントでは、やはり「既知の領域」というものが大きくなってしまいます(あー、そんな偉そうなこと言える程ではないのだけれど・・・ご勘弁をm(_ _)m)。

しかし、AWSについては「完全な無知」ではないにしても「知らない領域」が広く存在しています。
ですから、自分自身にとって知らないことが多く、

  • 「へー、そうなんだー」

とか

  • 「このサービスはAzureでは何に該当するの?」

とか、はたまた、

  • 「あれ!?AWSだとこんな問題があるの?もしかしてAzureだと、この機能を使ったら解決出来てるんじゃないかなぁ」

とかいろいろ興味深いお話を聞くことができます。

結局・・・

そして、各セッションに対する感想はそれぞれあったのですが、今私はお酒も入っているのでざっくり、個人的総括で締めさせていただきます!

  • 「”サーバーレス アーキテクチャ”、全然 実践投入 いけるじゃん!」という自信を持てた
  • そうそう、やっぱり NoOps こそ目指すべき道だよ!人的リソースを運用メンテにかけるより顧客へのValueの提供にかけるべきだよ!

あと、個人的思い・・・

  • 20代のフレッシュエンジニアも、40代のおっさんエンジニア(&マネージャ)も楽しめて、より多くのValueを生成出来て、売上・実利を上げられる仕事がしたいよ

そして、今日のセッションでは得られていない、ビジネス的興味、そして整理したい点は以下。

  • ServerLessやPaaSを使うことのコストメリットについての整理

ではではー。
いつものブログとちょっと違うテイストになっちゃったかも・・・

【Cosmos DB】有効期限(TTL)で自動削除されるデータを作る

1 概要

Cosmos DBでは「有効期限付きデータ(ドキュメント)」というものを作ることができます。
ログ情報とかユーザーセッション情報とかのデータを ドンドコドンドコ 投入して、一定期間経過したデータは自動で削除するようなケースで利用可能です。

「よくあるファイル転送サービス(一定期間で削除される)」だったり「スナップチャット(的な)有効期限で自動削除されるサービス」だったり、そんなところでも使えるケースがあるかと思います。

ただし、ドキュメントが単独で自動削除される機能であるため、「一連の関連データを削除」というようなケースでは、単純に適用することはできないと思います。

1つ、特徴として「自動削除では RU は発生しない(RUは消費しない)」という点があります。

自前プログラムで、裏側でバッチ削除的なことをする場合、RUを消費するので、上手く使えばメリットのある機能になると思います。

1.1 設定レベル

有効期限の設定は、以下の2つのレベルに対して設定可能です。

  • 「コレクション」に対して設定する
  • 「ドキュメント」に対して設定する

1.1.1 「コレクション」に対して設定する

「Off / On(no default) / On」の3つの設定から選択します。

(1) Off

Off は、このコレクションにおけるドキュメントの有効期限自動削除を無効にします。ドキュメントに対して明示的に有効期限を設定しても、自動削除機能は働きません。

(2) On(no default)

On(no default) は、このコレクションにおけるドキュメントの有効期限自動削除を有効にします。既定の有効期限時間は設定しません。
有効期限削除を有効にしたいドキュメントに対して、明示的に時間を設定します。

(3) On

On は、このコレクションにおけるドキュメントの有効期限自動削除を有効にします。既定の有効期限時間も設定します。
既定の有効期限は、各ドキュメントでオーバーライドすることが出来ます。

また、有効期限は「秒」単位で設定可能です。

1.1.2 「ドキュメント」に対して設定する

ドキュメントに対して有効期間を設定する場合は、保存するドキュメントオブジェクトに対してJSONプロパティ名=「ttl」というint?型プロパティを追加します。

 [JsonProperty(PropertyName = "ttl", 
  NullValueHandling = NullValueHandling.Ignore)]
 public int? TimeToLive { get; set; }

※後述リスト3が完全版オブジェクト実装

1.1.3 「コレクション設定」「ドキュメント設定」の相関関係

コレクションに対する設定とドキュメントに対する設定の相関関係を表にまとめると以下の通りです。

コレクション設定 = Off コレクション設定 = On(no default) コレクション設定 = On(既定値 n秒)
ドキュメント設定 = なし 有効期限削除は無効(削除されません) 有効期限削除は無効(削除されません) 最終更新日時からn秒後に削除されます
ドキュメント設定 = -1 有効期限削除は無効(削除されません) 有効期限削除は無効(削除されません) 有効期限削除は無効(削除されません)
ドキュメント設定 = m 有効期限削除は無効(削除されません) 最終更新日時からm秒後に削除されます 最終更新日時からm秒後に削除されます

※「ドキュメント設定 = xx」と記述しましたが、コード上では int? 型として有効期限(TTL)を指定する事ができます。

1.2 削除タイミング

削除のタイミングは以下となります。

ドキュメントが最終更新されてから、
有効期限として設定された時間(秒)が経過したタイミング   

「最終更新日時(Last modified timestamp)」からの経過時間で自動削除判定が行われます。
「最終参照日時」ではないので、単純に ”一定期間利用されていないデータを削除する” といった用途には適用できないのでご注意を。

1.2.1 最終更新日時(Last modified timestamp)とは

最終更新日時(Last modified timestamp)とは、具体的には以下のものを指します。

ドキュメントの「_ts」値  

_ts値は、ドキュメントに対してCosmos DBシステムによって設定される値です。
ドキュメントの最終更新日時を表し、ドキュメントの作成時・ドキュメントの更新時に自動的に更新されます。
値は、「UNIX時間」であらわされます。

Azureポータルのデータエクスプローラでドキュメントを参照すると、以下のように _ts 値を確認することができます。

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

Fiddlerでドキュメント取得を行うRESTリクエストをキャプチャしても、以下のように _ts 値を確認することができます。

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

2 実装(コード)

では具体的な実装(コード)で「ドキュメントの有効期限設定」を行う方法を説明します。

2.1 コレクションに対して有効期限を設定する

まず「コレクションに対して」有効期限を設定する方法になります。

// リスト1 コレクションに対してドキュメントの有効期限を設定

private DocumentClient client = new DocumentClient(
  new Uri("https://cosmosdb.documents.azure.com:443/"), // あなたのURLを設定してね
  "【キー】"); // あなたのキーを設定してね

var database = 
  await client.CreateDatabaseIfNotExistsAsync(
    new Database() { Id = "ExampleDb" });
  
var collection = 
  await client.CreateDocumentCollectionIfNotExistsAsync(
    UriFactory.CreateDatabaseUri("ExampleDb"),
      new DocumentCollection()
      {
        Id = "ExampleCollection",
        DefaultTimeToLive = 60
      });

上記リスト1はデータベース(ExampleDb)とコレクション(ExampleCollection)を「(存在しなければ)新規作成」するロジックとなります。
そして、「DefaultTimeToLive = 60」の部分が「コレクションに対する、ドキュメント有効期限設定」になります。
つまり、最終更新日時から60秒でドキュメントが自動削除される設定となります。
「未設定 もしくは null」を設定すると、「Off」となります。

        DefaultTimeToLive = null

「-1」を設定すると、「On(no default)」となります。

        DefaultTimeToLive = -1

2.1.1 DocumentCollection.DefaultTimeToLive と 「Off / On(no default) / On」設定

コレクションに対する有効期限設定(DocumentCollection.DefaultTimeToLive)と「Off / On(no default) / On」の概念的設定の関連性は以下の通りとなります。

  • DocumentCollection.DefaultTimeToLive = n
    「On(no default)」に該当します。
    ドキュメントは、最終更新日時のn秒後に自動削除されます。

  • DocumentCollection.DefaultTimeToLive = null
    「Off」に該当します。

  • DocumentCollection.DefaultTimeToLive = -1
    「On(no default)」に該当します。
    ドキュメントの自動削除は有効ですが、自動削除時間はドキュメントの設定に依存します。

2.2 ドキュメントに対して有効期限を設定する

「個々のドキュメントに対して」有効期限を設定する例が以下のリスト2になります。

// リスト2 自動削除までの時間=100秒のドキュメントを作成

// コレクションに保存するオブジェクトを作成
UserAccount userAccount = 
  new UserAccount() { 
    UserID = "ryuichi111std", 
    UserName = "Ryuichi Daigo", 
    UpdateCount = 0, 
    TimeToLive=100 };

// オブジェクトをコレクションに保存(ドキュメントとして保存)
var document = await client.CreateDocumentAsync(
  UriFactory.CreateDocumentCollectionUri("ExampleDb", "ExampleCollection"),
  userAccount);

Cosmos DB(DocumentDB)に保存するデータ型「UserAccount」が唐突に登場していますが、これは以下のリスト3の型となります。
「TimeToLiveプロパティ」値が有効期限(TTL)となります(ドキュメントの有効期限は100秒)。
リスト3にあるように「jsonプロパティ名 = ttl」とします。これがドキュメントの有効期限を示すプロパティとなります。

// リスト3 保存するドキュメントの型はこれ

using System;
using Newtonsoft.Json;

namespace AutoExpireDataExample
{
  public class UserAccount
  {
    [JsonProperty(PropertyName = "id")]

    public string UserID { get; set; }

    public string UserName { get; set; }

    public int UpdateCount { get; set; }

    [JsonProperty(PropertyName = "ttl", NullValueHandling = NullValueHandling.Ignore)]
    public int? TimeToLive { get; set; }
  }
}

3 まとめ

ということで「有効期限で自動削除されるデータ(ドキュメント)」についてのエントリーでした。
ドキュメントの削除処理に関したは「単に特定コレクションドキュメントが単独で消えていい例」それから「プログラマティックに関連データも同時にカスタム実行により削除したい例」とあると思います。
その上で、RU消費のない「ドキュメントの有効期限自動削除」は魅力的な機能でもあります。
使いどころの難しさはありますが、うまく活用すれば有益な機能の1つであると思います。

Azure FunctionsからCosmos DBに出力バインドする(2)~.csコンパイル編

1. はじめに

前回のエントリーに引き続きの投稿になります。
Azure Functions 出力バインドを利用し Cosmos DB にデータ出力を行います。

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

前回はAzureポータルのみでFunctionsの作成を行いました。
また、ソースコードの実装は .csx(C#スクリプト) をポータル上で編集しました。

今回は .csファイル で実装を行い、コンパイルアセンブリをAzure Functionsに発行することにします。
.csxでの実装に比べ、実行時コンパイル処理が省かれるので、起動時の動作が早くなります。

本エントリーのみでも理解できる内容として記述しますが、前回のエントリーと重複する部分は手短に記述します。ということで、前回のエントリーを見ていただけるとより理解しやすいかと思います。

ryuichi111std.hatenablog.com

2. 開発環境

コンパイル実装をするため、本エントリーでは以下の環境を利用します。

※ 2017/7/20現在、Stableな Visual Studio 2017 では 「Function Tools for Visual Studio 2017」がサポートされていません。

3. 前提条件

以下のAzure環境を前提条件とします。

3.1 Cosmos DB アカウント

以下のような 出力先 Cosmos DB アカウント を用意している前提とします。

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

API(データモデル)は「SQL (DocumentDB)」です。

3.2 Function App

前回エントリーで作成した以下の「Function App」を用意している前提とします。

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

「アプリ名」:RdExampleFunctionApp
「リソースグループ」:RdExampleFunctionApp
ホスティングプラン」:従量課金プラン
「場所」:西日本
「Storage」:新規作成 rdexamplefunctionapp

4. ② VS2017 Preview(Ver.15.3)を使ったcsコンパイルによる実装

VSを使用した実装に移りますが、「4.1 基本的なAzure Functionsプロジェクトの作成から発行まで」と、その上に「4.2 osmos DB出力バインドの追加からパブリッシュまで」の2段階で進めたいと思います。

4.1 プロジェクトの作成から基本パブリッシュまで

まず第1段階として、基本的なAzure Functionsプロジェクトの作成から発行までを行います。

(1) Visual Studio 2017 Preview(15.3)の起動

Visual Studio 2017 Preview(15.3)を起動します。
「Azure Function Tools for Visual Studio 2017」のインストールを済ませておいてください。

(2) Azure Functionsプロジェクトの作成

「ファイル → 新規作成 → プロジェクト」を選択します。
「新しいプロジェクト」ウィンドウで、テンプレートカテゴリーから「Cloud」を選択、プロジェクトテンプレートとして「Azure Functions」を選択します。
プロジェクト名は、ここでは「CosmosDbBindExampleFunction」としました。

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

以下のようなプロジェクトが作成されます。

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

(3) Functionソース(.cs)の追加

新規作成したプロジェクトには、まだ1つもFunction実装がありません。
Functionの実装ソース(.cs)を追加します。
ソリューションエクスプローラでプロジェクト名「CosmosDbBindExampleFunction」をマウス右ボタンクリック。表示されたメニューから「追加 → 新しい項目」を選択します。

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

表示された「新しい項目の追加」ウィンドウから「Azure Function」を選択し、名前を入力します。ここではデフォルトのまま「Function1.cs」としました。

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

「New Azure Function」ウィンドウが表示されます。

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

ここでは、設定内容は以下とします。

  • トリガーの種類: HttpTrigger
  • AccessRights: Anonymous(今回は簡易に匿名で利用できるようにします)
  • FunctionName: HttpTriggerCSharp

自動生成された Function1.cs は以下の通りです。

// 自動生成された Function1.cs 

using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;

namespace CosmosDbBindExampleFunction
{
  public static class Function1
  {
    [FunctionName("HttpTriggerCSharp")]
    public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]HttpRequestMessage req, TraceWriter log)
    {
      log.Info("C# HTTP trigger function processed a request.");

      // parse query parameter
      string name = req.GetQueryNameValuePairs()
        .FirstOrDefault(q => string.Compare(q.Key, "name", true) == 0)
        .Value;

      // Get request body
      dynamic data = await req.Content.ReadAsAsync<object>();

      // Set name to query string or body data
      name = name ?? data?.name;

      return name == null
        ? req.CreateResponse(HttpStatusCode.BadRequest, "Please pass a name on the query string or in the request body")
        : req.CreateResponse(HttpStatusCode.OK, "Hello " + name);
    }
  }
}

(4) Nugetパッケージの更新

自動生成されたプロジェクトおよびfunction1.csは、2017/7/20現在の確認では、そのままではビルドが通りません。
Nugetパッケージの更新を行う必要があります。
ソリューションエクスプローラでプロジェクト名「CosmosDbBindExampleFunction」をマウス右ボタンクリック。表示されたメニューから「NuGetパッケージの管理」を選択します。

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

「更新プログラム」を選択、「プレリリースを含める」にチェック、「すべてのパッケージを選択」にチェックし、「更新」ボタンをクリックします。

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

(5) ビルド&発行

ソリューションエクスプローラでプロジェクト名「CosmosDbBindExampleFunction」をマウス右ボタンクリック。表示されたメニューから「発行」を選択します。

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

発行のUIが表示されるので「既存のものを選択」を選択して「発行」ボタンをクリックします(今回はAzureポータル上に既に RdExampleFunctionApp というFunction Appを作成済みの為)。

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

発行対象のFunction Appである「RdExampleFunctionApp」を選択して、「OK」ボタンをクリックします。

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

発行が完了したらAzureポータルでFunctionを確認します。
HttpTriggerCSharp関数が発行されて事を確認することができます。

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

4.2 Cosmos DB出力バインドの追加からパブリッシュまで

次に第2段階として、Cosmos DB出力バインドの追加を行います。

(1) DocumentDB Extensionの追加

Cosmos DBへの出力バインドを実装するために、以下のNuGetパッケージを追加します。

  • Microsoft.Azure.WebJobs.Extensions.DocumentDB

NuGetパッケージの管理画面から「Microsoft.Azure.WebJobs.Extensions.DocumentDB」を検索&選択して「インストール」を行います。

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

(2) Function1.csの実装を修正

Function実装を Cosmos DB出力バインド を行うように修正します。

// Cosmos DB出力バインド実装を加えた Function1.cs

using System.Linq;
using System.Net;
using System.Net.Http;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;

namespace FunctionApp2
{
  public static class Function2
  {
    [FunctionName("HttpTriggerCSharp")]
    public static HttpResponseMessage Run(
      [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]
      HttpRequestMessage req,
      [DocumentDB("CommunicationDB", "MessageCol",
            CreateIfNotExists = true,
            ConnectionStringSetting = "cosmosdb_DOCUMENTDB")]
      out object messageDocument,
      TraceWriter log)
    {
      string yourName = req.GetQueryNameValuePairs()
        .FirstOrDefault(q => string.Compare(q.Key, "yourName", true) == 0)
        .Value;

      string message = req.GetQueryNameValuePairs()
        .FirstOrDefault(q => string.Compare(q.Key, "message", true) == 0)
        .Value;

      // 出力パラメータmessageDocumentに設定された値がCosmos DBに出力される
      messageDocument = new
      {
        yourName = yourName,
        message = message
      };

      if (!string.IsNullOrEmpty(yourName) && !string.IsNullOrEmpty(message))
      {
        return req.CreateResponse(HttpStatusCode.OK);
      }
      else
      {
        return req.CreateResponse(HttpStatusCode.BadRequest);
      }
    }
  }
}

上記実装のうち重要なポイントを2つ以下に記述します。

①メインの Run() メソッドの引数

前回の.csxファイル形式での実装では、引数のバインドをAzureポータルのGUI上で行いました。
AzureポータルGUI上での操作は、実際には function.json ファイルに反映されます。つまり、GUI操作はエディタとしての機能であり、本質的には function.json への記述で設定を行いました。
コンパイルベースのFunction定義では、メソッド引数への属性設定により同様の設定を行います(つまり、引数属性設定により入出力バインドの設定を行うことができます)。

  • 第1引数

    [HttpTrigger(AuthorizationLevel.Anonymous, “get”, “post”, Route = null)] HttpRequestMessage req

入力のHttpTrigger定義となります。匿名ユーザーのアクセスを許可し、GET / POST を受け入れます。ルーティング定義はnullとしています。

  • 第2引数

    [DocumentDB(“CommunicationDB”, “MessageCol”, CreateIfNotExists = true, ConnectionStringSetting = “cosmosdb_DOCUMENTDB”)] out object messageDocument

こちらがCosmos DB出力バインドの定義になります。
CommunicationDBデータベース、MessageColコレクションへの出力としています。
CreateIfNotExists=trueは、本Functionの実行時にCosmos DB側のデータベース・コレクションが存在しなかった場合に自動生成するかどうかの設定です。ここでは、trueなので存在しなかったら作成する意味となります。
ConnectionStringSetting 値は、出力先のCosmos DBへの接続文字列を表します。ただし、接続文字列自体ではなく、接続文字列を設定したアプリケーション設定キーとなります。
アプリケーション設定キーは、Azureポータルで設定・確認することができます。
RdExampleFunctionAppを選択し、「アプリケーション設定」をクリックします。

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

「アプリ設定」項目に「キーと値」の設定が行えるので、ここに「cosmosdb_DOCUMENTDB」キーを追加し、値としてCosmos DBアカウントへの接続文字列を追加します(前回の.csx形式によるブログエントリーを実施している場合、既に当該キーは追加されていると思います)。

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

②Cosmos DB ドキュメントの作成

出力バインド定義が行われた引数 messageDocument をnewで生成することで、作成するCosmos DB(DocumentDB)ドキュメントを指定することができます。

messageDocument = new
{
yourName = yourName,
message = message
};

(3) 発行

VSから発行を行います。
※ 既に前述で発行方法は説明したので、ここでは割愛します。

(4) テスト実行

今回発行したFunctionはHttpTriggerで起動します。
匿名ユーザーによるリクエストも許可している為、以下のURLによりキックすることができます。

https://rdexamplefunctionapp.azurewebsites.net/api/HttpTriggerCSharp?yourName=ryuichi.daigo&message=hello azure functions

ブラウザで上記URLをリクエストします。

AzureポータルからCosmos DBのデータエクスプローラを確認すると、以下のようにドキュメントが作成されたことを確認することができます。

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

5 まとめ

Azure Functionsは、超基本でいうとあくまで「Function(関数)」なのですが、今回紹介したBindを含め、やはり色々な機能を持っています。最近ではDurable Functionsなんてのも出てきましたし。
そんな進化し続けるAzure Functionsですが、Visual StudioのToolkitのリリースはやや遅めな印象を持っています。とはいえ、そろそろ正式版も出るんじゃないかなあ・・・とも思うので、そのあたりの開発インフラ事情が更新されたら、本エントリーも改めて修正していきたいと思います。

Azure FunctionsからCosmos DBに出力バインドする(1)~.csx編

Azure Functionsには、その実行に関して「トリガー」「バインド」という概念が存在します。

  • 「トリガー」
    ファンクションの実行のきっかけとなるもの。「1つのファンクション」に「1つのトリガー」が定義されます。
  • 「バインド」
    そのファンクションで処理する入力方向および出力方向のデータを表します。

※Azure Functionsのトリガーとバインドの本質的な解説は以下のあたりを参考に・・・

docs.microsoft.com

1. 今回の実装概要

今回はAzure Functionsの出力バインドとして Cosmos DB を利用する方法についてまとめます。

実装するものは以下のようなイメージになります。

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

まず、用意する Azure Functions は、HTTPリクエストをトリガーとして実行されます。
FunctionsはCosmos DBを出力バインドとします。

つまり、Azure FunctionsをホストするURLに対してHTTPリクエストが行われると、Functionが実行され、結果としてCosmos DBに対してドキュメントが作成される(出力される)、という動きになります。

前提条件

今回は出力バインドとして Cosmos DB を用います。
ということで、前提条件として以下のような Cosmos DB アカウントを用意していることを前提とします。

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

API(データモデル)は「SQL (DocumentDB)」です。

2. 2つの実装方式

Azure Functionsは実運用にも耐えうる状態と考えられますが、開発ツールの統合という意味ではまだまだ過渡期的な状況です(2017/7/19現在)。
今回は以下の2つの方法でAzure Functionsを実装します。

  • ① Azureポータルのみを使ったcsxによる実装
    Azureポータル上の操作のみで実装します。実装ファイルはcsx(C#Script)で行います。

  • Visual Studio 2017 Preview(Ver.15.3)を使ったcsコンパイルによる実装
    実装はcsで行います。つまりコンパイルイメージをAzure Functionsにアップします。

共通の初期設定

まず、「1 Azureポータルのみを使ったcsxによる実装」「2 Visual Studio 2017 Preview(Ver.15.3)を使ったcsコンパイルによる実装」の両方に共通の設定を行います。

「Function App」の作成

Azureポータルにログインします。

(1) Function Appの検索

「+新規」をクリックし、検索ボックスに「Function App」と入力します。
候補一覧に表示された「Function App」を選択します。

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

(2) 作成

「作成」ボタンをクリックします。

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

(3) パラメータの入力

アプリ名等々を適時入力して「作成」ボタンをクリックします。
ここでは以下の内容としました。
「アプリ名」:RdExampleFunctionApp
「リソースグループ」:RdExampleFunctionApp
ホスティングプラン」:従量課金プラン
「場所」:西日本
「Storage」:新規作成 rdexamplefunctionapp

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

(4) 作成されたリソースの確認

(3)の後、しばらく待っているとAzure Functionsを構成するリソースが作成されます。
作成したリソースグループ「RdExampleFunctionApp」は以下となります。

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

「App Serviceプラン」「ストレージ アカウント」「App Service」の3つのリソースが作成されました。
Azure Functionsは基本的にApp Serviceであり、また、その動作の際にはAzure Storageを利用します。その為、上記のような3つのリソースが生成されました。

3. 「① Azureポータルのみを使った.csxによる実装」

では、Functionsの作成に移ります。

3.1 Function Appの選択

リソースグループから(もしくはリソースの一覧から)「RdExampleFunctionApp」を選択します。

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

3.2 関数の追加

関数の「+」マークをクリックし、右側のペインから「カスタム関数を作成する」をクリックします。

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

カスタム関数作成画面が表示されます。

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

以下の作成パラメータを入力します。
テンプレート:「HttpTrigger - C#
関数名の指定:「HttpTrigger1」
承認レベル:「anonymous」 ※認証なしのHTTPリクエストを受け入れるトリガーとします。

「作成」ボタンをクリックすると関数(Azure Function)が作成されます。

3.3 関数の実行

自動生成された HttpTrigger1 ファンクションを実行してみます。

実行ボタンをクリックします。

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

画面下側の「ログ」ぺインに、コード「log.Info(…)」で実装されたログ文字列が表示されるのを確認することができます。
ひとまず、デフォルトで生成されるファンクションの実行をすることができました。

3.4 Cosmos DB出力バインドの設定

次にCosmos DB出力バインドの設定を行います。
左側のファンクション メニューから「統合」を選択します。

(1) 出力の作成

更に、右側に表示された設定画面から、出力→「+新しい出力」をクリックします。

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

(2) DocumentDB形式の選択

一覧から「Azure DocumentDB ドキュメント」を選択し「選択」ボタンをクリックします。

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

(3) 出力バインドの詳細設定

Azure DocumentDB(Cosmos DB)出力バインド設定を行います。
設定項目は以下の通りです。

  • ドキュメントパラメーター名:ファンクションの引数名。この名前で受け取った引数(変数)でCosmos DBのドキュメントを操作します。
  • データベース名:操作対象のデータベース名を設定します。
  • コレクション名:操作対象のコレクション名を設定します。
  • DocumentDBのデータベースとコレクションが作成されるようにしますか?:データベース・コレクションが未作成状態の時に、自動生成するかどうかのフラグを設定します。

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

続けて「DocumentDBアカウント接続」→「新規」をクリックします。

以下の画面のように、Cosmos DBアカウント一覧が表示されるので、操作対象のCosmos DBアカウント「cosmosdb」を選択します。

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

以上で、Cosmos DBへの出力バインドの設定が完了しました。
以下が設定完了後の画面です。

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


※2017/7/20追記
上記、Azureポータル上でのバインド設定は結果として function.json に反映されます(以下の画面)。

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

AzureポータルGUI操作によるバインド設定は、function.jsonを編集する為のエディタであり、本質的な設定記述場所は function.json となります。


3.5 関数(ファンクション)実装の修正

次に、関数(ファンクション)の実装を修正します。
左側のメニュー(?)から「HttpTrigger1」を選択し、run.csxのエディタ画面を表示します。

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

実装を以下に修正して、「保存」ボタンをクリックします。

using System.Net;

public static HttpResponseMessage Run(HttpRequestMessage req, out object outputDocument, TraceWriter log)
{
  string yourName = req.GetQueryNameValuePairs()
    .FirstOrDefault(q => string.Compare(q.Key, "yourName", true) == 0)
    .Value;

  string message = req.GetQueryNameValuePairs()
    .FirstOrDefault(q => string.Compare(q.Key, "message", true) == 0)
    .Value;

  outputDocument = new {
    yourName = yourName,
    message = message
  };


  if (!string.IsNullOrEmpty(yourName) && !string.IsNullOrEmpty(message)) {
    return req.CreateResponse(HttpStatusCode.OK);
  }
  else {
    return req.CreateResponse(HttpStatusCode.BadRequest);
  }
}

実装内容のポイントは、以下の通り。

  • クエリーパラメータとして「yourName」「message」の2つのパラメータを文字列で受けとります。
  • 受け取ったパラメータからCosmos DBのドキュメントを生成します。
  • 引数「outputDocument」は、出力バインド設定「1.4 (3)」の「ドキュメントパラメーター名」と一致している必要があります。

3.6 テスト実行

右側の「テスト」をクリックし、HTTPメソッドを「GET」に設定します。
「クエリ」には「パラメータの追加」で「yourName」および「message」を追加します。
これらは既に実装した関数(ファンクション)の仕様に合わせた呼び出し方法となります。
「実行」ボタンをクリックします。

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

3.7 テスト結果の確認

AzureポータルでCosmos DB(cosmosdbアカウント)の「データエクスプローラ」を表示します。
以下の画面のように ドキュメント が生成されていることを確認することができます。

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

4. まとめ(&つづく・・・)

ということで、思ったより長くなったので「(2) Visual Studio 2017 Preview(Ver.15.3)を使ったcsコンパイルによる実装」は、あらためエントリーを分けて投稿したいと思いますm(_ _)m。

おまけ

AzureポータルGUIで設定した Cosmos DB出力バインド による接続文字列設定は、以下の場所に保存されています。
作成した Azure Functions「RdEzampleFunctionApp」を選択し、「アプリケーション設定」を選択します。

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

「アプリ設定」の下に構成設定として設定値が保存されています。

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

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入門」を読んでいただいた皆様、ありがとうございました!