DVA

Lambdaハンドラーメソッドの外側でRDSに接続してパフォーマンス向上

スポンサーリンク
Lambdaハンドラーメソッドの外側でRDSに接続してパフォーマンス向上 DVA
スポンサーリンク
スポンサーリンク

Lambda関数のハンドラーメソッドの内外でRDSに接続してパフォーマンスを測定する

AWS DVAの出題範囲であるリファクタリングに関する内容です。

Lambda関数からRDSに接続する際のベストプラクティスの1つは、ハンドラーメソッドの外部でRDSに接続することです。

関数ハンドラーメソッドの外部で宣言されたオブジェクトは、初期化されたままとなり、関数が再度呼び出されると追加の最適化を提供します。例えば、Lambda 関数がデータベース接続を確立する場合、連続した呼び出しでは接続を再確立する代わりに元の接続が使用されます。

AWS Lambda 実行環境

本ページでは、2つのLambda関数を作成し、それぞれハンドラーメソッドの内部・外部でRDSに接続することで、どの程度パフォーマンスが向上するか測定します。

構築する環境

AWS認定アソシエイト3資格対策~ソリューションアーキテクト、デベロッパー、SysOpsアドミニストレーター~ | 平山毅, 堀内康弘, 福垣内孝造, 岡智也, 池田大, 原江梨佳, 澤田拓也, 原俊太郎, 仲村勇亮, 上村祐輝, 鳥谷部昭寛 | 工学 | Kindleストア | Amazon
Amazonで平山毅, 堀内康弘, 福垣内孝造, 岡智也, 池田大, 原江梨佳, 澤田拓也, 原俊太郎, 仲村勇亮, 上村祐輝, 鳥谷部昭寛のAWS認定アソシエイト3資格対策~ソリューションアーキテクト、デベロッパー、SysOpsアドミニストレーター~。アマゾンならポイント還元本が多数。一度購入いただいた電子書籍は、K...
Diagram of connect to RDS outside of Lambda handler method to improve performance.

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テンプレートを配置しています。

awstut-dva/04/001 at main · awstut-an-r/awstut-dva
Contribute to awstut-an-r/awstut-dva development by creating an account on GitHub.

テンプレートファイルのポイント解説

本ページでは、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にアクセスし、同関数を実行します。

The result of Lambda Function 3 invocation.

テーブルが作成されて、現在保存されている全データが返ってきました。
初期化処理は正常に完了です。

検証1:ハンドラーメソッドの内部でDBインスタンスに接続する

準備が整いましたので、検証を開始します。
一応、関数1の動作を確認します。

The result of Lambda Function 1 invocation.

現在日時が返ってきました。
正常に関数が動作しています。

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の動作を確認します。

The result of Lambda Function 2 invocation.

現在日時が返ってきました。
正常に関数が動作しています。

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という結果になりました。

検証まとめ

今回の検証結果をまとめます。

Speed Comparison Results

いずれの指標も外部で接続した方が速度的に有利ということを示しています。
平均値を見ると、今回の検証では、外部の方が内部よりも4.2倍程度速かったということがわかりました。

まとめ

Lambda関数でRDSに接続する際に、ハンドラーメソッドの内外のどちらで接続するかによって、どの程度パフォーマンスが変化するかを確認しました。
今回の検証では、ハンドラーメソッドの外部で接続した方が、内部で接続するよりも、約4.2倍パフォーマンスが向上することがわかりました。

タイトルとURLをコピーしました