まず前提として
2020/07/01にRDS ProxyがGAとなりました。(2019/12/03にプレビュー;2020/4/8にPostgreSQLも対応; 東京も使えます。プレビューからGAまで半年ちょっとと予想通りのタイミング。)
https://aws.amazon.com/jp/blogs/aws/amazon-rds-proxy-now-generally-available/
MySQLまたはAurora、PostgreSQLを利用するシーンでは、RDS Proxyを視野に入れながら検討するのが良いでしょう。
コールドスタート対策にはProvisioned Concurrencyを検討しましょう。
(2020/06/12追記)クラスメソッドさんの記事も大変参考になります。
LambdaからRDS/RDBを利用する際に意識したいポイント5選 | Developers.IO
目的
なぜ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 をアタッチ。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:CreateNetworkInterface",
"ec2:DescribeNetworkInterfaces",
"ec2:DeleteNetworkInterface"
],
"Resource": "*"
}
]
}
CloudWatchにログを作る場合は以下も追加します。
{
"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してアップロード。
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秒近くかかっていました。
(2019/07/26修正)Lambdaのアーキテクチャが変更され、コールドスタートでも1秒程度まで改善しています。素晴らしい。
結果
14:30頃に振動しているのは、ハンドラー内で接続~切断させてみたときの結果です。
14:40頃に実行したのを最後に放置して、15:55頃に再度実行しています。
大体30分ぐらいはLambdaのコンテナが生きていて、その間は再実行してもコネクションが増えず、使いまわせている様子。
感想とか今後
お試しのためアクセス数が少なく、Lambdaのコンテナがスケールした場合を見ていないため、これがプロダクション環境でも問題ないかは負荷テストなどを通して検証する必要があります。
また、Lambdaを使う以上、コールドスタートは避けられないため、別途”暖機運転”(定期的に実行する)はしつつも、実行時間に不都合あればメモリサイズを大きくするなどの工夫が必要と思われます。
※Lambdaが実行されるインスタンスサイズは設定したメモリ量に比例するらしく、EC2同様に大きければ大きいほどマシン性能があがる、らしいです。
※2019/12/03にProvisioned Concurrencyが発表されました。もちろんコストが追加でかかってきますので、コールドスタートによるレイテンシ増大が与えるビジネスへの影響を鑑みて。
※VPC内でLambdaを実行する場合は、コールドスタート時にどうしてもENI作成~アタッチが必要になるため、これに10秒は必要だそう。今後の改善に期待。 ←改善してました!(2019/07/26更新)
負荷テスト後に新たなことがわかれば記事更新しようと思います。
結局のところ
都度接続で問題ない処理では都度接続を選択しましょう。
RDSを使わなくて良い環境では当然DynamoDBを使いましょう。。