Arquitetura de Softwar1

7
Arquitetura de Software Arquitetura: Princípios para alcançar Desempenho e Escalabilidade em Aplicações Otavio Pecego Coelho Arquiteto de Soluções DPE - Microsoft Brasil Novembro - 2004 Introdução Trabalhando 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ípios Sã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.

description

Arquitetura de Software-1

Transcript of Arquitetura de Softwar1

Page 1: Arquitetura de Softwar1

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

Page 2: Arquitetura de Softwar1

• 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.

Page 3: Arquitetura de Softwar1

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

Page 4: Arquitetura de Softwar1

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.

Page 5: Arquitetura de Softwar1

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

Page 6: Arquitetura de Softwar1

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