MongoDB
 sql >> Base de Dados >  >> NoSQL >> MongoDB

Como usar o pool de conexão do MongoDB no AWS Lambda


Nesta postagem, mostraremos como usar o pool de conexões do MongoDB no AWS Lambda usando drivers Node.js e Java.

O que é AWS Lambda?


AWS Lambda é um serviço de computação sem servidor e orientado a eventos fornecido pela Amazon Web Services . Ele permite que um usuário execute código sem nenhuma tarefa administrativa, diferentemente das instâncias do EC2 em que um usuário é responsável pelo provisionamento de servidores, dimensionamento, alta disponibilidade etc. Em vez disso, você só precisa fazer upload do código e configurar o acionador de evento, e o AWS Lambda cuida de todo o resto automaticamente.

O AWS Lambda oferece suporte a vários ambientes de execução, incluindo Node.js , Python , Java e Ir . Ele pode ser acionado diretamente por serviços da AWS como S3 , DynamoDB , Cinese , SNS , etc. Em nosso exemplo, usamos o gateway de API da AWS para acionar as funções do Lambda.

O que é um pool de conexões?


Abrir e fechar uma conexão de banco de dados é uma operação cara, pois envolve tempo de CPU e memória. Se um aplicativo precisar abrir uma conexão de banco de dados para cada operação, isso terá um impacto severo no desempenho.

E se tivermos várias conexões de banco de dados que são mantidas vivas em um cache? Sempre que um aplicativo precisa executar uma operação de banco de dados, ele pode emprestar uma conexão do cache, executar a operação necessária e devolvê-la. Ao usar essa abordagem, podemos economizar o tempo necessário para estabelecer uma nova conexão a cada vez e reutilizar as conexões. Esse cache é conhecido como pool de conexão .

O tamanho do pool de conexões é configurável na maioria dos drivers do MongoDB e o tamanho do pool padrão varia de driver para driver. Por exemplo, é 5 no driver Node.js, enquanto é 100 no driver Java. O tamanho do pool de conexões determina o número máximo de solicitações paralelas que seu driver pode manipular em um determinado momento. Se o limite do pool de conexões for atingido, quaisquer novas solicitações serão feitas para aguardar até que as existentes sejam concluídas. Portanto, o tamanho do pool precisa ser escolhido com cuidado, considerando a carga do aplicativo e a simultaneidade a ser alcançada.

Pools de conexão do MongoDB no AWS Lambda


Neste post, mostraremos exemplos envolvendo o Node.js e o driver Java para MongoDB. Para este tutorial, usamos o MongoDB hospedado no ScaleGrid usando instâncias do AWS EC2. A configuração leva menos de 5 minutos e você pode criar uma avaliação gratuita de 30 dias aqui para começar.
Como usar o pool de conexões #MongoDB no AWS Lambda usando Node.js e drivers LambdaClick To Tweet

Pool de conexão MongoDB do driver Java


Aqui está o código para habilitar o pool de conexão do MongoDB usando o driver Java na função do manipulador do 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");
	}

}


O pool de conexões é obtido aqui declarando um sgMongoClient variável fora da função do manipulador. As variáveis ​​declaradas fora do método do manipulador permanecem inicializadas nas chamadas, desde que o mesmo contêiner seja reutilizado. Isso vale para qualquer outra linguagem de programação compatível com o AWS Lambda.

Pool de conexão do MongoDB do driver Node.js


Para o driver Node.js, declarar a variável de conexão no escopo global também fará o truque. No entanto, há uma configuração especial sem a qual o pool de conexões não é possível. Esse parâmetro é callbackWaitsForEmptyEventLoop que pertence ao objeto de contexto do Lambda. Definir essa propriedade como false fará com que o AWS Lambda congele o processo e quaisquer dados de estado. Isso é feito logo após o retorno de chamada ser chamado, mesmo se houver eventos no loop de eventos.

Aqui está o código para habilitar o pool de conexão do MongoDB usando o driver Node.js na função de manipulador do 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 };
	}
}

Análise e observações do pool de conexões do AWS Lambda


