serverless
AzureFunctions
DurableFunctions

Durable Functions を始めるときに知っていると幸せになれる7つの Tip

Durable Functions に初めて入門する方のために、Durable Functions を始めるときに知っておくと良い7つのティップスをまとめておきました。英語のオリジナルはこちらです。

  • Tip 1. クラスと責務
  • Tip 2. Storage Emulator を使う
  • Tip 3. Orchestrator はI/O やランダム値禁止
  • Tip 4. Sub Orchestrator はいつ使うべきか
  • Tip 5. DevOps CD pipeline
  • Tip 6. HttpClient は static にする
  • Tip 7. Run-From-Zip デプロイメントを使う

Tip 1. クラスと責務

Durable Functions の基本的なクラス構造は次のようになっています。

Overview.jpg

Activity Functions

Activity Functions は 普通の Azure Functions とあまり変わりません。普段の Azure Functions と同じようにコードを書いてください。一つ大きな違いがあるとすると、Orchestrator から実行指示を受け付けるためにActivityTrigger が必要になることです。

[FunctionName("Sample_Hello")]
public static string SayHello([ActivityTrigger] string name, TraceWriter log)
{
log.Info($"Saying hello to {name}.");
return $"Hello {name}!";
}

Orchestrator

Orchestrator は、Activity Functions をオーケストレートします。これは、OrchestrationTrigger を必要とします。

[FunctionName("Sample")]
public static async Task<List<string>> RunOrchestrator(
[OrchestrationTrigger] DurableOrchestrationContext context)
{
var outputs = new List<string>();
// Replace "hello" with the name of your Durable Activity Function.
outputs.Add(await context.CallActivityAsync<string>("Sample_Hello", "Tokyo"));
outputs.Add(await context.CallActivityAsync<string>("Sample_Hello", "Seattle"));
outputs.Add(await context.CallActivityAsync<string>("Sample_Hello", "London"));
// returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]
return outputs;
}

Orchestrator は、Activity Functions を下記のように呼びます。ふつうのメソッドを呼んでいるだけに見えますが、実際は、work-item queue と呼ばれる Queue にメッセージを送信しています。Activity Functions は、Activity Trigger によって、Queue を受け取り、ロジックを実行します。 Activity Function が実行を終了すると、レスポンスを、Control queue というQueue に書き込みます。この Queue によって、OrchestrationTrigger経由でOrchestrator が起動されます。これが基本的な Durable Functions に振る舞いです。

await context.CallActivityAsync<string>("Sample_Hello", "Tokyo")

一度 Durable Functions がスタートすると、4つの control queues と、一つの worker queue をを作成します。さらに、DurableFunctionHubHistory, DurableFunctionHubInstance Azure Storage Table を作成します。 Durable Functions を使っている Storage Account で皆さんも見つけることができるでしょう。

queueandtable.JPG

scale-diagram.png

OrchestrationClient

OrchestrationClient は、Orchestrator を開始/終了して、ステータスをモニタリングするためのクライアントです。次のサンプルの Function で、HTTPリクエストを受け取ると、Orchestratorをスタートします。この Function は、OrchestrationClient binding を必要とします。

[FunctionName("Sample_HttpStart")]
public static async Task<HttpResponseMessage> HttpStart(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")]HttpRequestMessage req,
[OrchestrationClient]DurableOrchestrationClient starter,
TraceWriter log)
{
// Function input comes from the request content.
string instanceId = await starter.StartNewAsync("Sample", null);
log.Info($"Started orchestration with ID = '{instanceId}'.");
return starter.CreateCheckStatusResponse(req, instanceId);
}

コードを書き始めるときは、Visual Studio の Durable Functions template を使うのをお勧めします。このテンプレートのコードがおそらく一番簡単に全体像を把握できると思います。

.NET Core ベースの V2 を使っているときは良いのですが、V1 だとなぜかテンプレートがありません。ただ、V2同じコードが動きますので、私の GitHub にコードを上げておきました。V1 の Function を空で作って、コードをコピーして、Microsoft.WebJobs.Extensions.DurableTask nuget package をインストールすると V1 でも動作するようになります。

残念ながら、何もしなければ、現在Newtonsoft.Json バージョンミスマッチ関係のエラーが起こることがあります。(V2) その場合は次の Issue を参考にしてください。

ちなみに、Durable Fuctions のサンプルはここにあります。

Tip 2. Storage Emulator を使う

Visual Studio で Durable Functions を開発するときは、Azure Storage Emulator を使うことを強く推薦します。

StorageEmulator.JPG

これは、Visual Studio と、Azure SDK がインストールされていれば既にインストール済みのはずです。詳細は次のドキュメントをどうぞ。

Durable Functions は、ステートを Azure Storage Table と Azure Storage Queue に保存します。複数人で開発しているケースで、同じ Storage Account を使うと、ステート共有されて不思議な動きをします。だから、開発の時は、開発者で個別の Storage Account を使うのが良いのでこの方法をお勧めします。ちなみに、次のコマンドで、Storage Emulator をクリアすることができます。

AzureStorageEmulator.exe clear all

local.settings.json の記述は次のようになるはずです。

{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"AzureWebJobsDashboard": "UseDevelopmentStorage=true"
}
}

Tip 3. Orchestrator はI/O やランダム値禁止

