MongoDB
 sql >> Database >  >> NoSQL >> MongoDB

Come utilizzare il pool di connessioni MongoDB su AWS Lambda

In questo post, ti mostreremo come utilizzare il pool di connessioni MongoDB su AWS Lambda utilizzando i driver Node.js e Java.

Cos'è AWS Lambda?

AWS Lambda è un servizio di elaborazione serverless basato su eventi fornito da Amazon Web Services . Consente a un utente di eseguire codice senza alcuna attività amministrativa, a differenza di istanze EC2 dove un utente è responsabile del provisioning dei server, del dimensionamento, dell'elevata disponibilità e così via. Devi solo caricare il codice e configurare l'attivazione dell'evento e AWS Lambda si occupa automaticamente di tutto il resto.

AWS Lambda supporta vari runtime, tra cui Node.js , Pitone , Java e Vai . Può essere attivato direttamente da servizi AWS come S3 , DynamoDB , Cinesi , SNS , ecc. Nel nostro esempio, utilizziamo il gateway API AWS per attivare le funzioni Lambda.

Cos'è un Connection Pool?

L'apertura e la chiusura di una connessione al database è un'operazione costosa poiché coinvolge sia il tempo della CPU che la memoria. Se un'applicazione deve aprire una connessione al database per ogni operazione, ciò avrà un forte impatto sulle prestazioni.

E se avessimo un mucchio di connessioni al database che sono mantenute in vita in una cache? Ogni volta che un'applicazione deve eseguire un'operazione sul database, può prendere in prestito una connessione dalla cache, eseguire l'operazione richiesta e restituirla. Utilizzando questo approccio, possiamo risparmiare il tempo necessario per stabilire ogni volta una nuova connessione e riutilizzare le connessioni. Questa cache è nota come pool di connessione .

La dimensione del pool di connessioni è configurabile nella maggior parte dei driver MongoDB e la dimensione del pool predefinito varia da driver a driver. Ad esempio, è 5 nel driver Node.js, mentre è 100 nel driver Java. La dimensione del pool di connessioni determina il numero massimo di richieste parallele che il driver può gestire in un determinato momento. Se viene raggiunto il limite del pool di connessioni, eventuali nuove richieste verranno fatte attendere fino al completamento di quelle esistenti. Pertanto, la dimensione del pool deve essere scelta con attenzione, considerando il carico dell'applicazione e la concorrenza da raggiungere.

Pool di connessione MongoDB in AWS Lambda

In questo post, ti mostreremo esempi che coinvolgono sia Node.js che il driver Java per MongoDB. Per questo tutorial, utilizziamo MongoDB ospitato su ScaleGrid utilizzando istanze AWS EC2. La configurazione richiede meno di 5 minuti e puoi creare una prova gratuita di 30 giorni qui per iniziare.
Come utilizzare il pool di connessioni #MongoDB su AWS Lambda utilizzando i driver Node.js e LambdaFai clic per twittare

Pool di connessione MongoDB del driver Java

Ecco il codice per abilitare il pool di connessioni MongoDB utilizzando il driver Java nella funzione del gestore 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");
	}

}

Il pool di connessioni si ottiene qui dichiarando un sgMongoClient variabile al di fuori della funzione di gestione. Le variabili dichiarate al di fuori del metodo del gestore rimangono inizializzate tra le chiamate, purché lo stesso contenitore venga riutilizzato. Questo vale per qualsiasi altro linguaggio di programmazione supportato da AWS Lambda.

Pool di connessione MongoDB del driver Node.js

Per il driver Node.js, anche la dichiarazione della variabile di connessione nell'ambito globale farà il trucco. Tuttavia, esiste un'impostazione speciale senza la quale il pool di connessioni non è possibile. Tale parametro è callbackWaitsForEmptyEventLoop che appartiene all'oggetto contesto di Lambda. L'impostazione di questa proprietà su false farà sì che AWS Lambda blocchi il processo e tutti i dati di stato. Questa operazione viene eseguita subito dopo la chiamata alla richiamata, anche se sono presenti eventi nel loop degli eventi.

Ecco il codice per abilitare il pool di connessioni MongoDB utilizzando il driver Node.js nella funzione del gestore 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 };
	}
}

Analisi e osservazioni del pool di connessioni AWS Lambda

