Sistemas Distribuídos: Conceito e Definições

Marcelo M. Gonçalves
Sicredi Tech
Published in
11 min readSep 4, 2022

Introdução

Sistemas distribuídos pertencem a uma classe de computação chamada computação paralela (parallel computing), classificados de acordo com o nível de paralelismo suportado pelo hardware, além da distância entre os processos pertencentes ao landscape de uma aplicação. Estão presentes neste grupo, além dos sistemas distribuídos, outras categorias de arquiteturas como Cluster computing, Multi-core computing, Symmetric multiprocessing, Grid computing.

Em computação distribuída, os elementos computacionais em termos de unidade são descritos como nós (nodes), caracterizando-se como um processo de software, equipado com uma lista de outros processos, capazes de enviar mensagens diretamente uns aos outros, estimulando-os a executar e concluir as operações de negócio descritas pelo sistema.

Sistemas distribuídos são dinâmicos no sentido de que dispositivos podem juntar-se ou deixar uma unidade de execução a qualquer momento, sem impactos na performance ou topologia do cluster. Tratam-se de componentes que colaboram para realizar uma tarefa predeterminada, comunicando-se através do envio de mensagens, podendo referenciar máquinas físicas ou processos de software rodando em cloud.

“Uma coleção de elementos computacionais autônomos, trabalhando em conjunto, mas que aparentam ao usuário final sendo um sistema único” — Distributed Systems, Steen e Tanenbaum.

No contexto de um sistema distribuído adequadamente elaborado, usuários, pessoas ou aplicações, acreditam estar lidando com um sistema único para fins de processamento quando na verdade estão na presença de nodes computacionais representados tanto como dispositivos de hardware quanto um processo de software. Operando em conjunto, estes componentes entregam uma visão unificada ao usuário final. Sendo o ideal que este usuário sequer note que está lidando com processos e dados dispersos, controlados ao longo de uma rede de computadores distribuída.

Nodes computacionais autônomos precisam colaborar para efetivamente realizar seu trabalho. Na prática, sistemas distribuídos são frequentemente organizados como overlay networks, a exemplo das peer-to-peer (P2P) networks: Structured overlay ou Unstructured overlay. Independente do tipo, redes P2P necessitam de esforço especial para a organização e gerenciamento dos nodes, sendo uma das partes mais complexas quando nos referimos a sistemas distribuídos.

Aplicações distribuídas modernas são compostas tanto por computadores de alta performance quanto pequenos dispositivos móveis, trabalhando de forma independente mas não isolada em favor da colaboração, um dos princípios fundamentais para esse tipo de mecanismo funcionar.

Trabalhando independentemente, cada nó terá sua própria noção de tempo, sincronização e coordenação em sistemas distribuídos, precisando lidar com o desafio da ausência de um global clock. Por este motivo, o conceito de admission control pode ser aplicado com o objetivo de gerenciar a comunicação e o controle de membros para grupos fechados dentro de um cluster de elementos. Assim, mecanismos de autenticação e comunicação entre membros de grupos fechados e abertos podem apresentar problemas de segurança que necessitem ser superados de forma a prosperarem neste tipo de arquitetura de comunicação.

Sistemas distribuídos atuando de forma coerente significa que a sensação proposta de percepção de um único sistema atendendo suas necessidades foi bem sucedida, sustentado pelo princípio de design distribution transparency, formalmente encontrado na computação distribuída. Em contrapartida, precisamos entender que a partir do momento em que sistemas distribuídos são compostos por diversos nodes espalhados pela rede, podendo falhar a qualquer momento em qualquer ponto, estaremos sujeitos ao desencadeamento de comportamentos inesperados, adicionando complexidade ao processo final.

Para facilitar o gerenciamento das aplicações distribuídas em um sistema complexo, o conceito de middleware layer pode ser aplicado, atuando como uma espécie de container, oferecendo uma camada de abstração para a infraestrutura e hardware, facilitando a intercomunicação entre as aplicações pertencentes ao mesmo cluster de execução.

Componentes distribuídos precisam ser altamente escaláveis, disponíveis, resilientes e tolerantes à falha. Precisam atender requisitos de performance, fisicamente impossíveis de realizar com um único node de execução. Nesse sentido, existem desafios fundamentais para desenhar, construir e operar sistemas distribuídos, categorizados como: comunicação, coordenação, escalabilidade, resiliência e operação.

Anatomia de um Sistema Distribuído

Encontramos sistemas distribuídos de todos os tamanhos e formas. Atualmente, a maior parte dos sistemas em larga escala são formados por backend services trabalhando em conjunto para entregar business features. Existem diversos níveis de abstração pertencentes à jornada de implementação de componentes com natureza distribuída. Porém, antes de dedicarmos esforços para entender seus fundamentos técnicos, o principal desafio da arquitetura para componentes distribuídos é entendermos as diferentes maneiras existentes para decomposição de um bloco de software em partes menores, descobrindo seus relacionamentos e responsabilidades.

