Cloud Intelligence™Cloud Intelligence™

Cloud Intelligence™

Desemaranhando microsserviços: equilibrando a complexidade em sistemas distribuídos

By Vladik KhononovApr 20, 202015 min read

Esta página também está disponível em English, Deutsch, Español, Français, Italiano e 日本語.

1 czu57jtrrnxvxh8btn xzw

A lua de mel dos microsserviços acabou. A Uber está refatorando milhares de microsserviços em uma solução mais gerenciável [1]; Kelsey Hightower prevê que os monólitos são o futuro [2]; e até o Sam Newman afirma que microsserviços nunca devem ser a escolha padrão, e sim o último recurso [3].

O que está acontecendo? Por que tantos projetos se tornaram impossíveis de manter, apesar da promessa de simplicidade e flexibilidade dos microsserviços? Ou será que os monólitos são melhores, no fim das contas?

Neste post, quero discutir essas questões. Você vai conhecer problemas comuns de design que transformam microsserviços em grandes bolas de lama distribuídas — e, claro, como evitá-los.

Mas, antes de mais nada, vamos deixar claro o que é um monólito.

Monólito

Os microsserviços sempre foram apresentados como solução para bases de código monolíticas. Mas será que monólitos são, necessariamente, um problema? Segundo a definição da Wikipédia [4], uma aplicação monolítica é autossuficiente e independente de outras aplicações. Independência de outras aplicações? Não é justamente isso que tentamos alcançar — muitas vezes em vão — quando projetamos microsserviços? David Heinemeier Hansson [5] foi rápido em denunciar essa demonização dos monólitos. Ele alertou para os riscos e desafios inerentes aos sistemas distribuídos e usou o Basecamp para provar que um sistema de grande porte, atendendo milhões de clientes, pode ser implementado e mantido em uma base de código monolítica.

Ou seja, microsserviços não "consertam" monólitos. O verdadeiro problema que os microsserviços deveriam resolver é a incapacidade de entregar objetivos de negócio. Muitas vezes, as equipes não atingem essas metas por causa do custo crescente de forma exponencial — ou, pior ainda, imprevisível — de cada mudança. Em outras palavras, o sistema não consegue acompanhar as necessidades do negócio. Esse custo descontrolado da mudança não é uma característica do monólito, e sim de uma grande bola de lama [6]:

Uma Grande Bola de Lama é uma selva desestruturada, espalhada, desleixada, feita com fita adesiva e arame, repleta de código espaguete. Esses sistemas mostram sinais inequívocos de crescimento desregulado e remendos repetidos e improvisados. Informações são compartilhadas promiscuamente entre elementos distantes do sistema, frequentemente a ponto de quase todas as informações importantes se tornarem globais ou duplicadas.

A complexidade de alterar e evoluir uma grande bola de lama pode ter várias causas: coordenação entre múltiplas equipes, requisitos não funcionais conflitantes ou um domínio de negócio intrincado. De qualquer forma, costumamos tentar lidar com essa complexidade decompondo essas soluções desajeitadas em microsserviços.

Micro o quê?

O termo "microsserviço" sugere que algumas partes de um serviço podem ser medidas e que esse valor deve ser minimizado. Mas o que significa microsserviço, exatamente? Vamos analisar algumas das formas mais comuns como o termo é usado.

Microequipes

A primeira é o tamanho da equipe que trabalha no serviço. E essa métrica deveria ser medida em pizzas. Sério. Dizem que, se uma equipe que trabalha em um serviço pode ser alimentada com duas pizzas, então é um microsserviço. Acho essa heurística no mínimo curiosa, já que construí projetos com equipes que poderiam ser alimentadas com uma única pizza… e desafio qualquer um a chamar aquelas bolas de lama de microsserviços!

Micro bases de código

Outra abordagem comum é projetar microsserviços com base no tamanho da base de código. Alguns levam essa ideia ao extremo e tentam limitar o tamanho dos serviços a um determinado número de linhas de código. Dito isso, o número exato de linhas necessárias para constituir um microsserviço ainda está por ser descoberto. Quando esse santo graal da arquitetura de software for revelado, partiremos para a próxima pergunta — qual é a largura recomendada do editor para construir microsserviços?

