In diesem Beitrag zeigen wir Ihnen, wie Sie das MongoDB-Verbindungspooling auf AWS Lambda mit Node.js- und Java-Treibern verwenden.
Was ist AWS Lambda?
AWS Lambda ist ein ereignisgesteuerter, serverloser Computerdienst, der von Amazon Web Services bereitgestellt wird . Anders als bei EC2-Instanzen ermöglicht es einem Nutzer, Code ohne administrative Aufgaben auszuführen wo ein Benutzer für die Bereitstellung von Servern, Skalierung, Hochverfügbarkeit usw. verantwortlich ist. Stattdessen müssen Sie nur den Code hochladen und den Ereignisauslöser einrichten, und AWS Lambda kümmert sich automatisch um alles andere.
AWS Lambda unterstützt verschiedene Laufzeiten, einschließlich Node.js , Python , Java und Los . Es kann direkt von AWS-Services wie S3 ausgelöst werden , DynamoDB , Kinese , SNS usw. In unserem Beispiel verwenden wir das AWS-API-Gateway, um die Lambda-Funktionen auszulösen.
Was ist ein Verbindungspool?
Das Öffnen und Schließen einer Datenbankverbindung ist ein teurer Vorgang, da er sowohl CPU-Zeit als auch Arbeitsspeicher beansprucht. Wenn eine Anwendung für jeden Vorgang eine Datenbankverbindung öffnen muss, hat dies schwerwiegende Auswirkungen auf die Leistung.
Was ist, wenn wir eine Reihe von Datenbankverbindungen haben, die in einem Cache am Leben erhalten werden? Wann immer eine Anwendung eine Datenbankoperation ausführen muss, kann sie eine Verbindung aus dem Cache ausleihen, die erforderliche Operation ausführen und sie zurückgeben. Durch die Verwendung dieses Ansatzes können wir die Zeit sparen, die erforderlich ist, um jedes Mal eine neue Verbindung herzustellen, und die Verbindungen wiederverwenden. Dieser Cache wird als Verbindungspool bezeichnet .
Die Größe des Verbindungspools ist in den meisten MongoDB-Treibern konfigurierbar, und die Standardpoolgröße variiert von Treiber zu Treiber. Zum Beispiel ist es 5 im Node.js-Treiber, während es 100 im Java-Treiber ist. Die Größe des Verbindungspools bestimmt die maximale Anzahl paralleler Anforderungen, die Ihr Treiber zu einem bestimmten Zeitpunkt verarbeiten kann. Wenn das Limit des Verbindungspools erreicht ist, werden alle neuen Anforderungen so lange gewartet, bis die vorhandenen abgeschlossen sind. Daher muss die Poolgröße unter Berücksichtigung der Anwendungslast und der zu erreichenden Parallelität sorgfältig ausgewählt werden.
MongoDB-Verbindungspools in AWS Lambda
In diesem Beitrag zeigen wir Ihnen Beispiele mit Node.js und dem Java-Treiber für MongoDB. Für dieses Tutorial verwenden wir MongoDB, das auf ScaleGrid mit AWS EC2-Instances gehostet wird. Die Einrichtung dauert weniger als 5 Minuten und Sie können eine kostenlose 30-Tage-Testversion erstellen hier, um loszulegen.
So verwenden Sie #MongoDB Connection Pooling auf AWS Lambda mit Node.js und Lambda-TreibernClick To Tweet
MongoDB-Verbindungspool für Java-Treiber
Hier ist der Code zum Aktivieren des MongoDB-Verbindungspools mithilfe des Java-Treibers in der AWS Lambda-Handler-Funktion:
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");
}
}
Das Verbindungspooling wird hier durch die Deklaration eines sgMongoClient erreicht Variable außerhalb der Handler-Funktion. Die außerhalb der Behandlungsmethode deklarierten Variablen bleiben über Aufrufe hinweg initialisiert, solange derselbe Container wiederverwendet wird. Dies gilt für jede andere von AWS Lambda unterstützte Programmiersprache.
MongoDB-Verbindungspool des Node.js-Treibers
Für den Node.js-Treiber reicht es auch aus, die Verbindungsvariable im globalen Bereich zu deklarieren. Allerdings gibt es eine spezielle Einstellung, ohne die das Verbindungspooling nicht möglich ist. Dieser Parameter ist callbackWaitsForEmptyEventLoop die zum Kontextobjekt von Lambda gehört. Wenn Sie diese Eigenschaft auf „false“ setzen, friert AWS Lambda den Prozess und alle Zustandsdaten ein. Dies geschieht kurz nach dem Aufruf des Callbacks, selbst wenn es Ereignisse in der Ereignisschleife gibt.
Hier ist der Code zum Aktivieren des MongoDB-Verbindungspools mithilfe des Node.js-Treibers in der AWS Lambda-Handler-Funktion:
'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 };
}
}
Analyse und Beobachtungen des AWS Lambda-Verbindungspools
Um die Leistung und Optimierung der Verwendung von Verbindungspools zu überprüfen, haben wir einige Tests für Java- und Node.js-Lambda-Funktionen durchgeführt. Unter Verwendung des AWS-API-Gateways als Auslöser haben wir die Funktionen in einem Burst von 50 Anfragen pro Iteration aufgerufen und die durchschnittliche Antwortzeit für eine Anfrage in jeder Iteration bestimmt. Dieser Test wurde für Lambda-Funktionen ohne anfängliche Verwendung des Verbindungspools und später mit dem Verbindungspool wiederholt.
Die obigen Diagramme stellen die durchschnittliche Antwortzeit einer Anfrage in jeder Iteration dar. Hier sehen Sie den Unterschied in der Antwortzeit, wenn ein Verbindungspool zum Ausführen von Datenbankvorgängen verwendet wird. Die Antwortzeit bei Verwendung eines Verbindungspools ist erheblich geringer, da der Verbindungspool einmal initialisiert und die Verbindung wiederverwendet, anstatt die Verbindung für jede Datenbankoperation zu öffnen und zu schließen.
Der einzige bemerkenswerte Unterschied zwischen Java- und Node.js-Lambda-Funktionen ist die Kaltstartzeit.
Was ist die Kaltstartzeit?
Die Kaltstartzeit bezieht sich auf die Zeit, die die AWS Lambda-Funktion für die Initialisierung benötigt. Wenn die Lambda-Funktion ihre erste Anfrage erhält, initialisiert sie den Container und die erforderliche Prozessumgebung. In den obigen Diagrammen enthält die Antwortzeit von Anfrage 1 die Kaltstartzeit, die sich je nach Programmiersprache, die für die AWS Lambda-Funktion verwendet wird, erheblich unterscheidet.
Muss ich mir Sorgen um die Kaltstartzeit machen?
Wenn Sie das AWS-API-Gateway als Auslöser für die Lambda-Funktion verwenden, müssen Sie die Kaltstartzeit berücksichtigen. Die API-Gateway-Antwort schlägt fehl, wenn die AWS Lambda-Integrationsfunktion nicht innerhalb des angegebenen Zeitraums initialisiert wird. Das Zeitlimit für die API-Gateway-Integration liegt zwischen 50 Millisekunden und 29 Sekunden.
In der Grafik für die Java-AWS-Lambda-Funktion sehen Sie, dass die erste Anfrage mehr als 29 Sekunden gedauert hat, daher ist die API-Gateway-Antwort fehlgeschlagen. Die Kaltstartzeit für die mit Java geschriebene AWS Lambda-Funktion ist im Vergleich zu anderen unterstützten Programmiersprachen höher. Um diese Probleme mit der Kaltstartzeit zu beheben, können Sie vor dem eigentlichen Aufruf eine Initialisierungsanforderung auslösen. Die andere Alternative ist ein erneuter Versuch auf der Clientseite. Wenn die Anfrage aufgrund der Kaltstartzeit fehlschlägt, ist die Wiederholung erfolgreich.
Was passiert mit der AWS Lambda-Funktion bei Inaktivität?
Bei unseren Tests haben wir auch beobachtet, dass AWS Lambda-Hosting-Container gestoppt wurden, wenn sie eine Weile inaktiv waren. Dieses Intervall variierte von 7 bis 20 Minuten. Wenn Ihre Lambda-Funktionen also nicht häufig verwendet werden, müssen Sie erwägen, sie am Leben zu erhalten, indem Sie entweder Heartbeat-Anforderungen auslösen oder Wiederholungen auf der Client-Seite hinzufügen.
Was passiert, wenn ich gleichzeitig Lambda-Funktionen aufrufe?
Wenn Lambda-Funktionen gleichzeitig aufgerufen werden, verwendet Lambda viele Container, um die Anfrage zu bedienen. Standardmäßig bietet AWS Lambda uneingeschränkte Parallelität von 1000 Anfragen und ist für eine bestimmte Lambda-Funktion konfigurierbar.
Hier müssen Sie auf die Größe des Verbindungspools achten, da gleichzeitige Anfragen zu viele Verbindungen öffnen können. Daher müssen Sie die Größe des Verbindungspools für Ihre Funktion optimal halten. Sobald die Container jedoch gestoppt sind, werden Verbindungen basierend auf dem Timeout vom MongoDB-Server freigegeben.
Fazit zum AWS Lambda-Verbindungspooling
Lambda-Funktionen sind zustandslos und asynchron, und durch die Verwendung des Datenbankverbindungspools können Sie ihm einen Zustand hinzufügen. Dies hilft jedoch nur, wenn die Behälter wiederverwendet werden, wodurch Sie viel Zeit sparen. Das Verbindungspooling mit AWS EC2 ist einfacher zu verwalten, da eine einzelne Instanz den Status ihres Verbindungspools problemlos verfolgen kann. Daher reduziert die Verwendung von AWS EC2 das Risiko, dass keine Datenbankverbindungen mehr vorhanden sind. AWS Lambda wurde entwickelt, um besser zu funktionieren, wenn es einfach auf eine API zugreifen kann und keine Verbindung zu einer Datenbank-Engine herstellen muss.