Step Functions: コールバックパターンを使ったサーバレスな並列分散処理①

# はじめに
ひさしく触っていなかったAWSを触ることになったので、せっかくだからサーバレスに並列分散処理が組めないか検討してみたというお話です。
構築まで含めると長くなるので、今回は構成の検討と実際の動作だけ紹介して、次回以降で構築をしていきます。

# サーバレスな並列分散処理
Lambdaを使ったサーバレスなシステムでは、実行時間等の制約からひとつの関数ですべての処理を終わらせるのでなく、細かいプロセスに分けた複数の関数を組み合わせて実行することが多くなります。
![img01-001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322093/66fc8d82-daa4-d2ac-0e14-364d5b7b99b3.png)
処理の中には並列化やホストを分散させても問題ないものもあるので、ワークフローに関数を複数同時起動させて分散処理を行い、単一のプロセスに収束させる仕組みを組み込んで処理時間短縮を行うことができます。
ただ、サーバレスの環境において単一のプロセスに収束させるのは意外と組み込むのが大変な処理になります。
分散処理は、S3 Putのようなトリガーで対処するなり、単に複数の関数を起動するスクリプトを前段プロセスの関数に記述するなりでできますが、プロセスの収束は分散したプロセスがすべて終了したうえで、後段プロセスを**確実に一回だけ実行**させられることが求められます。
![img01-002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322093/eb59569a-36f6-4006-14b9-2ad307e7256f.png)
実装として真っ先に考え付くのは、分散したプロセスがDynamoDB等にログを書き出し、終了時に他のプロセスのログを確認して、最後に終了したプロセスが後段プロセスをトリガーするという構成です。
非常に稀なケースではあるものの、最後に終了するプロセスが2つ以上同時になることも考えられ、プロセスが同時に後段プロセスをトリガーしてしまう可能性や実装によっては後段プロセスをトリガーされない可能性があります。
![img01-003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322093/16593f86-8c83-2cb8-14fd-d157ba3f568b.png)
確実に一回だけ後段プロセスを実行させたければ、分散させたプロセスとは別の関数から定期的に状態をポーリングし、すべてのプロセスが終了したことを確認して後段プロセスをトリガーするという方法があります。
ただ、ポーリング間隔によっては処理を行っていない無駄な時間が発生し、なによりイベント駆動型のサービスを使いながらポーリングに頼るのはあまりナウい感じがしません。
![img01-004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322093/569e6706-7d52-aebe-6a00-c861ec74acdd.png)

# Step FunctionのParallelステート
Step Functionには、ワークフローの途中に並列処理を挿入する「Parallel」ステートがあります。JSON形式で並列処理のワークフローを記述して簡単に並列処理を実装できます。
![img01-005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322093/20fb5211-ce95-e91f-2bf1-8275325d63c2.png)
ただ、このParallelステートは、並列処理部分を決め打ちで設計しなければならず、動的に並列数を決定できません。
数十の並列実行を記述しようものなら、ワークフロー定義のJSONもVisual Workflowも長大で可読性に乏しくなるといった問題があります。
![img01-006.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322093/6d1c02b1-fa50-bbd9-cf55-a8b3489d6586.png)

# Step Functionsのコールバックパターン
Step FunctionのTaskステートでは、Lambda、ECS/Fargate、SNS、SQSにおいてResourceに「.waitForTaskToken」をつけることでコールバック待機状態にすることができます。

```json
{  
   "StartAt":"Run Workers",
   "States":{  
      "Run Workers":{  
         "Type":"Task",
         "Resource":"arn:aws:states:::lambda:invoke.waitForTaskToken",
         "Parameters":{  
            "FunctionName":"run-workers",
            "Payload":{  
               "Payload.$":"$",
               "TaskToken.$":"$$.Task.Token"
            }
         },
         "End":true
      }
   }
}
```

この待機状態は、ステートマシンで発行した「Task Token」を外部プロセスから「SendTaskSuccess」のAPIで受け取ることで解除することができ、複数のTask Tokenを受け取ったとしても、最初の1回以外は棄却されるようになっています。
これを応用すれば、分散プロセスから確実に単一プロセスに復帰させることができそうです。
![img01-007.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322093/2c7c96ff-121c-a2d8-7cbf-88ea0acc3672.png)

# コールバックパターンを使った並列分散処理
コールバックパターンを使って並列分散処理を構成してみました。
構成としては、メインのステートマシンと分散処理のワーカーにあたるサブのステートマシンを用意して、メインのステートマシンのLambdaからサブのステートマシンを任意の数だけ起動して並列分散処理を実行します。
![img01-008.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322093/75c7827b-81a4-2e78-3c92-15f318bbe17b.png)

# 実際に動かしてみた
上記の構成を実際に組んで実行してみました。
以下は、構成図の「create-task」で0~2のランダムな数値(float)を100個キューに投げて、10並列で起動させたサブステートマシンの「executor」で順次メッセージを受け取って数値の秒数Sleepするという簡単な実行例です。
メインステートマシンを実行するとサブステートマシンが10個実行され、順次成功してすべて完了するとメインステートマシンの「Start Workers」も「進行中」から「成功」に変化することが確認できました。
![img01-009.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322093/99ed6678-1f60-bf28-1382-60ea440943f4.gif)

# 参考
- AWSドキュメント: AWS Step Functions
    - [サービス統合パターン](https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/connect-to-resource.html)
    - [Step Functions で Lambda を呼び出す](https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/connect-lambda.html)