Escalabilidade e Performance

Para alguns desenvolvedores, falar sobre estes assuntos é como chover no molhado. Mas ainda existem muitos profissionais da TI que fazem confusão com estas duas terminologias. Escalar e performar são duas coisas distintas, e não necessariamente simples de serem colocadas juntas, mas é muito desejável, e até um diferencial competitivo importante, quando uma solução em informática, processo ou sistema possua ambas características simultaneamente.

Existem inúmeros artigos que abordam este tema, e explicações em diferentes níveis de detalhamento. Vou tentar colocar de uma forma bem simplista: Desempenho está relacionado a velocidade de um processo, o quão rápido é entregue o resultado de um processamento, a escalabilidade indica a capacidade do sistema ou processo de manipular uma porção crescente de trabalho de forma uniforme, ou estar preparado para crescer, verticalmente (melhorando o desempenho de recursos computacionais de um servidor) ou horizontalmente (acrescentando mais recursos computacionais ou computadores), para dar conta de processar volumes maiores de informações mantendo o desempenho.

Como nem tudo é tão simples quanto a explicação do manual, um sistema heterogêneo possui módulos e etapas de processamento que não são escaláveis horizontalmente (não dá para reduzir a gestação de um bebê de nove para um mês usando nove mulheres), mas possuem pontos em comum que podem ser optimizados ou repensados para trabalharem de forma optimizada.

Existem mais grandezas sistêmicas do que as citadas aqui, mas para efeitos práticos estas quatro são suficientes: CPU (processamento), Memória (volátil,RAM), Disco (armazenamento não-volátil) e Rede (tráfego de informações entre os componentes do sistema). Cada um destes componentes possui um limite: Velocidade (Clock do processador, velocidade de leitura e escrita, velocidade de transmissão e recepção).

Enquanto nenhum limite de consumo de algum destes recursos foi atingido, e as rotinas de um sistema informatizado não concorrerem entre si por pelo mesmo recurso em modo exclusivo, tudo é executado na velocidade máxima que o ambiente computacional pode oferecer. Quanto mais leves, ou seja, quanto menos recursos de cada componente um processo consome, naturalmente eles ficarão mais leves e mais rápidos.Nesta perspectiva, abordamos as questões de desempenho ou performance.

Quando nos utilizamos de múltiplos processos, para atender a requisições de múltiplos usuários em um sistema complexo, mais cedo ou mais tarde algum limite de algum recurso será atingido, e isto vai afetar o tempo de resposta de uma ou mais rotinas, ou em casos mais extremos inviabilizar o uso de determinadas rotinas, ou até mesmo levar o sistema a um estado de colapso. Neste ponto abordamos a escalabilidade, onde é desejável que o sistema seja escalável horizontalmente, permitindo acrescentar mais recursos computacionais ao ambiente e apresentando um desempenho proporcional aos recursos acrescentados. Na escalabilidade, avaliamos o comportamento sistêmico quando um limite é atingido, prejudicando o tempo de resposta de um ou mais processos.

Baseado em uma publicação muito interessante, do arquiteto de soluções da Microsoft Otávio Coelho (ver referências bibliográficas no final do post), existe uma lista de princípios aplicável normalmente a grande maioria dos sistemas computacionais, independentemente da linguagem ou ambiente onde os mesmos foram desenvolvidos, que ao ser observada e seguida, traz excelentes resultados:

  • Aproximar o algoritmo dos dados
  • Aumentar o paralelismo
  • Não estabelecer afinidade
  • Minimizar contenções
  • Minimizar o uso de recursos
  • Pré-alocar e compartilhar recursos caros

Tudo isso não é exatamente novidade, muitos destes princípios são utilizados por aplicações de mercado escaláveis, através da aplicação de técnicas e modelos de desenvolvimento. Um cache, um pool de conexões ou recursos, “Working Threads”, balanceamento de carga, proxy reverso e outros mecanismos desta natureza foram criados e são usados justamente para possibilitar a aplicação dos princípios acima.

A aplicação de cada princípio mencionado acima requer o entendimento da motivação da existência do princípio, o peso e o custo de cada um dentro do contexto da rotina ou sistema que está sendo desenvolvido. Nem sempre é possível ou compensa aplicar todos os princípios simultaneamente em todas as partes de um sistema. Eles não devem ser vistos como uma tábua de mandamentos, como “atenda a todos estes princípios e seu sistema será mágico”, mas devem ser vistos como alternativas a serem levadas em conta para componentes ou partes de um sistema que necessitem atender a determinados requisitos de processamento e disponibilidade. Nos próximos tópicos vamos ver cada um dos princípios acima e sua motivação.

  • Aproximar algoritmo dos Dados

Dados e algoritmos (que operam sobre estes dados) devem estar o mais próximo possível. Se estiverem distantes, o tempo de acesso aos dados se deteriora, causando um desempenho pior. O lugar mais próximo entre os dois ocorre dentro da mesma máquina e do mesmo processo. Se o dado estiver em disco, já existe retardo – que é considerável em relação à memória local. Se estiver em outra máquina interligada via rede, o retardo será comparativamente imenso.

Velocidade e latência são os dois atributos principais. Quanto mais veloz o meio para o acesso e quanto menor a latência, melhor. A velocidade pode ser a da luz, mas se a latência devido a distância for muito grande, não temos desempenho.

  • Aumentar o paralelismo

