しばやん雑記

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

Azure Functions v2 の Graceful Shutdown の挙動を試した

これまで Azure Functions では再起動やデプロイなどでホストが再起動されるタイミングを正しく検知できず、Function App が処理を実行中でも強制的にシャットダウンされていましたが、最近にリリースされたバージョンでシャットダウンの検知が出来るようになっています。

自分の環境では Runtime Version 2.0.12562.0 で動作を確認出来ています。

対応されたコミットを見る限りでは Azure Functions v2 だけ対応のようなので、v1 では動作しないでしょう。アップデートもほぼ無くなってきたので、さっさと移行するのが吉です。

Graceful Shutdown への基本的な対応

ホストのシャットダウンが検知できるということは、ユーザーコード側でシャットダウンに備えた処理が行えるようになるということです。ひとまず検証のため以下のようなコードを用意しました。

public class Function1
{
    [FunctionName("Function1")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
        CancellationToken cancellationToken,
        ILogger log)
    {
        log.LogInformation("C# HTTP trigger function start.");

        try
        {
            await Task.Delay(TimeSpan.FromSeconds(60), cancellationToken);
        }
        catch (OperationCanceledException)
        {
            log.LogInformation("C# HTTP trigger function canceled.");

            return new BadRequestResult();
        }

        log.LogInformation("C# HTTP trigger function end.");

        return new OkObjectResult("OK");
    }
}

コードから分かるように、これまでは CancellationToken は受け取れてもキャンセル状態にならないという致命的な問題があったのですが、それが今回直ったという話です。

このシャットダウンに使われる CancellationToken は ASP.NET Core 側の IApplicationLifetime が提供しているものなので、挙動に関しては IIS + In-Prcess でホスティングする ASP.NET Core と同じです。*1

この Function の実行中に再起動やデプロイを実行すると、ちゃんと 400 エラーが返ってきます。

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

Function は Task.Delay によって 60 秒ほど待機するようなコードになっていますが、応答時間から CancellationToken によって待機が途中でキャンセルされたことが分かります。

そしてログを見ると OperationCanceledException をハンドリングできていることが確認できます。

2019-07-05T06:04:27.494 [Information] Executing 'Function1' (Reason='This function was programmatically called via the host APIs.', Id=1d4ecf1a-243d-49fb-9339-4d4230122a04)
2019-07-05T06:04:27.510 [Information] C# HTTP trigger function start.
2019-07-05T06:04:51.449 [Information] C# HTTP trigger function canceled.
2019-07-05T06:04:51.449 [Information] Executed 'Function1' (Succeeded, Id=1d4ecf1a-243d-49fb-9339-4d4230122a04)
2019-07-05T06:05:43.382 [Information] Executing 'Function1' (Reason='This function was programmatically called via the host APIs.', Id=bbb71852-f75c-4d4b-a28a-347c7b3a2d2c)
2019-07-05T06:05:43.499 [Information] C# HTTP trigger function start.
2019-07-05T06:05:51.443 [Information] C# HTTP trigger function canceled.
2019-07-05T06:05:51.453 [Information] Executed 'Function1' (Succeeded, Id=bbb71852-f75c-4d4b-a28a-347c7b3a2d2c)

強制的なシャットダウンではアプリケーションの状態が完全に不明になってしまいますが、Graceful Shutdown への対応を行うことで状態を壊すことなく安全にアプリケーションを終了できます。

シャットダウン中の処理について

これまでは常にホストが強制的にシャットダウンされていたのが、改善されていることが確認出来ました。

そこで気になるのは CancellationToken によってキャンセルが通知されてから、シャットダウンまでどのくらいの時間的猶予があるかという点です。

公式に情報は出ていないようですが、Azure Functions v2 は ANCM v2 によって In-Process でホスティングされていることから、基本的には shutdownTimeLimit で指定された時間だけシャットダウンを待つようです。

デフォルトでは shutdownTimeLimit は 10 秒になっています。別の Function を用意して検証した感じでは、確かに CancellationToken でキャンセルが通知されてから約 10 秒後にシャットダウンされていました。

HTTP リクエスト中にホストが強制的にシャットダウンされた場合は ARR が 502 を返します。もちろんシャットダウン中は 503 を返すので、新規のリクエストは受け付けません。

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

このタイムアウト値である 10 秒はユーザー側からは変更が不可能なので、Function 側では最低限の処理だけを行ってエラーなりで終了させるのが良さそうです。

現在の Trigger や Binding がどのような実装になっているかわからないですが、QueueTrigger に関してはキャンセル後には新しく処理は行われないようでした。処理中の場合は ThrowIfCancellationRequested などを使って例外を投げてしまえば、後ほどリトライされるはずです。

Azure Functions では稼働中のアプリケーションに対するデプロイで悩む部分がありましたが、適切に CancellationToken を使っておけば安全にシャットダウン後、デプロイが行えるようになりそうです。

注意:Durable Functions での動作について

状態を持つ Function として最強なのが Durable Functions ですが、確認した限りでは ActivityTrigger で受け取れる CancellationToken はちゃんと動作していないようで、キャンセルが通知されませんでした。

Event Sourcing のおかげで、途中で強制シャットダウンされても問題なく続きから処理は再開されますが、Activity 内の処理によっては状態が壊れる可能性がありそうです。

Function Runtime 側が直ったので、上の Issue で議論を再開していく予定です。

Durable Functions は Long-running なワークフローが簡単に書けるため、特にデプロイには気を使う必要がありましたが、上手くシャットダウン時の処理を制御できれば安全にデプロイが可能になると考えています。

*1:ANCM が app_offline.htm を利用したシャットダウン検知を行っている