Azure Cosmos DB 400RU/s縛りの世界から

この記事は、Microsoft Azure Advent Calendar 7日目の記事です。
はい、寝落ちしてました。ごめんなさい :bow: 

# はじめに :santa:

今年4月から始めた新規サービスの立ち上げるため、PoCを素早く回していくためのデータストレージとして、Azure Cosmos DBを選びました。
選んだ理由は、3つあります。

1. SQL Database は利用経験が無かった。
2. Azure Database for MySQL, PostgreSQLもあったけどまだプレビューだった。
3. チームメンバーがMongoDBの利用経験があった。

Azure Cosmos DBはMongoDB Write APIを、完全ではないのですが、サポートしています。ですので、どの言語でもMongoDBのドライバからアクセスが可能です。
しかしながら、利用した時点では稼ぎは無かったため、最安価な構成としてRU/s を最小値の400で使ってきました。
特殊な状況かとは思いますが、そういった状況で使い続けてきた中で発見したことをまとめてみようと思います :christmas_tree: 

# Azure Cosmos DB :rocket: 

Azure Cosmos DBは複数のAPIをサポートするAzureのデータベースサービスです。ドキュメント型データベースとしてDocumentDB API、MongoDB APIを、その他にTable APIとネットワークを表現できるGraph APIがあります。それに加えて、現在、Cassandra APIがプレビューとして提供されています。

単にNoSQLを提供するサービスというわけではなく、マルチリージョンでの一貫性をサポートしています。例えば、USにあるCosmos DBに書き込まれたデータが、すぐに東日本のCosmos DBから読み書きできるようになります。また一貫性のレベルも選択することができ、要件に応じてプロビジョニング後にも変更することが可能です。
また、性能については、同一リージョン内での通信速度は高いSLAで提供しています。

# 構成 :cake: 

我々が利用しているAzure Cosmos DBの構成は、少し特殊な構成です。
DocumentDB APIとして作成していますが、接続はMongoDB APIを使っています。
我々はアプリケーションをPHPで開発しています。ですが、PHPのDocumentDB APIライブラリがあるのですが、長くメンテされていない状態であったため、使い慣れているMongoDB APIで接続しています。
素直に、MongoDB APIとしてプロビジョニングすれば良いかと思われるかもしれませんが、他のAzureのサービス、例えばAzure FunctionsのbindingsやAzure DataFactoryなどがDocumentDB APIでの接続をサポートしているため、DocumentDB APIとしてプロビジョニングしました。
つまり、Azureのサービスとの連携と、自分たちの使いやすさの両方を取ることができました。


# MongoDB API :monkey: 

Azure Cosmos DBは、MongoDB Wire Protocolでの接続をサポートしています。

* [MongoDB Wire Protocol — MongoDB Manual 3.6](https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/)

つまり、MongoDBへ接続できるものであれば、基本的にAzure Cosmos DBにも接続することができます。

ですが、全てのMongoDBのコマンドをサポートしている訳ではありません。使い始めた今春の時点では、aggregation関連のコマンドは一切使うことが出来ませんでした（最近、プレビューとして公開されているようです。）。サポートされているコマンドについては、下記をご参照ください。

