Utilizando uma conexão "persistente" com o banco de dados em um ambiente Serverless/Stateless
Tenho visto muita gente implementando seus microsserviços em ambientes serverless, como o AWS Lambda, o que é bastante positivo e demonstra uma adoção cada dia maior desta forma de desenvolvimento.
A utilização dessa tecnologia é extremamente vantajosa para as aplicações, pois alia redução de custos e eliminação da necessidade de manutenção de infraestrutura a alta disponibilidade dos serviços.
- Quanto à redução de custos: pagar somente pelo uso, não pelo tempo ocioso do recurso é, por si só, um atrativo gigante.
- A eliminação da necessidade da manutenção da infraestrutura: além de entrar como uma redução de custo (afinal suportar infra não custa tão barato assim), não se preocupar com atualização de sistema operacional, patch de segurança, drivers e afins. Bem como a parte física propriamente dita: rede, servidor, etc.
- Alta disponibilidade: com a infra suportada e gerenciada pelo "Cloud Provider", a aplicação (e o desenvolvedor) não se preocupa em como o escalonamento (seja ele vertical ou horizontal) será feito. Mas ele funcionará sem a necessidade da intervenção da aplicação.
Voltando um pouco ao tema inicial, esses serviços se autodenominam "stateless" e por que isso?
Essa denominação acontece pelo fato de, por se tratar de um recurso "sem servidor", qualquer dado (não persistido em um recurso externo) é perdido ao final da execução da aplicação. Isto é, não guarda o "estado".
Isso é parcialmente verdade quando pensamos que uma aplicação já ativa pode (ou não) receber uma outra requisição antes que essa "instância" possa ser "reciclada". Trocando em miúdos: um arquivo escrito em disco ou uma variável em memória (no nosso caso uma conexão com o banco de dados) pode sim estar disponível de uma requisição para outra, mesmo sabendo que você nunca deve contar com isso!
Há algum tempo, um amigo confidenciou vários problemas com limite de conexões com o banco de dados. Ao analisar a aplicação, reparei que o código (rodando no AWS Lambda), abria uma nova conexão a cada nova requisição. Mesmo com o fechamento dessas conexões no final do código, o problema das conexões persistia. Isto porque, por se tratar de uma aplicação com uma carga grande de requisições, muitas chamadas eram feitas de forma simultânea, consequentemente, muitas conexões com a base sempre estavam abertas.
Com isso chegamos ao ponto: como utilizar uma conexão persistente com o banco de dados quando a aplicação está em um ambiente serverless?
Primeiro, o que é uma conexão não persistente? Segundo a Wikipedia:
A conexão TCP é desfeita ao final da entrega de cada objeto. A conexão NÃO PERSISTE para outros objetos. O browser pode abrir várias conexões TCP simultâneas (paralelismo). Pode sobrecarregar o Servidor (administração de mais buffers e variáveis TCP no lado Cliente e Servidor) e tem maior tempo de resposta (requisição de conexão a cada objeto solicitado).
E uma conexão persistente? Ainda na Wikipedia:
Múltiplos objetos podem ser enviados sobre uma mesma conexão TCP (com paralelismo ou sem paralelismo).
Embora a ótica utilizada tenha sido de um browser, essa afirmação é também verdadeira quando se trata de uma conexão com o banco de dados (que também é uma conexão TCP no final das contas).
Para exemplificar o approach que utilizei para contornar o problema, escolhi Node.js com um banco MongoDB (utilizando o Mongoose como ODM), mas a técnica funciona com qualquer combinação.
Primeiramente, temos o helper de conexão helper.connection.js
let connection = null; const connectionString = 'mongodb+srv://user:pass@my.database.host'; const mongoose = require('mongoose'); exports.getConnection = async () => { if (connection == null || connection.readyState !== 1) { connection = await mongoose.createConnection(connectionString); } return connection; };
Aqui criamos um model bem simples no arquivo model.example.js
const mongoose = require('mongoose'); mongoose.Promise = global.Promise; const myModelSchema = new mongoose.Schema({ id: { type: Number, required: true }, name: { type: String, required: true }, }); module.exports = mongoose.model('MyModel', myModelSchema);
E finalmente, o arquivo principal da aplicação, o index.js
const db = require('helper.connection'); require('model.example'); exports.handler = async (event) => { const conn = await connection.getConnection(); const MyModel = conn.model('MyModel'); const results = await MyModel.find({ 'id': 1 }); return { statusCode: results.length ? 200 : 204, body: JSON.stringify(results), }; };
A vantagem desse formato é a reutilização das conexões abertas com o banco de dados. Podemos observar isso justamente no arquivo helper.connection.js onde eu testo se a conexão é nula ou seu estado não é válido.
Isso previne, por exemplo, que seu banco de dados chegue ao limite de conexões abertas (através de uma conexão persistente), uma vez que, de outra forma, uma nova conexão seria aberta a cada requisição em sua aplicação.
É uma dica extremamente simples, mas que pode te salvar de uma dor de cabeça gigante.
Caso tenha alguma dúvida, crítica ou sugestão, ficarei feliz em recebê-la.