Azure CosmosDBの制限事項に関して纏めてみる

# 前提条件
1. 使うAPIはSQLAPI
2. RUはContainerに対して割り当てを行い、AutoScaleとします。

# １ドキュメント当たりの制限
- CosmosDBには1つのＩＴＥＭ当たり2MBの制約があります。[参照](https://docs.microsoft.com/ja-jp/azure/cosmos-db/concepts-limits#per-item-limits )
- ２ＭＢのJSONを書き込んだ場合どのようなエラーが出るかを確認します。

## サンプルコード
- C#で以下のようなコードを作成しました。

``` sample1.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using Microsoft.Azure.Cosmos;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace ConsoleApp1
{
    class Program
    {
        private static string connectString =
            "XXXXXXXXX";
        async static Task Main(string[] args)
        {
            var data = CreateData(2 * 1024 * 1024);
            data.Partitionkey = "Part1";
            using (var client = new CosmosClient(connectString, new CosmosClientOptions()))
            {

                var container = client.GetContainer("dbname", "containername");
                try
                {
                    await container.CreateItemAsync<TodoLists>(data, new PartitionKey(data.Partitionkey));

                }
                catch (CosmosException e)
                {
                    Console.WriteLine($"message={e.Message}");
                    throw;
                }

            }

        }

        private static TodoLists CreateData(int limitSize)
        {
            var str = "";
            var retData =new TodoLists();
            var count = 0;
            var serializer = JsonSerializer.CreateDefault();
            using (var stream = new MemoryStream())
            {
                var writer = new JsonTextWriter(new StreamWriter(stream));
                while (stream.Length <= limitSize)
                {
                    count++;
                    retData.TodoDatas.Add(new TodoData
                    {
                        UserId = Guid.NewGuid().ToString(),
                        UserName = $"Taro {count}",
                        Title = $"Title {count}",
                        Contents = $"Contents {count}"
                    });
                    stream.Position = 0;
                    serializer.Serialize(writer,retData);
                }

            }

            return retData;
        }
    }



    public class TodoData
    {
        public  string UserId { get; set; }

        public string UserName { get; set; }
        
        public string Title { get; set; }

        public string Contents { get; set; }


    }

    public class TodoLists
    {
        [JsonProperty("id")] 
        public string Id { get; set; } = Guid.NewGuid().ToString();

        public string Partitionkey { get; set; }

        public IList<TodoData> TodoDatas { get; set; } = new List<TodoData>();


    }
}

```

## 結果
- 以下のようなエラーが出力されました。
![RequestLarge.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613576/3f836535-c0b3-22b7-79bb-459f35e5cd9c.png)
- ステータスコード413 RequestEntityTooLarge が返却されることがわかりました。

# 1パーティション当たりの制限
- 最初のころCosmosDBのパーティションの設計を失敗しました、別の案件でも結構見かけました。**(論理) パーティションあたりの最大ストレージ20GB**昔は12GBだったような気がします。[参照](https://docs.microsoft.com/ja-jp/azure/cosmos-db/concepts-limits#provisioned-throughput)
- 注目したいのは下記です
    - すべての項目にわたる、(論理) パーティションあたりの最大ストレージ20GB
    - 個別の (論理) パーティション キーの最大数　無制限

    >[Azure Cosmos DB では、無制限のストレージとスループットを提供します。](https://docs.microsoft.com/ja-jp/azure/cosmos-db/optimize-cost-storage)

これは、1パーティション当たり20GBを超えないパーティションキーを設定した場合に成り立ちます。パーティションKEYの最大数は無制限なので大丈夫です。
## パーティションKEY設計に対する現時点での考察
- 20GBの制約があるため、大容量を要求する案件では20GBを超えない且つ均等分散をするようなKEY設計場合によっては複合KEY（性別＋年代)とかを検討する。
- IDでしかアクセスしないようなコンテナはIDをパーティションKEYでもいいと思われる。
- クロスパーティションで検索するようになってしまう項目は設定しない。

## 1パーティション当たり２０GBまで書き込んでみる

- C#で以下のようなコードを作成し20GBまで書き込む

``` sample2.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using Microsoft.Azure.Cosmos;
using System.Text;
using System.Threading.Tasks;
using System.Xml.XPath;
using Newtonsoft.Json;

namespace ConsoleApp1
{
    class Program
    {
        private static BatchStatus status = new BatchStatus();
        private static string connectString =
            "XXXXXXXX";
        async static Task Main(string[] args)
        {
            using (var client = new CosmosClient(connectString, new CosmosClientOptions { RequestTimeout = TimeSpan.FromMinutes(10), AllowBulkExecution = true, MaxRetryAttemptsOnRateLimitedRequests = int.MaxValue, MaxRetryWaitTimeOnRateLimitedRequests = TimeSpan.FromMinutes(10) }))
            {

                var container = client.GetContainer("dbname", "container1");
                for (int count2 = 0; count2 < 40; count2++)
                {
                    var tasks = new List<Task>();

                    for (var count = 0; count < 2000; count++)
                    {
                        var data = CreateData();
                        data.Partitionkey = "Part1";

                        tasks.Add(container.CreateItemAsync(data, new PartitionKey(data.Partitionkey)).ContinueWith(response =>
                        {

                            lock (status)
                            {
                                if (!response.IsCompletedSuccessfully)

                                {
                                    var tempex = response.Exception.InnerExceptions[0] as CosmosException;
                                    Console.WriteLine($"error  {tempex.ToString()}");
                                    status.Error++;
                                }
                                else
                                {
                                    Console.WriteLine($" done RU use {response.Result.RequestCharge}");
                                    status.Success++;
                                }

                            }
                            Console.WriteLine($"Success ={status.Success} Error = {status.Error}");
                        }));

                    }
                    await Task.WhenAll(tasks);
                }
            }

        }

        private static TodoLists CreateData()
        {
            var retData = new TodoLists();
            for (var count = 1; count < 4273; count++)
            {
                retData.TodoDatas.Add(new TodoData
                {
                    UserId = Guid.NewGuid().ToString(),
                    UserName = $"Taro {count}",
                    Title = $"Title {count}",
                    Contents = $"Contents {count}"
                });
            }
            return retData;
        }
    }



    public class TodoData
    {
        public string UserId { get; set; }

        public string UserName { get; set; }

        public string Title { get; set; }

        public string Contents { get; set; }


    }

    public class TodoLists
    {
        [JsonProperty("id")]
        public string Id { get; set; } = Guid.NewGuid().ToString();

        public string Partitionkey { get; set; }

        public IList<TodoData> TodoDatas { get; set; } = new List<TodoData>();


    }

    public class BatchStatus
    {
        public int Success { get; set; } = 0;

        public int Error { get; set; } = 0;
    }
}

```
- 書き込んだ結果はAzureポータルから確認ができます。Part1というパーティションKEYで20GB書き込んでいることが確認できます。
![Cap1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613576/4598fc1a-70d4-2d90-8858-d0f6eeb591e2.png)

## 結果
- 前述のSample1.cs を実行してみてどのようなエラーが出るか確認してみる。
![PartOverError.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613576/eaf50238-d36a-892b-d8f7-dda890b216cb.png)
- パーティションKEYが20GBを超えて書き込みが行えないことが確認できました。
- また、この状態で書き込み時に使用したSample2.csで実行するとエラーが下記のようになり、Reasonが消えてしまう。これはBulkInsertの問題なのか、別の方法を使えば取得できるのかはわからないので、今後調べる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613576/a96566be-982a-7154-8cd2-38626cf1e9f4.png)

# 物理パーティションの最大サイズ
- 1つの物理パーティションは50GBまでです。[参照](https://docs.microsoft.com/ja-jp/azure/cosmos-db/partition-data#physical-partitions)
- 論理パーティションは物理パーティションにマッピングされて格納されます。複数の論理パーティション50GBを超えない範囲で物理パーティションにマッピングされます。同一論理パーティションのデータは1つの物理パーティションに格納されます。物理パーティションはまたぎません。論理パーティションは20GBの制約があります。20GBを超えない範囲で物理パーティションにマッピングされ、複数の論理パーティションで50GBを超える場合は次の物理パーティションが作成されて、CoSMOSDBで判断され均等にデータが分散されます。またAzureポータルで指定されたRUは物理パーティションで分割されるイメージです。

## 物理パーティション50GBを超えて書き込んでみる。

- C#で以下のようなコードを作成し50GBまで書き込む

``` sample3.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using Microsoft.Azure.Cosmos;
using System.Text;
using System.Threading.Tasks;
using System.Xml.XPath;
using Newtonsoft.Json;

namespace ConsoleApp1
{
    class Program
    {
        private static BatchStatus status = new BatchStatus();
        private static string connectString =
            "XXXXXXXXXXXXXXX";
        async static Task Main(string[] args)
        {
            using (var client = new CosmosClient(connectString, new CosmosClientOptions { RequestTimeout = TimeSpan.FromMinutes(10), AllowBulkExecution = true, MaxRetryAttemptsOnRateLimitedRequests = int.MaxValue, MaxRetryWaitTimeOnRateLimitedRequests = TimeSpan.FromMinutes(10) }))
            {
                var tasks = new List<Task>();
                var container = client.GetContainer("dbname", "containername");
                for (int count2 = 0; count2 < 80; count2++)
                {
                    tasks.Clear();
                    foreach (var partkey in new []{ "Part1","Part2","Part3","Part4", "Part5", "Part6" })
                    {
                        for (var count = 0; count < 300; count++)
                        {
                            var data = CreateData();
                            data.Partitionkey = partkey;

                            tasks.Add(container.CreateItemAsync(data, new PartitionKey(data.Partitionkey)).ContinueWith(response =>
                            {

                                    if (!response.IsCompletedSuccessfully)
                                    {

                                        var tempex = response.Exception.InnerExceptions[0] as CosmosException;
                                        Console.WriteLine($"error  {tempex.Message} ");
                                        lock (status)
                                        {
                                            status.Error++;
                                        }
                                    }
                                    else
                                    {
                                        Console.WriteLine($" done RU use {response.Result.RequestCharge}");
                                        lock (status)
                                        {
                                            status.Success++;

                                        }
                                }

                                Console.WriteLine($"Success ={status.Success} Error = {status.Error}");
                            }));

                        }

                    }

                    await Task.WhenAll(tasks);
                }
            }

        }

        private static TodoLists CreateData()
        {
            var retData = new TodoLists();
            for (var count = 1; count < 8546; count++)
            {
                retData.TodoDatas.Add(new TodoData
                {
                    UserId = Guid.NewGuid().ToString(),
                    UserName = $"Taro {count}",
                    Title = $"Title {count}",
                    Contents = $"Contents {count}"
                });
            }
            return retData;
        }
    }



    public class TodoData
    {
        public string UserId { get; set; }

        public string UserName { get; set; }

        public string Title { get; set; }

        public string Contents { get; set; }


    }

    public class TodoLists
    {
        [JsonProperty("id")]
        public string Id { get; set; } = Guid.NewGuid().ToString();

        public string Partitionkey { get; set; }

        public IList<TodoData> TodoDatas { get; set; } = new List<TodoData>();


    }

    public class BatchStatus
    {
        public int Success { get; set; } = 0;

        public int Error { get; set; } = 0;
    }
}
```

## 書き込み結果
- 物理パーティションの状況はAzureポータルから確認できます。また、このようにどのパーティションKEYが割り当てられたか上位３位まで確認可能です。
![Part1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613576/edac66aa-8e74-71e3-b6da-1206995bc916.png)
![Part2_LI.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613576/4b7c3b47-d0d8-ce84-fab0-a22dd03d42f9.jpeg)
- 物理パーティションにどのKEYをマッピングするのかは決められません。
- またこの場合指定したRUは物理パーティション間で均等に割り当てられます。
- RUの最低は400RU 、AutoScaleの場合は4000RUからですが、この下減も変更されてしまいます。4000RUから自動で変更されていました。6000RUへ
![RU.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613576/66ef0148-676c-7428-8658-d858738c45cb.png)
- そして4000RUへ戻そうとするとエラーが起きます。
![Ru2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613576/70e57f0b-b924-4eb4-9698-14a52e3ccbc6.png)

# 感想・まとめ
- CosmosDBの気になる部分の制限事項を確認してみました。
- 気になる部分はもう少しあるので今後も確認していきたい。