I det här inlägget visar vi dig hur du använder MongoDB-anslutningspoolning på AWS Lambda med både Node.js och Java-drivrutiner.
Vad är AWS Lambda?
AWS Lambda är en händelsestyrd, serverlös datortjänst som tillhandahålls av Amazon Web Services . Det gör att en användare kan köra kod utan någon av de administrativa uppgifterna, till skillnad från EC2-instanser där en användare ansvarar för provisionering av servrar, skalning, hög tillgänglighet etc. Istället behöver du bara ladda upp koden och ställa in händelseutlösaren, och AWS Lambda tar automatiskt hand om allt annat.
AWS Lambda stöder olika körtider, inklusive Node.js , Python , Java och Gå . Det kan utlösas direkt av AWS-tjänster som S3 , DynamoDB , Kinesis , SNS , etc. I vårt exempel använder vi AWS API-gateway för att trigga Lambda-funktionerna.
Vad är en anslutningspool?
Att öppna och stänga en databasanslutning är en dyr operation eftersom det involverar både CPU-tid och minne. Om ett program behöver öppna en databasanslutning för varje operation, kommer det att ha en allvarlig inverkan på prestanda.
Vad händer om vi har ett gäng databasanslutningar som hålls vid liv i en cache? Närhelst ett program behöver utföra en databasoperation kan det låna en anslutning från cachen, utföra den nödvändiga operationen och ge tillbaka den. Genom att använda detta tillvägagångssätt kan vi spara den tid som krävs för att upprätta en ny anslutning varje gång och återanvända anslutningarna. Denna cache är känd som anslutningspoolen .
Storleken på anslutningspoolen är konfigurerbar i de flesta MongoDB-drivrutiner, och standardpoolstorleken varierar från drivrutin till drivrutin. Till exempel är det 5 i Node.js-drivrutinen, medan det är 100 i Java-drivrutinen. Anslutningspoolens storlek bestämmer det maximala antalet parallella förfrågningar som din förare kan hantera vid en given tidpunkt. Om gränsen för anslutningspoolen nås, kommer alla nya förfrågningar att göras för att vänta tills de befintliga är slutförda. Därför måste poolstorleken väljas noggrant, med hänsyn till applikationsbelastningen och samtidighet som ska uppnås.
MongoDB Connection Pools i AWS Lambda
I det här inlägget kommer vi att visa dig exempel som involverar både Node.js och Java-drivrutin för MongoDB. För den här handledningen använder vi MongoDB som är värd på ScaleGrid med AWS EC2-instanser. Det tar mindre än 5 minuter att konfigurera, och du kan skapa en kostnadsfri 30-dagars provperiod här för att komma igång.
Hur man använder #MongoDB Connection Pooling på AWS Lambda med Node.js och Lambda DriversKlicka för att tweeta
Java-drivrutin MongoDB Connection Pool
Här är koden för att aktivera MongoDB-anslutningspoolen med Java-drivrutinen i AWS Lambda-hanterarfunktion:
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");
}
}
Anslutningspoolningen uppnås här genom att deklarera en sgMongoClient variabel utanför hanterarfunktionen. Variablerna som deklareras utanför hanterarmetoden förblir initierade över anrop, så länge som samma behållare återanvänds. Detta gäller för alla andra programmeringsspråk som stöds av AWS Lambda.
Node.js-drivrutin MongoDB Connection Pool
För Node.js-drivrutinen kommer det också att göra susen att deklarera anslutningsvariabeln i globalt omfång. Det finns dock en speciell inställning utan vilken anslutningspoolning inte är möjlig. Den parametern är callbackWaitsForEmptyEventLoop som tillhör Lambdas kontextobjekt. Om du ställer in den här egenskapen till false kommer AWS Lambda att frysa processen och eventuella tillståndsdata. Detta görs strax efter att återuppringningen har anropats, även om det finns händelser i händelseslingan.
Här är koden för att aktivera MongoDB-anslutningspoolen med hjälp av Node.js-drivrutinen i AWS Lambda-hanterarfunktion:
'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 };
}
}
AWS Lambda Connection Pool Analysis and Observations
För att verifiera prestanda och optimering av att använda anslutningspooler, körde vi några tester för både Java och Node.js Lambda-funktioner. Med hjälp av AWS API-gatewayen som en utlösare anropade vi funktionerna i en serie på 50 förfrågningar per iteration och fastställde den genomsnittliga svarstiden för en begäran i varje iteration. Detta test upprepades för Lambda-funktioner utan att använda anslutningspoolen initialt och senare med anslutningspoolen.
Diagrammen ovan representerar den genomsnittliga svarstiden för en begäran i varje iteration. Här kan du se skillnaden i svarstid när en anslutningspool används för att utföra databasoperationer. Svarstiden med en anslutningspool är betydligt lägre på grund av att anslutningspoolen initieras en gång och återanvänder anslutningen istället för att öppna och stänga anslutningen för varje databasoperation.
Den enda anmärkningsvärda skillnaden mellan Java och Node.js Lambda-funktioner är kallstartstiden.
Vad är kallstartstid?
Kallstartstid hänvisar till den tid som AWS Lambda-funktionen tar för initiering. När lambdafunktionen tar emot sin första begäran kommer den att initiera behållaren och den nödvändiga processmiljön. I diagrammen ovan inkluderar svarstiden för begäran 1 kallstartstiden, som skiljer sig markant beroende på det programmeringsspråk som används för AWS Lambda-funktion.
Behöver jag oroa mig för kallstarttid?
Om du använder AWS API-gateway som en utlösare för Lambda-funktionen måste du ta kallstartstiden i beaktande. API-gatewaysvaret kommer att felas om AWS Lambda-integreringsfunktionen inte initieras inom det givna tidsintervallet. API-gateway-integreringens timeout sträcker sig från 50 millisekunder till 29 sekunder.
I diagrammet för Java AWS Lambda-funktionen kan du se att den första förfrågan tog mer än 29 sekunder, och därför misslyckades API-gatewaysvaret. Kallstarttiden för AWS Lambda-funktion skriven med Java är högre jämfört med andra programmeringsspråk som stöds. För att lösa dessa kallstartsproblem kan du skicka en initieringsbegäran innan den faktiska anropet. Det andra alternativet är att göra ett nytt försök på klientsidan. På så sätt, om begäran misslyckas på grund av kallstartstid, kommer försöket att lyckas igen.
Vad händer med AWS Lambda-funktion under inaktivitet?
I våra tester observerade vi också att AWS Lambda-värdbehållare stoppades när de var inaktiva ett tag. Detta intervall varierade från 7 till 20 minuter. Så om dina Lambda-funktioner inte används ofta, måste du överväga att hålla dem vid liv genom att antingen skicka hjärtslagsbegäranden eller lägga till omförsök på klientsidan.
Vad händer när jag anropar Lambda-funktioner samtidigt?
Om Lambda-funktioner anropas samtidigt, kommer Lambda att använda många behållare för att betjäna begäran. Som standard tillhandahåller AWS Lambda oreserverad samtidighet på 1000 förfrågningar och är konfigurerbar för en given Lambdafunktion.
Det är här du måste vara försiktig med anslutningspoolens storlek eftersom samtidiga förfrågningar kan öppna för många anslutningar. Så du måste hålla anslutningspoolens storlek optimal för din funktion. Men när behållarna har stoppats kommer anslutningar att släppas baserat på timeout från MongoDB-servern.
AWS Lambda Connection Pooling Slutsats
Lambdafunktioner är tillståndslösa och asynkrona, och genom att använda databasanslutningspoolen kommer du att kunna lägga till ett tillstånd till den. Detta hjälper dock bara när behållarna återanvänds, vilket gör att du sparar mycket tid. Anslutningspoolning med AWS EC2 är lättare att hantera eftersom en enda instans kan spåra tillståndet för sin anslutningspool utan problem. Användning av AWS EC2 minskar således risken för att ta slut på databasanslutningar. AWS Lambda är designad för att fungera bättre när den bara kan träffa ett API och inte behöver ansluta till en databasmotor.