Azure Cosmos DB入門(1)

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

ryuichi111std.hatenablog.com

1 Cosmos DBとは

Cosmos DBはAzureで提供されるデータベースサービスの1つです。データベースの種類としては、いわゆる「NoSQL」データベースとなります。
公式ドキュメントにおいては、まず以下のように説明されています。

「グローバル分散可能なマルチモデルデータベースサービス」

1.1 特徴

Cosmos DBの代表的な特徴は以下ような点があげられます。

  • グローバル分散可能
  • 柔軟なスケーリングが可能
  • 超高速な応答速度
  • マルチデータモデル
  • 多様な一貫性モデル
  • 高いレベルのSLA

上記項目について、それぞれ順番に見ていきましょう。

1.1.1 グローバル分散可能

Azureは世界30ヶ所以上のリージョンでサービスが提供されています。Cosmos DBは、これらのリージョンに対してレプリケーションが可能です。
つまり、世界中に分散してデータを保持することができます。
世界規模のサービスを提供する場合、データベース(Cosmos DB)とアプリケーション(例えばAppService)を各リージョンにレプリカ&配置することで、エンドユーザーが最寄りのリージョンに対してのアクセスでサービスを利用するようにすることができます。つまり、地理的に遠いリージョンへのアクセスによるネットワーク遅延を回避することが可能になります。勿論、そのうえでレプリケーションされたCosmos DBはバックエンドで自動的にリージョン間のデータ同期が行われます。また、特定リージョンのダウン時には自動的なフェールオーバーが行われます。

1.1.2 柔軟なスケーリングが可能

スループットのスケーリングもCosmos DBの大きな特徴になります。
クラウドサービスの特徴として自由自在にスケール調整が可能なことがあげられます。
Cosmos DBは、更に、オンライン状態のまま5秒以内にパフォーマンスのスケール変更が可能となっています。スケーリングの調整は、Azureポータル上からもプログラム上からも簡単に可能となります。
また、Cosmos DBにおけるスループットは「RU(Request Unit)」という単位になっており、これが課金対象の大きな要素となります。
ストレージ容量についてもペタバイト単位までスケール可能となっています。
そしてCosmos DBは無制限にスケーリングすることが可能です。

1.1.3 超高速な応答速度

Cosmos DBに限らず、AWSのDynamoDBにしても、それからオンプレ運用可能なApache CASSANDRAにしても、NoSQLデータベースの特徴として「超高速である」ということは周知の事実でしょう。
対比されるデータベースシステムとしてRDBがありますが、NoSQLでは「テーブル間リレーションという概念を持たない」「限られたトランザクション機能とする」等の機能実装上の方針の違いから「高速な動作」を手に入れています。
Cosmos DBでは、同一リージョン内における読み取り・書き込みのレイテンシーに対して以下の表に示すようなミリ秒単位の速度を保証しています。

Read(1KB) Indexed Write(1KB)
50% < 2ms < 6ms
99% < 10ms < 15ms

※「同一リージョン内」でのレイテンシーとは、つまり「Japan WestにApp Serviceをパブリッシュ&同じくJapan WestにCosmos DBを作成」というケースにおける App ServiceからCosmos DBへのデータアクセスにおけるレイテンシーを指します。

1.1.4 マルチデータモデル

これもCosmos DBの大きな特徴になります。
従来のNoSQLでは、CASSANDRAは「Key-Value」、MongoDBは「ドキュメント」・・・のようにデータベースプロダクト(サービス)と保持するデータモデル形式は固定化されていました。
しかし、Cosmos DBでは以下のような異なるデータモデルを保持することができます。

  • 「ドキュメント」
  • 「Graph」
  • 「Key-Value」

またそれらのデータモデルに合わせて、アクセス用のAPIもそれぞれ用意されています。

  • 「DocumentDB用API(ドキュメント)」
  • 「MongoDB用API(ドキュメント)」
  • 「Gremlin用API(Graph)」
  • 「Tables用API(Key-Value)」

