Fundamentos para arquitetura de sistemas escaláveis

Sorte é o que acontece quando a preparação encontra a oportunidade.
Sêneca

A google define boa engenharia de software como a capacidade de suportar as mudanças de escala, ao longo do tempo, preservando a eficiência. A arquitetura adequada é fundamental para que sistemas “escalem bem”.

Embora frequentemente mencionada, a escalabilidade nem sempre é devidamente tratada nas arquiteturas. A consequência grave disso é a incapacidade de atender o crescimento inesperado do workload e, quando não há componentes adequados de retenção, instabilidades.

Arquiteturas não planejadas para a escalabilidade – onde todas as esperanças para suportar aumentos súbitos de demanda ficam depositadas na utilização de load balancers ou na contratação de recursos na nuvem – se convertem rapidamente em custo. Pior que isso, algumas vezes, nem mesmo “despejar dinheiro” é suficiente.

Antes de falar sobre escalabilidade, disponibilidade

De pouco importam as features de um sistema se elas não estiverem disponíveis quando os usuários precisam.

Um sistema está disponível quando está funcionando conforme esperado com desempenho adequado. Um serviço, por exemplo, está disponível quando responde corretamente, em um response time igual ou menor do que o acordado.

Se o tempo de resposta de um serviço for maior do que o acordado, mesmo que este responda corretamente, não poderá ser considerado disponível.

A fórmula para calcular a disponibilidade de um sistema é:

Onde Tu representa em uptime, quando tudo está funcionando com desempenho adequado, e Td representa o downtime, quando o comportamento do sistema está incorreto ou o desempenho é insuficiente.

Manutenções programadas e paradas planejadas devem ser consideradas tempo em downtime, logo afetando a disponibilidade.

Definindo escalabilidade (e elasticidade)

Um sistema é escalável quando consegue suportar variações nos workloads – aumentos ou reduções -, mantendo-se disponível, com ajustes proporcionalmente favoráveis nos recursos alocados. Ou seja, um sistema web, por exemplo, será escalável se conseguir suportar o dobro de usuários ativos, no máximo, dobrando a infra.

Quanto mais favoráveis forem as demandas de alocação de recursos frente ao aumento das cargas, melhor será a escalabilidade de um sistema.

As mudanças nos recursos podem acontecer pelo scaling up/down (vertical) – a atualização dos dispositivos computacionais utilizados em termos de memória, processador, etc – ou pelo scaling out, modificando a quantidade de dispositivos computacionais.

A escalabilidade horizontal é preferível a vertical pois mitiga riscos de indisponibilidade.

A diferença principal da escalabilidade para a elasticidade é que a segunda acontece de forma rápida e dinâmica, incrementando recursos quando o workload cresce ou decrementando quando diminui. A elasticidade é extremamente importante na nuvem, quando o investimento é determinado pelo uso.

A necessidade da escalabilidade (e elasticidade)

No passado, sistemas eram desenvolvidos para serem utilizados em contextos bem demarcados. Tanto volume de dados quanto taxas de mudança eram características relativamente fáceis de serem previstas. Entretanto, em tempos de transformação digital,  tudo mudou.

Hoje, as empresas usam tecnologia para estreitar relações com clientes, fornecedores e parceiros – sistemas de software inicialmente desenvolvidos para utilização dentro das empresas, estão “vazando” para uso externo. Dessa forma, aumentos significativos de workloads tem acontecido rapidamente e, não raro, de maneira inesperada.

O impacto da escalabilidade no custo

Planejar um sistema para escalar implica em priorizar também disponibilidade e desempenho, impactando diretamente o custo.

Suportar disponibilidade implica em aplicar redundância como forma de suportar manutenções previstas ou dificuldades imprevistas.

Quase todos os padrões e estilos arquiteturais voltados para a escalabilidade também exercem pressão de custos de deploy e operação para, por exemplo, suportar orquestração e mensageria.

Escalar é a “arte de gerenciar gargalos”

Em todo sistema que precisa escalar, há sempre, um elemento que oferece a restrição. Não raro, esse recurso é o banco de dados.

O trabalho da arquitetura de software, nessa perspectiva, é identificar esse gargalo e adotar estratégias de mitigar riscos inerentes dele.

