C#
Azure
serverless
AzureFunctions

Azure Functions の 超イケてる Durable Functions を使ってみる

Azure のサーバーレスサービスである Azure Functions で、Durable Functionsという新しい機能が公開されました。もちろんまだ GA ではありませんが、自分的には相当熱い機能です。これを教えてくれた Kanio と、これあったら、もうサーバーイランのとちゃうん!と盛り上がりました。これもいつも通り、自分のためのメモとしてかいていきます。

1. 全体像

Durable Fucntions は Azure の extension で、ロングランニングで、ステートフルなFunction のオーケストレーターです。これだけではよくわからないと思うので、ユースケースを見てみましょう。

今回は公開されたというレベルなので、発展途上ですが、既にこんな機能が実装されています。具体的な実装例が、ここで見れます。actor.png

1.1. Function Chaining

Function から Function を簡単に呼べる Function Chaining パターン

function-chaining.png

1.2. Fan-out, Fan-in

並列実行して、それが全部終わったら、次のファンクションに流す。非同期実行を、最後で待ち合わせてくれるのがみそ。

fan-out-fan-in.png

1.3. Async HTTP APIs

これはロングランニングの非同期 HTTP API の実行で役にたつパターン。ロングランニングの非同期実行のfunction のステータスを問い合わせられるAPIが作られる。

async-http-api.png

1.4. Lightweight Actors

これが自分的には熱いのですが、Service Fabric でもおなじみのActor を使える。つまりStateful を扱えます。 Service Fabric の実装とは異なってライトウェイトでシンプルな感じです。

1.5. Human interaction and timeouts

人が介入して承認とかするようなパターンです

approval.png

2. プログラムと実行

手始めに、自分で複数のfunction を並列実行させて、その戻り値を返すようなFunction を書いて、ローカル、Azure の両方で動かしてみたので、シェアしたい。

2.1. インストール

Install Durable Functionsの指示通り、実施すればよい。

現在のところ、C# のみであるが、Visual Studio 2017 Preview (2) がDurable Function サポートしている。大まかにいうと、

Visual Studio

  • Visual Studio 2017 Preview (2) をインストールする
  • DurableFunctionsBinding.zip というDLL集を、C:\BindingExtensions に展開する。
  • local.setting.json というファイルがあるので、そこにBindingExtensionsの場所を記述する
  • NuGet パッケージで、DurableTask というパッケージをインストールする
  • .csproj ファイルに<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.DurableTask" Version="0.1.0-alpha" />を追加
  • ローカルに Azure Storage Emulator を入れて起動しておく。

という感じだ。ちなみに、local.setting.jsonのサンプルをあげておくとこんな感じ。Azure Functions はローカルで起動してデバッグできるが、その設定。

{
    "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true;DevelopmentStorageProxyUri=http://127.0.0.1:10002/",
    "AzureWebJobsDashboard": "UseDevelopmentStorage=true;DevelopmentStorageProxyUri=http://127.0.0.1:10002/",
    "WEBSITE_HOSTNAME": "localhost:7071",
    "AzureWebJobs_ExtensionsPath": "C:\\BindingExtensions"
  }
}

Azure

ちなみに、Visual Studio がなくても、Azure だけあれば、実施できる。その場合は下記の感じ。これも同じく先のページの手順でいける。

  • DurableFunctionsBinding.zip を、kudu で D:/home/BindingExtensionsに展開する。
  • Functions の Application Settings > App Settings に、AzureWebJobs_ExtensionsPathD:\home\BindingExtensions を登録する。

これで、基本的な設定は終了だ。ちなみにサンプルソースは、ここに置いておいた。

2.2 スタートアップ Function

最初に、最初のリクエストを受け付けて、該当する Functions を実行する Function を作成する。このコードはほぼ定型になるだろう。

using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;
using Newtonsoft.Json;

namespace DFSample
{
    public static class ControllerFunction
    {
        [FunctionName("ControllerFunction")]
        public static async Task<object> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, methods: "post", Route = "controller/{functionName}") ] HttpRequestMessage req,
            [OrchestrationClient] DurableOrchestrationClient starter,
            string functionName,
            TraceWriter log)
        {
            log.Info($"ControllerFunction controller/{functionName} was triggered!");

            dynamic eventData = await req.Content.ReadAsAsync<object>();
            string instanceId = await starter.StartNewAsync(functionName, eventData);
            log.Info($"Started controller with ID = '{instanceId}'");

            return starter.CreateCheckStatusResponse(req, instanceId);


        }
    }
}

ポイントは、アノテーションで、ファンクション名を指定しているところと、OrchestrationClientというアノテーションで、DurableOrchestrationClient をRun の引数にとっているところだ。 ここにわたってきた、functionName をもとに次のファンクションが実行される。

今回の構造は、

ControllerFunction -> ParallelExec -> EchoExec

というシンプルな構成で、ParallelExec が EchoExec を3並列で走らせて、結果が戻って着しだい、呼び出し元に対して結果を返すというものだ。先のパターンでいうと。1.2.に該当する。

