Developers.IO 2019 in 福岡で「劇的改善??VPC Lambda Before&After」というテーマで発表させていただきました #cmdevio
CX事業本部の岩田です。
2019/10/26に開催されたDevelopers.IO 2019 in 福岡で「劇的改善??VPC Lambda Before&After」というテーマで発表させていただきました。お越し下さった皆様ありがとうございました。モツ鍋うまかったです。
発表資料はこちらです。
このブログでは発表内容について簡単に説明させて頂きます。
AWS Lambdaのアーキテクチャおさらい
Lambdaのアーキテクチャはこのようなレイヤ構成になっています。
Lambda Workerと呼ばれるEC2インスタンスの上でMicroVMと呼ばれる仮想マシンが起動し、さらにMicroVMがLinuxカーネルの
- cgroups
- namespaces
- seccomp
- iptables
- chroot
といった機能を使って、プロセス毎に隔離されたサンドボックス環境を構築します。構築されたサンドボックス環境内にNode.js、Pythonといった言語のランタイムが乗っかり、さらにその上で我々ユーザーの作成した関数のコードが実行されます。
Lambdaの実行要求があった場合に、構築済みのサンドボックス環境が存在しない場合は、新たにサンドボックス環境の構築や初期化を伴います。これがいわゆるコールドスタートです。また、大量アクセスによって多数のサンドボックス環境を起動するようなケースでは、WorkerManagerやPlacement Serviceといったコンポーネントと連携して、Worker自体の割り当てから実行されます。さらに、このモデルはEC2モデルとFirecrackerの2種類に分かれます。
EC2モデル
まずはEC2モデルです。こちらのモデルはLambdaというサービスが登場した当初から利用されているモデルです。NitroプラットフォームのEC2上で動作するMicroVMにLambdaの実行環境が構築されます。
こちらのモデルでは1つのMiroVM上に複数のLamba実行環境が構築されます。また、WorkerはAWSアカウントと1:1に紐付きます。AWSアカウントをまたいでWokkerが共有されることはなく、Worker単位で環境分離が実現されます。
Firecrackerモデル
続いてFirecrackerモデルです。まずFirecrackerとは何か?ですがKVMをベースにAWSがクラウド時代に向けて開発した新しい仮想化技術です。これまでの仮想化技術に比べて、高速かつメモリの消費量を抑えて仮想マシンを起動することが可能です。このFirecrackerはFargateの基盤で利用されているのに加えて、Lambdaの基盤でも利用されています。
Firecrackerモデルでは、MicroVMを起動するオーバーヘッドが非常に小さいため、1つのEC2ベアメタルインスタンス上で何10万ものMicroVMが起動しているそうです。FirecrackerモデルではAWSアカウント間の境界はMicroVMになり、複数のAWSアカウントが1つのWorkerを共有利用することになります。
VPC Lambdaのアーキテクチャ(旧)
ここからはVPC Lambdaのアーキテクチャについての話です。元々のVPCLamdbaのアーキテクチャはこの図のようになっていました。
※画像はAnnouncing improved VPC networking for AWS Lambda functionsより引用
まずLambdaの実行環境はAWSが管理する専用のVPC内に構築されます。そして、Lambdaが顧客のVPC内のリソースにアクセスする際には顧客のVPC内にENIを作成し、作成したENIを経由してVPC内のリソースにアクセスするようなモデルです。このENIはLambda実行環境のメモリ使用量3G毎に1つ作成されていました。
Lambdaのメモリ割り当て上限は3Gですが、メモリを3G割り当てたLambdaが1つ起動した場合はENIを1つ、メモリを3G割り当てたLambdaが2つ同時に起動した場合はENIを2つ消費するようなモデルです。
少し表現を変えるとこのようなイメージになります。
※画像はA Serverless Journey: AWS Lambda Under the Hood (SRV409-R1) - AWS re:Invent 2018より引用
こちらはre:invent2018のセッションで紹介されていたVPC Lambdaのモデルです。Lambda実行環境から顧客VPC内のENIのNAT処理はWoker内で実行されるという説明です。旧アーキテクチャのVPC Lambdaでは1つのENIに対して1つのWorkerがアタッチされ、Workerを跨いでENIを共有することができませんでした。
まとめると旧アーキテクチャでは
- Worlkerを跨いでENIを共有できない
- メモリ3GB毎にENIを消費する
という特徴がありました。
VPC Lambdaが抱えていた課題
このようなモデルの旧アーキテクチャは様々な問題を抱えていました。
- ENIの枯渇問題
- ENI作成のRate Limit
- IPアドレス枯渇問題
- ENI作成を伴うコールドスタート時の遅延
- インターネットアクセスにNAT Gatewayが必要
などです。
また、VPC Lambdaを使う動機としてはRDB(S)を利用したいという動機が一番多いと思います。このRDB(S)とLambdaの組み合わせにも課題があります。
- 同時接続数の問題
- コネクションプーリングが使えない
- RDBはスケールアウトではなくスケールアップ
- コスト最適化の課題
といった課題です。 この辺りは以前の発表で詳しく取り上げているので良ければご参照下さい。
これらの課題から、VPC Lambdaはアンチパターンとされていました。
VPC Lambdaのアーキテクチャ(新)
ここからVPC Lambdaの新アーキテクチャについてです。
旧アーキテクチャではLambdaのコールドスタート時に必要に応じてENIを作成してWorkerにアタッチしていましたが、新アーキテクチャではVPC Lambda作成時にサブネットとセキュリティグループの組み合わせごとにENIを1つ作成します。Lambdaのサンドボックス環境はWorkerを跨いでこの1つのENIを共有利用します。
※画像はAnnouncing improved VPC networking for AWS Lambda functionsより引用
Lambda実行環境と顧客VPC内のENIのNAT処理に関しては、Lambda実行環境とENIの間のHyperplaneENIというコンポーネントが実行するようになります。
Hyperplaneについて
このHyperplaneとはAWSのサービス内部で利用されているSDNの技術です。元々はS3LoadBalancerで利用されていた技術をベースに開発されたコンポーネントです。
- EFS
- NLB
- Private Link
- Managed NAT(NAT Gateway,Transit Gateway)
で利用されており
- デフォルト5Gbit/secの性能をもつ
- Tbit/secレベルまでスケールする
- msレベルのレイテンシー
といった特徴を持ちます。
※画像はAmazon VPC: Security at the Speed Of Light (NET313) - AWS re:Invent 2018より引用
VPC LambdaでもこのHyperplaneの技術を利用して顧客のVPC内のENIにNAT処理を行うことで、最小限のオーバーヘッドでVPC内のリソースと通信することが可能になりました。
東京リージョンに新アーキテクチャの適用が完了しました!!
Update – September 27, 2019: We have fully rolled out the changes to the following Regions: US East (Ohio), EU (Frankfurt), and Asia Pacific (Tokyo). All AWS accounts in these Regions will see the improvements outlined in the original post.
Announcing improved VPC networking for AWS Lambda functions
という訳で9/27に東京リージョンへの新アーキテクチャ適用が完了したというアナウンスがありました。
改善効果について
新アーキテクチャで色々検証してみました。ここからは私の方で実施したいくつかの検証結果のご紹介です。
検証1 EC2インスタンスからheyコマンドを50並列で流した場合
5/29に実施した時点では、500回中50回のリクエストに関しては10秒以上の大きな遅延が発生していました。
Summary: Total: 13.4614 secs Slowest: 13.0460 secs Fastest: 0.0194 secs Average: 1.2528 secs Requests/sec: 37.1433 Total data: 2500 bytes Size/request: 5 bytes Response time histogram: 0.019 [1] | 1.322 [448] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 2.625 [1] | 3.927 [0] | 5.230 [0] | 6.533 [0] | 7.835 [0] | 9.138 [0] | 10.441 [4] | 11.743 [9] |■ 13.046 [37] |■■■
また、9/16に実施した時点でも、10秒以上の大きな遅延が発生したり、しなかったりという状況でした。※9/16時点ではAWSからVPC Lambdaの改善提供完了の正式アナウンスはありませんでした。順次更新中という段階だったと思われます。
10/20に改めて同一の検証を流したところ、1秒以上の遅延は一度も発生しませんでした。
Summary: Total: 1.3694 secs Slowest: 0.5470 secs Fastest: 0.0225 secs Average: 0.0970 secs Requests/sec: 365.1342 Total data: 2500 bytes Size/request: 5 bytes Response time histogram: 0.023 [1] | 0.075 [411] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 0.127 [25] |■■ 0.180 [1] | 0.232 [0] | 0.285 [0] | 0.337 [0] | 0.390 [9] |■ 0.442 [7] |■ 0.495 [33] |■■■ 0.547 [13] |■
検証2 EC2インスタンスからheyコマンドを500並列で流した場合
新アーキテクチャではWorkerを跨いでENIを共有できるようになり、メモリ使用量合計3Gの制限はなくなりました。ただ、常に消費するENIが1つだけかというと、明確なドキュメントの記載は見つけられませんでした。Lambdaの同時実行数がどんどん増えていった場合に、果たしてENIが追加作成されることはないのでしょうか?
※2019/11/30追記 Lambdaのドキュメントに以下のような記載が追加されていました。
関数の数が多い場合や、非常に使用率が高い関数がある場合は、Lambda で追加のネットワークインターフェイスが作成されることがあります。
VPC 内のリソースにアクセスするように Lambda 関数を設定する ※2019/11/30追記 ここまで
Lambdaの同時実行数を高い水準に保つため、
- Lambdaに2秒のスリープを追加
- heyの並列数を500に引き上げ
と、条件を変更して改めて検証しました。
Summary: Total: 21.8108 secs Slowest: 3.4042 secs Fastest: 2.0223 secs Average: 2.1532 secs Requests/sec: 458.4892 Total data: 50000 bytes Size/request: 5 bytes Response time histogram: 2.022 [1] | 2.160 [8998] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 2.299 [1] | 2.437 [0] | 2.575 [0] | 2.713 [0] | 2.851 [0] | 2.990 [0] | 3.128 [113] |■ 3.266 [844] |■■■■ 3.404 [43] |
この検証パターンでも大きな遅延は発生せず、ENIが追加作成されることはありませんでした。
※スリープを2秒追加しているので、純粋な処理時間という意味では2秒引いて考えて下さい。
検証3 Locustのクラスタを作ってLambdaの同時実行数上限を超える負荷をかけた場合
さらに負荷を上げて、Lambdaの同時実行数上限を上回るレベルのアクセスを行います。クライアントとして利用しているEC2のスペックがボトルネックになることを避けるため、Fargateを使ってコンテナ20台で構成されるLocustのクラスタを構築してテストを行いました。
Locustを使った負荷テストはこちらの記事も参照して下さい。
MQTTの負荷テストもバッチリ!!Locustを活用した分散負荷テスト環境の構築
このブログを書いたときはCFnでサービスディスカバリの設定が出来なかったのですが、今はCFnが対応しているので設定が楽になりました。使用したテンプレートはこちらです。
AWSTemplateFormatVersion: 2010-09-09 Description: Setup Stress Test Environment Parameters: UserGIP: Description: The IP address range that can be used to Locust WebUI Type: String MinLength: '9' MaxLength: '18' Default: 0.0.0.0/0 AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})" ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x. TargetBaseUrl: Description: Target host name for test Type: String Resources: VPC: Type: AWS::EC2::VPC Properties: CidrBlock: 10.0.0.0/16 EnableDnsSupport: 'true' EnableDnsHostnames: 'true' InstanceTenancy: default Tags: - Key: Name Value: locust VPC PrivateNamespace: Type: AWS::ServiceDiscovery::PrivateDnsNamespace Properties: Name: locust.internal Vpc: !Ref VPC DiscoveryService: Type: AWS::ServiceDiscovery::Service Properties: Description: Discovery Service for Locust Name: master DnsConfig: RoutingPolicy: MULTIVALUE DnsRecords: - TTL: 60 Type: A HealthCheckCustomConfig: FailureThreshold: 5 NamespaceId: !Ref PrivateNamespace VPCPublicRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: VPCPublicRouteTable VPCPublicSubnetA: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC CidrBlock: 10.0.0.0/24 AvailabilityZone: ap-northeast-1a MapPublicIpOnLaunch: true Tags: - Key: Name Value: VPCPublicSubnetA VPCPublicSubnetARouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref VPCPublicSubnetA RouteTableId: !Ref VPCPublicRouteTable VPCInternetGateway: Type: AWS::EC2::InternetGateway VPCAttachGateway: Type: AWS::EC2::VPCGatewayAttachment Properties: VpcId: !Ref VPC InternetGatewayId: !Ref VPCInternetGateway VPCRoute: Type: AWS::EC2::Route DependsOn: VPCInternetGateway Properties: RouteTableId: !Ref VPCPublicRouteTable DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref VPCInternetGateway VPCLocustSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Allow Access To Locust VpcId: !Ref VPC VPCLocustSecurityGroupIngressWebUIInner: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref VPCLocustSecurityGroup IpProtocol: tcp FromPort: 8089 ToPort: 8089 SourceSecurityGroupId: !Ref VPCLocustSecurityGroup VPCLocustSecurityGroupIngressMasterSlaveInner: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref VPCLocustSecurityGroup IpProtocol: tcp FromPort: 5557 ToPort: 5558 SourceSecurityGroupId: !Ref VPCLocustSecurityGroup VPCLocustSecurityGroupIngressWebUI: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref VPCLocustSecurityGroup IpProtocol: tcp FromPort: 8089 ToPort: 8089 CidrIp: !Sub ${UserGIP} VPCLocustSecurityGroupIngressMasterSlave: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref VPCLocustSecurityGroup IpProtocol: tcp FromPort: 5557 ToPort: 5557 CidrIp: !Sub ${UserGIP} VPCLocustSecurityGroupIngressSSH: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref VPCLocustSecurityGroup IpProtocol: tcp FromPort: 22 ToPort: 22 CidrIp: !Sub ${UserGIP} ECSTaskExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - ecs-tasks.amazonaws.com Action: - sts:AssumeRole Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess ECRRepository: Type: AWS::ECR::Repository Properties: RepositoryName: lambdatest/locust LocustCluster: Type: AWS::ECS::Cluster Properties: ClusterName: LocustCluster LocustMasterTaskDef: Type: AWS::ECS::TaskDefinition Properties: Cpu: 512 Family: locust-master Memory: 1GB NetworkMode: awsvpc RequiresCompatibilities: - FARGATE ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn ContainerDefinitions: - Command: - "locust" - "--master" - "-f" - "test.py" - "--host" - !Sub ${TargetBaseUrl} Image: !Sub - '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${RepoName}' - {RepoName: !Ref ECRRepository} Name: locust-master LogConfiguration: LogDriver: awslogs Options: awslogs-region: !Sub '${AWS::Region}' awslogs-group: !Ref MasterLog awslogs-stream-prefix: !Ref MasterLog LocustSlaveTaskDef: Type: AWS::ECS::TaskDefinition Properties: Cpu: 2048 Family: locust-slave Memory: 4GB NetworkMode: awsvpc RequiresCompatibilities: - FARGATE ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn ContainerDefinitions: - Command: - "locust" - "--slave" - "-f" - "test.py" - "--master-host" - "master.locust.internal" Image: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRRepository}' Name: locust-slave LogConfiguration: LogDriver: awslogs Options: awslogs-region: !Sub '${AWS::Region}' awslogs-group: !Ref SlaveLog awslogs-stream-prefix: !Ref SlaveLog LocustMasterService: Type: AWS::ECS::Service Properties: Cluster: !GetAtt LocustCluster.Arn DesiredCount: 0 LaunchType: FARGATE NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: ENABLED SecurityGroups: - !Ref VPCLocustSecurityGroup Subnets: - !Ref VPCPublicSubnetA ServiceName: locust-master TaskDefinition: !Ref LocustMasterTaskDef ServiceRegistries: - RegistryArn: !GetAtt DiscoveryService.Arn LocustSlaveService: Type: AWS::ECS::Service Properties: Cluster: !GetAtt LocustCluster.Arn DesiredCount: 0 LaunchType: FARGATE NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: ENABLED SecurityGroups: - !Ref VPCLocustSecurityGroup Subnets: - !Ref VPCPublicSubnetA ServiceName: locust-slave TaskDefinition: !Ref LocustSlaveTaskDef MasterLog: Type: AWS::Logs::LogGroup Properties: LogGroupName: /ecs/locust-master SlaveLog: Type: AWS::Logs::LogGroup Properties: LogGroupName: /ecs/locust-slave Outputs: LocustCluster: Description: LocustCluster Value: !GetAtt LocustCluster.Arn VPCSecurityGroup: Description: VPCSecurityGroup Value: !Ref VPCLocustSecurityGroup VPCPublicSubnet: Description: VPCPublicSubnet Value: !Ref VPCPublicSubnetA LocustMasterTaskDef: Description: LocustMasterTaskDef Value: !Ref LocustMasterTaskDef LocustSlaveTaskDef: Description: LocustSlaveTaskDef Value: !Ref LocustSlaveTaskDef
テスト用のコードです
from locust import HttpLocust, TaskSet, Locust, task, runners from locust.events import request_success, request_failure import os import requests API_PATH = os.getenv('API_PATH', '/Prod/test') class APITaskSet(TaskSet): @task def call_api(self): self.client.get(API_PATH) # エラーをどう扱うかによって↑のコードと以下のコードを使い分ける # with self.client.get(API_PATH, catch_response=True) as res: # if res.status_code == 200: # res.success() # else: # res.failure() class ColdStartTest(HttpLocust): task_set = APITaskSet min_wait = 1 max_wait = 1
Dockerfileです
FROM python:3.6 WORKDIR /app ADD Pipfile /app/ ADD Pipfile.lock /app/ RUN pip install pipenv && LIBRARY_PATH=/lib:/usr/lib pipenv install --system --ignore-pipfile ADD . /app/
- Lambdaは3秒Sleep後にレスポンスを返却する処理
- Usersは1,100に設定
- Hatch rateは100に設定
- 同時実行数の上限に達するようなワークロード
という設定で、Lambdaの同時実行数上限に到達してスロットリングが頻発されるまで追い込みましたが大きな遅延はありませんでした。
検証4 検証3のレスポンスを1Mに
これまでの検証はLambdaからのレスポンスとしてHelloWorldを返すだけの単純な処理でした。データ転送量が増えた場合にHyperplaneやENIが詰まることが無いか検証するために、Lambdaのレスポンスを1Mに変えて検証しました。
が、、、よく考えたらLambdaから返すレスポンスのサイズを増やしてもHyperplaneやENIの負荷は変わりません。本来はVPC内に何かしら大きめのレスポンスを返してくれるモックサービスを立てて検証するべきでした。この検証は無意味だったので、Speakerdeckにアップした資料からは削除させて頂きましたm(_ _)m
データ転送量の観点など、十分な検証は行えていませんが、それでも今回の検証範囲では新アーキテクチャのVPC Lambdaは非VPC Lambdaと同等レベルの耐久性・可用性がありそうだという結果に終わりました。
どのように考え方を変えるべきか
新アーキテクチャへの移行によって、ENI関連の様々な問題が改善されました。一方でENIと無関係な課題については引き続き課題事項として残り続けます。特にVPC Lambdaを使いたい動機としてニーズの高いRDBに関する同時接続数の課題などは残ったままです。
ただし、本当に同時接続数が問題になるのかは注意が必要です。例えばPostgres互換のAuroraであれば、選択可能な最小サイズのインスタンスであってもデフォルトの最大同時接続数は1600で、Lambdaの同時実行数制限を上回っています。つまり同時実行数の上限緩和申請が必要ないようなワークロードではDBの最大同時接続数に達することは無いわけです。
※Lambdaから複数のDB接続を確立しない実装になっている という前提です
まとめると
- コールドスタートによる
10秒~20秒数100ミリ秒~数秒程度の遅延が許容できるワークロード - 利用予定のVPCリソースが想定される最大アクセス数を問題なく処理可能なワークロード
ではVPC Lambdaを採用しても問題ないケースが増えたと言えるでしょう。 例えば、アクセス数が安定しており、数秒程度のレイテンシが許容できるB2Bサービスのバックエンドなどです。
その他 考慮しておきたいこと
最後にその他の考慮事項をいくつか挙げさせて頂きました。
- VPC Lambdaを使うということは何かしらのVPCリソースにアクセスしたいわけで、そのためのライブラリが必要になることが多いです。自然とLambdaのパッケージやLayerが肥大化しがちで、それに伴ってコールドスタートが遅くなりがちです。
- 上記の話とも関連するのですが、RDBをバックエンドにしてWeb APIを開発する場合など、何かしらのアプリケーションフレームワークを採用するのか?も要検討だと思います。重量級のフレームワークを採用すると当然コールドスタートは遅くなります。かといってある程度の規模のシステムになるとフレームワークを使わないことによる辛身も出てきます。パフォーマンスや開発の生産性、保守性といった要素から総合的に判断してフレームワークの利用有無を判断しましょう。
- VPC Lambaが改善されたということで、既存のアプリケーションをLambaに載せ換えることを検討されるケースもあるかもしれません。ちょっとした便利ツール程度であれば良いのですが、中規模〜大規模なモノリシックなアプリケーションをVPCLamdbaに乗せ替えようとしていないでしょうか??マイクロサービス志向で関数単位でデプロイを行うLambdaのモデルとミスマッチにならないかは良く検討した方が良いでしょう。
まとめ
元々VPC Lambdaが抱えていた課題のうち、いくつかの課題は解消されました。しかし、継続して残り続ける課題もあります。とはいえVPC Lambdaを採用しても問題の無いユースケースは増えました。
このようにAWSを利用していると、サービスのアップデートに伴って、それまでの常識が通用しなくなることが頻繁に起こり得ます。常に最新の情報をウォッチしながら自分の知識をアップデートし続けましょう!