Lambda関数のハンドラーメソッドの内外でRDSに接続してパフォーマンスを測定する
AWS DVAの出題範囲であるリファクタリングに関する内容です。
Lambda関数からRDSに接続する際のベストプラクティスの1つは、ハンドラーメソッドの外部でRDSに接続することです。
関数ハンドラーメソッドの外部で宣言されたオブジェクトは、初期化されたままとなり、関数が再度呼び出されると追加の最適化を提供します。例えば、Lambda 関数がデータベース接続を確立する場合、連続した呼び出しでは接続を再確立する代わりに元の接続が使用されます。
AWS Lambda 実行環境
本ページでは、2つのLambda関数を作成し、それぞれハンドラーメソッドの内部・外部でRDSに接続することで、どの程度パフォーマンスが向上するか測定します。
構築する環境
VPC内に4つのサブネットを作成します。
2つはLambda用です。
残り2つはRDS用です。今回は1つMySQLタイプのDBインスタンスを作成します。
RDS Proxyを作成します。
RDS ProxyのターゲットグループにDBインスタンスを登録することで、Lambda関数はRDS Proxyを経由して、DBインスタンスに接続することができます。
Lambda関数を2つ作成します。
両関数はDBインスタンスに接続後、現在日時を保存して返す働きをします。
両関数の違いはDBインスタンスに接続する部分の実装です。
関数1はハンドラーメソッドの内部でDBインスタンスに接続するのに対して、関数2は外部で接続します。
なお関数のランタイム環境はPython3.8とし、Function URLを作成します。
検証はApache Benchを使用します。
各関数のFunction URLに対して、100回リクエストを送り、処理にかかった時間を計測します。
CloudFormationテンプレートファイル
上記の構成をCloudFormationで構築します。
以下のURLにCloudFormationテンプレートを配置しています。
https://github.com/awstut-an-r/awstut-dva/tree/main/04/001
テンプレートファイルのポイント解説
本ページでは、Lambda関数内のDBインスタンスに接続方法に関する内容を中心に取り上げます。
Lambda関数からRDS Proxy経由でRDSに接続する方法については、以下のページをご確認ください。
MySQLタイプのDBインスタンスに接続するために、mysql-connector-pythonを使用します。
Lambdaレイヤーを作成し、同パッケージをこれに配置します。
Lambdaレイヤーの詳細については、以下のページをご確認ください。
作成したLambda関数はFunction URLを使用して実行します。
Function URLに関する詳細は、以下のページをご参照ください。
ハンドラーメソッドの内部でDBインスタンスに接続する
ハンドラーメソッド内部で接続する関数(関数1)のコードを確認します。
import datetime
import mysql.connector
import os
db_endpoint_port = os.environ['DB_ENDPOINT_PORT']
db_name = os.environ['DB_NAME']
db_password = os.environ['DB_PASSWORD']
db_proxy_endpoint_address = os.environ['DB_PROXY_ENDPOINT_ADDRESS']
db_tablename = os.environ['DB_TABLENAME']
db_user = os.environ['DB_USER']
region = os.environ['REGION']
def lambda_handler(event, context):
conn = mysql.connector.connect(
host=db_proxy_endpoint_address,
port=db_endpoint_port,
user=db_user,
password=db_password,
database=db_name
)
cur = conn.cursor()
now = datetime.datetime.now()
now_str = now.strftime('%Y-%m-%d %H:%M:%S')
write_sql = 'insert into {tbl} values ("{now}");'.format(
tbl=db_tablename,
now=now_str
)
cur.execute(write_sql)
cur.close()
conn.commit()
return {
'statusCode': 200,
'body': str(now)
}
Code language: Python (python)
mysql.connector.connectでDBインスタンスへの接続用オブジェクトを作成します。
ご覧の通り、ハンドラーメソッド内で実行しています。
ハンドラーメソッドの外部でDBインスタンスに接続する
続いてハンドラーメソッド外部で接続する関数(関数2)のコードです。
import datetime
import mysql.connector
import os
db_endpoint_port = os.environ['DB_ENDPOINT_PORT']
db_name = os.environ['DB_NAME']
db_password = os.environ['DB_PASSWORD']
db_proxy_endpoint_address = os.environ['DB_PROXY_ENDPOINT_ADDRESS']
db_tablename = os.environ['DB_TABLENAME']
db_user = os.environ['DB_USER']
region = os.environ['REGION']
conn = mysql.connector.connect(
host=db_proxy_endpoint_address,
port=db_endpoint_port,
user=db_user,
password=db_password,
database=db_name
)
def lambda_handler(event, context):
cur = conn.cursor()
now = datetime.datetime.now()
now_str = now.strftime('%Y-%m-%d %H:%M:%S')
write_sql = 'insert into {tbl} values ("{now}");'.format(
tbl=db_tablename,
now=now_str
)
cur.execute(write_sql)
cur.close()
conn.commit()
return {
'statusCode': 200,
'body': str(now)
}
Code language: Python (python)
先ほどのコードとほとんど同じです。
唯一異なる点は、mysql.connector.connectで接続用オブジェクトを作成しているスコープが、グローバルスコープであるという点です。そのためオブジェクトはグローバル変数として定義されたということになります。
(参考)DBインスタンスの初期化処理
現在日時を書き込むためには、DBインスタンスに日時データ用のテーブルを作成する必要があります。
今回は初期化処理用の関数(関数3)を用意して実行します。
参考までに、関数3のコードを記載しておきます。
import datetime
import json
import mysql.connector
import os
db_endpoint_port = os.environ['DB_ENDPOINT_PORT']
db_name = os.environ['DB_NAME']
db_password = os.environ['DB_PASSWORD']
db_proxy_endpoint_address = os.environ['DB_PROXY_ENDPOINT_ADDRESS']
db_tablename = os.environ['DB_TABLENAME']
db_user = os.environ['DB_USER']
region = os.environ['REGION']
def lambda_handler(event, context):
conn = mysql.connector.connect(
host=db_proxy_endpoint_address,
port=db_endpoint_port,
user=db_user,
password=db_password,
database=db_name
)
cur = conn.cursor()
table_sql = 'create table if not exists {db}.{tbl} (dt datetime);'.format(
db=db_name,
tbl=db_tablename
)
cur.execute(table_sql)
now = datetime.datetime.now()
now_str = now.strftime('%Y-%m-%d %H:%M:%S')
write_sql = 'insert into {tbl} values ("{now}");'.format(
tbl=db_tablename,
now=now_str
)
cur.execute(write_sql)
cur.close()
conn.commit()
cur = conn.cursor()
read_sql = 'select * from {tbl};'.format(tbl=db_tablename)
cur.execute(read_sql)
content = [record[0].strftime('%Y-%m-%d %H:%M:%S') for record in cur]
cur.close()
conn.close()
return {
'statusCode': 200,
'body': json.dumps(content, indent=2)
}
Code language: Python (python)
テーブルが存在しない場合は、新規作成する内容となっています。
その後、保存されている全データを取得して返す働きをします。
環境構築
CloudFormationを使用して、本環境を構築し、実際の挙動を確認します。
Lambda関数のデプロイパッケージを用意する
Lambda関数を作成する場合、3つの方法があります。
今回はデプロイパッケージをS3バケットにアップロードする方法を選択します。
詳細につきましては、以下のページをご確認ください。
CloudFormationスタックを作成し、スタック内のリソースを確認する
CloudFormationスタックを作成します。スタックの作成および各スタックの確認方法については、以下のページをご確認ください。
各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。
- 関数1のFunction URL:https://42lzbfe7dehb5jcnphzp555hly0evucx.lambda-url.ap-northeast-1.on.aws/
- 関数2のFunction URL:https://6oy3eianevtfz7xw3dopmbnnrm0ijdyr.lambda-url.ap-northeast-1.on.aws/
- 関数3のFunction URL:https://7poj6cjntirj24de2hlbz2xeia0katuf.lambda-url.ap-northeast-1.on.aws/
動作確認
事前準備
まずDBインスタンス内に、日時データを保存するためのテーブルを作成します。
関数3のFunction URLにアクセスし、同関数を実行します。
テーブルが作成されて、現在保存されている全データが返ってきました。
初期化処理は正常に完了です。
検証1:ハンドラーメソッドの内部でDBインスタンスに接続する
準備が整いましたので、検証を開始します。
一応、関数1の動作を確認します。
現在日時が返ってきました。
正常に関数が動作しています。
Apache Benchを使って、本関数の性能を評価します。
以下が実行結果です。
% ab -n 100 https://42lzbfe7dehb5jcnphzp555hly0evucx.lambda-url.ap-northeast-1.on.aws/
This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking 42lzbfe7dehb5jcnphzp555hly0evucx.lambda-url.ap-northeast-1.on.aws (be patient).....done
Server Software:
Server Hostname: 42lzbfe7dehb5jcnphzp555hly0evucx.lambda-url.ap-northeast-1.on.aws
Server Port: 443
SSL/TLS Protocol: TLSv1.2,ECDHE-RSA-AES128-GCM-SHA256,2048,128
Server Temp Key: ECDH P-256 256 bits
TLS Server Name: 42lzbfe7dehb5jcnphzp555hly0evucx.lambda-url.ap-northeast-1.on.aws
Document Path: /
Document Length: 26 bytes
Concurrency Level: 1
Time taken for tests: 50.102 seconds
Complete requests: 100
Failed requests: 0
Total transferred: 27800 bytes
HTML transferred: 2600 bytes
Requests per second: 2.00 [#/sec] (mean)
Time per request: 501.024 [ms] (mean)
Time per request: 501.024 [ms] (mean, across all concurrent requests)
Transfer rate: 0.54 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 50 57 4.5 56 78
Processing: 377 443 96.2 426 1307
Waiting: 377 443 96.2 426 1307
Total: 427 501 96.0 482 1359
Percentage of the requests served within a certain time (ms)
50% 482
66% 507
75% 512
80% 512
90% 534
95% 580
98% 717
99% 1359
100% 1359 (longest request)
Code language: plaintext (plaintext)
平均処理速度が501msという結果になりました。
検証2:ハンドラーメソッドの外部でDBインスタンスに接続する
同様の方法で検証を行います。
まず関数2の動作を確認します。
現在日時が返ってきました。
正常に関数が動作しています。
Apache Benchを使って、本関数の性能を評価します。
以下が実行結果です。
% ab -n 100 https://6oy3eianevtfz7xw3dopmbnnrm0ijdyr.lambda-url.ap-northeast-1.on.aws/
This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking 6oy3eianevtfz7xw3dopmbnnrm0ijdyr.lambda-url.ap-northeast-1.on.aws (be patient).....done
Server Software:
Server Hostname: 6oy3eianevtfz7xw3dopmbnnrm0ijdyr.lambda-url.ap-northeast-1.on.aws
Server Port: 443
SSL/TLS Protocol: TLSv1.2,ECDHE-RSA-AES128-GCM-SHA256,2048,128
Server Temp Key: ECDH P-256 256 bits
TLS Server Name: 6oy3eianevtfz7xw3dopmbnnrm0ijdyr.lambda-url.ap-northeast-1.on.aws
Document Path: /
Document Length: 26 bytes
Concurrency Level: 1
Time taken for tests: 11.825 seconds
Complete requests: 100
Failed requests: 0
Total transferred: 27800 bytes
HTML transferred: 2600 bytes
Requests per second: 8.46 [#/sec] (mean)
Time per request: 118.255 [ms] (mean)
Time per request: 118.255 [ms] (mean, across all concurrent requests)
Transfer rate: 2.30 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 47 57 5.1 56 82
Processing: 41 61 75.4 51 798
Waiting: 41 61 75.4 51 798
Total: 90 118 76.2 109 860
Percentage of the requests served within a certain time (ms)
50% 109
66% 113
75% 115
80% 118
90% 124
95% 137
98% 180
99% 860
100% 860 (longest request)
Code language: plaintext (plaintext)
平均処理速度が118msという結果になりました。
検証まとめ
今回の検証結果をまとめます。
いずれの指標も外部で接続した方が速度的に有利ということを示しています。
平均値を見ると、今回の検証では、外部の方が内部よりも4.2倍程度速かったということがわかりました。
まとめ
Lambda関数でRDSに接続する際に、ハンドラーメソッドの内外のどちらで接続するかによって、どの程度パフォーマンスが変化するかを確認しました。
今回の検証では、ハンドラーメソッドの外部で接続した方が、内部で接続するよりも、約4.2倍パフォーマンスが向上することがわかりました。