1.1.5 多様な一貫性モデル

従来の一般的なNoSQLデータベースでは、データの一貫性に関して以下の2つのモデルを提供していました(AWS DynamoDBの該当)。

  • 「Strong」 : 強固な一貫性を提供する(ただし速度性能を犠牲にする)
  • 「Eventual」 : (一貫性を維持するためにタイムラグを要するが)最終的にデータの一貫性を提供する(速度性能重視)

Cosmos DBでは、上記に加えて「Bounded-staleness」「Session」「Consistent Prefix」という3つのモードが用意されています。(合計5つの一貫性モードが用意されています)

1.1.6 高いレベルのSLA

Cosmos DBでは以下について99.9%という非常に高いSLA保証を行っています。

SLAの詳細は以下に公開されています。
https://azure.microsoft.com/en-us/support/legal/sla/cosmos-db/v1_0/

1.1.7 Cosmos DBは自分たちが普通に使うものか?

上記で紹介した6つの特徴から「Cosmos DBは"なんかすごい"」という事が感じ取れると思います。と同時に「世界規模の分散」や「無制限なスケーリング」などを必要とするような、例えばfacebookだったりtwitterだったりのようなシステムを自分たちは作りはしないし、これって使う必要あるの?という思いを持つ方も多いことでしょう。
しかし、Cosmos DBがレイテンシーに関して「読み込み <10ms、書き込み <15ms」の保証をしているデータベースシステムである、という点だけでも使う価値を見出せるのではないかと思います。
多くの既存の、そして現役の開発の現場(これは、特定企業内のBtoBなシステム、不特定多数の企業を対象としたBtoBのクラウドサービス、不特定多数の個人を対象としたBtoCのサービス、を含む)においても、RDBによるデータアクセスがボトルネックとなるケース、その為のパフォーマンスチューニングを追加で講じる必要に直面するケースは多いのではないでしょうか。
RDBをCosmos DBにすべて置き換える」という選択肢もありますし、「RDBとCosmos DBのハイブリット運用」という選択肢もあるでしょう。
また、Cosmos DBはACIDなトランザクション処理をサポートしていますが、従来のRDBよりも制約があり設計上の工夫が必要である点があります。その様なことから、マスタデータをRDBで、読み取り効率を上げたい部分に関してはRDB → Cosmos DBへのデータ連携を走らせ、Cosmos DBからのリードパフォーマンスの優位性を享受する、ということも考えられます。
それから、データ要件によってはRDBのテーブル構造がマッチしないケースもあります。例えば私が所属する会社の製品にワークフロー(稟議書)を扱うものがあります。稟議書は「申請者から何人もの承認者を経るパス」を持ちます。パスは単純なケースもあれば、稟議書の内容によって複雑な分岐をする場合もあります。下図のようなパス(フロー)をデータベースに保持する場合、テーブル構造よりもグラフ構造で保存した方が自然な形で保存することができますし、速度も圧倒的に早いでしょう。

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

※私自身もNoSQLをガンガンシステムに適用しているわけではないので、この辺りは業界全体としても経験を蓄積し、並行してさらなる技術の進歩が起こりながらベストプラクティスを求めていくことになると思います。

1.2 Hello Cosmos DB

ではHello Worldならぬ、Hello Cosmos DBとして、Cosmos DBを利用するファーストステップを行ってみようと思います。

1.2.1 AzureポータルでCosmos DBを作成

ブラウザで「https://portal.azure.com」を開きます。
サイドメニューから「新規」を選択します。

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

フィルターに「Azure Cosmos DB」と入力します。

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

「Azure Cosmos DB」を選択します。

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

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

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

任意の「ID」(ここでは cosmosdb)を入力します。
API」は「SQL(DocumentDB)」を選択します。 「リソースグループ」の任意の値(ここでは cosmosdb)を入力します。

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

以上で「cosmosdb」というデータベースアカウントのCosmos DB(DocumentDBデータモデル)が作成されました。

