RDS
lambda

AWS LambdaでAmazon RDS for MySQLに接続した場合のコネクションを使い回す

More than 1 year has passed since last update.

目的

なぜAWS LambdaとRDBMSの相性が悪いかを簡単に説明する
にもありますとおりで、Lambda+RDSはアンチパターン - Qiita
なのですが、いろいろな諸事情でVPC内のRDSにアクセスしないとならないことがございます。
Lambdaのコードで、ハンドラーの外でDB接続処理を行うことでLambda実行コンテナが使いまわされている間はコネクションも使いまわせるとのことで試してみました。

参考にしたサイト

以下の記事を参考にさせていただきました。
MySQL DBのコネクション数の確認とか - Qiita
AWS LambdaでRDS(MySQL)に接続してみた - Qiita
AWS LambdaでAmazon RDS for MySQLへ接続する(Node.js 4.3 + KMSで暗号化したMySQL接続パスワードをkms.decryptで復号化してMySQLヘ接続 + バッチ実行をSNS通知する) - Qiita

環境

インターネット接続のないVPC(vpc-11111111, CIDR:172.30.0.0/16)
サブネット2つ(subnet-22222222と33333333, CIDR:172.30.0.0/24, 172.30.1.0/24)
これをRDSとLambdaで共用。
RDS for MySQL 5.7.17
Lambda Node.js 6.10,VPC内,メモリ128MB

Lambda用ロール

ENIが作成できればいいので最低限のものを使用します。
マネージドポリシー AWSLambdaENIManagementAccess をアタッチ。

AWSLambdaENIManagementAccess
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ec2:CreateNetworkInterface",
        "ec2:DescribeNetworkInterfaces",
        "ec2:DeleteNetworkInterface"
      ],
      "Resource": "*"
    }
  ]
}

CloudWatchにログを作る場合は以下も追加します。

LoggingPolicy
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    }
  ]
}

Lambda関数コード

MySQLを使うのでnpm install mysqlしてnode_moduleフォルダごとZIPしてアップロード。

index.js
var mysql = require('mysql');

// 接続先のMySQLサーバ情報
var mysql_host = "<RDSのエンドポイント名>";
var mysql_user = "<ユーザー名>";
var mysql_dbname = "<DB名>";
var mysql_password = "<パスワード>";

var connection = null;

function createSingleConnection() {
    connection = mysql.createConnection({
        host     : mysql_host,
        user     : mysql_user,
        password : mysql_password,
        database : mysql_dbname
    });

    connection.on('error', (err) => {
        if (err.code === 'PROTOCOL_CONNECTION_LOST') {
            // サーバがコネクションを切った場合は再接続
            createSingleConnection();
            console.log(`Reconnected`);
        } else {
            throw err;
        }
    });
}

// MySQLデータベースへの接続 ここで接続することでコネクションを使いまわせる
createSingleConnection();

exports.handler = function(event, context){
    // 実行するSQL文
    var sql ="SELECT * FROM information_schema.PROCESSLIST";

    console.log("MySQL Server Name: " + mysql_host);
    console.log("MySQL User Name: " + mysql_user);
    console.log("MySQL Database Name: " + mysql_dbname);
    console.log("MySQL Exec SQL: " + sql);

    // MySQLデータベースでSQL実行
    connection.query(sql, function(err, rows, fields) {
        if (err) {
            console.log("MySQL Select Error");
            context.fail(err);
            throw err;
        } else {
            console.log("MySQL Select Success");
            console.log(rows);
        }
        context.done();
    });

    console.log('end');
};

Lambdaの実行時間は、
コールドスタート(初回実行):300ms程度
ウォームスタート(コンテナ使い回し): 10ms以下
となりました。
まだコンテナが生きている状態でRDS側を再起動してセッションを切っても10ms程度で再接続してクエリの実行に成功していました。

※なお、上記コードで「Reconnect」を標準出力していますが、この時のログは、1つ前のRequestIDで出力されていました。興味深い動きだなと思いました。

コールドスタートの場合、「ENI作成~コンテナの作成~モジュールロード」などがあるため、結果が戻ってくるまでかなりの時間を要します。
正確に計測していませんが体感で30秒近くかかっていました。

結果

DBConns.JPG
14:30頃に振動しているのは、ハンドラー内で接続~切断させてみたときの結果です。
14:40頃に実行したのを最後に放置して、15:55頃に再度実行しています。
大体30分ぐらいはLambdaのコンテナが生きていて、その間は再実行してもコネクションが増えず、使いまわせている様子。

感想とか今後

お試しのためアクセス数が少なく、Lambdaのコンテナがスケールした場合を見ていないため、これがプロダクション環境でも問題ないかは負荷テストなどを通して検証する必要があります。

また、Lambdaを使う以上、コールドスタートは避けられないため、別途”暖機運転”(定期的に実行する)はしつつも、実行時間に不都合あればメモリサイズを大きくするなどの工夫が必要と思われます。
※Lambdaが実行されるインスタンスサイズは設定したメモリ量に比例するらしく、EC2同様に大きければ大きいほどマシン性能があがる、らしいです。
※VPC内でLambdaを実行する場合は、コールドスタート時にどうしてもENI作成~アタッチが必要になるため、これに10秒は必要だそう。今後の改善に期待。

負荷テスト後に新たなことがわかれば記事更新しようと思います。

結局のところ

RDSを使わなくて良い環境では当然DynamoDBを使いましょう。。