Introdução aos Sistemas Distribuídos
Durante as primeiras disciplinas de programação, é comum construir algoritmos cuja execução ocorre integralmente em um único programa. O estudante declara variáveis, implementa funções, organiza estruturas de decisão e repetição e, por fim, executa todas as instruções em uma mesma máquina. Mesmo quando o código está dividido em módulos, classes ou arquivos, o programa costuma formar uma única unidade de execução, com acesso direto à memória e aos recursos locais.
Esse modelo é adequado para aprender lógica de programação e resolver muitos problemas computacionais. Um algoritmo de ordenação, por exemplo, recebe uma coleção de dados, executa comparações e movimentações na memória e devolve o resultado. A análise de desempenho normalmente considera tempo de processamento, consumo de memória e correção lógica.
Porém, em muitos momentos esse tipo de modelo torna-se insuficiente. Para estes cenários, deve-se optar pelos sistemas distribuídos. Van Steen e Tanenbaum (2023) descrevem um sistema distribuído como um conjunto de elementos computacionais autônomos que, para seus usuários, procura comportar-se como um sistema coerente. A palavra autônomos aqui é importante, pois indica que cada computador (ou processo) possui estado próprio, executa instruções localmente e pode continuar, atrasar ou falhar independentemente dos demais. Logo, a coerência percebida pelo usuário depende da coordenação entre esses componentes.
Isso significa que as preocupações de desempenho elencadas anteriormente (tempo de processamento, consumo de memória etc.) passam a coexistir com outras variáveis. Os desenvolvedores precisam se preocupar também com comunicação pela rede, concorrência, indisponibilidade de máquinas, atraso de mensagens e ausência de um relógio global perfeitamente compartilhado.
A transição para o modelo de sistemas distribuídos altera a unidade central de raciocínio do programador. Em um programa tradicional, pergunta-se principalmente: “qual função será executada em seguida?”. Em um sistema distribuído, também se pergunta: “em qual processo essa função será executada?”, “como os dados chegarão até ele?”, “quanto tempo a resposta poderá levar?” e “o que ocorrerá se um dos participantes não responder?”.
Essa mudança pode ser observada em aplicações comuns. Um sistema de catálogo de filmes pode começar como um único programa que mantém um vetor em memória. Em uma etapa posterior, a interface passa a ser executada no navegador, enquanto os dados são manipulados por um servidor. Depois, o servidor pode consultar um banco de dados, chamar um serviço de recomendação e publicar eventos para outros processos. O que antes era uma chamada local de função transforma-se em uma sequência de comunicações entre componentes independentes.
Do algoritmo monolítico ao sistema distribuído
Um sistema monolítico concentra suas principais responsabilidades em uma única aplicação implantável. Isso não significa, necessariamente, código desorganizado. Um monólito pode ser modular, possuir camadas bem definidas e empregar boas práticas de engenharia de software. A característica principal é que suas partes são empacotadas e executadas como uma unidade fortemente integrada.
Considere um backlog de filmes representado por um vetor JavaScript:
const filmes = [
{ id: 1, titulo: "Central do Brasil", assistido: false },
{ id: 2, titulo: "Blade Runner", assistido: true }
];
function listarFilmes() {
return filmes;
}
console.log(listarFilmes());Nesse exemplo, a função listarFilmes() acessa diretamente a variável filmes. A chamada ocorre dentro do mesmo processo, portanto não há transmissão pela rede, serialização dos dados ou espera por uma máquina remota. Se a função for executada corretamente, o resultado estará disponível quase imediatamente, limitado principalmente pela capacidade do processador e da memória local.
Em uma arquitetura distribuída, a interface pode estar em um processo cliente e a lista de filmes em um processo servidor. A operação lógica continua sendo “listar filmes”, mas a execução exige uma requisição, o transporte da mensagem, o processamento no servidor e o retorno de uma resposta. A chamada deixa de ser local e passa a ser remota.
Essa separação permite distribuir responsabilidades e recursos. Um servidor pode atender diversos clientes, vários servidores podem compartilhar a carga e serviços especializados podem evoluir separadamente. Ao mesmo tempo, a rede passa a fazer parte do comportamento do programa. Segundo Lamport (1978), a ordenação dos acontecimentos em um sistema distribuído não pode depender simplesmente de um relógio físico único e perfeitamente compartilhado. Em consequência, a coordenação entre processos exige protocolos e critérios explícitos.
Conceitos básicos
A computação distribuída acrescenta uma dimensão arquitetural e operacional. Um serviço distribuído pode ser implementado com programação estruturada, orientação a objetos, programação funcional ou uma combinação dessas abordagens. A diferença está na existência de múltiplos componentes que se comunicam e coordenam suas ações.
Por conta dessa nova dimensão, existem modelos específicos para programação distribuída. Entre eles estão o modelo cliente-servidor, a comunicação por troca de mensagens, a chamada de procedimento remoto, os sistemas orientados a eventos, a arquitetura de microsserviços e os modelos de processamento distribuído de dados.
Dean e Ghemawat (2004), por exemplo, propuseram o MapReduce como um modelo capaz de dividir o processamento de grandes conjuntos de dados entre muitas máquinas, delegando à infraestrutura tarefas como particionamento, escalonamento e tratamento de falhas. Portanto, a distribuição surge quando partes autônomas executam em processos ou máquinas diferentes e dependem de comunicação para alcançar um objetivo comum.
Neste texto focaremos no modelo cliente-servidor e, consequentemente, precisamos discutir seus principais conceitos.
Cliente e servidor
Um processo (ou máquina) cliente é o componente que inicia uma interação para solicitar dados ou operações. No contexto HTTP, o cliente cria uma mensagem de requisição, informa sua intenção por meio de um método e encaminha essa mensagem a um servidor de origem. O cliente pode ser um navegador, uma aplicação móvel, um programa de linha de comando ou outro serviço.
A RFC 9110 define que “um cliente HTTP é um programa que estabelece uma conexão com um servidor” (Fielding; Nottingham; Reschke, 2022, tradução nossa). Essa definição destaca que o cliente tem um papel de comunicação, e não de processamento pesado durante a execução do sistema.
Paralelamente, o processo (ou máquina) servidor é o componente que aguarda solicitações, interpreta as mensagens recebidas, executa alguma operação e devolve uma resposta. A RFC 9110 descreve o servidor HTTP como um programa que aceita conexões para atender requisições por meio do envio de respostas (Fielding; Nottingham; Reschke, 2022, tradução nossa).
Na prática, um servidor não é necessariamente um computador físico dedicado. Ele pode ser um processo em um notebook (lembre-se do conceito de processo em sistemas operacionais), um contêiner, uma máquina virtual, uma função em nuvem ou uma instância executada em um cluster. O termo descreve o papel exercido na interação.
Na Figura 1 apresentamos um representação de como ocorre a estrutura teórica do modelo cliente-servidor.