A arquitetura de um componente de software difere-se de acordo com a perspectiva observada. Do ponto de vista físico, um sistema distribuído não passa de um grupo de máquinas comunicando-se através de uma rede. Em runtime, um sistema pode ser composto por processos comunicando-se via mecanismos Inter-process communication (IPC) como HTTP, rodando em um Operating System (OS). Já sob ângulo de implementação, um sistema distribuído pode ser descrito como um conjunto de componentes, com baixo ou nenhum acoplamento, publicados e escalando individualmente cada qual com certo nível de autonomia durante a execução de um processo.

Um serviço trata-se da abstração de uma capacidade de negócio, consistente em torno de um conjunto de regras bem definidas, podendo expor interfaces para comunicação interna ou externamente. Interfaces de comunicação com o mundo externo podem ser de dois tipos, Inbound e Outbound, definindo suas operações expostas.

Dependendo do estilo de arquitetura adotado para os componentes de uma aplicação, a utilização de Ports and Adapters sugere a exposição de Application Programming Interfaces (API) como service inbound interfaces, para processamento das requisições recebidas via mecanismos Inter-process communication (IPC), como exemplo o HTTP. Ao mesmo tempo, exposições do tipo service outbound interfaces, garantem a aplicação (business logic) acesso externo a recursos (Data store, SMTP, HTTP, File system) durante a execução dos fluxos de negócio.

Neste cenário de uso, o business logic (core) faz referência somente a interfaces (ports) definidas como ponto de abstração para detalhes internos de implementação (adapters).

Comunicação e Coordenação

A comunicação entre processos em uma rede de computadores compõem o backbone de qualquer sistema distribuído. Para que a comunicação ocorra com sucesso, os protocolos de rede, responsáveis por ela, são divididos em uma stack onde cada camada responsabiliza-se em abstrair detalhes de seu funcionamento para as camadas superiores.

No contexto de sistemas distribuídos, a comunicação representa um fator chave para a qualidade dos componentes, direcionando ao sucesso ou fracasso dos mesmos.

Quando um processo envia uma mensagem para outro através de uma rede, até ser fisicamente transmitida esta mensagem percorre diversas as camadas, de cima para baixo, até encontrar camadas mais inferiores da stack, responsáveis pelo hardware (link layer), operando através dos Switches via Ethernet packets. Durante este processo, ocorre o roteamento de pacotes IP com mensagem na Internet layer, enviados do endereço A para o B (MAC-address) entre dois dispositivos, efetivamente em conjunto com o endereço IP, de origem e destino, dos computadores conectados na internet.

Durante a transmissão dos datagramas via Transport layer, o protocolo TCP encarrega-se de entregá-los, de forma ordenada, através de uma conexão estabelecida entre dois endereços e portas, local e remota. A comunicação efetuada através do protocolo TCP pode ser realizada de forma segura, adicionando um canal segurança (TLS). TLS layer estabelece uma espécie de links confiáveis em cima do TCP criptografando os payloads da comunicação, adicionando integridade na comunicação com TCP.

O processo de comunicação realizado através da internet, conta com o apoio do protocolo DNS, responsável pela resolução dos nomes (hosts) para endereços IP correspondentes, trabalhando de forma distribuída, hierárquica e eventualmente consistente.

Aplicações decompostas em serviços de menor granularidade tendem a comunicarem-se através da troca de mensagens. A comunicação neste formato ocorre de forma assíncrona, possibilitando o desacoplando temporal entre produtores e consumidores através de um canal de comunicação (message channel). Ao abraçarmos a comunicação por meio de mensagens em sistemas distribuídos, estamos sujeitos a escolha de padrões como One-way messaging, Request-response messaging e Broadcast messaging. dependendo da necessidade e contexto de utilização, sendo estes os trade-offs relacionados a comunicação.

Para garantir processamento ordenado (offset), message brokers como o Kafka dividem os canais (topics) de comunicação em subcanais (partitions), possibilitando que consumidores exclusivamente responsabilizem-se pela consistência em seu consumo das mensagens. Exclusivamente, cada message broker apresenta comportamentos particulares durante o fluxo de sua execução. Independente da implementação do message broker adotada, a presença dos brokers são ingredientes essenciais no gerenciamento de processos distribuídos.

Para que sistemas distribuídos sejam devidamente construídos, precisamos de um mínimo de coordenação entre os processos e componentes, trabalhando em conjunto e de forma ordenada.