1.2.2 Azureポータルでデータを操作

実際の運用アプリケーションでは、データの作成・検索・更新等の操作はプログラムから行われますが、ここではまず、Azureポータル上からCosmos DBのデータ操作を行いその雰囲気を感じ取ります。

(1) データの入れ物の準備(データベースとコレクション)

作成したCosmos DBをAzureポータルで表示した画面が以下です。

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

「Data Explorer(Preview)」を選択すると、以下の画面が表示されます。
SQL ServerのManagement Studioのようなノリでデータ操作を行うことができます。

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

「New Collection」をクリックすると以下の画面が表示されます。
このデータベースアカウント「cosmosdb」に対して、「データベース」→「コレクション」を作成します。コレクションは「データ項目」を入れる入れ物になります(詳細は後述)。
ここでは以下の内容にてデータベースとコレクションを作成します。
* Database id = ExampleDb1
* Collection id = ExampleCol1
* Storage capacity = Fixed(10GB)
* Throughput = 400
* Partition key = なし

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

データを格納する入れ物が完成しました(コレクションを作成すると「Throughput = 400」というパフォーマンスを提供するためのリソースがAzure上に確保された状態となります。そ の為、課金の対象となりますので、作成→放置→課金継続・・・にはご注意ください)。

(2) データ(ドキュメント)の作成

データベース・コレクションの作成を行った後の「Data Explorer(Preview)」の表示は以下の通りです。
左側のツリーから「Documents」を選択し、「New Document」を選択します。

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

JSONエディタが表示されるので、適当にデータを編集してSaveボタンをクリックします。
※本チュートリアルでは、Cosmos DBを作成する際に、APIとして「SQL(DocumentDB)」を選択したためデータモデルが「ドキュメント(つまりJSONデータモデル)」となっています。

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

作成されたデータは以下の通りです。自ら編集した物以外の項目「rid」「self」「etag」「attachments」「_ts」が付加されていますが、これらはCosmos DBが内部的に自動で付加したものです。

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

この後の上記手順を繰り返し、適当なデータ5件を作成しました。

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

作成したデータ一覧
[
  {
    "id": "1",
    "FirstName": "Ryuichi",
    "LastName": "Daigo",
    "FavoriteDB": "Cosmos DB",
    "Age": 17,
    "_rid": "2cRoAIH5HAACAAAAAAAAAA==",
    "_self": "dbs/2cRoAA==/colls/2cRoAIH5HAA=/docs/2cRoAIH5HAACAAAAAAAAAA==/",
    "_etag": "\"0f00fff9-0000-0000-0000-591fcf700000\"",
    "_attachments": "attachments/",
    "_ts": 1495256943
  },
  {
    "id": "2",
    "FirstName": "Hiroaki",
    "LastName": "Sakamoto",
    "FavoriteDB": "SQL Server",
    "Age": 25,
    "_rid": "2cRoAIH5HAADAAAAAAAAAA==",
    "_self": "dbs/2cRoAA==/colls/2cRoAIH5HAA=/docs/2cRoAIH5HAADAAAAAAAAAA==/",
    "_etag": "\"0f0006fa-0000-0000-0000-591fd0400000\"",
    "_attachments": "attachments/",
    "_ts": 1495257151
  },
  {
    "id": "3",
    "FirstName": "Susumu",
    "LastName": "Akiyama",
    "FavoriteDB": "Neo4j",
    "Age": 36,
    "_rid": "2cRoAIH5HAAEAAAAAAAAAA==",
    "_self": "dbs/2cRoAA==/colls/2cRoAIH5HAA=/docs/2cRoAIH5HAAEAAAAAAAAAA==/",
    "_etag": "\"0f0007fa-0000-0000-0000-591fd05b0000\"",
    "_attachments": "attachments/",
    "_ts": 1495257178
  },
  {
    "id": "4",
    "FirstName": "Hiroko",
    "LastName": "Takada",
    "FavoriteDB": "Oracle",
    "Age": 32,
    "_rid": "2cRoAIH5HAAFAAAAAAAAAA==",
    "_self": "dbs/2cRoAA==/colls/2cRoAIH5HAA=/docs/2cRoAIH5HAAFAAAAAAAAAA==/",
    "_etag": "\"0f0008fa-0000-0000-0000-591fd07d0000\"",
    "_attachments": "attachments/",
    "_ts": 1495257212
  },
  {
    "id": "5",
    "FirstName": "Yoko",
    "LastName": "Kitahara",
    "FavoriteDB": "Mongo DB",
    "Age": 18,
    "_rid": "2cRoAIH5HAAGAAAAAAAAAA==",
    "_self": "dbs/2cRoAA==/colls/2cRoAIH5HAA=/docs/2cRoAIH5HAAGAAAAAAAAAA==/",
    "_etag": "\"0f000efa-0000-0000-0000-591fd0aa0000\"",
    "_attachments": "attachments/",
    "_ts": 1495257257
  }
]