Falando mais sério, uma versão menos extrema dessa abordagem é a mais comum. O tamanho da base de código costuma ser usado como heurística para decidir se algo é ou não um microsserviço.

De certa forma, essa abordagem faz sentido. Quanto menor a base de código, menor o escopo do domínio de negócio. Logo, fica mais fácil entender, implementar e evoluir. Além disso, uma base de código menor tem menos chances de virar uma grande bola de lama — e, se isso acontecer, é mais simples refatorar.

Infelizmente, essa simplicidade é apenas uma ilusão. Quando avaliamos o design de um serviço a partir dele mesmo, deixamos de lado uma parte fundamental do design de sistemas. Esquecemos do sistema em si, do qual o serviço é um componente.

"Existem muitas heurísticas úteis e reveladoras para definir os limites de um serviço. O tamanho é uma das menos úteis." ~ Nick Tune

Construímos sistemas!

Construímos sistemas, não conjuntos de serviços. Usamos arquitetura baseada em microsserviços para otimizar o design de um sistema, não o design de serviços individuais. Não importa o que digam por aí: microsserviços não podem — e nunca serão — completamente desacoplados nem totalmente independentes. Não dá para construir um sistema a partir de componentes separados! Isso iria contra a própria definição da palavra "sistema" [7]:

1. Um conjunto de coisas ou dispositivos conectados que operam juntos

2. Um conjunto de equipamentos e programas de computador usados em conjunto para um propósito específico

Os serviços sempre vão precisar interagir entre si para formar um sistema. Se você projeta um sistema otimizando seus serviços, mas ignora as interações entre eles, é nisso que você pode acabar se metendo:

1 fdju5v0tdu9sesmm5u6cfa

Esses "microsserviços" podem até ser simples individualmente, mas o sistema em si vira um inferno de complexidade!

Então, como projetar microsserviços que enfrentem a complexidade não só dos serviços, mas do sistema como um todo?

É uma pergunta difícil, mas, para a nossa sorte, ela já foi respondida há muito tempo.

Uma visão sistêmica da complexidade

Há quarenta anos, não existia computação em nuvem, nem requisitos de escala global, nem necessidade de fazer deploy de um sistema a cada 11,7 segundos. Mas os engenheiros já precisavam domar a complexidade dos sistemas. Embora as ferramentas daquela época fossem diferentes, os desafios — e, mais importante, a solução — continuam relevantes hoje e podem ser aplicados ao design de sistemas baseados em microsserviços.

Em seu livro "Composite/Structured Design" [8], Glenford J. Myers discute como estruturar código procedural para reduzir sua complexidade. Logo na primeira página, ele escreve:

O assunto da complexidade vai muito além de simplesmente tentar minimizar a complexidade local de cada parte de um programa. Um tipo muito mais importante de complexidade é a complexidade global: a complexidade da estrutura geral de um programa ou sistema (ou seja, o grau de associação ou interdependência entre as principais partes de um programa).

No nosso contexto, complexidade local é a complexidade de cada microsserviço individual, enquanto complexidade global é a complexidade do sistema como um todo. A complexidade local depende da implementação de um serviço; a complexidade global é definida pelas interações e dependências entre os serviços.

Então, qual complexidade é mais importante — a local ou a global? Vamos ver o que acontece quando só uma delas é levada em conta.

É surpreendentemente fácil reduzir a complexidade global ao mínimo. Basta eliminar qualquer interação entre os componentes do sistema — ou seja, implementar toda a funcionalidade em um único serviço monolítico. Como já vimos, essa estratégia pode funcionar em cenários específicos. Em outros, pode levar à temida grande bola de lama — provavelmente o nível mais alto possível de complexidade local.

Por outro lado, sabemos o que acontece quando se otimiza apenas a complexidade local e se negligencia a complexidade global do sistema — a ainda mais temida grande bola de lama distribuída.

1 zcygywao9vmjw3cxkygs5q

Portanto, se nos concentramos em apenas um tipo de complexidade, não importa qual seja a escolha. Em um sistema distribuído razoavelmente complexo, a complexidade oposta vai disparar. Por isso, não dá para otimizar só uma. Precisamos equilibrar tanto a complexidade local quanto a global.

Curiosamente, os meios para equilibrar a complexidade descritos no livro "Composite/Structured Design" não valem só para sistemas distribuídos. Eles também trazem insights sobre como projetar microsserviços.