Fonte: Autoria própria.
Latência
Latência é o tempo decorrido entre o início de uma operação e a percepção de seu resultado. Em uma comunicação cliente-servidor, ela pode incluir o tempo de preparação da requisição, transmissão pela rede, espera em filas, processamento no servidor, acesso ao banco de dados e retorno da resposta.
Em uma função local simples, o custo de comunicação é muito pequeno. Em uma requisição remota, mesmo um processamento rápido pode produzir uma resposta perceptivelmente lenta por causa da distância, congestionamento, sobrecarga ou dependências internas. Por isso, o desempenho de um sistema distribuído não deve ser avaliado apenas pelo tempo de CPU.
Podemos representar de forma simplificada a latência total como:
latência total = envio da requisição
+ espera na rede
+ espera em fila
+ processamento
+ acesso a dependências
+ retorno da resposta
Essa representação indica que a latência também pode variar bastante, de forma que duas requisições idênticas podem levar tempos diferentes. Essa variação é chamada frequentemente de jitter. Sistemas reais precisam considerar médias, percentis e valores extremos, pois uma média baixa pode esconder uma pequena parcela de respostas muito lentas.
O código seguinte introduz um atraso artificial no servidor:
const http = require("node:http");
const filmes = [
{ id: 1, titulo: "Central do Brasil", assistido: false },
{ id: 2, titulo: "Blade Runner", assistido: true }
];
const servidor = http.createServer((requisicao, resposta) => {
if (requisicao.method === "GET" && requisicao.url === "/filmes") {
const atrasoEmMilissegundos = 1500;
setTimeout(() => {
resposta.writeHead(200, {
"Content-Type": "application/json; charset=utf-8"
});
resposta.end(JSON.stringify(filmes));
}, atrasoEmMilissegundos);
return;
}
resposta.writeHead(404);
resposta.end();
});
servidor.listen(3000);O serviço continua correto, mas ficou mais lento. No cliente, pode-se medir o tempo total:
async function medirConsulta() {
const inicio = performance.now();
const resposta = await fetch("http://localhost:3000/filmes");
const filmes = await resposta.json();
const fim = performance.now();
console.log(filmes);
console.log(`Latência observada: ${(fim - inicio).toFixed(2)} ms`);
}
medirConsulta();Esse tempo é observado pelo cliente e inclui mais do que o processamento interno do servidor. Em ambientes distribuídos, a experiência do usuário depende desse percurso completo.
Falhas
Falhas são eventos que impedem um componente de cumprir o comportamento esperado. Elas podem ocorrer no processo cliente, no servidor, na rede, no armazenamento, no sistema operacional ou em uma dependência externa. Van Steen e Tanenbaum (2023) incluem a tolerância a falhas entre os fundamentos dos sistemas distribuídos porque componentes autônomos podem falhar independentemente.
Em um programa monolítico simples, uma falha grave pode encerrar toda a aplicação. Em um sistema distribuído, é possível ocorrer uma falha parcial, ou seja, um componente deixa de responder enquanto outros continuam funcionando. O navegador pode estar operacional, a rede local pode funcionar e, ainda assim, o servidor de filmes pode estar indisponível.
Entre as falhas comuns estão:
- falha por parada, quando um processo deixa de executar;
- falha de comunicação, quando mensagens são perdidas, atrasadas ou interrompidas;
- falha de temporização, quando uma resposta chega depois do limite aceitável;
- falha de resposta, quando o componente devolve um valor incorreto;
- falha arbitrária, quando o comportamento deixa de seguir o protocolo previsto.
O problema central é que atraso e falha podem ser difíceis de distinguir. Se o servidor não respondeu em dois segundos, ele pode estar inativo ou apenas sobrecarregado. O cliente usa limites de tempo, chamados timeouts, para impedir espera indefinida, mas o timeout representa uma decisão operacional, não uma prova absoluta de que o servidor falhou.
Vejamos agora um exemplo de cliente com timeout:
async function carregarFilmesComTimeout() {
const controlador = new AbortController();
const limite = setTimeout(() => controlador.abort(), 2000);
try {
const resposta = await fetch("http://localhost:3000/filmes", {
signal: controlador.signal
});
if (!resposta.ok) {
throw new Error(`Servidor respondeu com status ${resposta.status}`);
}
return await resposta.json();
} catch (erro) {
if (erro.name === "AbortError") {
throw new Error("O servidor excedeu o tempo máximo de resposta.");
}
throw erro;
} finally {
clearTimeout(limite);
}
}
carregarFilmesComTimeout()
.then((filmes) => console.log(filmes))
.catch((erro) => console.error(erro.message));O timeout evita que o cliente espere indefinidamente. Contudo, uma nova tentativa deve ser planejada com cuidado. Se a operação original for apenas uma consulta, repeti-la geralmente é segura. Em operações de escrita, como cadastrar um filme, uma repetição automática pode criar duplicidades caso o servidor tenha concluído a primeira operação, mas a resposta tenha se perdido.
Idempotência
Esse cenário introduz o conceito de idempotência, que “é a propriedade em matemática e ciência da computação que permite a certas operações serem aplicadas múltiplas vezes sem alterar o resultado além da aplicação inicial” (Pereira, 2024). Uma operação idempotente pode ser repetida sem produzir efeitos adicionais depois da primeira aplicação bem-sucedida.
Por exemplo, imagine que o usuário deseja marcar um filme como assistido no backlog. Se essa operação for executada uma vez, o filme fica como assistido. Se for executada dez vezes, o estado final continua o mesmo, ou seja, assistido. Portanto, essa operação tende a ser idempotente. Da mesma forma, se o usuário deseja atualizar o título de um filme para um valor específico, repetir essa requisição fará com que o recurso continue com os mesmos dados. O efeito final não muda após a primeira execução bem-sucedida.
Agora, um exemplo não idempotente é incrementar um contador. Se a requisição for repetida por causa de uma falha de rede, o sistema pode registrar visualizações extras indevidamente, pois cada execução aumenta o contador. Portanto, a quantidade de repetições altera o efeito final após a primeira aplicação.
No contexto do protocolo HTTP, de forma geral, GET, PUT e DELETE costumam ser projetados como idempotentes. Por outro lado, o verbo POST normalmente não é. Mas, cuidado, o ponto central aqui é observar o efeito final no estado do sistema, não apenas o método usado. Por isso, em sistemas distribuídos, projetar identificadores únicos, verificar requisições duplicadas e escolher adequadamente os métodos HTTP são estratégias importantes para reduzir efeitos indesejados.
Relação entre os conceitos
Os conceitos apresentados formam uma sequência lógica. No exemplo de backlog de filmes, uma possível sequência é:
1. O usuário solicita a lista de filmes.
2. O processo cliente cria uma requisição HTTP.
3. A rede transporta a requisição.
4. O processo servidor recebe a mensagem.
5. O serviço consulta ou produz os dados.
6. O servidor cria uma resposta.
7. A rede transporta a resposta.
8. O cliente interpreta o resultado.
Cada etapa possui custos e possibilidades de falha. Essa decomposição permite diagnosticar problemas com maior precisão. Uma tela vazia pode ser causada por erro de interface, endereço incorreto, indisponibilidade do servidor, demora na consulta, resposta inválida ou falha na interpretação do JSON.
O domínio de sistemas distribuídos exige que o programador deixe de tratar a comunicação como um detalhe transparente. Chamadas remotas possuem semântica, custo e incerteza próprios. A boa arquitetura torna essas características explícitas por meio de contratos, observabilidade, limites de tempo, tratamento de erros e mecanismos de recuperação.
Vantagens e desvantagens dos sistemas distribuídos
A distribuição em sistema permite ampliar capacidade, aproximar serviços dos usuários, compartilhar recursos e separar responsabilidades. Vários servidores podem atender requisições simultaneamente, componentes podem ser implantados em locais diferentes e serviços especializados podem evoluir de forma relativamente independente. Sistemas distribuídos também podem apresentar maior disponibilidade quando existe redundância e substituição automática de componentes.
Entretanto, a distribuição acrescenta complexidade. O estado deixa de estar concentrado, a comunicação pode atrasar, mensagens podem ser duplicadas e falhas parciais tornam o diagnóstico mais difícil. Testes, monitoramento, segurança e implantação também precisam abranger vários componentes. Um sistema distribuído mal projetado pode ser mais lento e menos confiável do que uma solução monolítica simples.
A decisão de distribuir deve responder a uma necessidade concreta, como escala, disponibilidade, isolamento, integração ou distribuição geográfica. A arquitetura monolítica continua sendo adequada para muitos sistemas, principalmente em estágios iniciais ou domínios com baixa complexidade operacional, enquanto a distribuição deve ser tratada como uma escolha de engenharia acompanhada de custos e compromissos.
Conclusão
A passagem dos algoritmos tradicionais para os sistemas distribuídos amplia o campo de raciocínio do programador. Variáveis, funções e estruturas de controle continuam importantes, mas passam a integrar processos autônomos que trocam mensagens e dependem da rede.
As ideias de cliente, servidor, latência e falhas mostram que uma operação remota não possui as mesmas garantias de uma chamada local. A resposta pode demorar, não chegar ou representar apenas parte do estado do sistema. Por isso, aplicações distribuídas precisam definir contratos, timeouts, políticas de repetição, validação, observabilidade e mecanismos de recuperação.
O domínio desses fundamentos prepara o estudante para compreender APIs Web, aplicações em nuvem, bancos distribuídos, microsserviços, sistemas orientados a eventos e processamento de dados em larga escala. A principal mudança teórica consiste em reconhecer que os componentes não compartilham automaticamente memória, tempo ou destino. Eles cooperam por meio de protocolos, e a qualidade do sistema depende da forma como essa cooperação é projetada.
Obrigado pela leitura e bons estudos!
Referências
DEAN, Jeffrey; GHEMAWAT, Sanjay. MapReduce: simplified data processing on large clusters. In: SYMPOSIUM ON OPERATING SYSTEM DESIGN AND IMPLEMENTATION, 6., 2004, San Francisco. Proceedings […]. Berkeley: USENIX Association, 2004. p. 137-150. Disponível em: https://research.google/pubs/mapreduce-simplified-data-processing-on-large-clusters/. Acesso em: 30 jul. 2026.
FIELDING, Roy T.; NOTTINGHAM, Mark; RESCHKE, Julian. HTTP semantics. [S. l.]: RFC Editor, 2022. RFC 9110. Disponível em: https://www.rfc-editor.org/rfc/rfc9110.html. Acesso em: 30 jul. 2026.
LAMPORT, Leslie. Time, clocks, and the ordering of events in a distributed system. Communications of the ACM, New York, v. 21, n. 7, p. 558-565, jul. 1978. Disponível em: https://lamport.azurewebsites.net/pubs/time-clocks.pdf. Acesso em: 30 jul. 2026.
PEREIRA, Fábio. Idempotência e como corrigir solicitações duplicadas de APIs. 2024. Disponível em: https://medium.com/@fabio.alvaro/idempot%C3%AAncia-e-como-corrigir-solicita%C3%A7%C3%B5es-duplicadas-de-apis-ce08d1a51e72. Acesso em: 30 jul. 2026.
VAN STEEN, Maarten; TANENBAUM, Andrew S. Distributed systems. 4. ed. [S. l.]: distributed-systems.net, 2023. Disponível em: https://www.distributed-systems.net/index.php/books/ds4/. Acesso em: 30 jul. 2026.