(3) データの検索

引き続き「Data Explorer(Preview)」を使用します。
「Edit Filter」ボタンをクリックします。

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

フィルター入力テキストボックスが有効になるので、以下のように抽出条件を設定して「Apply Filter」ボタンをクリックします。抽出条件はSQLライクに記述可能です。

where c.Age > 20

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

条件に該当する3件に絞り込みが行われました。

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

同様の機能を持ったツールとして「ドキュメント エクスプローラー」「クエリ エクスプローラー」というものがAzureポータル上で提供されています。

(4) 後片付け

コレクションを作成したままにしておくと課金対象となってしまうので削除しておきましょう。
きれいにコレクションの上のデータベースごと削除することとします。
「Data Explorer(Preview)」でデータベース(ExampleDb1)の右側の「・・・」をクリックするとポップアップメニューが表示されます。「Delete Database」を選択します。

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

確認画面が表示されるので、削除対象のデータベース名を入力して「OK」ボタンをクリックします。

f:id:daigo-knowlbo:20170521011913p:plain f:id:daigo-knowlbo:20170521011834p:plain

1.2.3 プログラムでデータを操作

次に、前述の「Azureポータルでデータを操作」で行った操作をC#プログラムから行います。

(1) 接続情報の確認

Azureポータルで「キー」を選択します。「読み取り/書き込みキー」タブを選択します。
URI」と「プライマリキー」を控えておきましょう。

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

(2) Visual Studio 2017プロジェクトを作成

メニュー「ファイル→新規作成→プロジェクト」を選択し、表示された「新しいプロジェクト」に以下の内容を入力します。

  • プロジェクトテンプレート=「Windows クラシックデスクトップ→コンソールアプリ(.NET Framework)」
  • 名前=HelloCosmosDb

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

(3) CosmosDB接続ライブラリ(NuGetパッケージ)を追加

ソリューションエクスプローラからHelloCosmosDbプロジェクトをマウス右ボタンクリックして、表示されたポップアップメニューから「NuGet パッケージの管理」をクリックします。

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

表示されたNuGetパッケージ管理画面の参照タブで「Microsoft.Azure.DocumentDB」を選択して「インストール」ボタンをクリックします。

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

「ライセンスへの同意」ダイアログが表示されますが、そのまま「同意する」をクリックします。

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

(4) データの入れ物の準備(データベースとコレクション)

話を単純にする為に、コンソールアプリケーションのProgram.csにコードを追記していくこととします。
また、以下、各処理のコードスニペットを紹介し、最後に全体の実装を掲載する事とします。
Cosmos DB関連の名前空間を適時usingします。

using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client;

接続情報と作成するデータベース名・コレクション名を定数定義しておきます。

private const string EndpointUrl = "https://cosmosdb.documents.azure.com:443/";
private const string PrimaryKey = "キー";
private const string DatabaseId = "ExampleDb1";
private const string CollectionId = "ExampleCol1";