Usando Teoria das Restrições para melhorar a escalabilidade e performance de sistemas

A teoria das restrições é frequentemente associada a gestão fabril, mas tem, também aplicação direta para a arquitetura de software. Nesse vídeo, explico com mais detalhes como usar teoria das restrições para projetar sistemas escaláveis.

Acessar vídeo

A teoria das restrições nos ensina que há três medidas para tratar gargalos:

  1. elevar a capacidade de vazão – seja com otimização, scale up ou scale out – “quebrando” a restrição;
  2. subordinar toda a arquitetura aos limites do gargalo, estabelecendo alguma espécie de rate limiting
  3. admitir a formação de um “pulmão” maior, frente a restrição, tornando todas as suas interfaces assíncronas.

A segunda “solução”, na prática, raramente é aplicável pois restringe as capacidades do sistema de suportar a nova escala e deve ser adotada, apenas para preservar a estabilidade.

De qualquer forma, é importante lembrar que sempre que um gargalo é “quebrado”, emerge um outro gargalo. Por exemplo, “escalar” servidores de aplicação que não estão conseguindo suportar a demanda, frequentemente move o gargalo para o “cache” ou para o banco. Então, ao “escalar” estes componentes, a restrição volta a ser, com muita frequência os servidores de aplicação. O ponto importante é: ajustar a arquitetura para suportar a escala implica em, sempre, fazer análise holística – a força de uma corrente é sempre determinada pelo seu elo mais fraco.

Escalar é a “arte de gerenciar filas”

Na medida que identificamos, em nossos sistemas, componentes com diferentes níveis de capacidade para processamento, é necessário reconhecer que, eventualmente, “fazer o todo funcionar” implica em tratar adequadamente as filas que se formam.

A necessidade, por exemplo, de escalar na horizontal um servidor de aplicação é demonstrada quando a ritmo de chegada de novas requisições é maior de que a ritmo de atendimento.

