VPC Lambda(Python)から別のLambdaを同期実行した時にタイムアウトが発生する問題を回避する

ひと手間かますことでLambda実行環境でもboto3にTCP KeepAliveの設定が可能です
2020.11.27

CX事業本部@大阪の岩田です。

とある環境で発生していた問題を解決するためにLambdaの実装を修正する機会がありましたので、問題と解決方法についてご紹介します。なお紹介する解決方法は対処療法的なものになります。同様の事象に遭遇した場合、まず最初にアーキテクチャを見直せないかご検討下さい。

構成と発生していた問題

ざっくり以下のような環境下でVPC Lambdaがタイムアウトするという問題が発生していました

  • VPC Lambda A(ランタイムはPython)がLambda Bを同期呼び出し
  • Lambda Bは処理完了までに400秒程度かかる
  • 約400秒後にLambda Bは処理が完了するものの、Lambda Bの処理完了後もLambda Aが同期呼び出しのレスポンスを待機し続けてタイムアウトする

図にするとこんな感じです

※実際にはLambda AもVPCの中にあるわけではなくLambdaのサービスVPCからHyperplaneENI経由で接続されるので厳密には間違った図なのですが、分かりやすいよう便宜上こういう図にしています。

この現象ですが、恐らくNATゲートウェイのタイムアウトが原因で発生しているとあたりを付けました。公式ドキュメント記載の通り、NAT ゲートウェイは以下の仕様に基づいてタイムアウトします。

NAT ゲートウェイを使用する接続が 350 秒以上アイドル状態のままになっていると、その接続はタイムアウトします。

NAT ゲートウェイのトラブルシューティング

また、タイムアウトの際の振る舞いについては以下のように記載されています。

接続がタイムアウトになると、NAT ゲートウェイは、NAT ゲートウェイの背後で接続を継続しようとするリソースすべてに RST パケットを返します (FIN パケットは送信しません)。

NAT インスタンスと NAT ゲートウェイの比較

以上のことからLambda Bの実行に350秒以上かかるためNATゲートウェイとLambda B間の接続がタイムアウトし、Lambda Aは戻ってくることのないレスポンスを待機し続けてタイムアウトしていると予想しました。原因切り分けのために一定時間SleepするだけのLambdaを作成してLamba Aから同期実行してみたところ、大体350秒を境にLambda Aがタイムアウトするようになったので、この予想は当たってそうです。

対応方法

実行に400秒程度必要なLambdaを同期実行しているのが良くないので、ここを非同期実行にするなりStepFunctionsで管理するなりしたいところではあるのですが、諸事情によりそれが難しい状況でした。なので、今回は対処療法的にLambda Aのコードを修正することで対応する方針としました。

まずNATゲートウェイのタイムアウトについては、先程紹介したドキュメントに回避方法が記載されています。

接続が中断されないように、接続を介して追加のトラフィックを開始することができます。または、インスタンスで、350 秒未満の値で TCP キープアライブを有効にできます。

ということでLambda AからLambda Bを呼び出す際の接続にTCP KeepAliveを設定するようにコードを修正すれば良さそうです。

Lambda(Python)にTCP KeepAliveを設定するには?

Lambda AのランタイムはPythonなのでboto3を利用してLambda Bを同期実行しています。boto3のClientクラスにTCP KeepAliveを設定すれば良さそうですね。boto3の公式ドキュメントを確認したところ、TCP KeepAliveを設定するには設定ファイル~/.aws/configtcp_keepalive = trueの記載を追加すれば良いようです。

Using a configuration file

boto3のTCP KeepAliveの設定はConfigオブジェクトや環境変数経由では設定できないというのが重要なポイントです。どうにかしてLambda実行環境の~/.aws/configを設定に適切したいところですが、Lambda実行環境ではユーザーから書き込めるディレクトリが制限されているため、~/.aws/configにファイルを作成することができません。

環境変数を利用して設定ファイルのパスを変える

設定ファイルのパス~/.aws/configですが、実は環境変数AWS_CONFIG_FILEを設定することで自由に変更可能です。この環境変数に、ユーザーが自由にファイルを配置できる/var/task/配下のパスを指定し、適切な設定ファイルを配置すればLambda(Python)の環境でもTCP KeepAliveを設定できそうです。

最終的には

  • Lambda Aの環境変数AWS_CONFIG_FILE/var/task/configを設定
  • configというファイルを以下の内容で作成
      [default]
      tcp_keepalive = true
  • Lambda Aのデプロイパッケージに作成したファイルconfigを追加してデプロイ

という手順でTCP KeepAliveを設定することができ、無事Lambda Aがタイムアウトしなくなりました。

(余談)VPCエンドポイントを使えば解決できるのか?

先日LambdaがVPCエンドポイントにも対応したので、現在はNATゲートウェイがなくてもVPC LambdaからVPCエンドポイント経由でLambdaを呼び出すことが可能になっています。試しにLambda AからVPCエンドポイント経由でLambda Bを呼び出すように構成を変更して検証してみたのですが、結果は変わらずタイムアウトが発生してしまいました。インターフェース型VPCエンドポイントのタイムアウト仕様については、それらしきドキュメントが見当たらなかったのですが、

  • NATゲートウェイ
  • VPCエンドポイント
  • NLB

といったコンポーネントは全てHyperplaneが利用されているので、350秒のタイムアウトというのはHyperplaneの共通仕様なのかもしれませんね。そう考えると、そもそもの問題はNAT Gatewayがタイムアウトしていたわけではなく、Lambda AのインターフェースからVPC内のENIにNATする部分のHyperplane ENIでタイムアウトが発生していたという可能性もありそうです。

Hyperplaneについては公開されている情報が少ないですが、re:Invent 2018のセッションで内部仕様についてある程度説明されています。

日本語で説明して欲しい!という方にはこちらの動画がオススメです

まとめ

VPC Lambdaで発生していたタイムアウト問題の解決方法についてご紹介しました。繰り返しになりますが、本来は実行に400秒もかかるようなLambdaを同期呼び出しするべきではありません。同様の事象に遭遇した際は、まずアーキテクチャの見直しから検討して下さい。そうはいってもやむにやまれぬ事情でアーキテクチャを変更できないケースもあるとは思いますので、その際はこのブログを参考にしてみて下さい。