sql >> Base de Datos >  >> NoSQL >> MongoDB

Cómo utilizar la agrupación de conexiones de MongoDB en AWS Lambda

En esta publicación, le mostraremos cómo usar la agrupación de conexiones de MongoDB en AWS Lambda usando controladores Node.js y Java.

¿Qué es AWS Lambda?

AWS Lambda es un servicio informático sin servidor basado en eventos proporcionado por Amazon Web Services . Permite a un usuario ejecutar código sin ninguna de las tareas administrativas, a diferencia de las instancias EC2. donde un usuario es responsable del aprovisionamiento de servidores, el escalado, la alta disponibilidad, etc. En su lugar, solo tiene que cargar el código y configurar el activador del evento, y AWS Lambda se encarga automáticamente de todo lo demás.

AWS Lambda admite varios tiempos de ejecución, incluido Node.js , Pitón , Java y Ir . Puede ser activado directamente por servicios de AWS como S3 , DynamoDB , Cinesis , redes sociales , etc. En nuestro ejemplo, usamos la puerta de enlace API de AWS para activar las funciones de Lambda.

¿Qué es un grupo de conexiones?

Abrir y cerrar una conexión a la base de datos es una operación costosa ya que involucra tanto tiempo de CPU como memoria. Si una aplicación necesita abrir una conexión de base de datos para cada operación, eso tendrá un impacto severo en el rendimiento.

¿Qué pasa si tenemos un montón de conexiones de bases de datos que se mantienen vivas en un caché? Cada vez que una aplicación necesita realizar una operación de base de datos, puede tomar prestada una conexión del caché, realizar la operación requerida y devolverla. Al usar este enfoque, podemos ahorrar el tiempo requerido para establecer una nueva conexión cada vez y reutilizar las conexiones. Este caché se conoce como el grupo de conexiones .

El tamaño del grupo de conexiones se puede configurar en la mayoría de los controladores de MongoDB, y el tamaño del grupo predeterminado varía de un controlador a otro. Por ejemplo, es 5 en el controlador Node.js, mientras que es 100 en el controlador Java. El tamaño del grupo de conexiones determina la cantidad máxima de solicitudes paralelas que su controlador puede manejar en un momento dado. Si se alcanza el límite del grupo de conexiones, se realizarán nuevas solicitudes para esperar hasta que se completen las existentes. Por lo tanto, el tamaño del grupo debe elegirse con cuidado, teniendo en cuenta la carga de la aplicación y la simultaneidad que se desea lograr.

Grupos de conexiones de MongoDB en AWS Lambda

En esta publicación, le mostraremos ejemplos relacionados con Node.js y el controlador Java para MongoDB. Para este tutorial, usamos MongoDB alojado en ScaleGrid usando instancias de AWS EC2. Se tarda menos de 5 minutos en configurarlo y puede crear una prueba gratuita de 30 días aquí para comenzar.
Cómo utilizar la agrupación de conexiones #MongoDB en AWS Lambda mediante Node.js y controladores LambdaHaga clic para twittear

Grupo de conexiones MongoDB del controlador Java

Este es el código para habilitar el grupo de conexiones de MongoDB mediante el controlador de Java en la función de controlador de 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");
	}

}

La agrupación de conexiones se logra aquí declarando un sgMongoClient variable fuera de la función del controlador. Las variables declaradas fuera del método del controlador permanecen inicializadas en todas las llamadas, siempre que se reutilice el mismo contenedor. Esto es cierto para cualquier otro lenguaje de programación compatible con AWS Lambda.

Grupo de conexiones MongoDB del controlador Node.js

Para el controlador Node.js, declarar la variable de conexión en el ámbito global también funcionará. Sin embargo, existe una configuración especial sin la cual no es posible la agrupación de conexiones. Ese parámetro es callbackWaitsForEmptyEventLoop que pertenece al objeto de contexto de Lambda. Establecer esta propiedad en falso hará que AWS Lambda congele el proceso y cualquier dato de estado. Esto se hace poco después de llamar a la devolución de llamada, incluso si hay eventos en el bucle de eventos.

Este es el código para habilitar el grupo de conexiones de MongoDB usando el controlador Node.js en la función de controlador de 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álisis y observaciones del conjunto de conexiones de AWS Lambda