データベースとコレクションを作成するコードは以下の通りです。

private DocumentClient client;
...
public async Task<bool> CreateDatabaseCollection()
{
  // 接続用クライアントオブジェクトの作成
  this.client = new DocumentClient(
    new Uri(EndpointUrl), PrimaryKey);

  // データベースの作成
  await this.client.CreateDatabaseIfNotExistsAsync( new Database { Id = DatabaseId });

  // コレクションの作成
  var collection = new DocumentCollection { Id = CollectionId };
  await this.client.CreateDocumentCollectionIfNotExistsAsync(
    UriFactory.CreateDatabaseUri(DatabaseId),
    collection);

  return true;
}

まずは、接続クライアントオブジェクト「DocumentClient」を作成します。作成したDocumentClientオブジェクトは、後で別の処理でも流用することを考慮し、clientフィールド変数に保持することにしました。
DocumentClientは接続オブジェクトであるため、エンドポイントおよび接続用キーをコンストラクタで渡すことになります。
データベースの作成には「CreateDatabaseIfNotExistsAsync()メソッド」を呼び出します。引数でデータベースIDを指定します。メソッド名から想像がつくように「存在しなかったら作成、存在したらそのまま処理が正常終了」します。
コレクションの作成には「CreateDocumentCollectionIfNotExistsAsync()メソッド」を呼び出します。第1引数に対して「UriFactory.CreateDatabaseUri()メソッド」呼び出しの戻り値を渡しています。第2引数のDocumentCollectionオブジェクトによりコレクションIDを指定しています。

(5) データ(ドキュメント)の作成

前述と同様に以下のようなスキーマのデータ(ドキュメント)を作成します。

{
  "id": "1",
  "FirstName": "Ryuichi",
  "LastName": "Daigo",
  "FavoriteDB": "Cosmos DB",
  "Age": 17,
}

上記スキーマに合致したモデルクラス Person を用意します。

namespace HelloCosmosDb
{
  public class Person
  {
    public string Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public string FavoriteDB { get; set; }

    public int Age { get; set; }
}

Personオブジェクトを5件の固定データで作成する実装は以下の通りです。
DocumentClientオブジェクト(this.client)の「UpsertDocumentAsync()」メソッドを呼び出します。Upsertの名前から分かるように「キーが同じドキュメントが既に存在すれば更新を、存在しなければ作成」処理が行われます。
第1引数は、どのコレクションに対してドキュメントを作成するかを表します。
第2引数は、作成するオブジェクトそのものを指定します。

public async Task<bool> CreateDocuments()
{
  string[] id = { "1", "2", "3", "4", "5" };
  string[] firstName = { "Ryuichi", "Hiroaki", "Susumu", "Hiroko", "Yoko" };
  string[] lastName = { "Daigo", "Sakamoto", "Akiyama", "Takada", "Kitahara" };
  string[] favoriteDB = { "Cosmos DB", "SQL Server", "Neo4J", "Oracle", "MongoDB" };
  int[] age = { 17, 25, 36, 32, 18 };
  for(int i=0; i<5; i++)
  {
    var person = new Person()
    {
      Id =id[i],
      FirstName=firstName[i],
      LastName=lastName[i],
      FavoriteDB = favoriteDB[i],
      Age = age[i]
    };

    var newDocument = 
      await this.client.UpsertDocumentAsync(
        UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId),
        person);
  }

  return true;
}

(6) データの検索

ここまでで、cosmosdbデータベースアカウント → ExampleDb1データベース → ExampleCol1 上に5件のドキュメントが作成された状態です。
Ageが20より多きドキュメントを検索してコンソール出力します。

public void SearchDocuments()
{
  Uri collectionUrl = UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId);

  var query =
    this.client.CreateDocumentQuery<Person>(collectionUrl)
    .Where(c => c.Age > 20);

  foreach (var person in query)
  {
    Console.WriteLine(person);
    Console.WriteLine("-----");
  }
}