Segue algumas ideias fundamentais para começarmos a avaliar filas em sistemas:

  • Servidor – componente(s) responsável por atender algum tipo de requisição;
  • Cliente – componente de onde partem requisições
  • Tempo de espera (Wait time) – Tempo em que um “cliente” aguarda em uma fila para ter sua demanda atendida
  • Tempo de serviço (Service time) – Tempo de “trabalho” de um componente servidor gerando a resposta para a requisição do cliente
  • Tempo de resposta (Response time) – soma dos tempos de espera e de serviço
  • Ritmo de chegada (arrival rate) – ritmo de chegada de novas requisições na fila
  • Ritmo de serviço (service rate– ritmo de “atendimento” das demandas (considerando apenas o tempo em “trabalho” dos componentes servidor)
  • Utilização (utilization) – proporção entre “tempo trabalhando” e “tempo total” de cada componente servidor
  • Tamanho da fila (queue length) – número total de requisições na fila
  •  Throughput – Quantidade de requisições atendidas em um determinado intervalo de tempo.

Um sistema será escalável quando conseguir preservar tempos de resposta ou throughput dentro de parâmetros aceitáveis, mediante ajuste proporcional favorável entre componentes servidores e o ritmo de chegada de requisições.

Boas medidas visando a melhoria da escalabilidade buscam:

  • Minimizar o tempo de espera, aperfeiçoando a eficiência dos componentes “servidor”, disponibilizando mais instâncias de suas instâncias, ou minimizando o volume de requisições por parte dos componentes “cliente”.
  • Minimizar o tempo de serviço, através do aperfeiçoamento do desempenho dos componentes “servidor”.

Leis que governam a escalabilidade

Há duas leis que “governam” a escalabilidade: Amdahl e Ghunter. Na prática, essas leis ensinam que um sistema terá mais potencial para escalabilidade na medida em que tiver menos partes sequenciais ou demandas por coerência.

Todas as ações para melhoria da escalabilidade visam reduzir os tempos de contenção e atrasos para coerência.

Lei de Amdahl

lei de Amdahl, é usada para encontrar a máxima melhora possível no desempenho de um sistema (throughput) como um todo, para um determinado processamento,  quando apenas uma única parte do trabalho pode ser paralelizada.

Argumento de Amdahl

O speedup de um processo usando múltiplos agentes paralelos é limitado pelo tempo necessário para completar parte não paralelizável deste processo.

Segundo a lei, por exemplo, um processamento de 20 horas, onde 1 hora de trabalho é não paralelizável, restringe o tempo mínimo a essa 1 hora. Ou seja, o speedup máximo será de 20 vezes.

O tempo não paralelizável de um processo é chamada de “tempo de contenção”.

Lei de Ghunter

Além da contenção, o atraso da coerência é outro fator que afeta as melhorias.

Atraso da coerência

Tempo demandado, em um sistema distribuído com diversos nós quem precisam compartilhar um estado coerente (mesmo nível de atualização), para que todos os nós atinjam coerência.


Em um cluster de banco de dados, por exemplo, é o tempo para que alterações realizadas em um nó sejam replicadas para os demais.

Basicamente, a lei de Ghunter postula que o aumento de nós com demanda de coerência, em um sistema, aumenta o atraso da coerência. Eventualmente, anulando os efeitos da adição de nós de processamento.

Argumento de Ghunter

Na medida em que adotamos scale up em um componente de um sistema que demande coerência, os custo de comunicação e coordenação aumentam até eventualmente superar quaisquer benefícios adjacentes.

Três estratégias genéricas para arquiteturas escaláveis (segundo a AKF)

Projetar sistemas escaláveis demanda organização e estrutura. Dentre os métodos mais comuns para obter sistemas escaláveis, destacam-se três estratégias genéricas previstas no “cubo da escalabilidade”.

O “cubo da escalabilidade”, proposto pela consultoria AKF,  é um modelo consolidado para segmentação de sistemas em serviços que cria uma linguagem comum para que equipes discutam opções relacionadas à escalabilidade no projeto de soluções.

O modelo prescreve três estratégias para escalabilidade:

  1. Escala X – Escalar na horizontal
  2. Escala Y – Decomposição em serviços
  3. Escala Z – Decomposição por sharding ou podding

As três estratégias podem ser aplicadas de maneira independente ou combinada.

Escala X – Escalar na horizontal

A primeira estratégia prevista no “cubo da escalabilidade” consiste em clonar instâncias de aplicações ou replicar bases de dados, consumidas através de um load balancer.

Trata-se de uma abordagem simples, relativamente rápida de implementar, que depende da “eliminação de recursos compartilhados”. Aplicável sobretudo quando a taxa de leituras e gravações seja de 5:1 ou mais.

Escala Y – Decomposição em serviços

A segunda estratégia prevista no “cubo da escalabilidade” consiste em decompor aplicações em serviços e conjuntos de dados independentes, por características funcionais.

Trata-se de uma abordagem significativamente mais complexa e lenta para implementar. Entretanto, tem vantagens de permitir também o crescimento da organização (em função da lei de Conway) e isolamento de falhas (melhor resiliência).

Escala Z – Decomposição por sharding ou podding

A terceira estratégia prevista no “cubo da escalabilidade” consistem em decompor aplicações, segmentando demanda em recursos dedicados. A ideia simples é utilizar um mesmo bloco de código para processar subconjuntos (ou shards) de dados.

Considerações fundamentais sobre caching

É praticamente impossível falar sobre escalabilidade sem, em algum momento, falar sobre caching.

Estratégias genéricas para escalar software de poucos usuários para milhões

Nesse vídeo mostro como caching, combinado com estratégias de scaling out, tem importância fundamental na jornada para a escalabilidade.

Acessar vídeo

Cache é uma forma de armazenamento de dados intermediário (hardware ou software) que pode servir seus dados mais rápido do que a fonte de dados original (banco de dados, serviço da web, etc.).

O armazenamento em cache é essencial porque pode melhorar o desempenho do aplicativo significativamente, economizando o tempo dos usuários e o dinheiro da empresa. Entretanto, com certeza, introduz uma nova categoria de complexidades arquitetônicas e de implementação (onde complexidade significa custo). Mas, se um aplicativo depende muito de rede, disco e outros recursos lentos, uma estratégia de cache adequada pode salvar o dia.

Quando cache pode ajudar

A adoção de caching pode ajudar consideravelmente a melhorar a performance e a escalabilidade de um sistema de software quando é necessário:

  • acessar recursos hospedados externamente (em outro contexto de hospedagem);
  • recuperar recursos que não mudam com frequência;
  • recuperar representações dos mesmos recursos continuamente;
  • produzir a mesma saída para demandas de computação frequentemente;
  • executar cálculos de agregação extensos múltiplas vezes.

Onde implementar caching

Para aplicativos cliente-servidor, um cache pode ser:

  • do lado do cliente , implementado no cliente (por exemplo, o navegador)
  • lado do servidor, implementado no servidor
  • em algum lugar no meio (CDN)

Em cenários distribuídos, o cache pode ser:

  • local , na mesma máquina (nó) onde o aplicativo está rodando
  • remoto , em outro computador, acessível através da infraestrutura de rede.

Finalmente, sempre que o cache é implementado localmente, pode ser:

  • em processo , executando no mesmo processo do sistema operacional do aplicativo de consumo (na memória)
  • fora do processo , em execução em outro processo do sistema operacional.

Existem prós e contras para cada um desses tipos. A opção certa depende do contexto.

Cuidado com o GC!

Código, tanto em .NET quanto em Java, é dito gerenciado. Isso significa que programadores têm a liberdade de programar sem codificar a “desalocação” de memória. Isso, entretanto, não significa que não seja necessário se preocupar com isso.

De forma simplificada, de tempos em tempos o runtime interrompe a execução do sistema para “coletar” objetos que não estejam mais referenciados. A frequência e a duração das interrupções está relacionada com a “voracidade” do sistema para criar objetos que possam ser descartados. Objetos que “sobrevivem” a coletas acabam, pelas heurísticas dos coletores, permanecem mais tempo na memória, podendo gerar pressão sobre os sistemas.

Objetos em cache local, no processo, podem causar “confusão” ao funcionamento do GC. Por isso, esta abordagem precisa ser criteriosa.

Estratégias genéricas para “hidratar” o cache

Existem duas estratégias comuns para gravar dados em um cache:

  1. Pré-caching de dados, para pequenos pedaços de dados, geralmente durante a inicialização do aplicativo, antes de qualquer solicitação.
  2. Sob demanda, verificando primeiro se os dados solicitados estão no cache (se os dados forem encontrados, é chamado de acerto de cache), utilizando-os, melhorando o desempenho da aplicação. Sempre que os dados solicitados não forem gravados no cache (perda de cache), o aplicativo precisará recuperá-los da fonte mais lenta e, em seguida, gravar os resultados no cache, economizando tempo nas solicitações subsequentes dos mesmos dados.

Caching no Netflix

Um dos “segredos” do Netflix para suportar a escala é o uso intensivo de caching.

A empresa mantém um CDN próprio, replicando seu conteúdo de streaming, utilizando estratégia de pré-caching.

A ideia por trás de um CDN é simples: colocar os vídeos o mais próximo possível dos usuários, espalhando os computadores pelo mundo. Assim, quando um usuário quiser assistir a um vídeo, bastará encontrar o computador mais próximo com o vídeo e fazer streaming para o dispositivo a partir dele. Os maiores benefícios, além da escalabilidade, para o Netflix, do CDN são velocidade e confiabilidade.

Estratégias de substituição de cache

Freqüentemente, o cache tem um tamanho fixo limitado. Portanto, sempre que você precisar gravar no cache (normalmente, após uma falha no cache), é preciso determinar se os dados recuperados da fonte mais lenta devem ou não ser gravados no cache e se o limite de tamanho foi atingido e, nesse caso, quais dados precisariam ser removidos dele.

Infelizmente, não existe uma solução global. Algumas vezes, a melhor estratégia seria remover do cache os dados menos solicitados recentemente. Mas, há situações em que será necessário levar em consideração outros aspectos, como, por exemplo, o custo de recuperar os dados da fonte mais lenta.

Qualquer estratégia de substituição de cache terá suas desvantagens e será necessário considerar o contexto de cada aplicativa aplicativo. A melhor abordagem será aquela que resulta em menos hit fails.

Estratégias de invalidação de cache

A invalidação do cache é o processo de determinar se um dado no cache deve ou não ser usado para atender a solicitações subsequentes.

Existem apenas duas coisas difíceis na Ciência da Computação: invalidação de cache e nomes de coisas. 

Phil Karlton

Visto que o aplicativo raramente deve servir dados obsoletos ou inválidos aos usuários, devemos sempre projetar algum mecanismo para invalidar os dados armazenados em cache.

As estratégias mais comuns para invalidação de cache são:

  • Tempo de expiração , onde o aplicativo sabe por quanto tempo os dados serão válidos. Após este tempo, os dados devem ser removidos do cache causando uma “perda de cache” em uma solicitação subsequente;
  • Verificação de atualização em cache , em que o aplicativo executa um procedimento leve para determinar se os dados ainda são válidos sempre que são recuperados. A desvantagem dessa alternativa é que ela produz algum overhead de execução;
  • Invalidação ativa , em que o aplicativo invalida ativamente os dados no cache, normalmente quando alguma mudança de estado é identificada.

Consolidando

Escalabilidade é um atributo desejável mas que sempre representa incremento na complexidade e, eventualmente, no custo. Por isso, é importante ter clareza sobre a prioridade deste atributo frente aos demais.

A experiência ensina que a primeira medida para suportar a escala é caching agressivo e a adoção de modelos assíncronos de comunicação. Depois, a solução mais pragmática é preparar os sistemas para o scale out. Eventualmente, pode ser necessário particionar o sistema por dados. Finalmente, depois de tudo isso, particionar o sistema por funcionalidades (serviços).

// TODO

Antes de avançar para o próximo capítulo, recomendo as seguintes reflexões:

  1. Os sistemas em que você trabalha tem “suporte” para escalabilidade?
  2. Quais das estratégias genéricas para escalabilidade está mais habituado a adotar?
  3. Qual relevância de caching para sua aplicação?

Referências bibliográficas

ABBOTT, Martin; FISCHER, Michael. Scalability Rules!: principles for scaling web sites. 2. ed. Crawsfordsville, In: Addison-Wesley, 2016.

ABBOTT, Martin; FISCHER, Michael. The Art of Scalability: scalable web architecture, processes, and organizations for the modern enterprise. 2. ed. Crawsfordsville, In: Addison-Wesley, 2015.

GOLDRATT, Eliyahu; COX, Jeff. A meta: teoria das restrições (TOC) aplicada a indústria. 4. ed. São Paulo: Nobel, 2015. 400 p. Edição comemorativa de 30 anos.

HIGH SCALABILITY. Netflix: What Happens When You Press Play? 2017. Disponível em: http://highscalability.com/blog/2017/12/11/netflix-what-happens-when-you-press-play.html. Acesso em: 25 jun. 2021.

LIU, Henry. Software performance and scalability: a quantitative approach. Danvers, Ma: Wiley Publisher, Inc, 2009. (Quantitative Software Engineering

WINTERS, Titus; MANSHRECK, Tom; WRIGHT, Hyrum (org.). Software Engineering at Google: lessons learned from programming over time. Gravenstein Highway North, Ca: O’Reilly Media, Inc, 2020.

Compartilhe este capítulo:

Compartilhe:

Comentários

Participe da construção deste capítulo deixando seu comentário:

Inscrever-se
Notify of
guest
0 Comentários
Feedbacks interativos
Ver todos os comentários

Fundador e CEO da EximiaCo, atua como tech trusted advisor ajudando diversas empresas a gerar mais resultados através da tecnologia.

Mentoria

para arquitetos de software

Imersão, em grupo, supervisionada por Elemar Júnior, onde serão discutidos tópicos avançados de arquitetura de software, extraídos de cenários reais, com ênfase em systems design.

Consultoria e Assessoria em

Arquitetura de Software

EximiaCo oferece a alocação de um Arquiteto de Software em sua empresa para orientar seu time no uso das melhores práticas de arquitetura para projetar a evolução consistente de suas aplicações.

ElemarJúnior

Fundador e CEO da EximiaCo, atua como tech trusted advisor ajudando diversas empresas a gerar mais resultados através da tecnologia.

+55 51 99942-0609 |  contato@eximia.co

+55 51 99942-0609  contato@eximia.co

0
Quero saber a sua opinião, deixe seu comentáriox
()
x