Aurora ServerlessのData APIがプーリングするコネクション数について調べてみた

CX事業本部@大阪の岩田です。 先日のブログでAurora ServerlessのData APIにはプロキシ型のコネクションプーリング機構があることを突き止めました。

Aurora ServerlessのData APIは裏側でコネクションプーリングを実現してくれているという話

その時の検証ではAurora自体のmax_connections180に対してData API用のコネクションが最大100まで確保されている状態でした。 この180:100という関係性は常に一定なのでしょうか?色々なパターンでData APIに並列アクセスしながらDBコネクションの状態を観察しようというのが今回の検証の趣旨になります。

環境

今回検証に利用した環境です

  • DBエンジン: Aurora PostgreSQL (compatible with PostgreSQL 10.7)
  • データベースの機能: サーバーレス
  • アイドル状態になった後の一時停止: 5分
  • マスターユーザー名: postgres
  • 検証用のデータベース(Data API用): dataapi_db
  • 検証用のDBユーザー(Data API用): dataapi_user
  • 検証用のデータベース(非Data API用): non_dataapi_db
  • 検証用のDBユーザー(非Data API用): non_dataapi_user
  • キャパシティの設定(最小):2(4GBRAM)
  • キャパシティの設定(最大):2(4GBRAM)
  • 検証用プログラムの実行環境
    • Node.js : v10.17.0
    • AWS-SDK : 2.573.0

検証1 ローカルのMacから100並列アクセス

まずローカルのMacから以下のプログラムを実行し、Data APIに100並列でアクセスしてみます。pg_sleep(5)を入れているので、前回のブログの検証結果から考えると5秒と少しで実行が完了する想定です。なおAurora Serverlessが一時停止しないよう事前にAWS CLIから1回Data APIを叩いています。

const AWS = require('aws-sdk');


const rdsData = new AWS.RDSDataService({region: 'ap-northeast-1'});
const params = {
  secretArn: 'シークレットのARN',
  resourceArn: 'DBクラスタのARN',
  sql: 'select pg_sleep(5)',
  database: 'dataapi_db'
};

const start = new Date().getTime();
const promises = [];
for(let i= 0; i < 100; i++){
    promises.push(rdsData.executeStatement(params).promise().then(()=>{
      return;
    }));
}

Promise.all(promises).then(() => {
  const end = new Date().getTime()
  console.log((end - start) / 1000);
})

イメージ的にはこんな構成です。

検証1の構成

実行してみます。

$ node data-api.js
13.062

予想に反して13秒かかりました。pg_stat_activityの状態を確認してみます。

postgres=> select datname,usename,count(*) from pg_stat_activity group by datname,usename;
  datname   |   usename    | count
------------+--------------+-------
            |              |     5
 postgres   | postgres     |     1
 dataapi_db | dataapi_user |    50
            | rdsadmin     |     1
 rdsadmin   | rdsadmin     |     3
(5 rows)

ん??前回のブログではData API用に100コネクション利用できていたのに、今回は50コネクションしか利用していないようです。検証用プログラムは100ループさせてるのでData API用に50コネクションしか使えない状況であれば10秒+αで約13秒という結果は納得です。なんで前回は100コネクション使えたのに今回は50コネクションしか使えないんでしょうか?

検証2 Lambdaから100並列アクセス

もう一度前回のブログと同じ構成を試してみます。

構成はこんなイメージです。

検証2の構成

Lambdaのコードです

import json
import boto3

rdsData = boto3.client('rds-data')

def lambda_handler(event, context):
    
    rdsData.execute_statement(
        resourceArn = <DBクラスタのARN>
        secretArn =  <シークレットのARN>
        database = 'dataapi_db', 
        sql = 'select pg_sleep(5)')
    
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

heyコマンドでAPI Gatewayに並列アクセスを行います。

hey -t 0 -n 200 -c 200 https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev

heyコマンド実行完了後のpg_stat_activityです。

postgres=> select datname,usename,count(*) from pg_stat_activity group by datname,usename;
  datname   |   usename    | count
------------+--------------+-------
            |              |     5
 postgres   | postgres     |     1
 dataapi_db | dataapi_user |   100
            | rdsadmin     |     1
 rdsadmin   | rdsadmin     |     3
(5 rows)

