Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9309

Durable Functionsことはじめ

$
0
0

この記事はSC(非公式) Advent Calendar 2019、22日目の記事です。

はじめに

簡単な仕様のバッチ処理を作るにあたり、Azure Functionsでサクっと作ろうと考えていました。
しかしAzure Functionsは従量課金プランで使用した場合に起動から5分(最大10分)でタイムアウトしてしまい、将来的にデータ件数が増えた場合にタイムアウトしてしまう懸念が......。
AppServiceプランを使用した場合、30分~無制限までタイムアウトを伸ばせますが、お金がそれなりにかかってしまうこともあり、うまい方法はないかと模索していたところ、「Durable Functionsでうまいことできるよ」と教えていただいたので使ってみることにしました。

Durable Functionsってなに?

Azure Functionsの拡張機能でステートフル関数を書くことが出来ます。
Azure Functions単体では難しくなる関数チェーンや並列処理をAzure Functions単体のそれよりも簡単に実装することができます。

ざっくりですがDurable Functionsは基本的に以下のような構成になります。

関数概要
DurableOrchestration
Client
HTTPやタイマートリガーによって
オーケストレーションを起動する。
OrchestratorActivity関数を制御
Activity個々の処理を行う。
単体ではAzure Functionsと同じようなもの

Orchestrator関数がActivity関数を取りまとめていて、
個々のActivityのinput outputの受渡を制御するイメージです。

コードで確認

VisualStudio2019でDurableFunctionsクラスを作成した際のデフォルトコードで上記構成を見てみます。

プロジェクトを右クリック -> 追加 -> 新しい項目 -> Azure関数 -> Durable Functions Orchestration
※今回のクラス名はMyDurableFuncにしました。

作成すると上述の関数がデフォルトで作成されますので、一つずつ見ていきます。
・DurableOrchestrationClient
・Orchestrator
・Activity

C# MyDurableFunc.cs
[FunctionName("MyDurableFunc_HttpStart")]publicstaticasyncTask<HttpResponseMessage>HttpStart([HttpTrigger(AuthorizationLevel.Anonymous,"get","post")]HttpRequestMessagereq,[OrchestrationClient]DurableOrchestrationClientstarter,ILoggerlog){stringinstanceId=awaitstarter.StartNewAsync("MyDurableFunc",null);returnstarter.CreateCheckStatusResponse(req,instanceId);}

上記がDurableOrchestrationClientになります。
HTTPをトリガーとしてawait starter.StartNewAsync("MyDurableFunc", null);でOrchestrator関数を呼び出しています。

