Granularidade em Microserviços: Granularity Disintegrators

Marcelo M. Gonçalves
6 min readMar 25, 2024

--

Granularidade não deve ser confundida com modularidade, exclusivamente por tratarem de coisas distintas em contextos distintos relacionados a arquitetura de software. Ambos os conceitos concentram-se em camadas diferentes na cadeia de construção, enquanto a granularidade explora formas para identificação de como numerosas partículas podem unir-se para formar unidades maiores a modularidade refere-se a como estas unidades são padronizadas de forma a garantir maior flexibilidade e menor acoplamento durante a fase de manutenção.

Os principais desafios relacionados à modelagem de software residem nos processos usados para determinar os níveis corretos de granularidade. Na prática, a modularidade preocupa-se em quebrar um sistema em “partes” menores de modo a permitir com que a granularidade tenha condições de endereçar desafios referentes ao tamanho que estas “partes” devem ter, baseando-se em seu conjunto de responsabilidades. Um bom exemplo da combinação da granularidade vs modularidade em execução seriam os microservices vs modular monoliths, onde percebemos a granularidade atuando externamente e a modularidade internamente em seu estilo/design de arquitetura.

De maneira a mensurarmos com maior assertividade a granularidade adequada dos serviços precisamos estabelecer métricas com base nas similaridades/distinções entre as ações executadas e operações expostas.

Existem “forças opostas” durante o processo de granularidade dos serviços: Granularity Disintegrators e Granularity Integrators, endereçando dúvidas como “Quando devemos desintegrar um serviço em partes menores?” ou “Quando deveríamos considerar agrupamentos entre elas?”, respectivamente. Para que seja possível alcançarmos níveis apropriados de granularidade existem princípios e técnicas de forma a proporcionarmos um balanço entre as “forças opostas” atingindo maior equilíbrio entre elas.

Granularity Disintegrators

Durante o processo de fragmentação, onde procuramos acertar os níveis de granularidade entre os building-blocks, precisamos justificar a decomposição e isso pode envolver architectural drivers, como quality attributes (segurança, escalabilidade, extensibilidade, performance), restrições do negócio, tendências, boas práticas, etc ou em muitos casos, a mistura de todos eles.

Ao segregar nossos componentes estamos olhando para as complexidades do domínio contendo os problemas a serem resolvidos. Quando nos referimos a granularidade do ponto de vista dos “desintegradores”, seus principais drivers de arquitetura variam desde o escopo e responsabilidades dos serviços até considerações relacionadas ao throughput e segurança, distinguindo-se de outras camadas de abstração onde a granularidade também pode ser encontrada: limites/fronteiras transnacionais, business capabilities ou subdomínios/bounded contexts.

Não podemos comparar processos de decomposição/desintegração executados em diferentes momentos com diferentes propósitos.

Serviço: Escopo e Funções

Nosso serviço está fazendo diversas coisas desconexas sem muita relação? Pode ser um sinal de que devemos quebrá-lo em pedaços menores, principalmente se considerarmos as arquiteturas de MSA (Microservices Architecture). Existem duas dimensões a considerar ao analisarmos a fragmentação (disintegrators): coesão e tamanho. Conceitualmente, coesão e tamanho dizem respeito ao grau de afinidade sob as quais determinadas coisas estão conectadas e a quantidade de elementos internos compondo cada função, respetivamente.

Para que seja possível analisar ou mesmo justificar qualquer consideração, precisamos de escopo. Os escopos ajudam a definir os níveis de coesão entre elementos, estabelecendo propósitos únicos para funcionalidades/serviços além de proteger-nos de interpretações e opiniões individuais/autoritativas a respeito das suas responsabilidades. Desta forma, building-blocks/componentes apresentando alta coesão não devem ser desintegrados/fragmentados.

A decomposisão considerando escopos/funções refere-se ao Single-responsibility principle incluindo propriedades relacionadas ao encapsulamento. Serviços fazendo mais do que deveriam apresentam desconexão entre suas operações e consequentemente fraca coesão, sugerindo que não devem residir no mesmo escopo, provavelmente tendo suas operações isoladas em contextos distintos ou devendo ser quebrado em partes menores.

Independente da escala de abstração, seja funções/módulos ou componentes/serviços, o relacionamento entre responsabilidade e coesão precisa estar alinhado.

Volatilidade no Código

A velocidade de mudança em códigos fonte pode ser considerada um driver importante, conceitualmente referindo-se a Volatility-based decomposition. Existem casos onde a fragmentação pode ser justificada através de mecanismos como frequência de alterações em certos trechos de código, quando a coesão atenda suficientemente.

Quanto temos serviços contendo trechos de código com baixa coesão, as mudanças efetuadas em determinados locais obrigam o desenvolvedor a testar e realizar deploy da unidade completa, impactando funcionalidades desconexas além de potencialmente deixá-las indisponíveis temporariamente. Ao optarmos pela separação, a frequência de mudanças no serviço A não interfere no B por conta do isolamento, resultando em reduções significativas em atividades de teste e riscos relacionados ao deployment.

