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

目次

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

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

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

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

AWS Lambda 実行環境

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

構築する環境

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

https://github.com/awstut-an-r/awstut-dva/tree/main/04/001

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

本ページでは、Lambda関数内のDBインスタンスに接続方法に関する内容を中心に取り上げます。

Lambda関数からRDS Proxy経由でRDSに接続する方法については、以下のページをご確認ください。

あわせて読みたい
VPC内のLambdaからRDS Proxy経由でRDSに接続する 【VPC内のLambdaからRDS Proxy経由でRDSに接続する】 VPC内にLambdaを配置し、RDSに接続する構成を考えます。LambdaからRDSにアクセスする際は、直接接続するのではなく...

MySQLタイプのDBインスタンスに接続するために、mysql-connector-pythonを使用します。
Lambdaレイヤーを作成し、同パッケージをこれに配置します。
Lambdaレイヤーの詳細については、以下のページをご確認ください。

あわせて読みたい
CFNでLambdaレイヤー作成 【CloudFormationでLambdaレイヤー作成】 本ページでは、CloudFormationでLambdaレイヤーを作成する方法を確認します。 Lambda レイヤーは、Lambda 関数で使用できるラ...

作成したLambda関数はFunction URLを使用して実行します。
Function URLに関する詳細は、以下のページをご参照ください。

あわせて読みたい
CFNでLambda Function URL – 認証方式: NONE 【CloudFormationでLambda Function URLを作成する(NONEバージョン)】 2022年4月22日にLambda Function URLがリリースされました。 この新機能は、AWS Lambda サービス...

ハンドラーメソッドの内部で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でLambdaを作成する3パータン(S3/インライン/コンテナ) 【CloudFormationでLambdaを作成する】 CloudFormationでLambdaを作成する場合、大別すると以下の3パターンあります。 S3バケットにコードをアップロードする インライ...

CloudFormationスタックを作成し、スタック内のリソースを確認する

CloudFormationスタックを作成します。スタックの作成および各スタックの確認方法については、以下のページをご確認ください。

あわせて読みたい
CloudFormationのネストされたスタックで環境を構築する 【CloudFormationのネストされたスタックで環境を構築する方法】 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倍パフォーマンスが向上することがわかりました。

目次