背景
AWS Lambda を使うと様々な処理を簡単かつサーバレスに実現することができます。お手軽に使える一方でいくつか考慮しておかないと実運用時に痛い目を見ることも沢山あります。いくつかある考慮ポイントの中で、実際にハマった冪等性の観点について少しまとめておこうと思います。
発生したこと
S3にオブジェクトをPutしたのをトリガとして S3 Object Lambda でオブジェクトの種類ごとに様々な処理を行っていました。当初は冪等性を考慮した作りになっていなかったのですが、多くないオブジェクト数(10~100)でテストした限りでは特段問題なく動作していたので本番環境へデプロイしました。実際の本番環境では、膨大な数のオブジェクト数(数百万~数億)を処理することになるのですが、次のような問題が発生してしまいした。
<問題点>
- 1.なぜかLambdaが複数回発火する場合がある
- 対象データ処理済の場合は、後で発火したLambdaの実行がエラーになる
- 前に発火したLambdaが正常終了でも最終ステータスがエラーになる
- ほぼ同時だと複数処理がデータを取り合って待ちやデータ破損が発生する
- 対象データ処理済の場合は、後で発火したLambdaの実行がエラーになる
- 2.無駄なLambdaのリトライが発生(初期設定では2回)してコストが嵩む
- データ起因なので、何度リトライしても確実に同じエラーが発生する
- リトライ対象のオブジェクト(画像)が巨大な場合は、ハイスペックで高単価なLambda(OOMが発生しないようスペックアップしてある)が複数回実行されることでコストが嵩む
- リトライ対象のオブジェクトが動画の場合は、後続の変換処理で高単価なAWS Elemental MediaConvertを使用していたためコストが嵩む
分析と対策
- その1
今回はS3にオブジェクトをPutしたのをトリガとして S3 Object Lambda を発火させていますが、AWS Lambdaでは発火イベントが意図せず複数回発生してしまうことがあります。これは「At Least Once(最低一回)」という仕様なので、これを前提にする必要があります。
【AWS Black Belt Online Seminar】Serverless モニタリング 【P.48】から抜粋
AWS Lambdaのベストプラクティス「Lambda 関数を冪等にする | AWS re:Post」では、次のように記載されています。
DynamoDB など、スケーリングが容易でスループットが高いサービスを使用してセッションデータを保存します。
このベストプラクティスに従ってDynamoDBを用いてLambdaの冪等性を確保するよう対策しました。具体的な実装のサンプルのソースコードはGithubに置いておきます。
こちらDynamoDBの操作【新規登録:put_item()、更新:put_item() 】の条件の記載方法が若干ややこしいので詳細は公式ドキュメントを参照してください。
- その2
無駄なLambdaのリトライについては単純にリトライしないようLambdaの「非同期呼び出し」の設定で「再試行(関数がエラーを返すときに再試行する最大回数)」を 0 に設定するだけで対策できます。
結果
以上の対策によりLambdaが複数回発火した場合でも、DynamoDBに登録したステータスのレコードをチェックすることで、直ちにLambdaを終了させることができるようになり、無事にAWS Lambdaの冪等性を確保することができました。これにより、前述の問題点をすべて解消することができました。
おわりに
前述したとおりAWS Lambdaでは発火イベントが意図せず複数回発生してしまう「At Least Once(最低一回)」という仕様なので、Lambdaが複数回発火すること自体を防ぐことはできません。そのため、Lambdaが複数回発火することを前提に冪等性を確保するよう実装する必要があります。また、Lambdaのリトライ回数や実行時間などの設定についても用途に合わせて適切にチューニングすることも併せて重要です。個々の処理は微々たるコストでも、今回のように取り扱うオブジェクト数が膨大になるとチリツモで、とんでもないコストを請求されることになりますので、くれぐれもご注意ください。(と、自戒の念を込めて…🙏)