Bons candidatos a desintegração através do driver volatilidade de código são representados pelas áreas contendo grande volume de alterações em códigos.

Escalabilidade e Throughput

Determinar as dimensões corretas para ações de escalabilidade em serviços, contendo diferentes operações/funções, pode ser executado em cima das demandas por sua utilização. Certas funcionalidades precisam escalar independentemente sem impactar componentes externos, resultando em custos menores em relação a elasticidade. A elasticidade precisa estar alinhada às demandas por throughput.

Throughput refere-se a taxa de transferência, na prática a quantidade de dados transferidos de um ponto a outro em um determinado espaço de tempo.

Para cada cenário, o processo de desintegração pode ser exercido individualmente ou em conjunto em relação a escalabilidade e throughput, representando uma boa referência para fragmentação. No contexto de Microservices, a escalabilidade horizontal representa um dos principais drivers a considerarmos para sua fragmentação.

Tolerância a Falhas

A tolerância a falhas descreve a habilidade de um serviço/aplicação continuar operando transparentemente mesmo sob a presença de erros. Serviços tolerantes a falhas precisam prover níveis de tolerância corretos a ponto dos seus fragmentos/funcionalidades não impactarem umas às outras sob circunstâncias de problemas. Desta forma, tanto o nível de Fault tolerance quanto a capacidade de Availability representam boas estratégias para desintegração de granularidade.

A nomenclatura dos serviços/componentes precisa de representatividade e forte coesão. Nos casos onde encontramos dificuldades em nomear um serviço por estar desempenhando atividades distintas, pode ser um indício de que precisa estar separado. Alguns componentes precisam ser mais tolerantes do que outros, de maneira a reduzir tanto esforços de arquitetura/aplicação quanto custos de infraestrutura precisamos identificar e calibrar cuidadosamente quais trechos/componentes necessitam de mais atenção.

Segurança

Ao nos referirmos a segurança não podemos focar exclusivamente nas camadas de persistência (storage), observando de igual forma o controle de acesso aos dados (sensíveis). Cada operação exige cuidados diferentes com relação à segurança dos dados e precisamos estar atentos à demanda por proteção referente a cada funcionalidade. Na prática, a segurança possui custos e desta forma nem todos os serviços/componentes precisam de altos níveis de segurança, enquanto alguns podem exigir criptografia incluindo algoritmos avançados (com chaves assimétricas), outros são mais permissivos em relação a estas restrições.

Quando os níveis de segurança são visivelmente distintos em relação aos riscos e necessidades entre os serviços precisamos da fragmentação, desta forma reduzimos impactos tanto na pipeline de desenvolvimento quanto em ações de manutenção/deploy e continuidade do serviço durante seu ciclo de vida.

Extensibilidade

A expansão contínua reflete a habilidade em adicionarmos funcionalidades a determinado serviço ou estender as existentes conforme o crescimento do seu escopo, demonstrando suas capacidades de extensibilidade. Neste contexto, precisamos avaliar as facilidades de gerenciamento das operações presentes em um building-block bem como seus custos de mudança (inclusão/exclusão) de comportamentos, concluindo que serviços apresentando dificuldade/complexidades durante estas ações precisam conviver isoladamente (fragmentação).

Fundamentalmente baseando-se em contexto/escopo, as características relacionadas à capacidade de extensão de um serviço pode ser misturada a outros drivers para que colaborativamente entreguem condições de customização com impactos reduzidos (internos/externos), acelerando o desenvolvimento, ciclos de teste e deployment.

Considerações Finais

A granularidade desempenha papel fundamental em todo o processo de construção de software, especialmente ao adotarmos arquiteturas distribuídas onde serviços tendem a representar propósitos únicos. A depender dos drivers utilizados para granular nosso domínio, as unidades de deployment serão separadamente publicadas. O alto preço a ser pago pelos sistemas distribuídos precisa ser considerado de forma mantermos equilíbrio entre a fragmentação e agrupamento/integradores das funcionalidades.

Em todos os casos, durante o processo de descoberta de granularidade será necessário entendermos todos os drivers de desintegração/integração envolvidos, de forma a construir uma visão madura em cima dos elementos e que facilite sermos mais assertivos em relação à fragmentação. A depender de cada serviço/funcionalidade a ser considerado, nem sempre a filosofia UNIX será o melhor caminho: “Faça somente uma coisa e faça ela bem feita!”.

Encontrarmos o balanço adequado para granularidade trata-se de um trabalho difícil. Entender os drivers, analisando os trade-offs correspondentes de forma isolada não será o bastante. Assim, para adquirirmos boas granularidades precisamos atuar de maneira colaborativa em toda a cadeia do processo, obtendo todo tipo de informação relevante com origens nos diferentes papéis envolvidos (BSA, PO, Domain Experts, Arquitetos, Engenheiros e Devs). Somente desta forma chegaremos a boas soluções finais, representando o “todo” e elevando a qualidade dos nossos softwares a níveis que acompanhem a evolução em constante movimento.

--

--