前回のブログと同様に100コネクション利用できています。

検証3 Lambdaから400並列アクセス

今度はheyコマンドの並列度を400に上げてみましょう。

hey -t 0 -n 400 -c 400 https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev

pg_stat_activityの状況です。

postgres=> select datname,usename,count(*) from pg_stat_activity group by datname,usename;
  datname   |   usename    | count
------------+--------------+-------
            |              |     5
 postgres   | postgres     |     1
 dataapi_db | dataapi_user |   171
            | rdsadmin     |     1
 rdsadmin   | rdsadmin     |     3
(5 rows)

Data API用のコネクションが171まで増えています。

前回のブログ執筆時はData APIからはmax_connectionsを使い切れないのかと思っていましが、そんなことは無さそうです。ある程度並列度が上がってくるとmax_connectionsの限界まで接続を確立しに行ってくれるようです。

検証4 ローカルのMacから500並列アクセス

改めて検証1と同じ構成でローカルのMacから並列アクセスを試してみます。検証用1のコードの

for(let i= 0; i < 100; i++){

の部分を

for(let i= 0; i < 500; i++){

に変更。並列度を上げて実行してみます。検証3と同様にコネクション数が171まで増えるのが期待値です。

$ node data-api.js
53.273

おや?53秒もかかってしまいました。

postgres=> select datname,usename,count(*) from pg_stat_activity group by datname,usename;
  datname   |   usename    | count
------------+--------------+-------
            |              |     5
 postgres   | postgres     |     1
 dataapi_db | dataapi_user |    50
            | rdsadmin     |     1
 rdsadmin   | rdsadmin     |     3
(5 rows)

並列度だけで比較すれば検証3よりも並列度は大きいはずですが、Data API用に50コネクションしか使えていないようです。何かしらの環境の違いに起因してコネクション数が絞られているようです。パッと思いついた環境の違いとして以下が思いつきました。

  • 接続元がLambdaか?
    • 接続元がLambdaの場合(というかAWSのサービスの場合)はNWのレイテンシが低くなるはずなので、AWS外からの接続よりは優遇されるのかなー?という予想です。
  • 並列に接続している接続元のGIPが同一か?
    • こっちが本命だと思いますが、同一の接続元に対して多くのコネクションを割り当てないように接続元GIP毎にコネクション数の上限があるのでは?という予想です。

検証5 単一のLambdda実行環境から500並列アクセス

検証4のプログラムをLambdaのプログラミングモデルに合わせて調整し、Lambdaのテストイベントから実行してみます。

const AWS = require('aws-sdk');
const rdsData = new AWS.RDSDataService({region: 'ap-northeast-1'});
const params = {
  secretArn: 'シークレットのARN',
  resourceArn: 'DBクラスタのARN',
  sql: 'select pg_sleep(5)',
  database: 'dataapi_db'
};


exports.handler = async (event) => {
    const start = new Date().getTime();
    const promises = [];
    for(let i= 0; i < 500; i++){
        promises.push(rdsData.executeStatement(params).promise().then(()=>{
          return;
        }));
    }
    
    return Promise.all(promises).then(() => {
      const end = new Date().getTime()
      console.log((end - start) / 1000);
      const response = {
        statusCode: 200,
        body: JSON.stringify('Hello from Lambda!'),
        };
        return response;
    });

};

検証2とは異なりLambda実行環境1つからの並列アクセスです。Data APIから見た接続元IPは全て同一になるはずです。実行後のpg_stat_activityの状態です。

postgres=> select datname,usename,count(*) from pg_stat_activity group by datname,usename;
  datname   |   usename    | count
------------+--------------+-------
            |              |     5
 dataapi_db | dataapi_user |    50
 postgres   | postgres     |     1
            | rdsadmin     |     1
 rdsadmin   | rdsadmin     |     3
(5 rows)

500並列で実行しましたが、Data API用のコネクションは50までしか使えませんでした。検証3が171コネクションまで伸びたのは接続元がLambdaだからという訳では無く、接続元のIPが分散していたことが原因という説が濃厚そうです。

検証6 通常のDB接続が多数確立している状態でData APIを叩いてみる

最後にこれまでとは別のパターンとして、通常のDB接続を多数確立した状態でData APIを叩いてみます。こんなイメージです。

検証6の構成

Auroraと同一VPC内に立てたEC2から以下のプログラムを実行して事前に非Data APIのコネクションを多数確立しておきます。

const { Client } = require('pg')


for(let i= 0; i < 170; i++){
    const client = new Client({
      host: '<Auroraのエンドポイント>',
      port: 5432,
      user: 'non_dataapi_user',
      password: '<パスワード>',
      database: 'non_dataapi_db'
    })
    client.connect();
    client.query('SELECT pg_sleep(60)').then(()=>{
      client.end();
    })
}

受け入れ可能なコネクション数のキャパが不足している状態に検証1で利用したプログラムを流して新たにData APIの実行を要求し、どういう挙動になるか観察します。

非Data APIから170並列で接続

pg_stat_activityの状況です。

postgres=> select datname,usename,count(*) from pg_stat_activity group by datname,usename;
    datname     |     usename      | count
----------------+------------------+-------
                |                  |     5
 non_dataapi_db | non_dataapi_user |   170
 dataapi_db     | dataapi_user     |     1
 postgres       | postgres         |     1
                | rdsadmin         |     1
 rdsadmin       | rdsadmin         |     3
(6 rows)

Data APIを利用する検証用のプログラムは以下のようなエラーを吐きました。

(node:2349) UnhandledPromiseRejectionWarning: StatementTimeoutException: null
    at Object.extractError (/Users/xxxxxxxxxx/node_modules/aws-sdk/lib/protocol/json.js:51:27)
    at Request.extractError (/Users/xxxxxxxxxx/node_modules/aws-sdk/lib/protocol/rest_json.js:55:8)
    at Request.callListeners (/Users/xxxxxxxxxx/node_modules/aws-sdk/lib/sequential_executor.js:106:20)
    at Request.emit (/Users/xxxxxxxxxx/node_modules/aws-sdk/lib/sequential_executor.js:78:10)
    at Request.emit (/Users/xxxxxxxxxx/node_modules/aws-sdk/lib/request.js:683:14)
    at Request.transition (/Users/xxxxxxxxxx/node_modules/aws-sdk/lib/request.js:22:10)
    at AcceptorStateMachine.runTo (/Users/xxxxxxxxxx/node_modules/aws-sdk/lib/state_machine.js:14:12)
    at /Users/xxxxxxxxxx/node_modules/aws-sdk/lib/state_machine.js:26:10
    at Request.<anonymous> (/Users/xxxxxxxxxx/node_modules/aws-sdk/lib/request.js:38:9)
    at Request.<anonymous> (/Users/xxxxxxxxxx/node_modules/aws-sdk/lib/request.js:685:12)
(node:2349) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:2349) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

Cloud Watch Logsを確認すると、最大同時接続数超過のエラーが多数出ていました。さすがに攻めすぎたようです。。。

Data API実行時の最大同時接続数超過エラー

非Data APIから130並列で接続

検証用プログラムの

for(let i= 0; i < 170; i++){

for(let i= 0; i < 130; i++){

に書き換えて非Data APIのコネクション数を減らして再チャレンジしてみます。Data APIからは41コネクション利用できる想定です。Data APIの基盤側が非Data APIのコネクション数を考慮せず50コネクション繋ぎにいくとエラーになる想定です。果たしてどうなるのでしょうか。

$ node data-api.js
16.946

エラー無しで終了しました。

pg_stat_activityの状況です。

postgres=> select datname,usename,count(*) from pg_stat_activity group by datname,usename;
  datname   |   usename    | count
------------+--------------+-------
            |              |     5
 dataapi_db | dataapi_user |    41
 postgres   | postgres     |     1
            | rdsadmin     |     1
 rdsadmin   | rdsadmin     |     3
(5 rows)

Cloud Watch Logsに関してもFATALという文字列の検索はヒットしませんでした。以上の結果から考察すると、Data APIの基盤は非Data APIによる接続状況も鑑みてうまくData API用のコネクション数を調整してくれているようです。

まとめ

Data APIがプーリングするコネクション数は基盤側が良い感じに制御してくれていることが分かりました!! ただし、非Data APIから限界近くまでコネクションを張った状態でData APIを利用すると最大同時接続数超過のエラーが出ることはあります。普通やらないとは思いますが一応ご注意下さい。