姿勢矯正ベルトで背筋を伸ばしながら書いています。ゲームデザイン(GD)部クライアントエンジニアの中村です。
最近、個人的にAPI Gateway+Lambdaを利用したサーバーレス環境でのサービス展開を模索する中で「LambdaとRDSは相性が悪い」という記事をよく見かけ、「なんだそうなのかー。仕方ないからDynamoDB使おう」程度に軽く考えていました。しかしながら、最近 AWS Certified Database – Specialty 取得のための学習を行っていたところ RDS Proxy というサービスに気づきました。これを利用することでLambda+RDSが利用できるようになるとのことだったので、そもそもの相性の悪さとその解決策まで、実際に利用して体験しました。
LambdaとRDSの相性の悪さ
LambdaとRDSの相性の悪さはデータベースコネクション数の最大値にあります。Lambdaはその特性上、リクエストに応じて大量にコンテナが実行されます。このとき起動されるコンテナの数を制御できないため、大量のコンテナからデータベースへのコネクションが発生すると上限を超えてしまう事態が発生するようです。
単純にデータベースコネクションの最大数を上げれば解決できそうですが、データベースが受け入れるコネクション数を増やすとそれに応じて使用メモリも増えていきます。そのため、AWS RDSではすでにチューニングされたデフォルト値が用意されています。この数値を変更することは推奨されておらず、コネクション数を増やすのであればインスタンススペックを上げる必要があります。
RubyOnRailsやLaravelなどのフレームワークではデータベースコネクションをプーリングして使いまわすことができるように設計されていますが、Lambdaでは実行コンテナごとに処理が独立しているためそれが行えません。大規模なリクエストに対して発生するデータベースコネクションをコントロールできないため、データベースインスタンスのスペックを上げても対応しきれない可能性があります。
今回はこの問題を体験するに当たって、以下のような構成を作成しました。
- Pythonで記述したシンプルなSELECTコード
- IAMロールでのDB認証
- RDSからdb.t2.microインスタンスにMySQLを起動
- menusテーブルにダミーデータとして定食屋のメニューが含まれる(このデータをSELECTする)
- locustから100ユーザによる合計10,000のアクセスを生成する
session = boto3.Session()
client = session.client('rds')
token = client.generate_db_auth_token(DBHostname=ENDPOINT, Port=PORT, DBUsername=USER, Region=REGION)
def lambda_handler(event, context):
statusCode = 200
connection = None
try:
connection = pymysql.connect(
host=ENDPOINT,
user=USER,
passwd=token,
port=PORT,
database=DB_NAME,
charset='utf8mb4',
ssl={'ca': CA_FILE, 'check_hostname': False}
)
with connection.cursor() as cursor:
cursor.execute("SELECT * FROM menus")
query_results=cursor.fetchall()
except Exception as e:
print(e)
statusCode = 500
finally:
if connection is not None:
connection.close()
id | name | price | image_prefix | takeout |
1 | 目玉焼き定食 | 500 | menu_med | 0 |
2 | 唐揚げ定食 | 750 | menu_kar | 0 |
from locust import task, SequentialTaskSet
class LocustTask(SequentialTaskSet):
def on_start(self):
self.user.reset()
@task
def simple_get(self):
request_params = {
}
self.client.get(
url="/",
params=request_params,
headers=self.user.request_headers(),
name=f"simple_get [GET /] "
)
@task
def end_task(self):
self.interrupt()
RDS Proxyを利用しない場合
直接データベースにコネクションを生成するのでコネクションに関する問題が発生する可能性があります。
今回使用しているデータベースインスタンスdb.t2.microは最大コネクション数が65 (推奨値 DBInstanceClassMemory/12,582,880)となっているため、65以上のコネクションを生成した際に問題が発生します。
mysql> show variables like 'max_connections';
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| max_connections | 65 |
+-----------------+-------+
これを確認するためにlocustから100ユーザーによる同時リクエストを行いました。
その結果、locust上では10,000件のリクエストの内3,500件のエラーが発生しました。 lambda_handler
内でtry-catch
した際のstatusCode=500
を応答しているため、コネクション生成時かクエリ実行時にエラーが出ていることがわかります。
実際にCloudWatchには以下のようなToo many connections
エラーが確認でき、コネクションの生成数が多すぎることが分かります。
2022-02-24T13:59:25.286+09:00 START RequestId: a17a17d2-1a12-4ce9-a5ce-ae7f7bb7eca4 Version: $LATEST
2022-02-24T13:59:25.740+09:00 (1040, 'Too many connections')
また、CloudWatchメトリクスでデータベースコネクション数を確認すると、上限の65を超えて75に達しているようです。
以上の結果から、LambdaとRDSに対するデータベースコネクションの相性の悪さが確認できました。
次にRDS Proxyを利用していきます。
RDS Proxyを利用する場合
RDS Proxyを利用する場合は以下のような構成図になります。
RDS Proxy作成
まずはRDS Proxyを作成します。RDSページのメニューから[プロキシ]を選択して作成メニューを表示します。
RDS Proxyは基本的にはデフォルト設定で、IAM認証を利用するためTLSが必要となります。
プロキシのターゲットは事前に作成しておいたproxy-targetデータベースです。接続プールの最大接続数は70%にしておきます。これでこのプロキシを経由するとproxy-targetデータベースにはコネクション最大数の70%までしか接続されないことになります。残りの30%は別のシステムからアクセスされる可能性を考慮しておきます。
プロキシからデータベースへの接続にはUser/Password認証を利用します。このときの認証情報はPlainTextではなくSecrectsManagerで保護されている必要があります。今回はキーローテーションなしでSecretsManagerを利用しています。さらにクライアントからプロキシへの認証はIAM認証を必須としています。
以上でプロキシの作成は完了です。プロキシが作成されると以下のような「プロキシエンドポイント」が利用できるようになります。
experiment-db-proxy.proxy-xxxxxxxxxxxx.us-east-1.rds.amazonaws.com
LambdaにRDS Proxyを導入
LambdaからRDS Proxyへの接続についてはデータベースエンドポイント用の環境変数ENDPOINT
をプロキシエンドポイントに切り替え、pymysqlが利用するssl認証ファイルの環境変数CA_FILE
をRDS Proxy用のものへ変更します。もともとIAM認証を導入しているシステムであればプロキシを利用するように簡単に切り替えることができます。
環境変数を切り替えてLambdaを実行したところデータベースから定食屋のメニューのレスポンスがあったので問題なく接続できているようです。
START RequestId: fcc8ae58-e350-4d84-b641-32e91760ab24 Version: $LATEST
Response
{
"statusCode": 200,
"body": [
[ 1, "目玉焼き定食", 500, "menu_med", 0 ],
[ 2, "唐揚げ定食", 750, "menu_kar", 0 ],
[ 3, "カツ丼", 650, "menu_kat", 1 ],
[ 4, "ラーメン", 600, "menu_ram", 1 ],
[ 5, "チャーハン", 400, "menu_cha", 1 ],
[ 6, "生姜焼き定食", 800, "menu_sho", 1 ],
[ 7, "うどん", 300, "menu_udo", 1 ],
[ 8, "牛丼", 380, "menu_gyu", 1 ],
[ 9, "カレーライス", 450, "menu_cur", 1 ]
]
}
それではこの状態でlocustから100ユーザーによる同時リクエストを実行します。その結果、10,000リクエスト中にエラーは発生しませんでした。代わりに若干平均応答時間が遅くなっているようです。
Lambdaの実行ログでもエラーは一切確認できませんでした。
2022-02-24T14:37:11.269+09:00 START RequestId: 273371cb-aa3e-4e31-a5f2-e8e1412e0c49 Version: $LATEST
2022-02-24T14:37:11.272+09:00 lambda_handler
2022-02-24T14:37:11.879+09:00 END Requ2022-02-24T14:37:11.269+09:00
CloudWatchメトリクスからデータベースコネクションを確認すると、RDS Proxyに対するコネクションは65に対して、データベースに対するコネクションはわずか8となりました。Too many Connections
が発生するほどのコネクションをRDS Proxyが吸収し、コネクションを使いまわしている状況を確認することができました。
まとめ
今回はLambdaからRDSを利用する際の問題点を確認しました。実際にToo many connections
が表示され、データベースのコネクション問題を見ることができました。さらにその解決策としてRDS Proxyを利用してデータベースコネクションが使い回されている状況を見ることができました。
サーバーレスアプリケーションでLambdaを利用することがあり、その環境でRDSが必要となるならばRDS Proxyも必須となるでしょう。プロジェクトにもぜひ利用していきたいと思います。