Concorrência e Tolerância a Falhas em Sistemas Distribuídos

Marcelo M. Gonçalves
7 min readApr 11, 2023

--

Se esquecermos a programação por um instante e nos concentramos no mundo real, perceberemos a concorrência e o paralelismo presentes em toda a parte. Desde andarmos pelo supermercado até dirigirmos pela auto estrada estaremos cientes da presença, a todo momento, das diversas entidades convivendo e interagindo entre si. Nosso cérebro está preparado para lidar, de forma empírica, com as adversidades impostas, caso contrário não sobreviveríamos.

Ao construirmos softwares que assemelham-se a elementos no mundo real, então suas estruturas precisarão ser implicitamente concorrentes e seu comportamento com possibilidade de paralelismo. Precisamos abraçar a diversidade da natureza dos eventos, considerar a timeline dos fluxos de execução e sua desordem em nossas pipelines de trabalho. Ao respeitar o caos de um ambiente complexo, e utilizando mecanismos e técnicas apropriadas, poderemos garantir certa integridade nas operações de software rodando sob esta perspectiva.

Essencialmente, conceitos como concorrência e estado mutável não convivem no mesmo ambiente. Assim, tentativas de construção de sistemas distribuídos, e consequentemente concorrentes ou paralelos, primeiramente precisam resolver as discrepâncias entre estes dois contextos antes de avançar.

Ao descrevermos tarefas do nosso cotidiano, termos como concorrente, simultâneo e paralelo podem ter o mesmo significado. No contexto de software, precisamos ser mais precisos e particularmente fornecer a distinção adequada entre concorrência e paralelismo. Simplificando: Computadores single-core executam somente uma tarefa por vez e consequentemente nunca serão capazes de executar atividades em paralelo. Como resultado, o Time-sharing de CPU, através da concorrência, possibilita simularmos a execução de tarefas em paralelo.

Quando bem desenhados e elaborados com a concorrência em mente, a inclusão de cores adicionais proporciona melhoras significativas ao software. Desta forma, os benefícios em resolvermos problemas usando padrões de concorrência resultam em programas com melhorias de performance, mais escaláveis e tolerantes a falhas, multiplicadas pela quantidade de processos executando em paralelo. Assim, uma execução paralela genuína, obrigatoriamente, exige a presença de mais de um núcleo físico de CPU.

Concorrência e Paralelismo

Componentes distribuídos não podem compartilhar atributos. O gerenciamento do ciclo de vida deve ser individual e a comunicação ocorrer através de abstrações. Na prática, o comportamento refere-se à observalidade do funcionamento do mundo real, com a comunicação baseada no envio de mensagens, garantindo que essas informações estão sendo recebidas e interpretadas corretamente. Neste contexto, os elementos operam independentemente e sem compartilhamento de recursos: memória e controle de estado privados, conferindo-lhe facilidades em gerenciamento e escalabilidade.

Idealmente, dadas sua rigidez e complexidade, sistemas concorrentes e distribuídos não devem valer-se de locks ou mutexes. A utilização de uma linguagem/estrutura desenhada para ser concorrente facilitará o desenvolvimento de componentes paralelos.

Programas sequenciais não são necessariamente lentos se comparados aos paralelos. Na realidade, seus conceitos e dimensões de atuação distintos impedem a comparação: ao contrário da concorrência, a qual refere-se a estrutura de um software, o paralelismo está relacionado ao hardware/infraestrutura. Compondo a distinção entre ambos os termos, programas concorrentes servem a propósitos como performance, escalabilidade e tolerância a falhas; sendo o paralelismo apenas uma replicação desta filosofia.

A concorrência deve passar por um processo de modelagem e neste caso, a assertividade na escolha de boas estratégias influenciará no design de concorrência estruturalmente adequado. Pode ser empregada técnicas como Concurrency Oriented Programming (COPL), e quando bem construídos, programas concorrentes serão capazes de rodar, de forma bem sucedida, mesmo em contextos não projetados inicialmente para serem paralelos, seja em computadores multicore, na nuvem ou em clusters de computadores em rede.

Programas concorrentes baseiam-se no conceito de modeling concurrency, podendo ser modularizados e em conjunto, mesmo que temporariamente, resolvem problemas a partir do agrupamento da execução de múltiplos processos sequenciais com finalidades e comportamentos similares.

Tolerância a Falhas

Precisamos nos perguntar: O que é um erro e como ele se faz conhecido?! Como podem afetar o sistema e quais seus possíveis efeitos colaterais. Ao construirmos sistemas tolerantes à falha, precisamos definir o preço a ser pago pelo tratamento dos erros, e como poderão ser suavizados sem que os usuários do sistema notem sua ocorrência. Para isso, a estratégia e semântica adequadas para sistemas concorrentes precisam ser adotadas.

A filosofia para detecção e controle de falhas em programas concorrentes envolve estratégias diferentes se comparadas ao modelo sequencial. Na medida em que a prevenção de erros e a prática de defensive programming encontram-se presentes em programas sequenciais, conceitos e habilidades necessárias para execuções concorrentes e paralelas aproximam-se da “Let It Crash” philosophy.

A observabilidade entre os processos, (links ou monitors) inter-processos e inter-machines, desempenham papel fundamental ao estruturarmos sistemas tolerantes a falha assumindo a presença de erros.

A utilização dos lightweight process são baratas para o processamento, se comparadas as OS threads, e permitem ações corretivas em processos supervisores independentes. Em situações de erro, processos linkados e monitorando uns aos outros, permanecem sob a responsabilidade em limpar o cenário e reconstruir o estado para o inicial. Caso um processo morra, outro processo recebe não somente uma notificação do erro ocorrido, mas adicionalmente o motivo através do envio de sinais.

