Anti-patterns de Performance - Parte 2

Adriano Croco
6 min readFeb 6, 2023

Olá!

Continuando o artigo anterior sobre performance, neste texto eu gostaria de abordar mais alguns problemas comuns.

Busy Database

Esse tipo de problema não acontece em Excel

Para começar, o que todos esses exemplos têm em comum?

  1. Um e-commerce com um grande volume de transações durante as vendas de black friday.
  2. Uma plataforma de mídia social com uma base de usuários que cresce exponencialmente.
  3. Um sistema financeiro processando grandes volumes de transações de ações durante a volatilidade de algum evento particular (ex: Joesley Day).

Em todos esses casos, por mais que o front-end ou até mesmo o back-end estejam adequadamente provisionados (ou seja, com a quantidade de instâncias para atender a carga da situação), um gargalo muito comum que aparece em situações dessa natureza são problemas no banco de dados.

É muito comum ver grandes picos de latência em queries ou frases famosas como o banco está topando (no sentido dos indicadores de CPU e uso de memória estarem no máximo) dita pelos DBAs em situações desse tipo. Além disso, Deadlocks e inconsistências de dados também podem ocorrer.

Dado que escalar um banco de dados não é tão simples quanto subir um pod no kubernetes, sobram poucas opções para se resolver esse problema. Eu consigo enxergar 4 possibilidades e irei comentar brevemente sobre cada uma delas.

Scaling out: Essa talvez seja a opção mais simples, apesar de relativamente mais cara. O jeito mais fácil de parar de ter problemas com um banco de dados sobrecarregado é jogar dinheiro no problema e comprar mais recursos computacionais. A maioria dos provedores de nuvem hoje em dia te deixam configurar o tamanho da máquina rapidamente e adicionar CPU, memória e disco de acordo com a necessidade. O grande problema aqui é que aumento de recursos assim aumentam o custo de forma definitiva (afinal, uma máquina que fica mais poderosa dificilmente reduz de tamanho). Além disso, essa solucão tem um limite de vezes em que isso pode ser feito (seja por orçamento da empresa ou por limite de tamanho de máquina).

Caching: Uma técnica muito útil e relativamente barata é adicionar no cache operações que são adequadas a esse formato de acesso, como: leituras de poucos dados que precisam ser rápidas, dados que não sofram muitas alterações e similares. Com o uso desse tipo de técnica é possível obter resultados bons com pouco esforço. Lembre-se: Quanto maior o cache-hit, menos a aplicação sofre.

Indexing: Essa solução é adequada quando existe uma alta frequência de consultas a uma determinada coluna de uma tabela usando algum tipo de filtro ou uma coluna que é muito usada em Joins. Ao adicionar um índice, geralmente há uma melhoria de performance. O cuidado aqui é gerenciar a saúde do índice em si, efetuando uma análise adequada do problema e cuidando das estatísticas do banco em si. Apesar disso, a maioria dos bancos atuais também oferecem ferramentas de análise (ex: aqui e aqui) que praticamente gritam sozinhas onde é necessário colocar um índice.

Optimizing the schema: Ao meu ver, a solução correta é essa. Aqui envolve modelar o banco de dados de acordo com o workload que será processado. Aqui envolve usar técnicas que ferem o que se aprende na faculdade, como normalização de dados. Se o foco é performance, pode ser que o banco fique meio desnormalizado e de certa forma, está tudo bem.

Busy Frontend

Esse problema ocorre quando um determinado componente de uma UI qualquer (um botão, por exemplo), dispara alguma tarefa um pouco mais demorada em segundo plano e a mesma thread que processa a UI tem que lidar com essa tarefa.

A percepção para o usuário nesse caso é que a tela simplesmente travou, dado que a thread responsável por manter a tela responsiva está ocupada processando uma tarefa pesada que deveria estar em segundo plano.

A solução é até que relativamente simples: Separe a thread de UI da thread de processamento. Somente com isso a percepção de responsividade para o usuário tende a melhorar bastante. O grande risco desse método é usar threads demais e acabar gerando um problema chamado thread starvation.

Aqui está um exemplo de código em NodeJS que causaria esse problema:

// resource-intensive-task.js
const { workerData, parentPort } = require('worker_threads');

// Perform resource-intensive task here
let result = performIntensiveTask(workerData);
parentPort.postMessage(result);

// main.js
const { Worker } = require('worker_threads');

//dispara 1000 threads
for (let i = 0; i < 1000; i++) {
let worker = new Worker('./resource-intensive-task.js', { workerData: 'start' });
worker.on('message', (result) => {
console.log(`Result from worker: ${result}`);
});
}

Aqui vale uma dica: Um bom ponto de partida para a pergunta quantas threads eu uso? É a quantidade de núcleos que a máquina tem menos um (ou seja, se sua máquina tem 16 núcleos, considere usar 15). Usar uma quantidade de threads maior que a quantidade de núcleos disponíveis da máquina pode gerar um problema devido a troca de contexto das threads. Uma outra referência para entender as limitações desse tipo de técnica é a Lei de Amdahl.

Improper Instantiation

Vamos a um outro problema visível pelo código. Logo abaixo tem um exemplo de uma classe de acesso a dados relativamente comum de se encontrar por aí:

class DatabaseConnection {
constructor(options) {
this.options = options;
this.connection = null;
}

connect() {
this.connection = new Connection(this.options);
}

query(sql) {
return this.connection.query(sql);
}
}

function getDataFromDatabase() {
let db = new DatabaseConnection({ host: 'localhost', user: 'root', password: 'secret' });
db.connect();
let result = db.query('SELECT * FROM users');
}

Imagine que a function getDataFromDatabase() é chamada múltiplas vezes por vários lugares do código. A cada chamada, é criada uma nova conexão com o banco de dados. Esse código rodando desse jeito em algum momento vai causar muitos problemas caso a quantidade de requests suba um pouco.

Como regra geral, recursos caros e pesados (como requests pela rede, conexões a banco de dados e handlers de arquivos), devem ser usados com extrema cautela, justamente devido ao ponto de IO ser mais lento que CPU e Memória que eu mencionei no artigo anterior.

Portanto, uma conexão ao banco de dados deixada dessa forma é uma forma de fazer a aplicação performar pouco, mesmo com um aparente uso baixo de recursos. Ou seja, é basicamente a definição literal de ineficiência.

Há várias formas de se resolver esse problema, talvez a mais simples delas seja o uso do Design Pattern Singleton:

Ops, não é esse não

Aqui está o verdadeiro:

class DatabaseConnection {
// a magica acontece aqui
// em caso de tentativa de criar uma nova classe pesada
// reusa uma instancia ao inves de se criar uma nova
static getInstance() {
if (!DatabaseConnection.instance) {
DatabaseConnection.instance = new DatabaseConnection();
}
return DatabaseConnection.instance;
}

constructor() {
this.options = { host: 'localhost', user: 'root', password: 'secret' };
this.connection = null;
this.connect();
}

connect() {
this.connection = new Connection(this.options);
}

query(sql) {
return this.connection.query(sql);
}
}

function getDataFromDatabase() {
let db = DatabaseConnection.getInstance();
let result = db.query('SELECT * FROM users');
// Do something with result
}

Com essa técnica simples, você mantém a criação de conexões ao banco baixas, usa poucas instâncias (ou seja, usa pouca memória) e melhora o tempo de resposta da aplicação de uma maneira geral. Essa pode ser a diferença entre suportar 10.000 ou 1 milhão de requests apenas mexendo em código.

Portanto, a mensagem é: Escrever código performático é ecológico! Pois consumir menos recursos computacionais gasta menos energia e faz bem para o planeta.

Pronto, agora eu te dei um motivo para estudar isso em profundidade. Vai lá escrever código rápido e salvar o mundo. E sim, já fizeram um estudo comparando diversas linguagens e elencaram qual é a mais verde delas. E sabe o que é engraçado? Por esse estudo, o Java não é tão ruim e pesado quanto o senso comum diz que ele é.

Espero que esse texto tenha te passado informações úteis de alguma forma.

Até a próxima!

Você gostou do conteúdo e gostaria de fazer mentoria comigo? Clique aqui e descubra como.

--

--