W tym poście pokażemy, jak korzystać z puli połączeń MongoDB w AWS Lambda przy użyciu sterowników Node.js i Java.
Co to jest AWS Lambda?
AWS Lambda to sterowana zdarzeniami, bezserwerowa usługa obliczeniowa świadczona przez Amazon Web Services . Pozwala użytkownikowi uruchamiać kod bez żadnych zadań administracyjnych, w przeciwieństwie do instancji EC2 gdzie użytkownik jest odpowiedzialny za udostępnianie serwerów, skalowanie, wysoką dostępność itp. Zamiast tego wystarczy przesłać kod i skonfigurować wyzwalacz zdarzenia, a AWS Lambda automatycznie zajmie się wszystkim innym.
AWS Lambda obsługuje różne środowiska wykonawcze, w tym Node.js , Python , Jawa i Idź . Może być bezpośrednio uruchamiany przez usługi AWS, takie jak S3 , DynamoDB , Kineza , SNS itp. W naszym przykładzie używamy bramy AWS API do wyzwalania funkcji Lambda.
Co to jest pula połączeń?
Otwieranie i zamykanie połączenia z bazą danych jest kosztowną operacją, ponieważ obejmuje zarówno czas procesora, jak i pamięć. Jeśli aplikacja musi otworzyć połączenie z bazą danych dla każdej operacji, będzie to miało poważny wpływ na wydajność.
A co, jeśli mamy kilka połączeń z bazą danych, które są utrzymywane w pamięci podręcznej? Za każdym razem, gdy aplikacja musi wykonać operację na bazie danych, może wypożyczyć połączenie z pamięci podręcznej, wykonać wymaganą operację i oddać je z powrotem. Stosując to podejście, możemy zaoszczędzić czas potrzebny na nawiązanie za każdym razem nowego połączenia i ponownie wykorzystać połączenia. Ta pamięć podręczna jest znana jako pula połączeń .
Rozmiar puli połączeń można konfigurować w większości sterowników MongoDB, a domyślny rozmiar puli różni się w zależności od sterownika. Na przykład jest to 5 w sterowniku Node.js, podczas gdy w sterowniku Java jest to 100. Rozmiar puli połączeń określa maksymalną liczbę równoległych żądań, które sterownik może obsłużyć w danym momencie. Jeśli zostanie osiągnięty limit puli połączeń, wszelkie nowe żądania będą czekać na zakończenie istniejących. Dlatego rozmiar puli należy starannie wybrać, biorąc pod uwagę obciążenie aplikacji i współbieżność, którą należy osiągnąć.
Pule połączeń MongoDB w AWS Lambda
W tym poście pokażemy przykłady dotyczące zarówno Node.js, jak i sterownika Java dla MongoDB. W tym samouczku używamy MongoDB hostowanego na ScaleGrid przy użyciu instancji AWS EC2. Konfiguracja zajmuje mniej niż 5 minut i możesz utworzyć bezpłatny 30-dniowy okres próbny tutaj, aby zacząć.
Jak korzystać z puli połączeń #MongoDB w AWS Lambda przy użyciu sterowników Node.js i LambdaKliknij, aby tweetować
Pula połączeń sterownika Java MongoDB
Oto kod umożliwiający włączenie puli połączeń MongoDB za pomocą sterownika Java w funkcji obsługi AWS Lambda:
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");
}
}
Pulę połączeń uzyskuje się tutaj poprzez zadeklarowanie sgMongoClient zmienna poza funkcją obsługi. Zmienne zadeklarowane poza metodą obsługi pozostają zainicjowane między wywołaniami, o ile ten sam kontener jest ponownie używany. Dotyczy to każdego innego języka programowania obsługiwanego przez AWS Lambda.
Pula połączeń sterownika Node.js MongoDB
W przypadku sterownika Node.js zadeklarowanie zmiennej połączenia w zasięgu globalnym również załatwi sprawę. Istnieje jednak specjalne ustawienie, bez którego łączenie połączeń nie jest możliwe. Ten parametr to callbackWaitsForEmptyEventLoop który należy do obiektu kontekstowego Lambdy. Ustawienie tej właściwości na false spowoduje, że AWS Lambda zamrozi proces i wszelkie dane stanu. Odbywa się to wkrótce po wywołaniu wywołania zwrotnego, nawet jeśli w pętli zdarzeń występują zdarzenia.
Oto kod umożliwiający włączenie puli połączeń MongoDB przy użyciu sterownika Node.js w funkcji obsługi AWS Lambda:
'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 };
}
}
Analiza i obserwacje puli połączeń AWS Lambda
Aby zweryfikować wydajność i optymalizację wykorzystania pul połączeń, przeprowadziliśmy kilka testów zarówno dla funkcji Java, jak i Node.js Lambda. Używając bramy AWS API jako wyzwalacza, wywołaliśmy funkcje w serii 50 żądań na iterację i określiliśmy średni czas odpowiedzi na żądanie w każdej iteracji. Ten test został powtórzony dla funkcji Lambda bez użycia puli połączeń początkowo, a później z puli połączeń.
Powyższe wykresy przedstawiają średni czas odpowiedzi na żądanie w każdej iteracji. Tutaj możesz zobaczyć różnicę w czasie odpowiedzi, gdy pula połączeń jest używana do wykonywania operacji na bazie danych. Czas odpowiedzi przy użyciu puli połączeń jest znacznie krótszy ze względu na fakt, że pula połączeń jest inicjowana raz i ponownie wykorzystuje połączenie zamiast otwierania i zamykania połączenia dla każdej operacji bazy danych.
Jedyną zauważalną różnicą między funkcjami Lambda w Javie i Node.js jest czas zimnego startu.
Co to jest czas zimnego startu?
Czas zimnego startu odnosi się do czasu potrzebnego do inicjalizacji funkcji AWS Lambda. Gdy funkcja Lambda otrzyma swoje pierwsze żądanie, zainicjuje kontener i wymagane środowisko procesowe. Na powyższych wykresach czas odpowiedzi żądania 1 obejmuje czas zimnego startu, który znacznie różni się w zależności od języka programowania używanego dla funkcji AWS Lambda.
Czy muszę się martwić o zimny czas startu?
Jeśli używasz bramy AWS API jako wyzwalacza funkcji Lambda, musisz wziąć pod uwagę czas zimnego startu. Odpowiedź bramy interfejsu API będzie błędna, jeśli funkcja integracji AWS Lambda nie zostanie zainicjowana w podanym zakresie czasu. Limit czasu integracji bramy API wynosi od 50 milisekund do 29 sekund.
Na wykresie funkcji Java AWS Lambda widać, że pierwsze żądanie zajęło ponad 29 sekund, stąd odpowiedź bramy interfejsu API była błędna. Czas zimnego startu funkcji AWS Lambda napisanej w Javie jest dłuższy w porównaniu do innych obsługiwanych języków programowania. Aby rozwiązać te problemy z zimnym czasem startu, możesz uruchomić żądanie inicjalizacji przed faktycznym wywołaniem. Inną alternatywą jest ponowna próba po stronie klienta. W ten sposób, jeśli żądanie nie powiedzie się z powodu zimnego startu, ponowna próba się powiedzie.
Co się dzieje z funkcją AWS Lambda podczas bezczynności?
W naszych testach zauważyliśmy również, że kontenery hostingowe AWS Lambda były zatrzymywane, gdy były nieaktywne przez jakiś czas. Interwał ten wahał się od 7 do 20 minut. Tak więc, jeśli twoje funkcje Lambda nie są często używane, musisz rozważyć utrzymywanie ich przy życiu przez uruchamianie żądań pulsu lub dodawanie ponownych prób po stronie klienta.
Co się dzieje, gdy jednocześnie wywołuję funkcje lambda?
Jeśli funkcje Lambda są wywoływane jednocześnie, Lambda użyje wielu kontenerów do obsługi żądania. Domyślnie AWS Lambda zapewnia niezarezerwowaną współbieżność 1000 żądań i jest konfigurowalna dla danej funkcji Lambda.
W tym miejscu należy uważać na wielkość puli połączeń, ponieważ współbieżne żądania mogą otwierać zbyt wiele połączeń. Dlatego musisz zachować optymalny rozmiar puli połączeń dla swojej funkcji. Jednak po zatrzymaniu kontenerów połączenia zostaną zwolnione na podstawie limitu czasu z serwera MongoDB.
Wniosek dotyczący łączenia połączeń AWS Lambda
Funkcje lambda są bezstanowe i asynchroniczne, a korzystając z puli połączeń bazy danych, będziesz mógł dodać do niej stan. Pomoże to jednak tylko wtedy, gdy kontenery zostaną ponownie użyte, co pozwoli Ci zaoszczędzić dużo czasu. Pula połączeń przy użyciu AWS EC2 jest łatwiejsza w zarządzaniu, ponieważ pojedyncza instancja może bez problemu śledzić stan swojej puli połączeń. Dzięki temu korzystanie z AWS EC2 znacznie zmniejsza ryzyko wyczerpania połączeń z bazą danych. AWS Lambda została zaprojektowana tak, aby działała lepiej, gdy może po prostu trafić na interfejs API i nie musi łączyć się z silnikiem bazy danych.