この投稿では、Node.jsとJavaドライバーの両方を使用してAWSLambdaでMongoDB接続プールを使用する方法を紹介します。
AWS Lambdaとは何ですか?
AWS Lambda は、アマゾンウェブサービスが提供するイベント駆動型のサーバーレスコンピューティングサービスです。 。 EC2インスタンスとは異なり、ユーザーは管理タスクなしでコードを実行できます。 サーバーのプロビジョニング、スケーリング、高可用性などをユーザーが担当します。代わりに、コードをアップロードしてイベントトリガーを設定するだけで、AWSLambdaが他のすべてを自動的に処理します。
AWS Lambdaは、 Node.jsを含むさまざまなランタイムをサポートしています 、 Python 、 Java 、および移動 。 S3などのAWSサービスによって直接トリガーできます 、 DynamoDB 、キネシス 、 SNS 、など。この例では、AWSAPIゲートウェイを使用してLambda関数をトリガーします。
接続プールとは何ですか?
データベース接続の開閉は、CPU時間とメモリの両方を伴うため、コストのかかる操作です。アプリケーションがすべての操作でデータベース接続を開く必要がある場合、パフォーマンスに深刻な影響を及ぼします。
キャッシュに保持されているデータベース接続がたくさんある場合はどうなりますか?アプリケーションがデータベース操作を実行する必要があるときはいつでも、キャッシュから接続を借用し、必要な操作を実行して、それを返すことができます。このアプローチを使用することで、毎回新しい接続を確立するために必要な時間を節約し、接続を再利用できます。このキャッシュは、接続プールと呼ばれます 。
接続プールのサイズは、ほとんどのMongoDBドライバーで構成可能であり、デフォルトのプールサイズはドライバーごとに異なります。たとえば、Node.jsドライバーでは5ですが、Javaドライバーでは100です。接続プールのサイズによって、ドライバーが特定の時間に処理できる並列要求の最大数が決まります。接続プールの制限に達すると、既存の要求が完了するまで待機する新しい要求が作成されます。したがって、達成するアプリケーションの負荷と同時実行性を考慮して、プールサイズを慎重に選択する必要があります。
AWSLambdaのMongoDB接続プール
この投稿では、MongoDB用のNode.jsとJavaドライバーの両方を含む例を紹介します。このチュートリアルでは、AWSEC2インスタンスを使用してScaleGridでホストされているMongoDBを使用します。セットアップには5分もかからず、30日間の無料トライアルを作成できます。
Node.jsとLambdaドライバーを使用してAWSLambdaで#MongoDB接続プールを使用する方法クリックしてツイート
JavaドライバーMongoDB接続プール
AWSLambdaハンドラー関数のJavaドライバーを使用してMongoDB接続プールを有効にするコードは次のとおりです。
public class LambdaFunctionHandler
implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
private MongoClient sgMongoClient;
private String sgMongoClusterURI;
private String sgMongoDbName;
@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent();
response.setStatusCode(200);
try {
context.getLogger().log("Input: " + new Gson().toJson(input));
init(context);
String body = getLastAlert(input, context);
context.getLogger().log("Result body: " + body);
response.setBody(body);
} catch (Exception e) {
response.setBody(e.getLocalizedMessage());
response.setStatusCode(500);
}
return response;
}
private MongoDatabase getDbConnection(String dbName, Context context) {
if (sgMongoClient == null) {
context.getLogger().log("Initializing new connection");
MongoClientOptions.Builder destDboptions = MongoClientOptions.builder();
destDboptions.socketKeepAlive(true);
sgMongoClient = new MongoClient(new MongoClientURI(sgMongoClusterURI, destDboptions));
return sgMongoClient.getDatabase(dbName);
}
context.getLogger().log("Reusing existing connection");
return sgMongoClient.getDatabase(dbName);
}
private String getLastAlert(APIGatewayProxyRequestEvent input, Context context) {
String userId = input.getPathParameters().get("userId");
MongoDatabase db = getDbConnection(sgMongoDbName, context);
MongoCollection coll = db.getCollection("useralerts");
Bson query = new Document("userId", Integer.parseInt(userId));
Object result = coll.find(query).sort(Sorts.descending("$natural")).limit(1).first();
context.getLogger().log("Result: " + result);
return new Gson().toJson(result);
}
private void init(Context context) {
sgMongoClusterURI = System.getenv("SCALEGRID_MONGO_CLUSTER_URI");
sgMongoDbName = System.getenv("SCALEGRID_MONGO_DB_NAME");
}
}
ここでは、 sgMongoClientを宣言することで接続プールを実現しています。 ハンドラー関数外の変数。ハンドラーメソッドの外部で宣言された変数は、同じコンテナーが再利用される限り、呼び出し間で初期化されたままになります。これは、AWSLambdaでサポートされている他のプログラミング言語にも当てはまります。
Node.jsドライバーMongoDB接続プール
Node.jsドライバーの場合、グローバルスコープで接続変数を宣言することでもうまくいきます。ただし、接続プールができない特別な設定があります。そのパラメータはcallbackWaitsForEmptyEventLoop これはLambdaのコンテキストオブジェクトに属しています。このプロパティをfalseに設定すると、AWSLambdaはプロセスと状態データをフリーズします。これは、イベントループにイベントがある場合でも、コールバックが呼び出された直後に実行されます。
AWSLambdaハンドラー関数のNode.jsドライバーを使用してMongoDB接続プールを有効にするコードは次のとおりです。
'use strict'
var MongoClient = require('mongodb').MongoClient;
let mongoDbConnectionPool = null;
let scalegridMongoURI = null;
let scalegridMongoDbName = null;
exports.handler = (event, context, callback) => {
console.log('Received event:', JSON.stringify(event));
console.log('remaining time =', context.getRemainingTimeInMillis());
console.log('functionName =', context.functionName);
console.log('AWSrequestID =', context.awsRequestId);
console.log('logGroupName =', context.logGroupName);
console.log('logStreamName =', context.logStreamName);
console.log('clientContext =', context.clientContext);
// This freezes node event loop when callback is invoked
context.callbackWaitsForEmptyEventLoop = false;
var mongoURIFromEnv = process.env['SCALEGRID_MONGO_CLUSTER_URI'];
var mongoDbNameFromEnv = process.env['SCALEGRID_MONGO_DB_NAME'];
if(!scalegridMongoURI) {
if(mongoURIFromEnv){
scalegridMongoURI = mongoURIFromEnv;
} else {
var errMsg = 'Scalegrid MongoDB cluster URI is not specified.';
console.log(errMsg);
var errResponse = prepareResponse(null, errMsg);
return callback(errResponse);
}
}
if(!scalegridMongoDbName) {
if(mongoDbNameFromEnv) {
scalegridMongoDbName = mongoDbNameFromEnv;
} else {
var errMsg = 'Scalegrid MongoDB name not specified.';
console.log(errMsg);
var errResponse = prepareResponse(null, errMsg);
return callback(errResponse);
}
}
handleEvent(event, context, callback);
};
function getMongoDbConnection(uri) {
if (mongoDbConnectionPool && mongoDbConnectionPool.isConnected(scalegridMongoDbName)) {
console.log('Reusing the connection from pool');
return Promise.resolve(mongoDbConnectionPool.db(scalegridMongoDbName));
}
console.log('Init the new connection pool');
return MongoClient.connect(uri, { poolSize: 10 })
.then(dbConnPool => {
mongoDbConnectionPool = dbConnPool;
return mongoDbConnectionPool.db(scalegridMongoDbName);
});
}
function handleEvent(event, context, callback) {
getMongoDbConnection(scalegridMongoURI)
.then(dbConn => {
console.log('retrieving userId from event.pathParameters');
var userId = event.pathParameters.userId;
getAlertForUser(dbConn, userId, context);
})
.then(response => {
console.log('getAlertForUser response: ', response);
callback(null, response);
})
.catch(err => {
console.log('=> an error occurred: ', err);
callback(prepareResponse(null, err));
});
}
function getAlertForUser(dbConn, userId, context) {
return dbConn.collection('useralerts').find({'userId': userId}).sort({$natural:1}).limit(1)
.toArray()
.then(docs => { return prepareResponse(docs, null);})
.catch(err => { return prepareResponse(null, err); });
}
function prepareResponse(result, err) {
if(err) {
return { statusCode:500, body: err };
} else {
return { statusCode:200, body: result };
}
}
AWSLambda接続プールの分析と観察
接続プールの使用のパフォーマンスと最適化を検証するために、Java関数とNode.jsLambda関数の両方に対していくつかのテストを実行しました。 AWS APIゲートウェイをトリガーとして使用して、反復ごとに50リクエストのバーストで関数を呼び出し、各反復でのリクエストの平均応答時間を決定しました。このテストは、最初は接続プールを使用せずに、後で接続プールを使用してLambda関数に対して繰り返されました。
上のグラフは、各反復でのリクエストの平均応答時間を表しています。ここで、データベース操作の実行に接続プールを使用した場合の応答時間の違いを確認できます。接続プールを使用した場合の応答時間は、接続プールが一度初期化され、データベース操作ごとに接続を開いたり閉じたりする代わりに接続を再利用するため、大幅に短縮されます。
Java関数とNode.jsLambda関数の唯一の注目すべき違いは、コールドスタート時間です。
コールドスタート時間とは何ですか?
コールドスタート時間とは、AWSLambda関数が初期化にかかる時間を指します。 Lambda関数は最初のリクエストを受け取ると、コンテナーと必要なプロセス環境を初期化します。上記のグラフでは、リクエスト1の応答時間にはコールドスタート時間が含まれていますが、これはAWSLambda関数に使用されるプログラミング言語によって大きく異なります。
コールドスタート時間について心配する必要がありますか?
Lambda関数のトリガーとしてAWSAPIゲートウェイを使用している場合は、コールドスタート時間を考慮する必要があります。 AWS Lambda統合関数が指定された時間範囲内に初期化されていない場合、APIゲートウェイの応答はエラーになります。 APIゲートウェイ統合のタイムアウトの範囲は50ミリ秒から29秒です。
Java AWS Lambda関数のグラフでは、最初のリクエストに29秒以上かかったため、APIゲートウェイの応答がエラーになっていることがわかります。 Javaを使用して記述されたAWSLambda関数のコールドスタート時間は、サポートされている他のプログラミング言語と比較して長くなっています。これらのコールドスタート時間の問題に対処するために、実際の呼び出しの前に初期化要求を起動できます。もう1つの方法は、クライアント側で再試行することです。そうすれば、コールドスタート時間のためにリクエストが失敗した場合、再試行は成功します。
非アクティブ時にAWSLambda機能はどうなりますか?
テストでは、AWSLambdaホスティングコンテナがしばらく非アクティブになったときに停止したことも確認しました。この間隔は7分から20分まで変化しました。したがって、Lambda関数が頻繁に使用されない場合は、ハートビートリクエストを起動するか、クライアント側で再試行を追加して、Lambda関数を存続させることを検討する必要があります。
Lambda関数を同時に呼び出すとどうなりますか?
Lambda関数が同時に呼び出された場合、Lambdaは多くのコンテナーを使用してリクエストを処理します。デフォルトでは、AWS Lambdaは1000リクエストの予約されていない同時実行性を提供し、特定のLambda関数に対して構成可能です。
同時リクエストは非常に多くの接続を開く可能性があるため、ここで接続プールのサイズに注意する必要があります。したがって、接続プールのサイズを関数に最適に保つ必要があります。ただし、コンテナが停止すると、MongoDBサーバーからのタイムアウトに基づいて接続が解放されます。
AWSLambda接続プールの結論
Lambda関数はステートレスで非同期であり、データベース接続プールを使用することで、それに状態を追加できます。ただし、これはコンテナを再利用する場合にのみ役立ち、時間を大幅に節約できます。 AWS EC2を使用した接続プールは、単一のインスタンスが問題なく接続プールの状態を追跡できるため、管理が容易です。したがって、AWS EC2を使用すると、データベース接続が不足するリスクが大幅に減少します。 AWS Lambdaは、APIにアクセスするだけで、データベースエンジンに接続する必要がない場合に、より適切に機能するように設計されています。