A utilização de linguagens com paradigmas funcionais facilitam o troubleshooting através da imutabilidade, possibilitando termos somente um local para investigar. Se comparadas a OOP, com o estado mutável dos objetos, em um processo de investigação, precisamos averiguar todos os locais onde uma variável possa ter sofrido alteração, adicionando complexidade.

Tanto serviços em nuvem quanto sistemas web escaláveis baseiam-se na presença de mecanismos confiáveis para controle de erros, incluindo procedimentos de detecção e prevenção de desastres. A redundância de infraestrutura e independência entre componentes compõem a base para sistemas tolerantes à falha. Neste contexto, não existe a interferência por erros inter-processos, evitando o alastramento de falhas em cascata.

O descompasso mecânico entre eventos paralelos ocorridos no mundo real, e o mindset sequencial presentes nas linguagens de programação torna o contexto complexo.

Processos: Sequenciais vs Concorrentes

Linguagens de programação influenciam a natureza de comunicação dos nossos softwares, sejam concorrentes ou paralelos..o nível de paralelismo pode estar a nível de aplicação, e consequente threads com wrapper one-to-one com o OS, ou a nível de VM, como uma camada de virtualização entre o OS e a aplicação.

Programas sequenciais executam tarefas em um formato step-by-step (Single-threaded Model), encadeando passos e utilizando o resultado das computações do passo anterior como argumentos de entrada para o passo seguinte. Em um modelo concorrente, e naturalmente mais complexo, estas computações/passos podem ser executadas independentemente e com ordem arbitrária tornando-se mais performático.

Paradigmas funcionais, através da imutabilidade, proporcionam execuções side-effects free, permitindo computações paralelas e facilidades em atividades de debugging.

Ao atribuirmos o modelo de concorrência diretamente ao OS, e discriminamos o contexto de execução de aplicações concorrentes, a concorrência depende do OS poderá comportar-se diferentemente ao variarmos sua plataforma de execução, a depender das abstrações dos modelos de concorrência pertencentes ao OS adjacente.

Modelos de concorrência tentam calibrar o nível de independência exigido para a execução de algumas operações, facilitando a formatação de pipelines de execução com mais performance.

Diferentemente do modelo de OS, quando o suporte oferecido for a partir da linguagem ou camada de virtualização (JVM — Loom, BEAM-VM) os recursos serão infinitamente mais baratos e seu comportamento será único exclusivamente dentro do contexto da VM executando a operação. Independente do modelo e contexto de abstração adotados, processos são a essência para a concorrência.

Softwares Distribuídos e Concorrentes

Programas concorrentes são compostos de pequenos processos independentes, facilitando sua escalabilidade de duas formas, seja aumentando o número de processos ou adicionando mais CPUs, balanceando a carga de trabalho entre os núcleos. Até pouco tempo, computadores multicore eram caros/raros e desta forma, muitas das linguagens convencionais possuem suporte pobre para a concorrência.

Quanto mais núcleos, melhor será a performance ao aproveitarmos plenamente o hardware, porém, sem suporte a partir da linguagem torna-se difícil atingirmos níveis maduros de concorrência em nossos softwares.

Conceitualmente, o Communicating sequential processes (CSP) introduzido em 1978 por Tony Hoare, descreve um estilo de comunicação referindo-se a combinação da execução sequencial através de threads e o envio de mensagens síncronas. Em 1973, em um modelo mais abstrato se comparado ao CSP, Carl Hewitt propôs o Actor Model, diferindo-se do CSP ao tornar o estilo de comunicação assíncrono através dos mailboxes. Em todos os casos, para que processos concorrentes e distribuídos executarem com sucesso, precisam de base sólida conceitual. Como implementações de referência do Actor Model, podemos citar Erlang-VM (BEAM) e Akka Framework.

Em 1965, as complexidades de implementação de mecanismos de locking foram demonstradas por Edsger W. Dijkstra em um problema originalmente formulado Dining philosophers. Desta forma, a história nos mostra que quando nos referimos a programas concorrentes, locks devem ser evitados sob pena de os problemas superarem as vantagens.

A complexidade intrínseca dos sistemas distribuídos pode ser compensada com algumas vantagens: performance, confiabilidade e escalabilidade. Em um contexto de programas concorrentes, a possibilidade de paralelizar representa uma pequena etapa no processo, e ao estruturarmos um software para executar concorrentemente, sua distribuição em N nodes funcionará de forma transparente e sua performance será multiplicada.

Considerações Finais

A definição de concorrência aponta para uma série de eventos ocorrendo simultaneamente, assim como no mundo real. Quando executamos uma simples ação, será necessária a análise massiva de dados/eventos a considerar as diversas possibilidades de resultado, e no dia a dia, fazemos isso naturalmente.

No contexto de programação convencional, com a lógica inversa, evitamos a concorrência dada sua complexidade e a execução de atividades em um formato sequencial torna-se um padrão. Não existe bala de prata e nem sempre a execução concorrente ou o paralelismo serão a solução para todos os problemas: são trade-offs. Quando necessário, e para sermos distribuídos e executar em paralelo, precisamos inevitavelmente ser projetados para sermos concorrentes.

Programas distribuídos são pensados para rodarem em uma rede de computadores interligada, coordenando sua comunicação pelo envio de mensagens. Na medida em que avançamos, paralelizando e distribuindo, precisamos de resiliência e tolerância a falhas, sendo a concorrência a base para possibilitar o sucesso neste contexto.

--

--