Microsserviços

Vamos começar definindo exatamente o que são esses serviços e microsserviços de que estamos falando.

O que é um serviço?

Segundo o OASIS Standard [9], um serviço é:

Um mecanismo para permitir acesso a uma ou mais capacidades, em que o acesso é fornecido por meio de uma interface prescrita.

A parte da interface prescrita é fundamental. A interface de um serviço define a funcionalidade que ele expõe ao mundo. Segundo Randy Shoup [10], a interface pública de um serviço é simplesmente qualquer mecanismo que faz dados entrarem ou saírem dele. Pode ser síncrona, como um modelo simples de requisição/resposta, ou assíncrona, que produz e consome eventos. Seja síncrona ou assíncrona, a interface pública é apenas o meio para entrada e saída de dados do serviço. Randy também descreve essas interfaces públicas como a porta da frente do serviço.

Um serviço é definido pela sua interface pública, e essa definição já basta para determinar o que faz de um serviço um microsserviço.

O que é um microsserviço?

Se um serviço é definido pela sua interface pública, então —

Um microsserviço é um serviço com uma micro interface pública — uma micro porta da frente.

Essa heurística simples já era seguida nos tempos da programação procedural e é mais do que relevante no universo dos sistemas distribuídos. Quanto menor o serviço que você expõe, mais simples sua implementação e menor sua complexidade local. Do ponto de vista da complexidade global, interfaces públicas menores geram menos dependências e conexões entre serviços.

A noção de micro-interfaces também explica a prática difundida de microsserviços não exporem seus bancos de dados. Nenhum microsserviço pode acessar o banco de dados de outro microsserviço diretamente, apenas pela sua interface pública. Por quê? — Bem, um banco de dados seria uma interface pública gigantesca! Basta pensar em quantas operações diferentes dá para executar em um banco de dados relacional.

Reforçando: em sistemas distribuídos, equilibramos as complexidades local e global minimizando as interfaces públicas dos serviços, transformando-os assim em micro serviços.

ATENÇÃO

Essa heurística pode parecer enganosamente simples. Se um microsserviço é só um serviço com uma micro interface pública, então basta limitar as interfaces públicas a um único método. Já que a porta da frente não pode ser menor que isso, esses seriam os microsserviços perfeitos, certo? Nem tanto. Para mostrar o porquê, vou usar o exemplo de outro post meu [11] sobre o tema:

Digamos que tenhamos o seguinte serviço de gerenciamento de backlog:

1 s1immc9gryz6stvotne5q

Ao decompô-lo em oito serviços, cada um com um único método público, obtemos serviços com complexidades locais perfeitas:

1 rxrzifqffvwocnuz32vkjq

Mas conseguimos conectá-los em um sistema que de fato gerencie o backlog? Nem tanto. Para formar o sistema, os serviços precisam interagir entre si e compartilhar mudanças no estado de cada um. Mas eles não conseguem. As interfaces públicas dos serviços não suportam isso.

Então, somos obrigados a estender as portas da frente com métodos públicos que permitam a integração entre os serviços:

1 g ixefgencu4csouqws0ha

Pronto. Se otimizamos a complexidade de cada serviço individualmente, a decomposição ingênua funciona muito bem. Porém, quando tentamos conectar os serviços em um sistema, a complexidade global entra em cena. Não só o sistema resultante vira uma bagunça emaranhada, como também precisamos estender as interfaces públicas além da nossa intenção original — em nome da integração. Parafraseando Randy Shoup, além da pequena porta da frente, criamos uma enorme entrada "somente para funcionários"! O que nos leva a um ponto importante:

Um serviço com mais métodos voltados à integração do que ao negócio é um forte indício de uma grande bola de lama distribuída em formação!

Portanto, o limite até onde a interface pública de um serviço pode ser minimizada depende não só do próprio serviço, mas (principalmente) do sistema do qual ele faz parte. Uma decomposição adequada em microsserviços deve equilibrar a complexidade global do sistema e as complexidades locais de seus serviços.

Projetando os limites dos serviços

"Encontrar os limites dos serviços é difícil pra caramba… Não existe fluxograma!" — Udi Dahan

