Lambdaを大量に使うETLジョブを運用してきて発生した問題と対策を挙げていく

## ジョブの概要
受け取ったファイルの情報を加工して、DynamoDB、RDS、S3に必要な情報をロードしていくジョブで、StepFunctionsのMapステートを用いて、受け取ったファイル単位にLambdaを並列に実行していきます。
このファイルが50件、Map処理の中のLambdaの数が10くらい。さらにこのStepFunctions自体が並列に10個くらい動く状態です。また、各Lambdaの実行状態をEFSで管理しているため、LambdaからEFSヘのアクセスが頻繁にあります。
![top.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/432063/acd861ed-dbc1-c1f3-4e7b-ca5fc195c17b.png)


## Lambda.Unknown
"Task timed out after 900.10 seconds"
StepFunctionsにはリトライする例外名、リトライの回数や間隔などを指定してリトライする機能があるのでそれを設定しました。
Lambda.SdkClientExceptionやLambda.AWSLambdaExceptionも一時的な問題によるもので、適切なリトライ処理が推奨されています。

## Lambda.ServiceException
"We currently do not have sufficient capacity in the region you requested. Our system will be working on provisioning additional capacity. You can avoid getting this error by temporarily reducing your request rate. "

使ってるリージョンでLambdaのキャパシティが枯渇しているという内容。アカウントごとにLambdaの同時実行数に制限があることは知っていましたが、リージョンでLambdaが足りなくなることがあるんですねー。
これも同様にリトライ設定をしました。

## botocore.exceptions.ClientError: An error occurred (ThrottlingException) when calling the GetParametersByPath operation

Lambda内でSDK経由でSSMパラメータを取得する処理があるのですが、Lambdaが並列で多種のパラメータを呼び出していたため、制限に引っかかりました。
そのため、以下の対策を取りました。
- パラメータストアが処理できる1秒あたりのトランザクション(TPS)の上限緩和申請。
- どれくらいの頻度でパラメータを取得すべきかの見直し。実行時にパラメータストアから取得する必要のあるパラメータなのか？そうでないならば実行時ではなくデプロイ時にパラメータストアから取得し、環境変数に入れておくことでパラメータストアへのアクセスの回数を減らせる。
- Lambda内でのキャッシュ機能を作成。キャッシュにないパラメータだけパラメータストアへ取得するようにする。Lambdaのホットスタート時にはキャッシュにあるパラメータを利用するだけでパラメータストアへはリクエストしないで済みます。

ちなみに最近LambdaのExtensionでSSMパラメータをキャッシュしてくれる機能がリリースされました。これも活用できそうですね。
https://docs.aws.amazon.com/systems-manager/latest/userguide/ps-integration-lambda-extensions.html
実態はLambda LayerでHTTP通信で取得するようです。

## Can't connect to MySQL server on [RDSProxyのホスト名](timed out)
RDSProxyに接続できずに発生するエラーです。
RDSProxyにはClientConnectionやDatabaseConnectionといったメトリクスがあります。
LambdaとRDSProxy間のコネクション数がClientConnectionで、RDSProxyとRDS間のコネクション数がDatabaseConnectionです。
ある事情から1件のupdateごとにコネクションを作成して削除すると言った動きをするLambdaがありました。これが大量に実行されたためClientConnectionが非常に大きくなり、エラーとなりました。
![rdsproxy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/432063/d4022521-c770-5f04-b981-e5e18ff4b3cc.png)

ORMにはSQLAlchemyを利用していたため、コネクションプーリングの機能を用いることでLambda内でコネクションを保持して使い回すことができてClientConnectionの数が大きく減りました。
![rdsafter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/432063/1ddf13aa-acc7-3ec3-e4eb-f1339d13ce93.png)
RDSProxyによってLambdaが大量に実行された場合でもLambda間でコネクションを使い回すだけでなく、SQLAlchemyのコネクションプーリング機能によってLambda内でもコネクションを使い回すようになったので、性能が改善されました。


## Lambda.EfsMountTimeoutException
LambdaにEFSをマウントする際に稀に発生します。これも一時的に発生するものなので、リトライすることやLambdaの同時実行数を制限することが推奨されています。
StepFunctionsでリトライしたり、Map内の同時実行数の制限をすることで対策しました。

## OSError: No space left on device
Lambdaのストレージが不足した時のエラーです。エフェメラルストレージは今年最大10Gにアップデートされるまでは最大512Mでした。なので大きな容量のファイルを処理する場合はEFSを利用していました。
エフェメラルストレージが10Gまで拡張されたので、他のプロセスと共有する必要のないファイルであればEFSではなくエフェメラルストレージに移行していきたいところです。
また、ファイルのUL, DLする際にはなるべくメモリ上で処理するなど実装上で工夫したりもしました。

## The execution reached the maximum number of history events (25000)
StepFunctionsのイベント履歴の制限が25000件です。1つのLambdaで4件（StateEntered,LambdaFunctionScheduled,LambdaFunctionStarted,LambdaFunctionSucceededを）消費するので6000Lambdaくらいまでの実行制限となります。
StepFunctions自体を分割することで制限を免れましたが、上限の緩和もできないのでそれしか方法はなさそうですね。

## 他に選択肢はなかったのか
今回は小規模なLambdaを組み合わせて規模の大きいETL処理を実現しましたが、
AWS環境で規模の大きいETL処理をするならGlueを利用することも選択肢のようです。
少し触ってみましたが、DynamicFrameを用いることで、Lambdaだと苦労するようなデータストアへの接続部分の実装を比較的ラクに実装できるのではないかと感じました。
AWSリソースの一時的な問題が発生した時に、特に利用者が設定していなくても自動でリトライしてくれる機能があったり、ジョブの実行状況を管理するジョブブックマーク機能が用意されていたりと（StepFuncitons+Lambdaの場合だと自前で実行状況の管理機能を実装する必要がありました。）ETL特化のマネージドサービスはすごいなと思いました。
一方でDBへの更新処理などで柔軟な対応が必要な場合、Glueの機能だけではできないのでそこはLambdaでORMを利用して更新処理を行う（※Glueの中でORMを使うこともできます）などStepFunctionsの中でLambdaだけでなく、Glueを組み合わせて使っていきたいなと思いました。