C# MyDurableFunc.cs
[FunctionName("MyDurableFunc")]publicstaticasyncTask<List<string>>RunOrchestrator([OrchestrationTrigger]DurableOrchestrationContextcontext){varoutputs=newList<string>();outputs.Add(awaitcontext.CallActivityAsync<string>("MyDurableFunc_Hello","Tokyo"));outputs.Add(awaitcontext.CallActivityAsync<string>("MyDurableFunc_Hello","Seattle"));outputs.Add(awaitcontext.CallActivityAsync<string>("MyDurableFunc_Hello","London"));// returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]returnoutputs;}

続いてOrchestrator関数です。
CallActivityAsyncでMyDurableFunc_Helloという名前のActivity関数を呼び出しています。
このOrchestrator関数内でブレークポイントを付けて実行すると、
一つ目のawait演算子で一度処理が中断されて、再度処理が頭から流れることが確認できます。
これはDurable Functionsのリプレイ動作によるものです。
Orchestratorは非同期処理が実行される度にスリープ状態になり、Activityの処理が完了すると再起動し、1行目から処理を実行しなおしています。

2回目の実行時オーケストレーターは
await context.CallActivityAsync<string>("MyDurableFunc_Hello", "Tokyo")
の処理が完了していることを履歴から確認し
await context.CallActivityAsync<string>("MyDurableFunc_Hello", "Seattle")
を実行して再度スリープ状態になり......を繰り返します。
このリプレイ動作のおかげで、処理の実行中にAzure側で何某か問題が起きても中断されたActivity関数から処理を実行しなおすことが出来る仕組みとなっています。

C# MyDurableFunc.cs
[FunctionName("MyDurableFunc_Hello")]publicstaticstringSayHello([ActivityTrigger]stringname,ILoggerlog){log.LogInformation($"Saying hello to {name}.");return$"Hello {name}!";}

最後はActivity関数です。個々のビジネスロジックはこちらに記述します。

その他

Durable Functionsを使う上で気になったことや、ハマったことなどについてツラツラと書いていきます。

Durable Functionsのタイムアウトについて

冒頭でタイムアウトの課題がある為にDurable Functionsを使うと書きましたが、
Orchestrator関数にはタイムアウトの制約がありません。
一方でOrchestrator関数から呼び出されるActivity関数は5~10分の制約があるため、
時間のかかる処理を実行する場合はActivity関数で一回の処理時間をタイムアウトの範囲内に収めるように工夫する必要があります。

課金体系

Azure Functionsと同様。
Azure Functionsはタイムアウトがある為に、持続的に動き続けてしまい料金が発生......のような心配はありませんが、Durable Functionsを使う場合は意識する必要がありそうです。
また、リプレイ動作毎に課金されるので、複数のActivity関数を呼び出す場合はそれなりに回数が嵩みます。

Orchestrator関数の並列実行に気を配る

ローカル環境でTimerTriggerを3分間隔で起動し、DBに登録・削除を行う処理を書いていたところ、
3分経過時点で後からキックされたOrchestrator関数が実行されたためにデッドロックしエラーとなってしまいました。

[FunctionName("MyTimerTriggerFunc")]publicstaticasyncTaskRunOrchestrator([TimerTrigger("0 */3 * * * *")]TimerInfoinfo,[DurableClientAttribute]IDurableOrchestrationClientstarter,ILoggerlog,ExecutionContextcontext){stringinstanceId=awaitstarter.StartNewAsync("MyDurableFunc",null);}

実運用では夜間1回しか動かないため、このようなことは起きない想定ではありますが、
以下のようにシングルトンにし、一つのOrchestratorのみ動くように制御しました。
ここでexistingInstance.RuntimeStatus == OrchestrationRuntimeStatus.Completedでステータスを見ているのは、一回目の起動以降ストレージにインスタンスの情報を持ってしまうため、同一名称のインスタンスで再実行できなくなるのを回避するためです。
失敗した場合に備えてOrchestrationRuntimeStatus.failなどの条件も加える必要はありそうです。

[FunctionName("MyTimerTriggerFunc")]publicstaticasyncTaskRunOrchestrator([TimerTrigger("0 */3 * * * *")]TimerInfoinfo,[DurableClientAttribute]IDurableOrchestrationClientstarter,ILoggerlog,ExecutionContextcontext){stringinstanceId="MyInstanceId";varchkStatus=awaitstarter.GetStatusAsync(instanceId);if(chkStatus==null||chkStatus.RuntimeStatus==OrchestrationRuntimeStatus.Completed){awaitstarter.StartNewAsync(OrchestratorName,instanceId);}else{log.LogInformation($"An instance with ID '{instanceId}' already exists."));}

参考リンク

おわりに

最後の方は内容が散らかってしまいましたが、リプレイ動作などの特徴さえ押さえておけば比較的使いやすいサービスのように思います。
それでは皆さんよいお年をお迎えください。

@sat0tabeさん、諸々ご教授ありがとうございました。

参考サイト様

Azure Durable Functions のドキュメント
多分わかりやすいDurable Functions
Durable Functions を始めるときに知っていると幸せになれる7つの Tip


Viewing all articles
Browse latest Browse all 9309