その他
    ホーム エンジニア AWS AWS RDS ProxyとLambdaを連携してみた
    AWS RDS ProxyとLambdaを連携してみた
     

    AWS RDS ProxyとLambdaを連携してみた

    姿勢矯正ベルトで背筋を伸ばしながら書いています。ゲームデザイン(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()
    idnamepriceimage_prefixtakeout
    1目玉焼き定食500menu_med0
    2唐揚げ定食750menu_kar0
    ダミーデータ

    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 を応答しているため、コネクション生成時かクエリ実行時にエラーが出ていることがわかります。

    locust実行結果-RDS Proxyなし

    実際に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に達しているようです。

    データベースコネクション数-RDS Proxyなし

    以上の結果から、LambdaとRDSに対するデータベースコネクションの相性の悪さが確認できました。

    次にRDS Proxyを利用していきます。

    RDS Proxyを利用する場合

    RDS Proxyを利用する場合は以下のような構成図になります。

    RDS Proxy作成

    まずはRDS Proxyを作成します。RDSページのメニューから[プロキシ]を選択して作成メニューを表示します。

    RDS Proxyは基本的にはデフォルト設定で、IAM認証を利用するためTLSが必要となります。

    RDS Proxy プロキシ設定

    プロキシのターゲットは事前に作成しておいた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リクエスト中にエラーは発生しませんでした。代わりに若干平均応答時間が遅くなっているようです。

    locust実行結果-RDS Proxyあり

    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が吸収し、コネクションを使いまわしている状況を確認することができました。

    データベースコネクション数-RDS Proxyあり

    まとめ

    今回はLambdaからRDSを利用する際の問題点を確認しました。実際にToo many connectionsが表示され、データベースのコネクション問題を見ることができました。さらにその解決策としてRDS Proxyを利用してデータベースコネクションが使い回されている状況を見ることができました。

    サーバーレスアプリケーションでLambdaを利用することがあり、その環境でRDSが必要となるならばRDS Proxyも必須となるでしょう。プロジェクトにもぜひ利用していきたいと思います。

    記事を共有