Para verificar o desempenho e a otimização do uso de pools de conexão, executamos alguns testes para as funções Java e Node.js Lambda. Usando o gateway de API da AWS como um trigger, invocamos as funções em uma sequência de 50 solicitações por iteração e determinamos o tempo médio de resposta de uma solicitação em cada iteração. Esse teste foi repetido para funções do Lambda sem usar o pool de conexões inicialmente e depois com o pool de conexões.





Os gráficos acima representam o tempo médio de resposta de uma solicitação em cada iteração. Você pode ver aqui a diferença no tempo de resposta quando um pool de conexões é usado para realizar operações de banco de dados. O tempo de resposta usando um pool de conexões é significativamente menor devido ao fato de que o pool de conexões inicializa uma vez e reutiliza a conexão em vez de abrir e fechar a conexão para cada operação do banco de dados.

A única diferença notável entre as funções Java e Node.js Lambda é o tempo de inicialização a frio.

O que é hora de partida a frio?


O tempo de inicialização a frio refere-se ao tempo gasto pela função AWS Lambda para inicialização. Quando a função do Lambda receber sua primeira solicitação, ela inicializará o contêiner e o ambiente de processo necessário. Nos gráficos acima, o tempo de resposta da solicitação 1 inclui o tempo de inicialização a frio, que difere significativamente com base na linguagem de programação usada para a função AWS Lambda.

Preciso me preocupar com o horário de início a frio?


Se você estiver usando o gateway de API da AWS como um gatilho para a função do Lambda, deverá levar em consideração o tempo de inicialização a frio. A resposta do gateway da API apresentará um erro se a função de integração do AWS Lambda não for inicializada no intervalo de tempo especificado. O tempo limite de integração do gateway da API varia de 50 milissegundos a 29 segundos.

No gráfico da função Java AWS Lambda, você pode ver que a primeira solicitação levou mais de 29 segundos, portanto, a resposta do gateway da API falhou. O tempo de inicialização a frio para a função AWS Lambda escrita usando Java é maior em comparação com outras linguagens de programação compatíveis. Para resolver esses problemas de tempo de inicialização a frio, você pode disparar uma solicitação de inicialização antes da chamada real. A outra alternativa é tentar novamente no lado do cliente. Dessa forma, se a solicitação falhar devido ao horário de inicialização a frio, a nova tentativa será bem-sucedida.

O que acontece com a função do AWS Lambda durante a inatividade?


Em nossos testes, também observamos que os contêineres de hospedagem do AWS Lambda foram interrompidos quando ficaram inativos por um tempo. Esse intervalo variou de 7 a 20 minutos. Portanto, se suas funções do Lambda não forem usadas com frequência, considere mantê-las ativas disparando solicitações de pulsação ou adicionando novas tentativas no lado do cliente.

O que acontece quando invoco funções do Lambda simultaneamente?


Se as funções do Lambda forem invocadas simultaneamente, o Lambda usará muitos contêineres para atender à solicitação. Por padrão, o AWS Lambda oferece simultaneidade não reservada de 1.000 solicitações e é configurável para uma determinada função do Lambda.

É aqui que você precisa ter cuidado com o tamanho do pool de conexões, pois solicitações simultâneas podem abrir muitas conexões. Portanto, você deve manter o tamanho do pool de conexões ideal para sua função. No entanto, assim que os contêineres forem interrompidos, as conexões serão liberadas com base no tempo limite do servidor MongoDB.

Conclusão do pool de conexões do AWS Lambda


As funções do Lambda são sem estado e assíncronas e, usando o pool de conexão do banco de dados, você poderá adicionar um estado a ele. No entanto, isso só ajudará quando os contêineres forem reutilizados, permitindo que você economize muito tempo. O pool de conexões usando o AWS EC2 é mais fácil de gerenciar porque uma única instância pode rastrear o estado de seu pool de conexões sem nenhum problema. Assim, usar o AWS EC2 reduz significativamente o risco de ficar sem conexões de banco de dados. O AWS Lambda foi projetado para funcionar melhor quando pode acessar uma API e não precisa se conectar a um mecanismo de banco de dados.