Aumentar o paralelismo nem sempre é factível – mas costuma ser na maioria das vezes. O princípio é simples: quanto mais hardware você conseguir manter trabalhando ao mesmo tempo, maior performance você vai ter. Nesta perspectiva, a grande dificuldade para a programação é evitar que o algoritmo se torne muito complexo.

Implementar o algoritmo tendo em mente e planejando para que uma tarefa possa ser executada simultaneamente aumenta significativamente a escalabilidade horizontal de uma rotina, pois podemos acrescenta mais hardware e recursos para mais tarefas serem realizadas ao mesmo tempo. Este é o caso ótimo – não o realista. Afinidade e Contenção são dois pontos que atrapalham o grau de paralelismo que podemos alcançar.

  • Não estabelecer afinidade

Afinidade é o nome a que se dá quando certa tarefa fica amarrada a um lugar físico. Por exemplo, se apenas uma máquina possuir um recurso (por exemplo, o Banco de Dados), todos seus usuários estarão amarrados à esta máquina. Isto causa problemas de contenção no acesso ao recurso (todos competem pela rede, CPU, memória e disco do Banco de Dados), quando poderíamos pensar no uso de um pool de recursos (conjunto de Bancos de Dados replicados em máquinas diferentes) para diminuir esta contenção e aumentar o paralelismo. Isto pode aumentar o custo ou a complexidade da manutenção e configuração do sistema

Este exemplo do Banco de Dados em várias máquinas não é usual, devido à dificuldade de implementá-lo. Quando implementado, a técnica normalmente usada é a da fragmentação de tabelas ou replicação. O uso de cache local nos clientes do Banco para uma fração dos dados é outra técnica possível e bem eficiente.

Note que podemos aumentar o paralelismo e ainda manter a afinidade. Por exemplo, podemos imaginar o aumento de máquinas para processamento de regras de negócio, mas mantendo a afinidade ao servidor de Banco de Dados. Se o limite do Banco não for atingido rapidamente, esta estratégia de paralelismo pode ser muito eficaz (sendo, creio eu, a técnica mais utilizada hoje em dia). Escala-se o banco de dados verticalmente, e a aplicação horizontalmente.

Outra técnica usual de paralelismo é o pipeline, onde cada máquina ou tarefa executa uma parte do processamento. A afinidade é total, mas a performance pode ser excelente dependendo da estrutura do problema/solução. Seguindo a mesma linha de não afinidade, desenvolver a aplicação baseada em S.O.A. (Service Oriented Architecture) também é uma escolha interessante.

  • Minimizar contenções

Contenção significa aguardar numa fila para poder ser servido. Existem várias filas na execução de tarefas em uma arquitetura moderna. Filas de disco, filas de espera da liberação de bloqueio de certos registros ou tabelas do Banco de Dados, etc.

Contenções são difíceis de serem tratadas e podem causar deadlocks. Embora muitas ferramentas implementem orquestradores de tarefas que reordenam sequências de operações conflitantes (que é o que acontece num Banco de Dados), muitas vezes temos que ajudar diretamente com esta ordenação em nossos algoritmos.

  • Minimizar o uso de recursos

Recursos serão sempre escassos a partir de certo número de usuários e tarefas concorrentes – seja CPU, memória, disco, rede, etc. Normalmente não nos damos conta de que estamos usando recursos demais nos nossos algoritmos. Por exemplo, costumamos passar mais parâmetros do que deveríamos em nossas chamadas a métodos e procedimentos – tudo em nome da generalidade. Outro exemplo é o total descuido que temos quanto ao tempo em que alocamos um recurso.

Já perdi a conta do número de vezes que observei bloqueio de linhas de tabelas de banco de dados serem feitos muito antes do uso real dos valores lidos – aumentando o tempo de contenção de outras tarefas. É comum também ver abuso no armazenamento de dados em variáveis de sessão (uso de memória) ou no envio de conjuntos de dados imensos entre camadas da aplicação.

Um último exemplo e muito comum é o abuso no volume de dados retornado por uma query de banco de dados. Quando fazemos isto estamos gastando tempo, rede e memória. Todos estes são exemplos da nossa tendência natural a gastar mais do que precisamos – o que fará falta em condições de stress. Quanto mais leve, mais rápido, e menos recursos do sistema serão consumidos, na prática isto significa que você pode ter uma quantidade maior de processos na mesma máquina antes de atingir o limite de algum recurso.

  • Pré-alocar e compartilhar recursos caros

Recursos caros devem ser pré-alocados e compartilhados. Recursos podem ter um tempo muito longo de construção (inicialização), como conexões de banco de dados ou rede. Tê-los construídos antes da hora do uso é parte do truque para obter melhor desempenho.

Porém, nem sempre estes recursos estarão em uso. Por que não deixá-lo descansando para a próxima vez? Este é um dos princípios do uso da técnica de Cache ou pool de recursos (pool de objetos, conexões, pool de Jobs dedicados ou Working Threads).

Conclusão

Neste post entramos no detalhamento da definição e conceito de escalabilidade e performance. No próximo post sobre este assunto, vamos abordar algumas técnicas que envolvem a aplicação de um ou mais destes princípios em conjunto, exemplificando a sua apicação no mundo real.


Lei de Amdahl  : “O ganho de desempenho que pode ser obtido melhorando uma determinada parte do sistema é limitado pela fração de tempo que essa parte é utilizada pelo sistema durante a sua operação.”


Referências

Anúncios

2 comentários sobre “Escalabilidade e Performance

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s