A frase acima, de Udi Dahan, é especialmente verdadeira para sistemas baseados em microsserviços. Projetar os limites dos microsserviços é difícil e, provavelmente, impossível de acertar de primeira. Por isso, projetar um sistema baseado em microsserviços razoavelmente complexo é um processo iterativo.

Sendo assim, é mais seguro começar com limites mais amplos — provavelmente os limites de bounded contexts adequados [12] — e decompô-los em microsserviços depois, à medida que ganhamos mais conhecimento sobre o sistema e seu domínio de negócio. Isso é especialmente relevante para serviços que englobam domínios de negócio centrais [13].

Microsserviços fora dos sistemas distribuídos

Embora os microsserviços tenham sido "inventados" só recentemente, dá para encontrar várias implementações dos mesmos princípios de design em outras áreas. Algumas delas:

Equipes multifuncionais

Todo mundo sabe que equipes multifuncionais são as mais eficazes. Esse tipo de equipe reúne um grupo diverso de profissionais trabalhando na mesma tarefa. Uma equipe multifuncional eficiente maximiza a comunicação interna e minimiza a comunicação externa.

Nossa indústria descobriu as equipes multifuncionais só recentemente, mas as forças-tarefa existem há muito tempo. Os princípios subjacentes são os mesmos de um sistema baseado em microsserviços: alta coesão dentro da equipe e baixo acoplamento entre equipes. A "interface pública" da equipe é minimizada incorporando à própria equipe as habilidades necessárias para executar a tarefa (ou seja, os detalhes de implementação).

Microprocessadores

Encontrei esse exemplo no maravilhoso blog de Vaughn Vernon sobre o mesmo tema. No post, Vaughn faz um paralelo interessante entre micro serviços e micro processadores. Em particular, ele discute a diferença entre processadores e microprocessadores:

Acho interessante que exista uma classificação por tamanho que ajuda a determinar se uma unidade central de processamento (CPU) é considerada ou não um Microprocessador: o tamanho do seu barramento de dados [21]

O barramento de dados de um microprocessador é a sua interface pública — ele define a quantidade de dados que pode ser trocada entre o microprocessador e outros componentes. Existe uma classificação rigorosa de tamanho para a interface pública que define se uma unidade central de processamento (CPU) é ou não considerada um microprocessador.

Filosofia Unix

A filosofia Unix, ou o jeito Unix, é um conjunto de normas culturais e abordagens filosóficas voltadas ao desenvolvimento de software minimalista e modular [22].

Alguém pode argumentar que a filosofia Unix contradiz minha tese de que não é possível construir um sistema com componentes totalmente independentes: os programas Unix não são totalmente independentes e ainda assim formam um sistema funcional? O contrário é que é verdade. O jeito Unix praticamente define que os programas precisam expor micro-interfaces. Vamos ver como os princípios da filosofia Unix se relacionam com a noção de microsserviços:

O primeiro princípio prega que as interfaces públicas dos programas exponham uma única função coerente, em vez de sobrecarregar os programas com funcionalidades alheias ao seu objetivo original:

Faça com que cada programa faça uma coisa bem-feita. Para realizar uma nova tarefa, construa do zero, em vez de complicar programas antigos adicionando novas "funcionalidades".

Embora os comandos Unix sejam considerados totalmente independentes uns dos outros, eles não são. Ainda precisam se comunicar com outros, e o segundo princípio define como as interfaces de comunicação devem ser projetadas:

Espere que a saída de cada programa se torne a entrada de outro, ainda desconhecido. Não polua a saída com informações estranhas. Evite formatos de entrada estritamente colunares ou binários. Não insista em entrada interativa.

Não só a interface de comunicação é estritamente limitada (entrada padrão, saída padrão e erro padrão), como, segundo esse princípio, os dados trocados entre os comandos também devem ser estritamente limitados. Ou seja, os comandos Unix precisam expor micro-interfaces e nunca depender dos detalhes de implementação uns dos outros.

E os _nano_sserviços?

O termo "nanosserviço" costuma ser usado para descrever um serviço que é pequeno demais. Dá para dizer que aqueles serviços ingênuos com um único método do exemplo anterior são nanosserviços. No entanto, eu não concordo necessariamente com essa classificação.