Orchestrator には禁止事項があります。

  • ランダムデータは禁止 (例えば Guid.NewGuid(), DateTime.Now など)
  • I/O (HttpClient やほかのバインディングなど)
  • Durable Functions の API でない非同期API (例えば Task.Delay)

もし、こういうったものを使いたい場合は、Activity Functions 内で使いましょう。Orchestrator Functions はそれ自体を何回かリプレイします。そして、History テーブルをアップデートします。Activity Functionにメッセージを送ったら、それ自体は、寝ます。Activity Function の実行が終了してメッセージがやってきたら、History テーブルを読み込んでコードをリプレイしながら、どこまで実行が進んでいるかを History テーブルを見ながら確認します。この具体的な動作に関しては4分のビデオを作成してみましたので、具体的な動作をご確認ください。

Durable Functions - Hitory table and Queue

これはもともと Azure Serverless Meetup で 作者の Chris Gillum が紹介してくれたもので、こちらになります。

Tip 4. Sub Orchestrator はいつ使うべきか

Durable Functions は、Sub Orchestrator をサポートしています。このフィーチャーは、複数の Activity Functions をオーケストレートする サブオーケストレータをつくることができます。では、いつ使えばいいでしょう?

SubOrchestrator.JPG

一つのユースケースとして、1つのオケストレーションをたくさん繰り返すケースがあります。例えば、100とか1000とか。そういったケースでサブオーケストレーター毎スケールさせます。他には複雑なオーケストレーションを整理したいケースもいいでしょう。

こういったケースに限りませんが、Activity Functions は、単一の責務を持つようにすることをお勧めします。Durable Functions はリトライポリシーの機能が強力で、設定するだけで面倒なリトライを実施してくれます。Activity Functions を単一の責務で、冪等性(何回実行しても、結果が同じになる性質)をもつように設計しておけば、リトライポリシーを使うのが簡単になります。この機能はコードをすごく削減してくれるのでお勧めです。

public static async Task Run(DurableOrchestrationContext context) {     var retryOptions = new RetryOptions(
         firstRetryInterval: TimeSpan.FromSeconds(5),           
         maxNumberOfAttempts: 3);
      await ctx.CallActivityWithRetryAsync("FlakyFunction", retryOptions, null);      
// ... 
}

Tip 5. DevOps CD pipeline

Durable Functionsでは、Deploy をするときに注意が必要です。もし、あなたが次のコードを変更したら、実行中のインスタンスは、エラーになります。

  • Orchestrator のコード
  • Activity Functions のインターフェイス

これらの変更は、History Table と、Queue のレイアウトの変更をもたらすので、現在実行中のインスタンスはエラーで終了します。

最も簡単なソリューションは、デプロイするときに、すべてのインスタンスが終わるまで待つというものです。現在 Durable Functions は2つの方法で、現在実行中のインスタンスがあるかを確認できます。

全てのインスタンスの状態を取得する

OrchestratorClientもしくは、HTTP API 経由で、全てのインスタンスの状態を取得することができます。もし、大量のインスタンスが実行される環境では、パフォーマンス問題が起こることが予測されます。私はこの機能を作ったので、将来コントリビューとして問題解決したいと思っています。

EventGrid publishing

もう一つの方法は、Azure Event Grid を使う方法です。この機能を設定して ON にすると、Durable Functions は、インスタンスが状態を変えた時に、Event Grid Topic に状態の変更を通知します。ですので、これを自分で管理しておくと、どのインスタンスが実行中かわかることになります。

もし、すべてのインスタンスの終了を待てないケースがあれば、次のブログが参考になるかもしれません。

Tip 6. HttpClient は static にする

これは、Durable Functions 特有の問題ではなく、Azure Functions の Tip です。もし、Azure Functions から 多くの新規の outbound のネットワークトラフィックを起こすと(例えば HttpClient) パフォーマンス問題が起こります。これは、 Scale Unit と呼ばれるものがあり、そこに Function App がいるのですが、そこから、outbound トラフィックが発生すると、Routing Table を書き換えます。これの処理がかかること、また、Outbound の TCP ポートの数が限られているという制限があります。だから、あまりに多くのコネクションを確立しようとすると、ポートの枯渇が起こります。コネクションの再利用に関しては次のドキュメントがよいでしょう。

Tip 7. Run-From-Zip デプロイメントを使う

これも、Durabel Functions 特有のTipであはりません。Azure Functions 一般の Tip です。Durable Function をデプロイする方法はいくつかありますが、通常の方法だと Consumption plan だと、cold-start 問題が起こるかもしれません。この問題を最小化するために、Run-From-Zip というデプロイ方法を使うことができます。Function のコードをビルドして、zip 化して、Blob ストレージなどに置いておきます。そのURLを AppSettings にセットするだけでデプロイが実行されます。
Consumption Plan では通常 Azure File ストレージとしてつかいます。これが Cold-Start を引き起こします。Run-From-Zip を使うと、zip はローカルで展開されて、メモリにロードされます。このデプロイ方法は、近い将来主流のデプロイ方法になると思われます。

まとめ

この 7 つの Tips を考慮することで、快適な Durable Functions ライフが楽しめると思います。

リソース

その他の参考になるリソースを貼っておきます。

衝撃的なことに、作者のChris が日本語で解説してくれているビデオもあります。彼は日本語勉強中でde:code の発表を日本語でやり切ったのは初めて見ました!
Serverless の世界を進化させるイノベーション - Durable Functions