Upload
paulo-jose-lopes-bovo
View
212
Download
0
Embed Size (px)
DESCRIPTION
Arquitetura de Software-1
Citation preview
Arquitetura de Software
Arquitetura: Princípios para alcançar Desempenho e Escalabilidade em AplicaçõesOtavio Pecego CoelhoArquiteto de Soluções
DPE - Microsoft Brasil
Novembro - 2004
IntroduçãoTrabalhando na Consultoria da Microsoft Brasil tive por várias vezes a oportunidade de lidar com
problemas de performance e escalabilidade em aplicações. Muitas vezes conseguimos avanços
espantosos com pequenas modificações no código ou estrutura dos módulos dos programas.
Hoje, como Arquiteto de Soluções da Microsoft, sou muitas vezes chamado para explicar de forma
concisa como conseguir boa performance e escabilidade com a tecnologia .Net. Neste exercício,
me dei conta que com alguns poucos princípios gerais conseguimos explicar muitas das boas
práticas de desenho e arquitetura que levam a bons resultados.
Este artigo apresenta estes princípios que considero como os mais básicos e enumera algumas das
suas conseqüências tanto para o design quanto para a implementação de aplicações.
Início da página
PrincípiosSão seis os princípios básicos que encontrei e que podem ajudar na decisão relativa à escolha de
uma tecnologia ou padrão de design.
Um ponto importante, que logo vocês irão notar, é que alguns princípios são conflitantes. Isto
acontece porque estamos tentando alcançar dois objetivos ao mesmo tempo: desempenho e
escalabilidade.
O desempenho está relacionado ao quão rápido uma tarefa computacional pode ser executada.
Idealmente, o desempenho não trata da questão do limite físico de uma máquina (seja CPU,
memória, disco, etc). Algoritmos podem ser classificados de acordo com a sua estrutura interna.
De acordo com este ponto de vista, minimizar o número de passos de uma computação é o
objetivo prioritário.
A escalabilidade, por sua vez, incorpora os limites físicos à questão do desempenho. Como se
comporta o desempenho quando atingimos os limites físicos das máquinas, e que estratégias
devemos utilizar para aumentar a quantidade de processamento disponível.
Duas são as linhas mestras para atingir maior escalabilidade: o incremento com novos hardwares
ou a substituição por novos hardwares de maior desempenho.
O primeiro é denominado escalabilidade horizontal. O exemplo típico é um farm Web, onde
podemos incorporar novas máquinas ao farm para dar conta do aumento da demanda pelos
usuários finais.
O segundo é a escalabilidade vertical, onde uma nova máquina mais possante substitui a antiga.
Na relação entre custo e benefício, é mais comum que a escalabilidade horizontal ganhe, não
só devido aos custos reais ($$), mas também devido aos benefícios de segunda ordem, como a
melhoria da disponibilidade.
Juntar estas duas perspectivas (desempenho e escalabilidade) é parte da arte de um arquiteto.
Neste exercício de equilíbrio, surgiram as arquiteturas cliente-servidor, 3-camadas, n-camadas,
uso de caching, monitores transacionais, etc.
Interessante notar que todas estas arquiteturas foram estruturadas de acordo com alguns
princípios básicos. Dentre eles, os que entendo serem mais relevantes são:
• Aproximar Algoritmos aos Dados
• Aumentar o Paralelismo
• Não Estabelecer Afinidade
• Minimizar Contenções
• Minimizar o Uso de Recursos
• Pré-alocar e Compartilhar Recursos Caros
Abaixo descrevo o que são e a motivação de cada um destes.
Princípio 1: Aproximar Algoritmos aos 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 poderá ser comparativamente imenso.
Este princípio já é utilizado há muito pelos arquitetos de hardware. CPUs costumam ter
registradores e estes têm velocidade de acesso muito maior do que a do acesso à memória RAM.
Parte do trabalho dos otimizadores dos compiladores modernos é o de alocar corretamente as
varáveis de um programa nos registradores da CPU.
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.
Podemos imaginar uma rede de alta velocidade, mas se o protocolo entre camadas for
inadequado, parte do benefício é roubado.
Princípio 2: Aumentar o Paralelismo
Aumentar o paralelismo nem sempre é factível - mas costuma ser factível na maioria das vezes. O
princípio é simples: quanto mais hardware você conseguir manter trabalhando ao mesmo tempo,
maior performance você vai ter.
Hoje, nossos computadores de casa e do escritório já são multi-CPUs e fazem isto para aumentar a
performance. Unidades de processamento aritmético, placas gráficas, controladoras de discos
rígidos, hyper-threading, etc. Os engenheiros estão sempre encontrando maneiras de paralelizar
processamento para ganhar desempenho.
Nesta perspectiva, a grande dificuldade para a programação é evitar que o algoritmo se torne
muito complexo.
O interessante é que a pesquisa sobre Processamento Transacional nas últimas 3 décadas facilitou
enormemente a implementação do paralelismo entre tarefas. Bancos de Dados é O grande
exemplo aqui. Hoje programamos nossas tarefas de negócio assumindo naturalmente que muitas
instâncias destas tarefas estarão sendo executadas ao mesmo tempo porque deixamos o controle
da concorrência para o Banco de Dados. Ao utilizar esta técnica, aumentamos potencialmente o
poder de escalabilidade horizontal, já que novas máquinas poderão estar sendo incorporadas para
realizar mais tarefas em paralelo e, portanto, aumentando no tempo o número total de tarefas que
podem ser realizadas.
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. Mas isto é tema para os dois próximos princípios.
Princípio 3: 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.
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. Estaremos vendo o porquê disto mais adiante. O uso de cache local nos
clientes do Banco é outra técnica possível.
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).
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.
Princípio 4: 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 do locking 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 schedulers que reordenam seqüências de operações conflitantes (que é o que
acontece num Banco de Dados), muitas vezes temos que ajudar com esta ordenação também em
nossos algoritmos - como veremos mais adiante.
Princípio 5: 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 locks de linhas de tabelas de banco de dados
serem feitas 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.
Princípio 6: 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, etc).
Princípio 7: Manter a Corretude e Simplicidade
Sim, este princípio não estava listado. Nem seria preciso caso não tivéssemos a mania de, por
vezes, procurar mais paralelismo e desempenho esquecendo-nos da corretude e simplicidade.
Nossos sistemas devem ser corretos - ponto!
A simplicidade nos ajuda a mantê-los corretos e com um custo aceitável. Realizamos isto via
abstrações - como tarefas, monitores de transação, etc.
Princípio 8: Use um Algoritmo e Estruturas de Dados Eficientes
Este também não estava listado... É óbvio, mas infelizmente ainda existe pouco conhecimento das
técnicas de análise da complexidade de algoritmos entre os desenvolvedores que tenho
conhecido. É freqüente o uso de estruturas de dados e algoritmos ineficientes. Existem boas
referências sobre este assunto, embora recheadas de matemática pesada...
Início da página
Técnicas para Performance e EscalabilidadeOs princípios aqui listados implicam em conseqüências de design. Como exercício, comento abaixo
algumas práticas atuais que são decorrentes do uso destes princípios.
Técnica 1: Minimize Interações entre Fronteiras
Fronteiras são caras (Princípio 1) mas podem escalar horizontalmente pois, muitas vezes,
permitem a possibilidade do aumento do paralelismo em processos ou CPUs diferentes (Princípio
2).
Este é um caso típico de conflito entre princípios, mas onde aceitamos pagar o preço de ter
fronteiras (o que significa retardos) para ganhar paralelismo.
Como queremos o benefício de processos em separado, e como sabemos que existe um custo
associado, a solução é minimizar as interações, seja minimizado o número de chamadas, e/ou
minimizando o volume de dados passados.
O bom uso da primeira técnica é quando em uma única chamada passamos todos os parâmetros
necessários para que a outra parte faça o seu trabalho e quando esta devolve apenas os dados
necessários do resultado. Nada de muitas idas e voltas de parâmetros e resultados devido a
chamadas intensas de métodos e acesso a propriedades.
Um exemplo de dados necessários está no retorno consciencioso das colunas realmente utilizadas
pelos clientes, no caso de uma query ao banco de dados.
Outro exemplo de conflito entre princípios está no retorno de um grande result set. Embora isto
minimize idas e vindas, esta técnica vai contra o princípio 5, pois causa excesso de uso de
memória e rede. Melhor usar a técnica de paginação já no acesso ao banco de dados.
Técnica 2: Evite Referências para objetos Remotos
Aqui estamos no famoso caso do stateless contra stateful. Referências a objetos remotos são o
caso do stateful e implicam em afinidade (o que vai contra o Princípio 3). Além disso, como os
objetos remotos ficam presos na memória esperando sua liberação pelo cliente, o uso de
referências vai contra o Princípio 5.
Técnica 3: Obtenha o mais tarde e Libere o Mais Cedo
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
designer das classes ou do algoritmo não leva em conta a performance. É comum fazer um
modelo de classes, 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 lock no banco de dados ou que simplesmente alocam
conexões de um pool. Em conseqüê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 garanta uma performance melhor é:
1. Coletar todos os dados possíveis que não precisem do Banco de Dados (parâmetros, cache,
etc);
2. Conectar com o Banco;
3. Coletar todos os dados (com o mínimo de interações) que não impliquem em lock, necessários
para realizar a ação;
4. Realizar contas e procedimentos possíveis de serem feitas só com estes dados;
5. Ir ao banco de dados (com o mínimo de interações) para realizar operações que exigem lock;
6. Liberar a conexão;
Técnica 4: Minimize o uso de Stored Procedures
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.
Stored Procedures têm várias vantagens. Primeiro, são pré-compiladas - o que significa menos
processamento. Segundo, não há nada mais perto de um dado no Banco de Dados.
No entanto, Banco de Dados são Monitores de Transação muito especializados em quatro funções:
Sort, Merge, gerência de Locks e Log para recuperação em caso de acidentes.
De todos estes, a gerência de Lock é uma das tarefas mais críticas para a implementação de
algoritmos distribuídos. Este é o real motivo de existirem poucos Bancos de Dados que possam
implementar a escabilidade horizontal. A gerência de Locks pede memória compartilhada
(Princípio 1) o que impede arquiteturas distribuídas (à menos que o problema de intercomunicação
seja minimizada - o que estão tentando fazer com HPC - High Performance Computing).
O que isto tem a ver com stored procedures? Bem, 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 que ela faça apenas o que sabe fazer bem.
Daí o conselho freqüente de deixar de fora do banco toda a manipulação de strings, cálculos e
decisões de negócio e implementar em stored procedures os comandos mais básicos. Como estão
em stored procedures, não são recompiladas. Como fazem apenas o que os Bancos melhor sabem
fazer, utilizamos melhor o recurso e adiamos, ao final, a necessidade de escalar verticalmente.
Técnica 5: Utilize Pool de objetos e Cache
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 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, dois cuidados devem ser levados em conta:
1. Não use espaço demasiado da memória - contradizendo o Princípio 5 para o item memória.
2. Se houver escrita e precisarmos de propriedades ACID, 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.
Objetos que custam caro para inicializar merecem uma técnica semelhante: o pool. Nele, varos
objetos são pré-inicializados e fica à espera de chamadas. Uma vez chamados, ficam alocados à
tarefa corrente até que, uma vez liberados, retornem ao pool.
Técnica 6: 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 (como o MSMQ), 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. 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)
Início da página
ConclusãoMuitas outras técnicas de performance podem ter seu impacto medido de acordo com os princípios
apresentados. O texto Improving .NET Application Performance and Scalability
(http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnpag/html/scalenet.asp) é uma
referência bem completa sobre este assunto. Meu conselho é tê-los em mente em toda decisão de
design a ser feita em um projeto. O balanceamento destes princípios pode significar o sucesso ou
fracasso da sua aplicação.
Lembre-se também que você não precisa de performance em todos os momentos. Como a
exigência de performance pode levar à técnicas de razoável complexidade, 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 reuso e
manutenabilidade e utilizar técnicas de performance em situações realmente necessárias.
Início da página
Versão para Impressão Enviar esta Página Adicionar a Favoritos
Fale Conosco |Imprima esta página |Adicione aos Favoritos©2005 Microsoft Corporation. Todos os direitos reservados. Nota Legal |Marcas comerciais |Política de Privacidade