O termo nanosserviços é usado para descrever serviços individuais, ignorando o sistema como um todo. No exemplo acima, ao colocarmos o sistema na equação, as interfaces dos serviços tiveram que crescer. Aliás, se compararmos a implementação original em um único serviço com a decomposição ingênua, vemos que, ao conectar os serviços em um sistema, o número total de métodos públicos passa de 8 para 38. Além disso, a média de métodos públicos por serviço salta dos desejados 1 para 4,75.

Portanto, se em vez de bases de código otimizarmos os serviços (interfaces públicas), o termo nano-serviço deixa de fazer sentido, já que o serviço é forçado a crescer de novo para atender aos casos de uso do sistema.

É só isso?

Não. Embora minimizar as interfaces públicas dos serviços seja um bom princípio orientador para o design de microsserviços, ainda é apenas uma heurística e não substitui o bom senso. Na verdade, a micro-interface é só uma espécie de abstração sobre os princípios de design mais fundamentais — porém muito mais complexos — de acoplamento e coesão.

Por exemplo, se dois serviços têm micro-interfaces públicas, mas precisam ser coordenados em uma transação distribuída, eles continuam fortemente acoplados.

Dito isso, buscar micro-interfaces continua sendo uma forte heurística que aborda diferentes tipos de acoplamento, como funcional, de desenvolvimento e semântico. Mas isso é tema para outro post.

Da teoria à prática

Infelizmente, ainda não temos uma forma objetiva de quantificar as complexidades local e global. Por outro lado, temos algumas heurísticas de design que podem melhorar o design de sistemas distribuídos.

A mensagem central deste post é que você deve avaliar continuamente as interfaces públicas dos seus serviços, perguntando-se:

  • Qual é a proporção entre endpoints orientados ao negócio e endpoints orientados à integração em um determinado serviço?
  • Existem, do ponto de vista do negócio, endpoints não relacionados em um mesmo serviço? É possível separá-los em dois ou mais serviços sem introduzir endpoints de integração?
  • A fusão de dois serviços eliminaria endpoints que foram adicionados para integrar os serviços originais?

Use essas heurísticas para orientar o design dos limites e das interfaces dos seus serviços.

Resumo

Quero encerrar com uma citação de Eliyahu Goldratt. Em seus livros, ele costumava repetir estas palavras:

"Diga-me como você me mede e eu lhe direi como vou me comportar" ~ Eliyahu Goldratt

Ao projetar sistemas baseados em microsserviços, é fundamental medir e otimizar a métrica certa. Definir limites para micro bases de código e micro equipes é, sem dúvida, mais fácil. Porém, para construir um sistema, precisamos levá-lo em conta. Microsserviços têm a ver com projetar sistemas, e não serviços individuais.

E isso nos traz de volta ao título do post — "Desemaranhando microsserviços, ou equilibrando a complexidade em sistemas distribuídos". A única maneira de desemaranhar microsserviços é equilibrar a complexidade local de cada serviço e a complexidade global do sistema como um todo.

Bibliografia

  1. Tweet de Gergely Orosz sobre a Uber
  2. Monoliths are the future
  3. Guru de microsserviços alerta devs de que a arquitetura da moda não deveria ser o padrão para todo app, e sim 'um último recurso'
  4. Aplicação Monolítica (Wikipédia)
  5. The Majestic Monolith — DHH
  6. Big Ball of Mud (Wikipédia)
  7. Definição de Sistema
  8. Composite/Structures Design — livro de Glenford J. Myers
  9. Modelo de Referência para Arquitetura Orientada a Serviços
  10. Managing Data in Microservices — palestra de Randy Shoup
  11. Tackling Complexity in Microservices
  12. Bounded Contexts are NOT Microservices
  13. Revisiting the Basics of Domain-Driven Design
  14. Implementing Domain-Driven Design — livro de Vaughn Vernon
  15. Modular Monolith: A Primer — Kamil Grzybek
  16. A Design Methodology for Reliable Software Systems — Barbara Liskov
  17. Designing Autonomous Teams and Services
  18. Emergent Boundaries — palestra de Mathias Verraes
  19. Long Sad Story of Microservices — palestra de Greg Young
  20. Principles of Design — Tim Berners-Lee
  21. Microservices and [Micro]services — Vaughn Vernon
  22. Filosofia Unix

Publicado originalmente em vladikk.com em 09/04/2020.