Este projeto configura uma arquitetura de microserviços em Node.js com um API Gateway/Consumidor (order-service), um serviço de destino instável (payment-service), um executor de testes de carga (k6-runner) e um gerador de relatórios em formato texto em ./results/report.txt.
Este é um case de resiliência: o payment-service simula um memory leak real (não latência/erro artificial) que cresce até o limite de memória do container, entra em crash-loop (OOM-kill + restart automático) sob carga contínua, e o order-service foi evoluindo, branch a branch, para lidar com isso — retry com backoff exponencial e jitter, circuit breaker e bulkhead.
order-service(Porta3000): Gateway de entrada. Chamadas aopayment-serviceque falham momentaneamente (timeout ou erro 5xx) são reenviadas automaticamente com backoff exponencial e jitter (até 3 tentativas por padrão — verorder-service/README.md). Por fora do retry, um circuit breaker acompanha o resultado final de cada pedido: depois de falhas consecutivas seguidas ele abre e passa a falhar rápido (sem chamar opayment-service) por alguns segundos, protegendo o serviço já degradado de ser sobrecarregado enquanto reinicia. Um bulkhead (limite de concorrência, padrão 8 chamadas simultâneas) isola o event loop doorder-service: pedidos além do limite são rejeitados na hora em vez de se acumularem esperando opayment-serviceresponder.payment-service(Porta3001interna/mapeada): Serviço instável simulando um memory leak real, fazendo a memória física crescer continuamente até se aproximar do limite de memória do container (256MB). Temrestart: on-failure, então após um OOM-kill o Docker o reinicia sozinho (memória volta a zero) — sob carga contínua isso forma um ciclo real de crash-loop.k6-runner: Executor de testes de carga do Grafana k6, gerando resumo estruturado em./results/k6-summary.json.
Você pode rodar todo o fluxo de teste e geração de relatório com um único comando:
./scripts/run-test.shCada padrão de resiliência foi implementado e testado isoladamente em seu próprio branch, cada um com seu README congelado descrevendo a implementação e os números da execução naquele ponto (os valores variam um pouco a cada rodada, já que o timing do memory leak/GC não é perfeitamente determinístico — servem para mostrar a tendência, não como benchmark exato):
| Branch | O que foi adicionado | Taxa de erro | p(95) |
|---|---|---|---|
feature/01-base-service-unstable |
Cenário base: payment-service com memory leak real, sem nenhuma proteção no order-service |
16.43% | 3005ms |
feature/02-pattern-retry-backoff |
Retry com backoff exponencial + jitter, e restart: on-failure no payment-service |
2.58% | 1148ms |
feature/03-pattern-circuit-breaker |
Circuit breaker: falha rápido quando o payment-service está degradado, em vez de insistir |
7.84% | 400ms |
feature/04-pattern-bulkhead-isolation (estado atual, refletido abaixo) |
Bulkhead: limite de concorrência simultânea ao payment-service, isolando o event loop do order-service |
7.00% | 205ms |
Vale notar que a taxa de erro sobe entre o branch 02 e o 03/04 — não é regressão, é o trade-off esperado: circuit breaker e bulkhead priorizam falhar rápido e proteger os dois serviços em vez de insistir tentando salvar cada pedido a qualquer custo (o que reduz bastante a latência p(95) e evita que uma indisponibilidade do payment-service derrube o order-service junto). O objetivo desses padrões é estabilidade do sistema sob falha parcial, não garantia de entrega de 100% dos pedidos — isso exigiria uma camada assíncrona (fila/outbox), que é um problema arquitetural diferente.
Resumo da última execução registrada em results/report.txt (retry com backoff/jitter + circuit breaker + bulkhead no order-service, restart: on-failure no payment-service, todos ativos):
- Requisições HTTP: 443 no total (12.56 req/s), com 7.00% de taxa de erro.
- Latência: média de 86.13ms, mediana de 16.08ms, p(95) de 204.89ms e máxima de 2561.69ms.
- Checks de negócio: 412 de 443 pedidos foram confirmados (201); 31 pedidos (7.00%) voltaram como 502/503.
- Ciclo de crash-loop do
payment-service: dois ciclos de leak dentro dos ~44s de teste (RAM sobe até a faixa de 250-256MB, cai bruscamente por volta de t+31s com o OOM-kill/restart, e sobe de novo até o fim do teste). - Bulkhead em ação: com
BULKHEAD_MAX_CONCURRENT=8e fila zero, 19 pedidos foram rejeitados na hora por excesso de concorrência ("8/8 em execução, 0/0 na fila"), nem chegando a consumir uma tentativa de retry ou contar como falha do circuit breaker. O circuito ainda abriu 1 vez (5 fail-fast) e o retry disparou 11 vezes. - Efeito comparado com a execução anterior (sem bulkhead): o p(95) caiu bastante (400ms → 205ms) — o bulkhead evita que o event loop fique represado esperando várias chamadas lentas ao mesmo tempo, então a maioria dos pedidos passa rápido. Em compensação, a máxima subiu (1371ms → 2562ms) nesta execução: como o bulkhead é sem fila (rejeita na hora em vez de esperar), ele não "segura" pedidos elegantemente — quem não rejeitado ainda pode encontrar o
payment-servicelento em um momento ruim do ciclo de memória. A taxa de erro ficou estável (7.84% → 7.00%), mostrando que o bulkhead trocou parte das falhas por rejeições mais baratas (sem gastar retry/rede) sem piorar o resultado geral.
Em resumo, os resultados mostram que a combinação de retry com backoff exponencial e jitter, circuit breaker e bulkhead torna o sistema significativamente mais resiliente a falhas parciais. Mesmo diante de um payment-service degradado por um memory leak e entrando em crash-loop, o order-service permanece estável, limita o impacto da indisponibilidade e evita que a falha se propague para o restante da aplicação. O custo dessa estratégia é aceitar a rejeição imediata ou a falha rápida de uma parcela das requisições, mas esse trade-off preserva a disponibilidade geral do sistema e impede que um único serviço comprometido provoque um efeito cascata capaz de indisponibilizar toda a arquitetura.
- Tolerância a falhas em microsserviços: avaliando técnicas sob gargalos de CPU e memória: https://docs.google.com/document/d/1PHCxxg0ah2dE4LjfvSHJYrzJFSjvsAHK