こんにちは。
エンジニアの遠藤です。
久々に Lambda (serverless framework)の記事となります。
今回は、とある小さめの案件で、 Lambda からアクセスする Database に SQLite を採用してみたので、構成などをまとめておきたいと思います。
プロジェクトの構成
プロジェクトの構成は以下のような内容です。
静的な Webページは S3 に配置しておき、 CloudFront 経由で配布します。
API Gateway + Lambda を使って中身のコンテンツを取得しています。
また、表示するデータは何かしらのDBに格納しておき、画面からの更新は行いません。
データの更新は、 EventBridge をトリガーに毎分実行されます。
Lambda が定期的に外部の API を呼び出して集計を行った結果で Update します。
Read は同時に複数アクセスされる可能性があり、 Write が複数箇所から同時に実行されることはない、という状況です。
データの件数は、最新の値のみ保持していればよく、アクセスするユーザ数や、ログなどに応じて増えることはありません。
常に数千件の集計結果が保持されているという状態になります。
マスターテーブルのようなイメージが近いかもしれません。
Database の選定
さて、 Database の選択肢ですが、一般的な用途で言えば、 RDS, Aurora あたりが採用されることが多いです。
また、設計さえしっかりできていれば、 DynamoDB を使う選択肢もあります。
今回の用途で言えば、特に複雑な設計ではないので、どの Database を採用することも可能です。
ただし、 RDS や DynamoDB は、ややお高めなことが引っかかっていました。
そこで、もうちょっとお手軽に使えるデータベースないかなと調べていたところ、 EFS が Lambda からも利用できるよ、ということで、 Lambda + EFS + SQLite の構成でどの程度の性能が出せるのか調べてみました。
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/services-efs.html
性能を測定する
というわけで、今回は実際に似たような案件で、過去に採用した Aurora RDS との比較をしてみたいと思います。
それぞれ 10,000 件ずつインサートした状態のテーブルに対して、全件セレクトと、アップデートをしてみて、その実行速度を計測してみます。
1回だと誤差が出そうなので、 100回ずつリクエストを行い、最大、最小、平均値を比較してみます。
それぞれ 100 回ずつ select 性能を測定し、最大、最小、平均を見てみます。
DB 構成
Aurora MySQL
AuroraParameterGroup:
Type: AWS::RDS::DBClusterParameterGroup
Properties:
Description: Cluster parameter group for aurora-mysql5.7
Family: aurora-mysql5.7
Parameters:
character_set_client: utf8mb4
character_set_server: utf8mb4
AuroraCluster:
Type: AWS::RDS::DBCluster
Properties:
DBClusterIdentifier: rds-sample
DatabaseName: mydatabase
Engine: aurora-mysql
DBClusterParameterGroupName:
Ref: AuroraParameterGroup
DBSubnetGroupName:
Ref: AuroraSubnetGroup
EnableHttpEndpoint: true
EngineMode: serverless
ScalingConfiguration:
AutoPause: true
MaxCapacity: 1
MinCapacity: 1
SecondsUntilAutoPause: 600
StorageEncrypted: true
VpcSecurityGroupIds:
- Fn::GetAtt:
- AuroraSecurityGroupDefinition
- GroupId
テーブル構成
Aurora MySQL
Column Name | Data Type | Constraints |
---|---|---|
user_id | BIGINT | NOT NULL |
organization_id | VARCHAR(64) CHARACTER SET utf8 | NOT NULL, COLLATE utf8_general_ci |
user_name | VARCHAR(64) CHARACTER SET utf8 | NOT NULL, COLLATE utf8_general_ci |
status | INT | NOT NULL |
Primary Key: user_id
SQLite
Column Name | Data Type | Constraints |
---|---|---|
user_id | INTEGER | PRIMARY KEY, NOT NULL |
organization_id | TEXT | NOT NULL |
user_name | TEXT | NOT NULL |
status | INTEGER | NOT NULL |
select
クエリはこんな感じ。
select * from sample_master limit 10000;
Aurora MySQL
最大時間: 2.911370515823364
最小時間: 1.679802656173706
平均時間: 2.2446921920776366
SQLite
最大時間: 1.0608539581298828
最小時間: 0.9547784328460693
平均時間: 1.0193749523162843
これは、想定外で、想像以上に SQLite が早かったです。
update
クエリはこんな感じで、これを 100 件更新します。
update sample_master set user_name="hoge" where user_id=1;
Aurora MySQL
最大時間: 4.00672483444214
最小時間: 2.2890918254852295
平均時間: 3.147315459251404
SQLite
最大時間: 0.1183171272277832
最小時間: 0.07905888557434082
平均時間: 0.09160815477371216
結果
というわけで、個人的に想像していた以上に SQLite が早く、想像以上に Aurora MySQL が遅いという結果になりました。
今回は Aurora MySQL への接続に、 boto3 の rds-data クライアント を使いました。
やはり、リクエストの都度ネットワーク処理が挟まるため、 select 1回とかであれば、そこまで差は大きな差は出ませんが、何度も update をリクエストするようなコードを書いてしまうと、差が顕著になるなと感じました。
一方 SQLite はストレージをマウントしているというだけあって、想像以上に処理が早かったです。
もちろん、 SQLite は同時書き込みに難があったり、クラスター化やバックアップ、復元などの面で、デメリットはあります。
EFS への同時接続数にも制約があります。
また、 DynamoDB のように VPC を作成しなくてもサッと使えるわけではないので、構築にはやや手間が必要です。
ただし、その辺りのデメリットを考慮しても、しっかり用途さえ見極めれば、費用面でも性能面でも、十分に選択肢の一つとしてよいパフォーマンスではないかと感じました。
CloudFormation, Serverless の定義
今回も環境構築には serverless framework を使用しました。
最後に、参考までに今回の検証で使用した VPC, EFS の CloudFormaion の記載と、 serverless.yml の内容を添付しておきます。
VPC
Resources:
VPCDefinition:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsHostnames: true
EnableDnsSupport: true
InstanceTenancy: default
InternetGateway:
Type: AWS::EC2::InternetGateway
GatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId:
Ref: VPCDefinition
InternetGatewayId:
Ref: InternetGateway
VPCPublicSubnet1Subnet:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.0.0/24
VpcId:
Ref: VPCDefinition
AvailabilityZone:
Fn::Select:
- 0
- Fn::GetAZs: ""
MapPublicIpOnLaunch: true
VPCPublicSubnetSharedRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId:
Ref: VPCDefinition
VPCPublicSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId:
Ref: VPCPublicSubnetSharedRouteTable
SubnetId:
Ref: VPCPublicSubnet1Subnet
VPCPublicSubnetDefaultRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId:
Ref: VPCPublicSubnetSharedRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId:
Ref: InternetGateway
DependsOn:
- GatewayAttachment
VPCEIP:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
NATGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId:
Fn::GetAtt:
- VPCEIP
- AllocationId
SubnetId:
Ref: VPCpublicSubnet1Subnet
VPCPublicSubnet2Subnet:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.1.0/24
VpcId:
Ref: VPCDefinition
AvailabilityZone:
Fn::Select:
- 1
- Fn::GetAZs: ""
MapPublicIpOnLaunch: true
VPCPublicSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId:
Ref: VPCPublicSubnetSharedRouteTable
SubnetId:
Ref: VPCPublicSubnet2Subnet
VPCLambdaSubnetDefaultRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId:
Ref: VPCLambdaSubnetSharedRouteTable
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId:
Ref: NATGateway
VPCLambdaSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId:
Ref: VPCLambdaSubnetSharedRouteTable
SubnetId:
Ref: VPCLambdaSubnet1Subnet
VPCLambdaSubnet1Subnet:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.2.0/24
VpcId:
Ref: VPCDefinition
AvailabilityZone:
Fn::Select:
- 0
- Fn::GetAZs: ""
MapPublicIpOnLaunch: false
VPCLambdaSubnetSharedRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId:
Ref: VPCDefinition
VPCLambdaSubnet2Subnet:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.3.0/24
VpcId:
Ref: VPCDefinition
AvailabilityZone:
Fn::Select:
- 1
- Fn::GetAZs: ""
MapPublicIpOnLaunch: false
VPCLambdaSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId:
Ref: VPCLambdaSubnetSharedRouteTable
SubnetId:
Ref: VPCLambdaSubnet2Subnet
VPCPrivateSubnet1Subnet:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.4.0/24
VpcId:
Ref: VPCDefinition
AvailabilityZone:
Fn::Select:
- 0
- Fn::GetAZs: ""
MapPublicIpOnLaunch: false
VPCPrivateSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId:
Ref: VPCLambdaSubnetSharedRouteTable
SubnetId:
Ref: VPCPrivateSubnet1Subnet
VPCPrivateSubnet2Subnet:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.5.0/24
VpcId:
Ref: VPCDefinition
AvailabilityZone:
Fn::Select:
- 1
- Fn::GetAZs: ""
MapPublicIpOnLaunch: false
VPCPrivateSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId:
Ref: VPCLambdaSubnetSharedRouteTable
SubnetId:
Ref: VPCPrivateSubnet2Subnet
LambdaSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: LambdaSecurityGroup
GroupName: ${self:service}-${self:provider.stage}-lambda-sg
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
Description: Allow all outbound traffic by default
IpProtocol: "-1"
VpcId:
Ref: VPCDefinition
LambdaSecurityGroupFromLambdaSecurityGroup:
Type: AWS::EC2::SecurityGroupIngress
Properties:
IpProtocol: "-1"
GroupId:
Fn::GetAtt:
- LambdaSecurityGroup
- GroupId
SourceSecurityGroupId:
Fn::GetAtt:
- LambdaSecurityGroup
- GroupId
EFSSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: EFS Allowed Ports
VpcId:
Ref: VPCDefinition
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 2049
ToPort: 2049
SourceSecurityGroupId:
Fn::GetAtt:
- LambdaSecurityGroup
- GroupId
Description: from Lambda
DependsOn:
- LambdaSecurityGroup
EFS
Resources:
EFSFileSystem:
Type: AWS::EFS::FileSystem
Properties:
FileSystemTags:
- Key: Name
Value: FileSystem
BackupPolicy:
Status: ENABLED
Encrypted: true
LifecyclePolicies:
- TransitionToIA: AFTER_30_DAYS
PerformanceMode: generalPurpose
DependsOn: VPCDefinition
EFSMountTarget:
Type: AWS::EFS::MountTarget
Properties:
FileSystemId: !Ref EFSFileSystem
SecurityGroups:
- !Ref EFSSecurityGroup
SubnetId: !Ref VPCPrivateSubnet1Subnet
DependsOn: EFSFileSystem
EFSAccessPoint:
Type: AWS::EFS::AccessPoint
Properties:
FileSystemId: !Ref EFSFileSystem
PosixUser:
Uid: 1001
Gid: 1001
RootDirectory:
Path: ${self:provider.environment.EFS_ROOT_DIR}
CreationInfo:
OwnerGid: 1001
OwnerUid: 1001
Permissions: 750
AccessPointTags:
- Key: Name
Value: AccessPoint
DependsOn: EFSFileSystem
Serverless.yml
service: sls-python-sample
frameworkVersion: '3'
provider:
name: aws
runtime: python3.9
stage: ${opt:stage, self:custom.defaultStage}
environment: ${file(./env/${self:provider.stage}.yml)}
iamRoleStatements: ${file(provider/iam.yml)}
vpc:
subnetIds:
- !Ref VPCPrivateSubnet1Subnet
securityGroupIds:
- !Ref LambdaSecurityGroup
resources:
- ${file(resources/vpc.yml)}
- ${file(resources/efs.yml)}
plugins:
- serverless-python-requirements
- serverless-prune-plugin
custom:
defaultStage: local
pythonRequirements:
pythonBin: python
prune:
automatic: true
number: 1
functions:
hello:
handler: handler.hello
timeout: 900
handler: handler.create_table_rds
fileSystemConfig:
localMountPath: ${self:provider.environment.EFS_MOUNT_DIR}
arn: !GetAtt ["EFSAccessPoint", "Arn"]
events:
- http:
path: /show_records
method: get