次に ParallelExec

using System.Linq;
using System.Net;
using System.Net.Http;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.WindowsAzure.Storage.Table;
using System.Threading.Tasks;
using System.Collections.Generic;

namespace DFSample
{
    public static class ParallelExec
    {
        [FunctionName("ParallelExec")]
        public static async Task<List<string>>  Run(
            [OrchestrationTrigger] DurableOrchestrationContext context, // Point 1
            TraceWriter log
        )
        {
            log.Info("ParallelExec started!");
            var tasks = new Task<string>[3];
            tasks[0] = context.CallFunctionAsync<string>(  // Point 2
                "EchoExec",
                "Hello, I'm Tsuyoshi"
                );
            tasks[1] = context.CallFunctionAsync<string>(
                "EchoExec",
                "Hello, I'm Kanio");
            tasks[2] = context.CallFunctionAsync<string>(
                "EchoExec",
                "Hello, I'm NEO");
            await Task.WhenAll(tasks);                     // Point 3
            var outputs = new List<string>();
            foreach(Task<string> task in tasks)
            {
                outputs.Add(task.Result);
            }

            log.Info("ParallelExec Done!");
            return outputs;
        }

        [FunctionName("EchoExec")]                       // Point 4
        public static string EchoExec (
            [ActivityTrigger] DurableActivityContext context, 
            TraceWriter log)
        {

            string message = context.GetInput<string>();
            log.Info($"EchoExec started with '{message}'");

            return message + "\nHi, I'm Azure Fucntions";
        }
    }
}

Point 1 で、OrchestrationTriggerのアノテーションを指定して、Context を取得している。Point 2 context.CallFunctionAsync で、別のファンクションを実行している。ここでは、EchoExec である。そこに、引数を渡して、非同期実行している。awaitキーワードは非同期実行の待ち合わせをして、同期処理のようにかけるが、スレッドはブロックしないため並列処理の効率が良い。 Point 3 Task.Wait(tasks) によって、3つの非同期処理が、3つとも3並列で動いているが、それがすべて終わるまで await で待ち合わせている。
最後に、 Point 4 で、さらに次の Function の実行部のロジックが記述されて、いる。

これを実行してみよう。

2.3 実行

F5 キーでローカルに Function が起動する。起動しおわったら、Postman などでアクセス可能になる。

001.png

このコンソールの一番最後にアクセス先が出てくるので、リクエストをポストする。おそらくhttp://localhost:7071/controller/ParallelExec になる。

002.png

動いた!この赤く囲ったURLをクリックしてみよう。これが結果を見るURLだ。

003.png

しっかり予想通りの結果になっている。

2.4. Azure への Push

Visual Studio のプロジェクトここでは DFSample を右クリックすると、Publish が選択できる。ウィザードに従えば、Azure Functions がサーバーに送信されて動くようになる。

004.png

先ほど書いたファイルがそれぞれUpload されている。ポイントは、ControllerFunction の1をクリックすると、このFunction にアクセスするURL が得られる。 2 もポイントである通常のAzure Functions だと、run.csx と、 function.json が少なくともあるのだが、ここにはない。function.json を見ると、最後にScriptFile で、dllを指定しているので、クライアントからは、dll 形式になってアップロードされていると思われる。

実行してみるともちろん結果は同じ

005.png

2.5. Azure のみで Durable Functions を作るケースの注意

さて、わたしは、Durable Functions を、Azure の Portal のみでも作ってみた。そのケースは、自分で、function.json を書く必要がある。例えばこんな感じ。将来はテンプレートが選択可能になるだろう。

function.json

{
  "bindings": [
    {
      "name": "context",
      "type": "orchestrationTrigger",
      "direction": "in"
    }
  ],
  "disabled": false
}

run.csx

#r "Microsoft.Azure.WebJobs.Extensions.DurableTask"

public static async Task<List<string>> Run(DurableOrchestrationContext context)
{
    var outputs = new List<string>();

    outputs.Add(await context.CallFunctionAsync<string>("E1_SayHello", "Tokyo"));
    outputs.Add(await context.CallFunctionAsync<string>("E1_SayHello", "Seattle"));
    outputs.Add(await context.CallFunctionAsync<string>("E1_SayHello", "London"));

    // returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]
    return outputs;
}

#r の指定、そして、アノテーションが無くなっていることに注意されたい。実はAzure Function を普通に書くケースはこのような感じで、クラスの形式になっていない。先のクライアントのコードは、Class でアノテーションがある。こちらのコードが先のような形式に変換されて実際はDLLに格納されるのかもしれない。

ちなみに、Azure Portal 用のコードは、Gistに置いておいたのでよかったらどうぞ。

3. おわりに

Azure Functions の Durable Functions は自分的には相当可能性を感じる。相当な複雑な並列実行ワークフローも簡単に書けるし、シンプル。しかも、ロングランニングなものの考慮、アクターの実行までできる。ほんまこれがあれば、サーバーの御守が不要になる未来が近づいているのを感じる。今後も継続的に機能を試していきたい。

4. リソース