Para verificar el rendimiento y la optimización del uso de grupos de conexiones, realizamos algunas pruebas para las funciones Lambda de Java y Node.js. Usando la puerta de enlace de la API de AWS como desencadenante, invocamos las funciones en una ráfaga de 50 solicitudes por iteración y determinamos el tiempo de respuesta promedio para una solicitud en cada iteración. Esta prueba se repitió para las funciones de Lambda sin usar el grupo de conexiones inicialmente y luego con el grupo de conexiones.

Los gráficos anteriores representan el tiempo de respuesta promedio de una solicitud en cada iteración. Puede ver aquí la diferencia en el tiempo de respuesta cuando se utiliza un grupo de conexiones para realizar operaciones de base de datos. El tiempo de respuesta al usar un grupo de conexiones es significativamente menor debido al hecho de que el grupo de conexiones se inicializa una vez y reutiliza la conexión en lugar de abrir y cerrar la conexión para cada operación de la base de datos.

La única diferencia notable entre las funciones Lambda de Java y Node.js es el tiempo de inicio en frío.

¿Qué es el tiempo de inicio en frío?

El tiempo de inicio en frío se refiere al tiempo que tarda la función de AWS Lambda en la inicialización. Cuando la función Lambda recibe su primera solicitud, inicializará el contenedor y el entorno de proceso requerido. En los gráficos anteriores, el tiempo de respuesta de la solicitud 1 incluye el tiempo de inicio en frío, que difiere significativamente según el lenguaje de programación utilizado para la función AWS Lambda.

¿Debo preocuparme por el tiempo de inicio en frío?

Si utiliza la puerta de enlace de la API de AWS como activador de la función Lambda, debe tener en cuenta el tiempo de inicio en frío. La respuesta de la puerta de enlace API generará un error si la función de integración de AWS Lambda no se inicializa en el intervalo de tiempo dado. El tiempo de espera de integración de la puerta de enlace de la API oscila entre 50 milisegundos y 29 segundos.

En el gráfico de la función AWS Lambda de Java, puede ver que la primera solicitud tardó más de 29 segundos, por lo tanto, la respuesta de la puerta de enlace API falló. El tiempo de inicio en frío para la función AWS Lambda escrita con Java es mayor en comparación con otros lenguajes de programación admitidos. Para abordar estos problemas de tiempo de inicio en frío, puede activar una solicitud de inicialización antes de la invocación real. La otra alternativa es tener un reintento en el lado del cliente. De esa forma, si la solicitud falla debido a la hora de inicio en frío, el reintento tendrá éxito.

¿Qué sucede con la función AWS Lambda durante la inactividad?

En nuestras pruebas, también observamos que los contenedores de alojamiento de AWS Lambda se detuvieron cuando estuvieron inactivos durante un tiempo. Este intervalo varió de 7 a 20 minutos. Por lo tanto, si sus funciones de Lambda no se usan con frecuencia, debe considerar mantenerlas activas activando solicitudes de latido o agregando reintentos en el lado del cliente.

¿Qué sucede cuando invoco funciones de Lambda simultáneamente?

Si las funciones de Lambda se invocan simultáneamente, Lambda utilizará muchos contenedores para atender la solicitud. De forma predeterminada, AWS Lambda proporciona simultaneidad sin reservas de 1000 solicitudes y se puede configurar para una función de Lambda determinada.

Aquí es donde debe tener cuidado con el tamaño del grupo de conexiones, ya que las solicitudes simultáneas pueden abrir demasiadas conexiones. Por lo tanto, debe mantener el tamaño del grupo de conexiones óptimo para su función. Sin embargo, una vez que se detengan los contenedores, las conexiones se liberarán según el tiempo de espera del servidor MongoDB.

Conclusión de la agrupación de conexiones de AWS Lambda

Las funciones de Lambda son sin estado y asíncronas, y al usar el grupo de conexiones de la base de datos, podrá agregarle un estado. Sin embargo, esto solo ayudará cuando los contenedores se reutilicen, lo que le permitirá ahorrar mucho tiempo. La agrupación de conexiones con AWS EC2 es más fácil de administrar porque una sola instancia puede rastrear el estado de su agrupación de conexiones sin ningún problema. Por lo tanto, el uso de AWS EC2 reduce significativamente el riesgo de quedarse sin conexiones a la base de datos. AWS Lambda está diseñado para funcionar mejor cuando solo puede acceder a una API y no tiene que conectarse a un motor de base de datos.