Fundamentos para arquitetura de sistemas escaláveis

Este capítulo possui uma versão mais recente:

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

Para a Google, engenharia de software compreende as práticas necessárias para suportar as mudanças de escala, ao longo do tempo, preservando ou melhorando a eficiência de um sistema. Entretanto, é importante reconhecer que as demandas por cuidados com escalabilidade variam significativamente.

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.
0
Concorda?x

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.

Definindo escalabilidade (e elasticidade)

Escalabilidade é a capacidade de sistemas suportarem o aumento (ou a redução) dos workloads incrementando (ou reduzindo) o custo em menor ou igual proporção. As mudanças no custo acontecem pelo scale up/down – a atualização dos dispositivos computacionais utilizados em termos de memória, processador, etc – ou pelo scale out, modificando a quantidade de dispositivos computacionais.

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

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, tudo mudou.

Em função da transformação digital – que aproxima empresas de seus 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 acontecem rapidamente e, não raro, de maneira inesperada. Daí, a necessidade de considerar escalabilidade up-front.

O impacto da escalabilidade no custo

Medidas relacionadas com a escalabilidade impactam diretamente em outros atributos de qualidade importantes, destacando disponibilidade, performance e, sobretudo, custo. Aliás, escalabilidade, geralmente, é o atributo com maior impacto no custo.
0
Concorda?x

Decisões arquiteturais para a escalabilidade têm impacto considerável sobre os custos de desenvolvimento e deploy, sobretudo pela inclusão de componentes, como mecanismos para orquestração e mensageria.

O impacto da escalabilidade na performance

Sistemas que demandam, ao mesmo tempo, escalabilidade e performance submetem uma série de desafios, trade-offs, para arquitetos. Afinal, suportar a escala implica no design de interfaces assíncronas.

Estratégias genéricas para a escalabilidade

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.

Isolando “instabilidades” com bulkheads

Quando se trata de suportar escala, não há soluções “bala de prata”. De maneira simplória, é fácil assumir que ter uma arquitetura que “escala horizontalmente”, rodando em uma infraestrutura poderosa (como a nuvem), é suficiente para impedir “quedas” de sistemas. Infelizmente, esse não é o caso!

Considere um sistema com três componentes: “A”, “B” e “C”. Onde “A” e “B” dependem sincronamente de “C” para funcionar.

Caso “C” fique instável, “A” e “B” podem ter dificuldades para continuar funcionando bem, o que pode espalhar falhas em “efeito dominó”. Ou seja, a instabilidade de “C” pode acabar gerando instabilidades em “A” e “B” que, eventualmente, irão causar instabilidades em outros componentes.


A maioria dos projetos de sistemas onde há dependência forte entre componentes tem alguma contingência para falhas mais graves. No exemplo, provavelmente “A” e “B” estão preparados para cenários onde “C” simplesmente pare de responder. Entretanto, poucas vezes encontramos arquiteturas preparadas para lidar com response times mais altos. Geralmente, lentidão, mais do que indisponibilidade total, é a causa raiz da maioria dos problemas mais críticos em produção.
0
Considerações?x

Uma forma comum de tentar mitigar os impactos de instabilidades em um componente é torná-lo escalável na horizontal para, então, levantar várias instâncias com demanda distribuída por um load balancer.

O problema, entretanto, é que, com frequência, instabilidades, sobretudo de lentidão, são ocasionadas por dificuldades de um componente para atender “requisições envenenadas” provenientes de um “ofensor externo”. Em nosso exemplo, caso “A” gere “requisições envenenadas” para “C”, estas podem acabar afetando todas as instâncias do serviço, mesmo escalados na horizontal, apenas retardando os efeitos ruins, sem evitá-los.

Uma alternativa eficiente para mitigar o risco dessa situação é compartimentar instâncias para as diversas origens de requisição. Ou seja, “levantar” instâncias específicas para cada natureza de solicitação, mantendo-as apartadas.

Dessa forma, a “pressão” de “A” sobre “C” não impactará “B”. Nesse mesmo raciocínio, a “pressão” de “B” sobre “C” também não causará problemas para “A”.

No “mundo real”, já vimos essa solução ser aplicada, por exemplo, em grandes varejistas para apartar o tratamento de requisições de aplicações móveis, site, robôs de comparação de preços e o Google (como você pode imaginar, para um varejista, é tremendamente prejudicial ser “penalizado” pelo gigante das buscas).
0
Mais exemplos?x

O nome dessa técnica de design é bulkhead, em alusão a forma como navios eram projetados para impedir que danos em uma parte do cascos causassem um naufrágio.

Bulkheads obviamente, introduzem ociosidade desnecessária (leia-se desperdício) de recursos a uma arquitetura, tornando-a economicamente menos interessante. Entretanto, essa ineficiência pode ser necessária e, geralmente, é mais do que compensada com a minimização e isolamento de downtimes. Um de seus principais atrativos é que pode ser adotada, frequentemente, com poucas alterações (ou até nenhuma) de código.
0
Considerações?x

Caching

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.

Assíncrono sempre que possível

De maneira geral, chamadas assíncronas, seja entre métodos ou entre serviços, são muito mais complexas de implementar e manter do que chamadas síncronas. Afinal, chamadas assíncronas exigem a adoção de algum modelo de coordenação (como callbacks).
0
Você concorda?x

A “dificuldade” para manter sistemas assíncronos, os modelos de coordenação, também representam sua principal força. Quando utilizadas entre serviços, chamadas assíncronas frequentemente utilizam alguma estratégia de resiliência que, por sua vez, coopera para a disponibilidade. Além disso, desconecta a taxa de demanda da taxa de atendimento.

Teoria das Restrições (ToC) e a Escalabilidade

Quando problemas acontecem para suportar a escala, costuma ser fácil identificar qual componente na arquitetura está operando como “gargalo”. Ou seja, qual é a principal restrição para suportar o aumento do workload.

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.
0
Cenários?x

Indicações e contraindicações

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).
0
Concorda?x

// 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
1 Comentário
Oldest
Newest Most Voted
Feedbacks interativos
Ver todos os comentários
Suely Sampaio Boccanera
Suely Sampaio Boccanera
2 anos atrás

Excelente texto, muito obrigada por compartilhar este conhecimento.

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

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