01 - Comunicação Síncrona (REST, gRPC)
video: https://youtu.be/... no frontmatter da nota.Comunicação Síncrona (REST, gRPC)
A comunicação síncrona é o modelo em que o chamador envia uma requisição e fica bloqueado aguardando a resposta do serviço chamado antes de continuar sua execução. É o padrão request-response clássico, materializado em produção por APIs REST sobre HTTP/1.1 e HTTP/2 e por serviços gRPC sobre HTTP/2 com protobuf. Esse modelo domina as integrações em que quem chama precisa da resposta imediata para avançar: validar um token, consultar saldo, buscar cadastro, autorizar um pagamento. Sua relevância em produção vem de duas frentes: é o padrão dominante nas interações com usuário, que esperam resposta em milissegundos, e é o padrão com as piores consequências em cascata quando algo falha. Entender acoplamento temporal, timeouts e propagação de falhas é o que separa uma arquitetura que degrada com controle de uma que entra em colapso em cadeia durante incidentes.
O modelo request-response
No modelo request-response, cada chamada ocupa um contexto de execução do chamador desde o envio até o recebimento da resposta ou o estouro do timeout. A operação só é considerada concluída quando o serviço remoto responde com um resultado ou um erro explícito. Isso dá ao chamador um fluxo de controle previsível: ele sabe, no mesmo ponto do código, se a operação teve sucesso e qual foi o retorno.
O custo desse conforto é o acoplamento temporal. O serviço chamador precisa estar de pé, com capacidade disponível, no exato instante da chamada. Se o destino está lento, o chamador fica lento; se o destino cai, o chamador falha na operação, mesmo que o trabalho dele, em essência, não dependesse daquele dado naquele momento.
Em produção, a regra prática é usar chamadas síncronas quando existe uma dependência real de dados no momento da execução: leitura para composição de resposta ao usuário, autorização, validação de unicidade. Quando a dependência é apenas de eventualidade, como enviar e-mail ou atualizar estatísticas, o modelo assíncrono costuma ser a escolha correta.
O modelo também dita o estilo de runtime do servidor. Em plataformas thread-per-request, cada chamada pendente ocupa uma thread inteira, e a concorrência útil é limitada pelo número de threads disponíveis; um destino 10 vezes mais lento reduz a capacidade efetiva em 10 vezes. Runtimes baseados em event loop ou coroutines seguram chamadas pendentes com custo de memória muito menor, o que aumenta a tolerância a latência, mas não elimina o problema: o timeout e o circuit breaker continuam sendo a proteção real, independente do runtime.
Acoplamento temporal e suas consequências
O acoplamento temporal cria uma cadeia de disponibilidade: a disponibilidade efetiva do serviço A é o produto da disponibilidade de todos os serviços que ele chama em linha. Um serviço com 99,9% de disponibilidade que faz três chamadas síncronas dependentes em uma única requisição tem, no pior caso, disponibilidade combinada próxima de 99,6%, o que representa mais de três horas de indisponibilidade por mês.
A latência sofre o mesmo efeito aditivo. Se cada dependência responde em 50 ms, uma cadeia de quatro chamadas empilha 200 ms antes de qualquer lógica própria. O problema se agrava porque latências em produção têm cauda: o p95 de uma dependência pode ser 10 vezes o p50, e o chamador herda essa cauda inteira em cada requisição de usuário.
Esse acoplamento também contamina os planos de capacidade. Um pico de tráfego no serviço B se propaga para o serviço C que o B chama, que se propaga para o D, e assim por diante. Sistemas síncronos em cadeia precisam ser dimensionados para o pico do elo mais exigente, e não para o pico médio de cada peça.
Timeouts, retries e falhas em cadeia
O timeout é o mecanismo que impede que o chamador fique preso indefinidamente esperando uma resposta que não vai chegar. Sem timeout explícito, um destino lento consome conexões e threads do chamador até esgotar seus recursos, transformando um problema de um serviço em uma pane generalizada. Timeout mal calibrado é igualmente grave: um valor maior que o timeout do gateway acima dele faz com que o chamador continue processando uma requisição que o cliente já abandonou.
O retry é a segunda peça, e é onde a maioria dos incidentes em cadeia se origina. Repetir automaticamente uma chamada contra um destino já sobrecarregado multiplica o tráfego justamente no momento em que o serviço menos aguenta. As regras de sobrevivência são: retry apenas em erros transitórios e idempotentes, com backoff exponencial e jitter, com limite máximo de tentativas e com um orçamento total de retry inferior ao timeout disponível.
Sem essas salvaguardas, o efeito combinado se chama retry storm: cada camada da cadeia multiplica as requisições da camada anterior, e o tráfego real pode crescer dezenas de vezes sobre um serviço já degradado. Em incidentes reais, a causa raiz costuma ser um único serviço lento, mas a queda em cascata vem dos retries dos chamadores.
# Exemplo de configuração de resiliência para chamadas síncronas (formato ilustrativo)
http_client:
connect_timeout: 200ms # falha rápida se não abre conexão
request_timeout: 800ms # limite total da chamada, abaixo do timeout do gateway
pool:
max_connections: 200 # protege contra esgotamento de recursos locais
retry:
max_attempts: 2 # no máximo 1 retry, nunca tempestade de tentativas
backoff: exponential # 200ms, 400ms, 800ms
jitter: true # evita sincronização de retries entre instâncias
retry_on: [TIMEOUT, UNAVAILABLE, RESOURCE_EXHAUSTED]
budget:
max_retry_ratio: 0.10 # retries nunca passam de 10% do tráfego total
circuit_breaker:
failure_threshold: 50% # abre com 50% de falhas na janela
window: 30s
half_open_after: 15s # tenta recuperação gradual
REST em produção
REST é o estilo dominante por razões práticas: usa HTTP como transporte e como semântica, com métodos e códigos de status padronizados, cache no nível de infraestrutura e legibilidade universal. Qualquer pessoa com curl consegue depurar uma API REST, o que reduz o custo operacional do dia a dia.
O ponto fraco aparece em três lugares. Primeiro, JSON é textual e verboso, o que custa CPU de serialização e banda em alto volume. Segundo, REST não define contrato rigoroso; sem OpenAPI bem mantida, o contrato vive em documentos que desatualizam. Terceiro, não há suporte nativo a streaming bidirecional, apenas streaming unidirecional por chunks.
Em produção, as decisões que mais importam em REST são versionamento de contrato, idempotência em POST (via chave de idempotência) e paginação consistente. APIs internas entre serviços devem definir códigos de status de forma uniforme, porque clientes vão construir lógica de retry sobre esses códigos.
gRPC em produção
gRPC troca JSON por protobuf e HTTP/1.1 por HTTP/2, entregando contratos tipados em arquivos .proto, serialização binária compacta, multiplexing de chamadas em uma única conexão e quatro modos de chamada: unary, server streaming, client streaming e bidirectional streaming.
O ganho típico em produção é ordem de grandeza de latência e throughput em comparação com JSON, além de eliminação de uma classe inteira de bugs de contrato, já que a quebra de compatibilidade falha no build e não em runtime. O streaming bidirecional permite padrões difíceis em REST, como chats internos, telemetria contínua e agentes que mantêm sessão viva.
O preço é operacional. gRPC exige tooling especializado, porque o formato binário não é legível em ferramentas comuns; balanceamento de carga L7 precisa entender HTTP/2, e o HTTP/2 de longa duração quebra balanceadores que assumem conexões curtas; e o ecossistema de browsers exige um proxy gRPC-Web. Isso torna gRPC natural para malha interna de serviços e incômodo para APIs públicas.
// Contrato gRPC de exemplo: definição tipada compartilhada entre produtor e consumidores
syntax = "proto3";
service PagamentoService {
// Chamada unary: autorização precisa de resposta imediata para o checkout avançar
rpc Autorizar (AutorizacaoRequest) returns (AutorizacaoResponse);
// Server streaming: emite eventos de status enquanto o processamento avança
rpc AcompanharStatus (ConsultaRequest) returns (stream StatusEvent);
}
message AutorizacaoRequest {
string pedido_id = 1; // chave de idempotência do lado do chamador
int64 valor_centavos = 2; // valores monetários em inteiro, nunca float
string cartao_token = 3; // token de vault, dado sensível nunca trafega puro
}
Comparação entre REST e gRPC
| Critério | REST (JSON) | gRPC (protobuf) |
|---|---|---|
| Contrato | OpenAPI, opcional e informal na prática | .proto obrigatório, tipado e versionado |
| Serialização | Texto JSON, verboso e legível | Binário compacto, 3 a 10 vezes menor |
| Transporte | HTTP/1.1 ou HTTP/2 | HTTP/2 com multiplexing |
| Streaming | Não nativo, via chunks | Nativo em 4 modos, incluindo bidirecional |
| Latência típica | Maior por parse de texto | Menor, binário e conexão reutilizada |
| Depuração | curl, browser e proxies comuns | Ferramentas específicas, grpcurl |
| Ecossistema de cliente | Universal | Geradores por linguagem, browsers via gRPC-Web |
| Caso ideal | APIs públicas, integrações externas | Malha interna de serviços, alto volume |
A escolha não é exclusiva. Um padrão comum é gRPC na comunicação interna entre serviços e REST na borda exposta a clientes externos, com um gateway convertendo entre os dois mundos.
Circuit breaker como proteção obrigatória
Em arquiteturas síncronas, o circuit breaker é o mecanismo que impede que o chamador continue batendo em um destino já caído. Ele opera em três estados: fechado, quando o tráfego flui normalmente; aberto, quando a taxa de falhas cruza o limiar e as chamadas falham instantaneamente sem tocar a rede; e meio aberto, quando uma pequena fração de tráfego testa a recuperação do destino.
A importância fica clara no cenário real: um banco de dados da dependência B trava, as respostas passam de 5 s, e todos os threads do serviço A ficam presos esperando. Em poucos minutos, A esgota sua pool e passa a rejeitar tudo, inclusive requisições que não dependiam de B. Com circuit breaker, A corta a dependência em segundos, responde degradado com dado em cache ou erro controlado, e preserva capacidade para as requisições saudáveis.
O comportamento de fallback é parte do desenho: valor default, cache local, resposta parcial ou erro semântico claro. Um circuit breaker sem fallback definido vira apenas um gerador mais rápido de erros 500.
Observabilidade e propagação de contexto
Chamadas síncronas em cadeia só são diagnosticáveis se o contexto de execução atravessa todos os elos. Na prática, isso significa propagar em cada requisição o trace id, o span id atual, flags de amostragem e metadados de autenticação. gRPC embute isso em metadata e no padrão OpenTelemetry; REST exige disciplina de headers, e a ausência de um único header em um elo quebra a continuidade do trace.
As métricas obrigatórias de cada chamada síncrona seguem o modelo RED: taxa de requisições, taxa de erros por código e distribuição de latência, sempre com percentis. Média de latência esconde exatamente o que importa em cadeia: um p99 de 2 s em uma dependência chamada 20 vezes por requisição garante que quase todo usuário sofre aquela cauda. Também vale expor a taxa de requisições abortadas por timeout e por circuit breaker aberto, porque elas medem proteção em ação.
O log estruturado deve carregar o mesmo correlation id usado no trace, permitindo juntar as linhas espalhadas pelos serviços. Em incidentes, a pergunta mais comum é qual elo da cadeia introduziu a latência; sem propagação de contexto, a resposta vira investigação manual entre dezenas de serviços, com cada equipe apontando para a vizinha.
Cenários reais de produção
Cenário de checkout: o serviço de pedidos chama estoque, pagamento e antifraude em sequência síncrona porque o usuário aguarda a confirmação na tela. O antifraude tem pico de 3 s em horário de promoção, e o timeout do pedidos é de 2,5 s; o resultado é alta taxa de falha justamente no pico de vendas. A correção combina timeout em degraus, circuit breaker com fallback de aprovação condicional e migração da parte não crítica, como emissão de nota, para fila.
Cenário de consulta pública: um gateway expõe dados que agregam cinco serviços internos. Cada chamada é independente, mas foi implementada em série; a latência da página é a soma das cinco. Paralelizar as chamadas independentes reduz a latência para a duração da mais lenta, e cache de resposta com TTL curto absorve o tráfego repetido. Esse é o ganho mais barato disponível em arquiteturas síncronas, e ainda assim é frequente encontrá-lo desperdiçado.
Cenário de dependência externa: um provedor de pagamento terceiro começa a responder 503. Sem circuit breaker, cada uma das milhares de tentativas por segundo consome conexão e thread no serviço próprio; com circuit breaker, as primeiras falhas abrem o circuito, o checkout passa a responder com erro semântico claro em milissegundos, e a equipe ganha tempo para acionar o provedor sem ter seu próprio serviço no chão.
Erros comuns e boas práticas
- Definir timeout sem olhar a cadeia inteira: o timeout de cada elo precisa ser menor que o do elo acima, senão o processamento continua após o cliente já ter desistido.
- Retentar POST sem chave de idempotência: cria duplicidade de pedidos e cobranças; toda mutação retritável precisa ser idempotente por contrato.
- Retentar sem limite ou sem backoff: converte lentidão de um serviço em queda de todos os chamadores; limite, backoff com jitter e orçamento de retry são obrigatórios.
- Chamar síncronamente tarefas que o usuário não espera na resposta: e-mail, relatório, sincronização com terceiro; cada uma dessas é candidata natural a fila.
- Ignorar códigos de status: tratar todo erro como retritável transforma 4xx de contrato em retry storm contra um bug.
- Não propagar contexto de rastreamento: sem correlation id e trace headers, uma falha em cadeia fica impossível de diagnosticar entre dezenas de serviços.
- Expor gRPC para clientes externos sem considerar ferramentas: o binário que dá performance interna cobra caro em suporte quando o consumidor é terceiro.
- Abrir o circuit breaker sem fallback pensado: o erro rápido é melhor que o timeout, mas a experiência degradada precisa estar desenhada, não improvisada.
- Deixar a concorrência do cliente ilimitada: sem semáforo ou bulkhead por dependência, um único destino lento consome todos os recursos de chamada e contamina operações que nada têm a ver com ele.
- Esquecer de medir o tempo consumido em serialização: em alto volume, o custo de parse de JSON pode ser comparável ao do próprio processamento de negócio, e só aparece em perfil, não em alarme.