DocumentClientオブジェクト(this.client)の「CreateDocumentQuery()」メソッドでクエリーオブジェクトを作成することができます。Linqメソッドk構文でWhere句を記述することができます。

(7) ソース全体と実行結果

上記に説明した実装およびコードスニペットをまとめた実装の全体コードは以下になります。

//Person.cs
using System;

namespace HelloCosmosDb
{
  public class Person
  {
    public string Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public string FavoriteDB { get; set; }

    public int Age { get; set; }

    public override string ToString()
    {
      return string.Format(
        "ID:{0}\r\nFirstName:{1}\r\nLastName:{2}\r\nFavoriteDB:{3}\r\nAge:{4}",
        this.Id, this.FirstName, this.LastName, this.FavoriteDB, this.Age);
    }
  }
}
// Program.cs
using System;
using System.Linq;
using System.Threading.Tasks;

using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client;

namespace HelloCosmosDb
{
  class Program
  {
    private const string EndpointUrl = "https://cosmosdb.documents.azure.com:443/";
    private const string PrimaryKey = "キー";
    private const string DatabaseId = "ExampleDb1";
    private const string CollectionId = "ExampleCol1";
    private DocumentClient client;

    static void Main(string[] args)
    {
      Console.WriteLine(UriFactory.CreateDatabaseUri(DatabaseId));

      var program = new Program();
      program.CreateDatabaseCollection().Wait();
      program.CreateDocuments().Wait();
      program.SearchDocuments();
    }

    public async Task<bool> CreateDatabaseCollection()
    {
      // 接続用クライアントオブジェクトの作成
      this.client = new DocumentClient(
        new Uri(EndpointUrl), PrimaryKey);

      // データベースの作成
      await this.client.CreateDatabaseIfNotExistsAsync( new Database { Id = DatabaseId });

      // コレクションの作成
      var collection = new DocumentCollection { Id = CollectionId };
      await this.client.CreateDocumentCollectionIfNotExistsAsync(
        UriFactory.CreateDatabaseUri(DatabaseId),
        collection);

      return true;
    }

    public async Task<bool> CreateDocuments()
    {
      string[] id = { "1", "2", "3", "4", "5" };
      string[] firstName = { "Ryuichi", "Hiroaki", "Susumu", "Hiroko", "Yoko" };
      string[] lastName = { "Daigo", "Sakamoto", "Akiyama", "Takada", "Kitahara" };
      string[] favoriteDB = { "Cosmos DB", "SQL Server", "Neo4J", "Oracle", "MongoDB" };
      int[] age = { 17, 25, 36, 32, 18 };
      for(int i=0; i<5; i++)
      {
        var person = new Person()
        {
          Id =id[i],
          FirstName=firstName[i],
          LastName=lastName[i],
          FavoriteDB = favoriteDB[i],
          Age = age[i]
        };

        var newDocument = 
          await this.client.UpsertDocumentAsync(
            UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId),
            person);
      }

      return true;
    }

    public void SearchDocuments()
    {
      Uri collectionUrl = UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId);

      var query =
        this.client.CreateDocumentQuery<Person>(collectionUrl)
        .Where(c => c.Age > 20);

      foreach (var person in query)
      {
        Console.WriteLine(person);
        Console.WriteLine("-----");
      }
    }
  }
}

実行結果は以下の通りです。

// 実行結果

ID:600bb1b7-fdfc-479c-b295-8fc3f589b45a
FirstName:Hiroaki
LastName:Sakamoto
FavoriteDB:SQL Server
Age:25
-----
ID:a2b5fd96-de4f-4283-bd10-78bfd5056505
FirstName:Susumu
LastName:Akiyama
FavoriteDB:Neo4J
Age:36
-----
ID:82e9770d-7a9f-4b0e-adbb-849051912154
FirstName:Hiroko
LastName:Takada
FavoriteDB:Oracle
Age:32
-----
続行するには何かキーを押してください . . .

Azure Cosmos DB入門(2)へ