* [MongoDB に対する Azure Cosmos DB 機能のサポート | Microsoft Docs](https://docs.microsoft.com/ja-jp/azure/cosmos-db/mongodb-feature-support)

基本的にCRUDはサポートされていますが、aggregationのように集計作業に必要なコマンドはサポートされていません。

# 課金 :money_with_wings: 

Azure Cosmos DBの課金は非常にシンプルです。
全てのコレクションが同一の設定とストレージ使用量を持っていると過程した場合、

```
1時間あたりのコスト = 
  1コレクションあたりの時間課金 * コレクション数 * レプリケーション数 * コレクション辺りのストレージ容量数に応じた金額 
  + 時間あたりのストレージ金額/GB
```

で算出できます。コレクションによって、使用するディスク容量は異なりますし、必要とするアクセス数もまた異なります。
この式が表すように、全く利用していなくてもコレクションが存在するだけで課金されます。

# 要求ユニット :dash: 

上記のコレクションごとに課金を決める要因が、要求ユニット（RU）です。
ドキュメントによると、RUとは、

> 要求単位は、要求の処理コストを標準化した測定単位です。 1 つの要求ユニットは、10 個の一意のプロパティ値 (システム プロパティを除く) で構成される 1 KB のアイテムの読み取り (self リンクまたは ID による) に必要な処理能力を表します。
> - [要求ユニットとスループットの推定 - Azure Cosmos DB | Microsoft Docs](https://docs.microsoft.com/ja-jp/azure/cosmos-db/request-units#a-request-unit-estimation-example)

つまり、基準となるドキュメント（10個のフィールドを持つ1KB）を処理する時に1秒あたりに必要な処理能力に対して、どれくらい処理能力が必要かをRU/sの値として設定します。

気をつけなければいけないのは、クライアント・サーバー間の処理能力ではなく、Azure Cosmos DBをホストしているサーバーに必要な処理能力ということです。例えば、以下のようなMongoDBのupdateManyコマンドを考えてみると、

```
db.users.updateMany({}, { $set: { foo: "bar" } })
```

これは全ドキュメントに対して`foo`というフィールドに`bar`という値を持たせようとします。サーバー・クライアント間でやりとりされる情報は大したものではありませんが、実際にCosmos DB上では全てのドキュメントに対して値をフィールドと追加する操作をします。この後者のような操作に対して必要な処理能力を指定するのがRU/sの設定です。

この値は予め概算することは非常に難しいので、想定されるドキュメントと量がわかっているのであれば試してみることと、運用開始後にRUの消費量を確認することをお勧めします。実際のRUの消費量は、コマンドとポータル上から確認ができます。

また、MongoDB APIだと、予めコレクションが作成されていなくても、最初にドキュメントがコレクションに保存しようとした時に自動で作成されます。この時に、1,000 RU/sとして自動的に設定されることにも注意してください。設定可能な最小値が400ですので、我々のように最安値で運用したい場合は、予めコレクションを作成する必要があります。ですが、MongoDB APIはRUの値を指定してコレクションを作成するコマンドは提供されていません。そのため、ポータルかDocumentDB APIを利用して、コレクション作成時に指定しましょう。指定した値は後から設定変更が可能です。

# RUを超えるとどうなるか :feelsgood: 

RUは、コマンド実行時に消費されていきます。ある1秒間内に、その消費量が指定したRU/sを超えた場合、処理は中断されエラーメッセージが返ってきます。そのエラーが発生した際に対処できるようにリトライするようにアプリケーションを実装する必要があります。

ですが、アクセス負荷やコレクション内のドキュメント数に比例して、消費するRUは増えていくので、コレクションのRU/sの定期的な見直し、そもそもの消費量を減らす試みも必要です。

## RU/sを増やす

これは最も簡単な解決方法です。
多くのRUを消費することが読めている処理が予めわかっており、なおかつその処理の起動が制御できるのであれば、一時的にRU/sを引き上げると良いでしょう。
ですが、もちろん設定したRU/sの通りに :money_with_wings: です。

## インデックスを作成する

上で説明したように、RUはAzure Cosmos DBに必要な処理能力を指定する設定です。
ですので、処理の効率を上げるために、インデックスを作成することは1つの有効な手段です。

Azure Cosmos DBは、デフォルトでインデックスの設定を持っています。ですが、この設定では、

1. ネストされたドキュメントはインデックスされない
2. 範囲検索（`>`や`<=`など）に使われるインデックスしか作成されない

1については、例えば

```sample.json
{
  "first_name": "Tatsuya",
  "last_name": "Sato",
  "social_accounts": {
     "twitter": "sato_ryu",
     "github" : "satoryu"
  }
}
```

のようなドキュメントを保存するコレクションがあるとすると、`first_name`と`last_name`はインデックスの作成対象になりますが、`social_accounts`とその中の値についてはインデックスは作成されません。

2については、クエリーで`{ user_name: "sato_ryu" }` のように書いても考慮されないことを意味しています。ですので、同値性のためのインデックスを必要に応じて作成する必要があります。

## 有効期限を設定する

キャッシュやセッションなど、ある期間を過ぎたら必要なくなる情報を扱う場合に有効です。
ドキュメントの削除を自前で用意する方法も良いのですが、上に書いたように、削除時にドキュメント全体を評価するようなコマンドを実行すると、消費するRUが大きくなります。ですので、その仕組み自体をAzure Cosmos DBにまかせてしまいます。幸いなことに、有効期限切れを削除する際に消費されるRUは小さいようです。

# 最後に

Azure Cosmos DBは、非常に高性能で柔軟性のあるドキュメント型データベースです。
ここに書いたRU関連に関する問題以外は、何も問題なく使えていました。
ですが、開発するアプリケーションの規模が、

* 日本国内に限らず各国のリージョンをまたいで提供し、
* 書き込みが多く、その速さも求められ、
* そのデータの一貫性も気にしなければいけない

ようなものでないと見合わないように感じました。
我々のように、コストを意識してRUを低めで使い続けており、またサービスの性質として国内に限って提供しているため、上のどれも満たすことが厳しいように感じています。また、現在我々のサービスに求められているのは入力されたデータの集計であり、それらがCosmos DBだけでは非常に厳しいと感じています。
自分たちがAzure Cosmos DBにマッチした使い方ではなかっただけであり、Cosmos DB自体は素晴らしいサービスだと思います。

ここに書いた経験が、これから使い始める誰かの助けになれば幸いです :christmas_tree: