Escalabilidade e performance – Técnicas

No post anterior, sobre escalabilidade e performance, entramos na definição de ambas, e relacionamos os princípios comuns que visam a aplicação atingir este objetivo. Apenas para refrescar a memória, os princípios são:

  1. Aproximar o algoritmo dos dados
  2. Aumentar o paralelismo
  3. Não estabelecer afinidade
  4. Minimizar contenções
  5. Minimizar o uso de recursos
  6. Pré-alocar e compartilhar recursos caros

Agora, vamos abordar as técnicas normalmente utilizadas nas aplicações atualmente, que fazem uso destes princípios.

Minimizar Interações entre fronteiras

Entre um algoritmo/programa e o SGDB (Banco de Dados) normalmente temos uma interface de rede, nem sempre ambos estarão na mesma máquina. Vamos chamar a interface de rede ou um gateway de acesso de “fronteira”.

Para conseguir aumentar o paralelismo de processos, podemos utilizar múltiplas CPUs diferentes (princípio 2), porém para isso ferimos o princípio 1 (Algoritmo mais perto dos dados). Neste caso, como queremos os processos separados e paralelos, aceitamos o preço de ter uma fronteira em prol do paralelismo, e como sabemos que o uso da fronteira têm um custo, a solução é minimizar as interações, seja diminuindo o número de chamadas, e/ou diminuindo o volume de dados passados para apenas o necessário

Obtenha o mais tarde e libere o mais cedo possível

Esta é uma recomendação bem entendida, mas muito pouco obedecida pelos desenvolvedores. São os Princípios 4 e 5 que pedem que esta regra seja obedecida, mas na maior parte dos casos, o modelo das classes ou do algoritmo não leva em conta a performance. É comum fazer um modelo de classes ou funções, determinar seus métodos, e perder o contexto do uso dos recursos que são utilizados por estes métodos. Logo estamos fazendo o diagrama de interação e não sabemos mais quando começaram as operações que fazem bloqueio de registros no banco de dados ou que simplesmente alocam conexões de um pool. Em consequência, aumentamos o tempo de contenção desnecessariamente, deteriorando o desempenho do sistema em momentos de stress.

Um esboço para um padrão de codificação que busca uma performance melhor deve incluir os seguintes passos:

  • Coletar todos os dados possíveis que não precisem do Banco de Dados (parâmetros, cache, etc);
  • Coletar todos os dados do SGDB (com o mínimo de interações) que não impliquem em lock, necessários para realizar a ação;
  • Realizar os processos só com estes dados;
  • Ir ao banco de dados (com o mínimo de interações) para realizar operações que exigem lock;
  • Não manter a transação aberta por muito tempo, para minimizar contenções e locks.

Minimize o uso de Stored Procedures

Este é um ponto aberto a discussão, depende muito de cada caso. Não é uma regra geral, existem pontos em um sistema onde uma stored procedure realmente faz diferença, mas seu uso excessivo ou como regra geral para tudo impõe outros custos e consequências. O Princípio 1 diria: “use apenas stored procedures”. No entanto, esta decisão pode causar grandes problemas para o Princípio 2 devido à escalabilidade. As Stored procedures têm a vantagem de ser pré-compiladas, e não há nada mais perto de um dado no Banco de Dados.

Porém Bancos de Dados transacionais são especializados em quatro funções: Sort, Merge, gerência de Locks e Log. A gerência de lock é uma das tarefas mais críticas para a implementação de algoritmos distribuídos, e este é o real motivo de existirem poucos Bancos de Dados que possam implementar a escalabilidade horizontal. Se as máquinas de Banco de Dados têm dificuldade de escalar horizontalmente, ela é um recurso escasso e precioso. Temos então que otimizar seu uso para não consumir excessivamente seus recursos a ponto de onerar os demais processos do ambiente. Isto acaba adiando a necessidade de escalar horizontalmente o SGBD.

Utilize Pool de objetos e Cache quando cabível

Os Princípios 1, 5 e 6 nos levam ao uso de Pool e Cache. Muitos dados são lidos com muita freqüência e são raramente modificados (um exemplo é o uso de tabelas de metadados e de certos cadastros básicos). Gastar tempo, banda de rede e CPU do Banco de Dados para trazê-los fere os Princípios 1 e 5. Melhor trazê-los no início (Princípio 6) e garantir que estão locais (Princípio 1).

No entanto, alguns cuidados devem ser levados em conta:

  • Não use espaço demasiado da memória – contradizendo o Princípio 5 para o item memória.
  • Se houver escrita e precisarmos de transacionamento, estaremos incorrendo no problema de sincronização e lock entre vários caches – já que um cache naturalmente introduz afinidade (ver Princípio 3). Se a política de renovar o cache de tempos em tempos não for suficiente, o melhor, neste caso, é usar o Banco de Dados. Deve-se preferencialmente usar o cache para leituras de dados com baixa volatilidade.

Objetos que custam caro para inicializar merecem uma técnica semelhante: o pool. Nele, vários objetos são pré-inicializados, e ficam à espera de chamadas. Uma vez chamados, ficam alocados à tarefa corrente até que, uma vez liberados, retornam ao pool. Esta implementação é conhecida também por pool de processos ou “Working Threads” .

Use, quando possível, técnicas assíncronas

Filas podem ser criadas por bons motivos: seja para manter a consistência de dados (ex.: filas de contenção devido à locks), ou para diminuir o risco do uso indiscriminado de recursos.O uso de técnicas assíncronas, quando permitido pelas regras de negócio, traz algumas oportunidades de otimização no uso de recursos. Com o enfileiramento de mensagens em sistemas de filas (Message Queues) , podemos alocar recursos limitados e suficientes para o tratamento das mensagens. Com isto, poderemos consumir aos poucos os elementos da fila sem aumentar os recursos em momentos de pico.

Por exemplo, disponibilizamos 10 threads para tratamento de um tipo de requisição e não estaremos usando mais recursos caso haja um pico de requisições. O tempo médio do tratamento será maior (devido ao tempo de espera), mas estaremos garantindo um limite no uso dos recursos (Princípio 5), além de permitir por exemplo que operações secundárias decorrentes de um processamento de interface, que normalmente precisa ser o mais rápido possível, possam ser realizados após o processamento principal ter sido completo e seu retorno será mais rápido ao cliente ou solicitante do processo enquanto a etapa secundária será processada na fila quando chegar a sua vez.

Conclusão

Muitas outras técnicas de performance podem ter seu impacto medido de acordo com os princípios apresentados. Meu conselho é tê-los em mente em toda decisão de design a ser feita em um projeto. O balanceamento no uso destes princípios pode significar o sucesso ou fracasso da sua aplicação. Lembre-se que cada caso de uso pode demandar o uso de uma ou mais técnicas, cada caso é um caso.

Lembre-se também que você não precisa de performance optimizada em todos os momentos ou rotinas do sistema. Como a exigência de performance pode levar a técnicas de razoável complexidade, na implementação ou refactoring, você poderá estar onerando em demasia o seu projeto. Por isto, estabeleça na fase de requerimentos os pontos críticos de performance a serem obtidos. Com isto, você poderá priorizar questões como reaproveitamento e facilidade de manutenção e utilizar técnicas de performance onde realmente necessário.

Até o próximo post 🙂

2 respostas em “Escalabilidade e performance – Técnicas

  1. Pingback: Escalabilidade e Performance – Stored Procedures | Tudo em AdvPL

  2. Pingback: Escalabilidade e Performance – Parelelismo – Parte 01 | Tudo em AdvPL

Deixe um comentário