Per verificare le prestazioni e l'ottimizzazione dell'utilizzo dei pool di connessioni, abbiamo eseguito alcuni test per le funzioni Lambda Java e Node.js. Utilizzando il gateway API AWS come trigger, abbiamo richiamato le funzioni in un burst di 50 richieste per iterazione e determinato il tempo di risposta medio per una richiesta in ogni iterazione. Questo test è stato ripetuto per le funzioni Lambda senza utilizzare inizialmente il pool di connessioni e successivamente con il pool di connessioni.

I grafici sopra rappresentano il tempo medio di risposta di una richiesta in ogni iterazione. Puoi vedere qui la differenza nel tempo di risposta quando un pool di connessioni viene utilizzato per eseguire operazioni di database. Il tempo di risposta utilizzando un pool di connessioni è notevolmente inferiore a causa del fatto che il pool di connessioni viene inizializzato una volta e riutilizza la connessione invece di aprire e chiudere la connessione per ogni operazione di database.

L'unica differenza notevole tra le funzioni Java e Node.js Lambda è l'ora di inizio a freddo.

Che cos'è l'ora di inizio a freddo?

L'ora di avvio a freddo si riferisce al tempo impiegato dalla funzione AWS Lambda per l'inizializzazione. Quando la funzione Lambda riceve la sua prima richiesta, inizializza il contenitore e l'ambiente di processo richiesto. Nei grafici precedenti, il tempo di risposta della richiesta 1 include il tempo di avvio a freddo, che differisce notevolmente in base al linguaggio di programmazione utilizzato per la funzione AWS Lambda.

Devo preoccuparmi dell'ora di inizio a freddo?

Se utilizzi il gateway API AWS come trigger per la funzione Lambda, devi prendere in considerazione l'ora di avvio a freddo. La risposta del gateway API emetterà un errore se la funzione di integrazione di AWS Lambda non viene inizializzata nell'intervallo di tempo specificato. Il timeout di integrazione del gateway API varia da 50 millisecondi a 29 secondi.

Nel grafico per la funzione Java AWS Lambda, puoi vedere che la prima richiesta ha richiesto più di 29 secondi, quindi la risposta del gateway API ha avuto un errore. Il tempo di avvio a freddo per la funzione AWS Lambda scritta utilizzando Java è maggiore rispetto ad altri linguaggi di programmazione supportati. Per risolvere questi problemi relativi all'ora di avvio a freddo, è possibile attivare una richiesta di inizializzazione prima dell'effettiva chiamata. L'altra alternativa consiste nel riprovare sul lato client. In questo modo, se la richiesta non riesce a causa dell'ora di inizio a freddo, il nuovo tentativo avrà esito positivo.

Cosa succede alla funzione AWS Lambda durante l'inattività?

Nei nostri test, abbiamo anche osservato che i container di hosting AWS Lambda sono stati interrotti quando erano inattivi per un po' di tempo. Questo intervallo variava da 7 a 20 minuti. Pertanto, se le tue funzioni Lambda non vengono utilizzate frequentemente, devi considerare di mantenerle attive inviando richieste di heartbeat o aggiungendo nuovi tentativi sul lato client.

Cosa succede quando invoco le funzioni Lambda contemporaneamente?

Se le funzioni Lambda vengono richiamate contemporaneamente, Lambda utilizzerà molti contenitori per soddisfare la richiesta. Per impostazione predefinita, AWS Lambda fornisce la simultaneità senza riserve di 1000 richieste ed è configurabile per una determinata funzione Lambda.

È qui che devi prestare attenzione alle dimensioni del pool di connessioni poiché le richieste simultanee possono aprire troppe connessioni. Pertanto, è necessario mantenere le dimensioni del pool di connessioni ottimali per la propria funzione. Tuttavia, una volta che i container vengono arrestati, le connessioni verranno rilasciate in base al timeout del server MongoDB.

Conclusione sul pool di connessioni AWS Lambda

Le funzioni Lambda sono stateless e asincrone e, utilizzando il pool di connessioni al database, potrai aggiungervi uno stato. Tuttavia, questo aiuterà solo quando i contenitori verranno riutilizzati, consentendoti di risparmiare molto tempo. Il pool di connessioni tramite AWS EC2 è più facile da gestire perché una singola istanza può tenere traccia dello stato del proprio pool di connessioni senza alcun problema. Pertanto, l'utilizzo di AWS EC2 riduce significativamente il rischio di esaurimento delle connessioni al database. AWS Lambda è progettato per funzionare meglio quando può semplicemente raggiungere un'API e non deve connettersi a un motore di database.