Na presença de erros, modelos e links de comunicação como fair-loss link, reliable link e authenticated reliable link possibilitam a construção de mecanismos confiáveis sob estruturas não confiáveis. Modelos tratam-se de uma abstração da realidade, e em um contexto de detecção e recuperação de falhas, algoritmos como arbitrary-fault, crash-recovery e crash-stop, em conjunto com modelos com noção de tempo, synchronous, asynchronous e partially synchronous são fundamentais para a composição bem sucedida dos sistemas distribuídos.

Garantias de entrega de mensagens mais comuns são: at-most-once, exactly-once e at-least-once.

Do ponto de vista de dados, sistemas com operações as quais possuem necessidade de alteração do estado, contam com modelos compostos por líderes e réplicas. Neste modelo, conceitos de consistência, garantias de entrega e replicação de estado em múltiplos nodes são sinônimos para atingirmos alta disponibilidade, escalabilidade e tolerância a falhas. Em um cenário distribuído, consequentemente dois consumidores da informação podem estar olhando para estados diferentes dos dados por conta do atraso na sincronização e replicação dos dados entre os nodes.

Solicitações de modificação do estado da aplicação podem ser efetuadas somente pelo líder, excluindo a necessidade de que operações de leitura precisem passar por ele, sob pena de o throughput estar limitado a um único processo, impactando a performance. Assim, operações de leitura nos dados, são servidas aos consumidores por qualquer réplica.

Naturalmente, existe um trade-off importante relacionado ao consistency model (strong, sequential ou eventual) adotado. Nesse caso, o CAP theorem reforça o fato de termos a consistência e a disponibilidade em lados opostos quando nos referimos aos sistemas distribuídos, devendo sacrificar uma em favor da outra.

O modelo de consistência define a propriedade responsável por manter atualizada, para todos os processos envolvidos em um cluster, a mesma visão sobre os dados em algum momento na linha do tempo, seja o modelo strong, sequential ou eventual.

Na ausência da atomicidade e transações distribuídas em múltiplos processos (ACID e 2PC — two-phase commit) envolvendo os sistemas distribuídos, abandonamos a strong consistency e dispensamos mecanismos para controle de concorrência e nível de isolamento em favor da eventual consistency e das BASE transactions incluindo etapas de compensação de erros utilizando Sagas como mecanismos para Process Manager. Deste modo, torna-se possível desfazermos etapas de erro na execução, reconstruindo o estado da instância de maneira consistente.

Escalabilidade e Resiliência

A escalabilidade representa uma das principais vantagens ao nos referirmos aos sistemas distribuídos, podendo ser encontrada nos formatos vertical (scaling up) e horizontal (scaling out). Uma aplicação escalável consegue aumentar sua capacidade de processamento horizontalmente de forma ágil, distribuindo a carga de trabalho entre diversos nodes e conseguindo dar vazão ao aumento da demanda, entregando a performance exigida.

Escalabilidade horizontal trata-se do formato mais comum encontrado em sistemas distribuídos modernos, distinguindo-se em três categorias: functional decomposition, partitioning e duplication.

Duplicação trata-se da forma mais comum encontrada em sistemas distribuídos, em conjunto com regras de roteamento e balanceamento dinâmico (load balancing) configura a forma mais barata e rápida de atender a demanda com o processamento exigido. Ao mesmo tempo, o processo de decomposição funcional propõe-se em dividir uma aplicação em partes menores, individuais e desacopladas podendo ter seu deploy executado de forma autônoma sem impactar o ecossistema.

A forma mais comum encontrada em sistemas modernos, representando o conceito de decomposição funcional refere-se a arquitetura de microsserviços.

Quando aplicações distribuídas escalam horizontalmente para atender cargas de trabalho maiores, maior a probabilidade da ocorrência de falhas. Para entregar alta disponibilidade, necessária em sistemas distribuídos, torna-se obrigatória operações de scale-out. Visando minimizar o impacto das falhas, adicionalmente, mecanismos de self-healing e controle de resiliência podem ser empregados.

Falhas podem surgir de diversos tipos: Single point of failure, Unreliable network, Slow processes, Unexpected load, Cascading failures. Mecanismos de resiliência e retentativas (retry — exponential backoff) ajudam a mitigar os riscos inerentes das mesmas.

Mecanismos de resiliência e proteção contra falhas podem ser de dois tipos, downstream e upstream. Sistemas distribuídos precisam evitar impactos de performance, dada a limitada capacidade dos servidores mantendo um limite de conexões disponíveis por porta. Na presença de carga excessiva, servidores podem se valer de técnicas como load-shedding, devolvendo código de status 503 (Service Unavailable).

