しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

Azure Cosmos DB を ASP.NET Core の分散キャッシュのバックエンドとして利用する

少し前に Cosmos DB を ASP.NET Core の Distributed Cache Provider として使うライブラリの正式バージョンがリリースされました。ASP.NET 向けにも同じようなライブラリは出ていましたが、こちらは Cosmos DB SDK v3 で構築された最新版です。

重要なことは全て公式ブログに書いているので特に説明しませんが、実際に利用する際に気になるポイントを調べたのでメモとして残しておきます。

基本となる設定は AddCosmosCache を呼び出して IDistributedCache として登録する部分です。ASP.NET Core はこのインターフェースをベースとしてセッション機能が構築されています。

オプション指定すれば自動的に Cosmos DB の Database / Container を作成出来ますが、検証や開発初期段階ぐらいでの利用に留めておきましょう。Container レベルのスループットで作成されるので、Database レベルでスループットを共有したい場合や Serverless で利用したい場合は手動で作る必要があります。

public void ConfigureServices(IServiceCollection services)
{
    services.AddCosmosCache(options =>
    {
        options.ClientBuilder = new CosmosClientBuilder(Configuration.GetConnectionString("CosmosConnection"));

        options.DatabaseName = "AspNetCore";
        options.ContainerName = "Session";

        // Database / Container を自動作成してほしい場合はコメントアウト
        //options.ContainerThroughput = 400;
        //options.CreateIfNotExists = true;
    });

    // 必要に応じて使う
    services.AddSession(options =>
    {
        options.IdleTimeout = TimeSpan.FromMinutes(10);

        options.Cookie.HttpOnly = true;
        options.Cookie.IsEssential = true;
    });
}

ここで ClientBuilder を指定すると Cache 向けに専用の CosmosClient が作成されるようになります。専用の Cosmos DB アカウントを指定する場合には ClientBuilder を設定したほうが良いです。

アプリケーションでも Cosmos DB を利用していて、Cache とアカウントを共有したい場合は以下のように CosmosClient 自体を共有したほうが効率が良いです。

public void ConfigureServices(IServiceCollection services)
{
    // Cosmos DB Account を共有する場合は CosmosClient も共有した方が良い
    var cosmosClient = new CosmosClient(Configuration.GetConnectionString("CosmosConnection"), new CosmosClientOptions
    {
        SerializerOptions = new CosmosSerializationOptions
        {
            PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
        }
    });

    // アプリケーション向けに CosmosClient を DI に追加
    services.AddSingleton(cosmosClient);

    // AddCosmosCache でも作成済みの CosmosClient をセット
    services.AddCosmosCache(options =>
    {
        options.CosmosClient = cosmosClient;

        options.DatabaseName = "AspNetCore";
        options.ContainerName = "Session";

        // Database / Container を自動作成してほしい場合はコメントアウト
        //options.ContainerThroughput = 400;
        //options.CreateIfNotExists = true;
    });
}

今回は指定していませんが、オプションにある DefaultTimeToLiveInMs は名前からはミリ秒単位で指定する雰囲気が出ていますが、実際には秒単位での指定となるので注意が必要です。*1

自動生成される Container はパーティションキーが /id 指定となっているので、適当に ASP.NET Core でセッションを生成させると以下のようなデータが入ります。

f:id:shiba-yan:20210823183730p:plain

パーティションキーのカスタマイズは出来るっぽいですが、特に行う必要もないでしょう。

データへのアクセスは id の値のみあれば問題ないので、自動生成される Container では他のプロパティがインデックスから除外されるように最適化されています。Change Feed メインで使う時の設定と同じです。

{
    "indexingMode": "consistent",
    "automatic": true,
    "includedPaths": [],
    "excludedPaths": [
        {
            "path": "/*"
        },
        {
            "path": "/\"_etag\"/?"
        }
    ]
}

案外インデックスの作成で RU を食われることが多いので、必要なプロパティのみに絞った方が効率的です。

Terraform を使って自動生成される Container と同じ設定のものを作成する場合は、以下のような定義が必要になります。もし DefaultTimeToLiveInMs を指定している場合は default_ttl の値も合わせておきます。

resource "azurerm_cosmosdb_sql_container" "session" {
  name                = "Session"
  resource_group_name = azurerm_cosmosdb_account.example.resource_group_name
  account_name        = azurerm_cosmosdb_account.example.name
  database_name       = azurerm_cosmosdb_sql_database.example.name
  partition_key_path  = "/id"
  default_ttl         = -1

  indexing_policy {
    indexing_mode = "Consistent"

    excluded_path {
      path = "/*"
    }
  }
}

元のブログにも分散環境で重要な設定として Cosmos DB の Consistency Level が挙げられている通り、利用環境に合わせて適切な Consistency Level を選択しておかないと、書き込んだはずのデータが読み込めなかったという事態が発生します。

多くの場合は Consistency Level としてデフォルトの Session が使われていると思いますが、名前の通り同一のセッション*2の場合のみ、書き込んだデータが直後に読み込めることが保証されています。

つまり Consistency Level が Session の場合、確実にデータを読み込むためにはユーザーは同じインスタンスにアクセスし続ける必要があります。

Front Door / Application Gateway / App Service はクッキーベースの Session Affinity が提供されているので、これらを利用可能な場合は Consistency Level は Session でも問題ないです。*3

Session Affinity が利用できない場合には Cosmos DB の Consistency Level を Session より高くすることで対応します。つまり Strong か Bounded Staleness のどちらかを選択することになります。

Strong を選択するのが一番手っ取り早く確実ではありますが、書き込みコストが高くレイテンシも悪化するので Bounded Staleness が現実的な選択肢となります。単一リージョンで使う分には Bounded Staleness は Strong と変わらないので安心です。

Consistency for clients in the same region for an account with single write region = Strong

Consistency levels in Azure Cosmos DB | Microsoft Docs

複数インスタンス環境下で Session のまま使っていると、セッションに書き込んだデータが消失したという報告が上がってきて悩むことになるので、Consistency Level はしっかりと検討しておきましょう。

実際に遭遇したことがありますが、稀に発生するので調査の難易度が高い問題になりがちです。

*1:この問題は次のリリースで修正されます `DefaultTimeToLiveInMs` specified in milliseconds? · Issue #51 · Azure/Microsoft.Extensions.Caching.Cosmos · GitHub

*2:具体的には Session Token を持つリクエスト

*3:ただしインスタンスが入れ替わった場合には読み込めない可能性もある