この記事について
この記事は Opt Technologies Advent Calendar 2019 17日目の記事です。
担当プロダクトで利用したStep Functionsにて、失敗を通じて得た知見をまとめたものです。
自分について
中途入社して1年ちょっとのエンジニア。チームマネージャになって半年くらい。
入社してからクラウドインフラを学習し始め現在もチーム内で主に担当をしていますが、アプリケーションコードも状況に応じて実装する感じです。
昨年はこんな記事書きました。
やらかし背景
著者の担当プロダクトにて、外部APIから取得できるそこそこのサイズのデータをプロダクト側のDBに格納する処理があります。
ELT的なステップを踏んで処理することを検討しStep Functionsを採用しました。
- Extractorが外部APIからデータファイル(csv)を取得しS3バケットに格納
- Loaderがデータファイルを中間テーブルに格納
- Transformerが変換SQLを実行し成形済みデータにしてテーブルに格納
一般的にはStep FunctionsではLambdaで処理を記述すると思うのですが、
- 当時Lambdaの起動時間限界が5分と短く不安が大きかった
- Webサーバや、今回の処理とは別のいくつかのバッチ処理をECS(Fargate)で実現していたため、そちらに寄せておいた方がコード管理上楽そうだった
という理由で、デファクトから外れることを認識しつつも、ここではFargateバッチを採用しました。
ところが、これが原因でいくつかのつらみを生んでしまったのでした。
つらみ1: 立ち上がりが遅い
当たり前といえば当たり前なのですが、Fargateは起動に時間がそこそこかかります。我々のプロダクトのバッチだとおおよそ45sec程度。
Step Functionsの立ち上がり直後にSlackへ通知をするような処理が必要なのですが、通知が来るまでに約1min程度かかりました。
つらいといえばつらいんですが、要件的に致命的な問題にならないため、ひとまずこちらは気にせずそのまま突き進みました。
つらみ2: 処理結果を次処理で利用することができない
当初はこんな感じで処理を進める予定でした。
ところが、Fargateバッチの出力は以下のようになります。
{
"name": "Extractor",
"output": {
"Attachments": [
{
"Details": [
{
"Name": "subnetId",
"Value": "subnet-XXXXXXXXXXXXXXXXX"
},
{
"Name": "networkInterfaceId",
"Value": "eni-XXXXXXXXXXXXXXXXX"
},
{
"Name": "macAddress",
"Value": "XX:XX:XX:XX:XX:XX"
},
{
"Name": "privateIPv4Address",
"Value": "xx.xx.xx.xx"
}
],
"Id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"Status": "DELETED",
"Type": "eni"
}
],
(略)
}
}
標準出力に吐いた結果とか出てくることを期待しましたが、淡い期待でした。
ECSタスクに関する詳細情報が出てきます。
なんとか他の手段で次のステートへ受け渡す方法を考えてみましたが、結局見つからず。
苦肉の策として、ステートマシンの入力側で対処しました。
{
"StagingTable": "STAGING_XXXXXXXX_YYYYMMDDHHmmssfff",
"S3Path": "Sync/Staging/XXXXXXXX/YYYYMMDDHHmmssfff/"
}
ステートマシンへの入力の時点で取得ファイルの格納S3ディレクトリ名と、作成する中間テーブル名を渡しています。
衝突を避けるため、外部APIから取得するデータのアカウントID(XXXXXXXX
)と
実行日時(ミリ秒単位:YYYYMMDDHHmmssfff
)で分けました。
既にだいぶつらい感じの作りですが…なんとかこれにより共通のデータに対してアクセスすることには成功できました。
さて、これでようやく実装できるぞ、と思って動かしてみたら、次の問題が発生しました。
つらみ3: ThrottlingExceptionの大量発生
さてこれで元気に動いてくれそうだなということで実装しまして。
では…ということで動かしてみたら、狙い通りうまくいきました!
ここでめでたしめでたし、となればいいのですが、現実は非情なものでした。
今回の処理は並行実行を結構な数で行うケースが想定されます。
その確認のため負荷試験を行ったところ、ボコボコとエラーで停止してしまいました。
そのときのエラーがこちら。
{
"resourceType": "ecs",
"resource": "runTask.sync",
"error": "ECS.AmazonECSException",
"cause": "Rate exceeded. (Service: AmazonECS; Status Code: 400; Error Code: ThrottlingException; Request ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx)"
}
調べてみると、どうもECSタスク起動のためのECS APIコール数が秒間1回まで
という制限にひっかかっていたらしく、
リトライ設定をしても結構な確率で再衝突しました。結果リトライ上限に達し無事死亡……。
取り急ぎの対応としては、リトライのバックオフレート設定です。
"Retry": [
{
"ErrorEquals": [ "States.Timeout" ],
"MaxAttempts": 0
},
{
"ErrorEquals": [ "States.ALL" ],
"IntervalSeconds": 10,
"MaxAttempts": 5,
"BackoffRate": 3.0
}
]
リトライタイミングがそれなりに散ってくれたので、ステートマシン全体の処理完了時間を犠牲にはしましたが、動作するようになりました。
ちなみにこちら、Lambdaの場合はほぼ無制限
…同時実行数も1,000
など、圧倒的に有利ですね。
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/limits.html
まとめ
Lambdaを選択できるときは迷わずLambdaを選択するのが良さそうですね……。
とはいえ現在もLambdaの制限時間は15分で、これを超えそうなバッチもいくつかある現状。
激重なバッチについては残念ながら現状維持になりそうです。
ですが、例えばSlackへの通知だとか、今回は説明都合で割愛したDBへの軽微な変更処理バッチなど、
速度面に不安のない処理については、チームで粛々とLambda化の対応を進めています。
Step FunctionsはLambda,Fargateの他にも、AWS BatchやSQS、EMR、DynamoDBなどが利用可能ですが、
組み合わせるとどんなことが起きるかについては利用前に事前調査をしなくちゃな、と思いました。
当然といえば当然のことですが、いやー、反省です。