Paralelamente, ao lidarmos com problemas de resiliência do tipo upstream, outras técnicas como Load leveling e Rate-limiting (throttling) auxiliam no controle e minimização dos impactos causados pela falta de recursos no servidor, necessários para atender a demanda requisitada pelos clientes do serviço com baixa latência e alto throughput.

A execução de health checks fornece métricas fundamentais referente o estado e saúde dos recursos (CPU, memória, disco) dos componentes em execução.

Tanto o isolamento de falhas, evitando sua propagação para outras partes do sistema, conhecido como bulkhead quanto padrões como circuit breaker são técnicas comumente aplicadas na maioria dos sistemas distribuídos, aumentando a confiabilidade do componente entregue e garantindo-lhe maior qualidade.

Arquitetura de Microsserviços

Conceitualmente, microsserviços refletem a utilização do padrão funcional decomposition, tanto em termos de business capabilities quanto da agilidade nas unidades de deployment. A nomenclatura adequada para este este tipo de arquitetura seria SOA (service-oriented architecture). Essencialmente, o estilo de arquitetura de microsserviços retratam SOA! O problema de utilizar termos de arquitetura que remete 10 anos atrás pode parecer antiquado e defasado, desta forma o mercado tratou de arrumar um novo nome para o mesmo conceito.

A aplicação prática dos microsserviços não se resume a rodar componentes em containers com o apoio de processos CI/CD. Na medida em que os componentes crescem, e percebemos problemas de dependência afetando uns aos outros, fica evidente de que não estamos construindo microsserviços da forma correta. Deste ponto em diante, precisamos ficar atentos aos microservice anti-patterns: distributed monolith.

Monólitos distribuídos assemelham-se aos microsserviços porém apresentam forte acoplamento (persistence layer, sync-calls), remetendo as aplicações monolíticas.

Na prática, nem todos os sistemas distribuídos são microsserviços, porém podemos afirmar que os microsserviços refletem um grupo particular de sistemas distribuídos. Desta forma, concluímos que os microsserviços referem-se a implementações relacionadas a camada de infraestrutura e redes (service mesh layer), mantendo seus limites transacionais bem definidos entre os contextos de negócio.

O codebase de um serviço pode tornar-se complexo o bastante, tornando-se incompreendido a ponto de dificultar a inclusão de novas features ou correção de bugs.

Durante a trajetória em direção aos microsserviços, SOA desempenhou papel fundamental por quase duas décadas, mantendo conceitos até hoje praticados em padrões sucessores, a exemplo dos microsserviços. Precisamos discriminar que a diferença existente entre microsserviços e SOA não são conceituais e sim de infraestrutura e agilidade no deploy, beneficiando-se da evolução no contexto tecnológico, um dos principais responsáveis pelo sucesso da arquitetura de microsserviços.

O processo de deployment de componentes SOA ainda eram rígidos e acoplados, em um momento onde não existiam, ou não eram comuns, tecnologias como docker e kubernetes.

No fim do dia, microsserviços são sistemas distribuídos, herdando implicitamente todas as vantagens e complexidades desse tipo de arquitetura. Definir os limites transacionais de cada componente bem como acertar a granularidade adequada tornam-se mais complexos em um contexto distribuído. Além disso, dividir uma aplicação em serviços menores adiciona complexidade, forçando-nos a abraçar a consistência eventual.

Sistemas distribuídos de sucesso podem ter iniciado seu ciclo de vida vida como aplicações monolíticas, evoluindo até concretizar-se como serviços menores, autogerenciáveis e autônomos. Desta forma, a transição entre aplicações monolíticas e sistemas distribuídos precisa ser realizada com cautela. Idealmente, quando um monólito está bem estruturado e definido, ao ser desmembrado terá maiores chances de performar com sucesso no universo dos sistemas distribuídos.

Considerações Finais

Aventurar-se com os sistemas distribuídos sempre será desafiador, portanto realizar as escolhas certas de tecnologia para contornar as complexidades deste tipo de ecossistema trata-se de algo crucial para o sucesso. Tecnicamente, tracing e logging, execução em containers, devOps, processos CI/CD, design e arquitetura de uma aplicação representam alguns dos ingredientes necessários para uma boa estadia neste mundo onde residem tais componentes tão peculiares.

Em grande parte, sistemas distribuídos são difíceis e complexos, com poucas chances de sucesso pleno. Escolhas erradas são muito fáceis de serem tomadas, aumentando as chances de comprometermos o ecossistema como um todo. Assim, será preciso mais do que simplesmente seguir receitas de bolo, pois estaremos expostos a um ambiente hostil, propício a falhas e intrinsecamente predestinado ao fracasso. Por fim, dependerá somente de nós o trabalho de converter estes atributos negativos em vantagens competitivas para o sucesso de nossos sistemas distribuídos.

--

--