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 nosso exemplo, provavelmente “A” e “B” estão preparados para, de alguma forma, cenários onde “C” simplesmente pare de responder. Entretanto, poucas vezes encontramos arquiteturas preparadas para lidar com response times mais altos. Em nossa experiência, lentidão, mais do que indisponibilidade total, é a causa raiz da maioria dos problemas mais críticos em produção.
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).
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.
NOTA DO AUTOR: Bulkheads, além de outros padrões arquitetura para escalabilidade e performance, é tema da mentoria de arquitetura que estarei iniciando em abril!