além do script

Graceful Degradation & Rate Limit Bypass com Redis em Sistemas Distribuídos

Este é um case técnico baseado em um cenário real de integração com a API do Google Sheets, focado em resolver problemas de escassez de quota e alta taxa de erros. No cenário original, requisições diretas de carga estouravam rapidamente o limite do ecossistema do Google (300 requisições/minuto), gerando falhas em cadeia (HTTP 429 - Too Many Requests). Para solucionar o gargalo, foi construído um mock em Go (sheets-mock-api) reproduzindo fielmente os limites reais e implementado o padrão de Graceful Degradation com Redis na aplicação principal em Laravel (main-service-api).

arquiteturagraceful-degrationrate-limitresiliencia

Nível: Avançado | Tempo de Leitura: 17 min

por Marcelo Macedo

Introdução

Este é um case técnico baseado em um cenário real de integração com a API do Google Sheets, focado em resolver problemas de escassez de quota e alta taxa de erros.

No cenário original, requisições diretas de carga estouravam rapidamente o limite do ecossistema do Google (300 requisições/minuto), gerando falhas em cadeia (HTTP 429 - Too Many Requests). Para solucionar o gargalo, foi construído um mock em Go (sheets-mock-api) reproduzindo fielmente os limites reais e implementado o padrão de Graceful Degradation com Redis na aplicação principal em Laravel (main-service-api).

  • Escopo: Sistemas Distribuídos / Rate Limiting & Tolerância a Falhas / Caching.
  • Linguagens e Stack: PHP (Laravel), Go (sheets-mock-api), Redis, Docker Compose, k6 (Grafana) para testes de carga.
  • Impacto de Negócio: A taxa de erro despencou de 78.3% para 0%, o throughput de requisições sustentado pela aplicação subiu de forma expressiva e as chamadas reais à API externa caíram acima de 95%, respeitando rigorosamente a quota do provedor (300 req/min).

Código Fonte e Relatórios de Carga: Repositório completo no GitHub contendo os serviços, scripts do k6 e instruções do Docker Compose: 🔗 php-graceful-degradation-rate-limit


O Problema

O ponto de partida (branch: feature/01-direct-api-unstable) do case é o cenário sem proteção de resiliência: o k6 dispara requisições de carga contra o main-service-api (Laravel), que por sua vez consulta diretamente o sheets-mock-api (Go) para cada requisição recebida.

O sheets-mock-api implementa um algoritmo rigoroso de Rate Limiting via Leaky/Token Bucket calibrado para 300 requisições por minuto (5 req/s em média), espelhando os limites reais de cota de escrita/leitura da API de planilhas do Google.

Quando a taxa de requisições do main-service-api excede esse teto, a API externa passa a responder imediatamente com HTTP 429 Too Many Requests. Sem uma camada de tratamento ou degradação graciosa, o main-service-api repassa esses erros para os clientes finais.

Durante os primeiros 160s, o sistema opera normalmente com 100% de sucesso, mas assim que o tráfego atinge o pico em t=180s, o limite de 300 req/min do Google Sheets é esgotado, colapsando a integração e gerando 100% de erro (HTTP 429 / 502) na janela. Mesmo com recuperações temporárias do Token Bucket com a queda de tráfego, o acúmulo de requisições provoca novos gargalos (como em t=240s), totalizando 160 falhas (12,7% de erro global).


A Arquitetura da Solução

Situação Inicial:

Arquitetura Final:

O main-service-api contabiliza continuamente as chamadas enviadas à API externa dentro de janelas móveis de 1 minuto. Enquanto o consumo se mantém igual ou abaixo de 50% da cota (até 150 req/min), a aplicação opera em Modo Direto, repassando leituras e gravações de forma síncrona ao sheets-mock-api sem a necessidade de passar por camadas intermediárias.

A inteligência da arquitetura entra em ação no momento em que a cota cruza o gatilho de 50%. A partir dessa marca, o sistema ativa automaticamente o modo de Graceful Degradation, alterando o fluxo de execução para proteger a quota externa sem comprometer a experiência do usuário.

Nas operações de leitura, o serviço interrompe as chamadas HTTP à API Go e passa a consultar o snapshot mantido em cache no Redis, combinando-o em tempo real com as alterações pendentes para entregar o dado perfeitamente atualizado de forma instantânea. Já nas gravações, em vez de arriscar o esgotamento do limite de 300 req/min com escritas síncronas, a mutação é registrada em uma fila de deltas (delta queue) no Redis e refletida imediatamente no cache de leitura local.

Com essa separação, assim que a janela móvel reseta e o consumo de cota retorna a níveis seguros (< 50%), um worker em segundo plano entra em cena para drenar assincronamente a fila de deltas, persistindo todas as gravações acumuladas na API do Google Sheets de forma cadenciada e sem gerar novos picos de requisição.


Evolução

BranchO que foi adicionado / alterado
feature/01-direct-api-unstableComunicação direta HTTP (Laravel $\rightarrow$ Go) sem proteção. Estouro de cota rápido no pico e taxa global de 12,7% de falhas (HTTP 429 / 502).
feature/02-redis-quota-counterContador de Cota no Redis: Introdução do monitoramento em janela móvel de 1 minuto. Identificação e acionamento do gatilho ao cruzar 50% da cota (150 req/min).
feature/03-graceful-read-degradationLeitura Degradada com Deltas: Ao passar dos 50%, intercepta chamadas de leitura e serve o dado via snapshot do Redis mesclado às alterações do buffer local em tempo real.
feature/04-delta-queue-async-workerEscrita Assíncrona & Worker: Gravações acima de 50% são enviadas para uma delta queue no Redis. Inclusão do worker de retaguarda que drena a fila e sincroniza na API Go quando a cota normaliza (< 50%).

Análise de Decisão

Informações detalhada sobre as principais decisões deste case:

DecisãoAlternativa ConsideradaPor que foi Escolhida?Trade-off Aceito
Gatilho de Ativação Dinâmica em 50% da CotaManter o Cache/Degradação ativo em 100% do tempoPreserva o comportamento de consulta e gravação direta à API síncrona enquanto há cota segura ($\le 150\text{ req/min}$), acionando a sobretaxa da camada intermediária apenas em momentos críticos.Maior consumo da cota da API em operação normal, em troca de evitar o custo contínuo de CPU, memória e processamento da camada de cache/degradação em todas as requisições.
Buffer de Deltas no Redis para GravaçõesBloquear escritas ou enviar gravações síncronas durante a degradaçãoEvita estourar o limite rígido de $300\text{ req/min}$ da API externa durante picos de carga e garante resposta instantânea ao usuário sem perda de dados.Eventual consistência: as gravações ficam retidas temporariamente na fila até a cota normalizar para serem persistidas de fato na API externa.
Drenagem Assíncrona via Worker (Delta Queue)Tentar sincronizar todas as mutações pendentes de uma só vez via HTTPPermite cadastrar e cadenciar as requisições acumuladas em segundo plano assim que a cota reseta ($< 50%$), evitando novos picos de tráfego na API externa.Complexidade adicional no gerenciamento de estado do worker (tratamento de retries, ordens de precedência e falhas de conexão durante a drenagem).
Leitura Combinada (Snapshot + Deltas Locais)Servir apenas o cache antigo (Stale) sem alterações recentesGarante que o usuário que acabou de gravar uma informação durante o modo de degradação veja sua alteração refletida imediatamente na consulta, sem consultar a API externa.Maior uso de memória no Redis para armazenar a lista de deltas por usuário/recurso e lógica de merge de dados na camada de aplicação (Laravel).
Serviço Mock em Go (sheets-mock-api)Utilizar a API real do Google Sheets nos testes de cargaPermite simulação determinística do algoritmo de Token Bucket ($300\text{ req/min}$) sem custos, throttling de rede de testes, bloqueio de conta ou dependência de credenciais de produção.O mock precisa espelhar com precisão cirúrgica os headers, status codes (HTTP 429) e latências reais do ecossistema Google.

Implementação Prática

Contador Atômico e Verificação de Cota

  • Trecho: main-service-api/app/Services/GoogleSheetsQuotaService.php
  • Branch: feature/02-redis-quota-counter

Em vez de resetar a contagem em minutos cheios (o que pode gerar picos nas bordas da janela), o GoogleSheetsQuotaService implementa um algoritmo de Sliding Window de 60 segundos segundo a segundo. Cada segundo gera uma chave individual no Redis com TTL de 60s, e o consumo total é calculado somando as chaves do intervalo atual de 60 segundos via MGET.

php
public function registerRequest(): int
{
	$now = time();
	$key = self::KEY_PREFIX . $now;

	$current = Redis::incr($key);
	if ($current === 1) {
		Redis::expire($key, self::TTL_SECONDS);
	}

	return $this->getQuotaConsumed();
}

...

public function isDegraded(?int $consumed = null): bool
{
	$consumed = $consumed ?? $this->getQuotaConsumed();
	return $consumed > $this->getThreshold();
}

Injeção de Observabilidade via Middleware

  • Trecho: main-service-api/app/Http/Middleware/QuotaTrackerMiddleware.php
  • Branch: feature/02-redis-quota-counter

O middleware intercepta a requisição, consulta o GoogleSheetsQuotaService para identificar se atingimos o limite de 50% da cota e injeta headers de controle de estado na resposta enviada ao cliente/k6.

php
public function handle(Request $request, Closure $next): Response
{
	$consumed = $this->quotaService->registerRequest();
	$isDegraded = $this->quotaService->isDegraded($consumed);

	$request->attributes->set('quota_consumed', $consumed);
	$request->attributes->set('is_degraded', $isDegraded);

	$response = $next($request);

	$response->headers->set('X-Quota-Consumed', (string) $consumed);
	$response->headers->set('X-System-Degraded', $isDegraded ? 'true' : 'false');

	return $response;
}

Orquestração de Leitura e Interceptação com Mesclagem de Deltas

  • Trecho: main-service-api/app/Services/GoogleSheetsService.php
  • Branch: feature/03-graceful-read-degradation

O GoogleSheetsServiceconsulta o GoogleSheetsQuotaService para avaliar o estado da cota em tempo real e decide se fará uma busca síncrona direta na API Go do Google Sheets ou se ativará o fluxo de Graceful Degradation combinando a última cópia válida (snapshot) com as alterações pendentes retidas no buffer local.

php
public function getRows(string $sheet = 'orders'): array
{
	$isDegraded = $this->quotaService->isDegraded();

	if ($isDegraded) {
		return $this->getDegradedMergedRows($sheet);
	}

	return $this->getDirectRows($sheet);
}

Worker de Drenagem Assíncrona e Sincronização em Retaguarda

Trecho: main-service-api/app/Console/Commands/DrainQuotaBufferCommand.php Branch: feature/04-delta-queue-async-worker

O DrainQuotaBufferCommand atua como um worker de segundo plano (long-running process) responsável por monitorar continuamente o consumo de cota no Redis. Ele garante a drenagem cadenciada e segura da fila atômica de escrita (sheets_write_buffer) para a API do Google Sheets somente quando a cota se estabiliza no nível seguro ($\le 150\text{ req/min}$).

php
public function handle(GoogleSheetsQuotaService $quotaService, GoogleSheetsService $sheetsService): int
{
	$sheet = $this->option('sheet');
	$loop = $this->option('loop');
	$sleep = (int) $this->option('sleep');

	$this->info("Iniciando Worker de Drenagem Assíncrona para a folha [{$sheet}]...");

	do {
		$consumed = $quotaService->getQuotaConsumed();
		$bufferLen = $sheetsService->getBufferLength($sheet);

		if ($consumed <= 150 && $bufferLen > 0) {
			$this->info("Cota Normalizada ({$consumed} req/min). Drenando {$bufferLen} itens pendentes do buffer...");
			
			$drained = $sheetsService->drainBuffer($sheet, 20);
			
			$this->info("Drenados {$drained} itens do buffer com sucesso.");
		}

		if ($loop) {
			sleep($sleep);
		}
	} while ($loop);

	return Command::SUCCESS;
}

Métricas, Resultados e Lições Aprendidas

Instante tEstado do ServiçoTotal ReqReq NormalReq DegradadoUso de CPU (%)Uso de RAM (MiB)Uso de Memória Redis (MB)
10sNORMAL222203.00%37.50 MiB0.85 MB
60sNORMAL303005.20%42.30 MiB1.12 MB
70sDEGRADADO320326.80%44.80 MiB1.18 MB
120sDEGRADADO4004010.40%52.80 MiB1.38 MB
180sDEGRADADO (Pico)8008015.40%60.50 MiB1.45 MB
200sDEGRADADO6706713.53%57.63 MiB1.42 MB
240sDEGRADADO400409.80%51.90 MiB1.36 MB
270sNORMAL (Recuperado)252505.10%42.00 MiB1.15 MB
300sNORMAL101002.10%37.50 MiB0.95 MB

Pontos observados

  • Preservação Rígida da Cota (Rate-Limit): O limite de requisições da API do Google Sheets não foi estourado em nenhum momento, pois a ativação do modo degradado interrompeu as chamadas externas síncronas assim que o consumo atingiu a margem de segurança.
  • Consistência Total dos Dados: As informações mantiveram-se perfeitamente atualizadas e coerentes para o usuário, já que todas as mutações/alterações registradas no buffer foram mescladas em tempo real com o snapshot do Redis durante as consultas.
  • Taxa de Erro Zero (100% de Sucesso): Não houve falhas ou interrupções no fluxo, garantindo 100% de sucesso no processamento de todas as requisições enviadas e eliminando completamente erros do tipo HTTP 429 e HTTP 502.
  • Sobretaxa de Recursos Computacionais: Apesar de o aumento de CPU e memória ter sido moderado na simulação (dadas as proporções do teste de carga), a execução do modo degradado exige maior processamento e retenção de dados em memória do que o fluxo direto.
  • Importância do Gatilho Dinâmico e Escalabilidade: As métricas evidenciam a importância de o modo degradado não ficar ativo o tempo todo, entrando em cena exclusivamente nos momentos de pico de tráfego. Além disso, em cenários de alta carga sustentada, o ambiente precisará ser escalado horizontalmente/verticalmente para suportar o consumo adicional de recursos.

O código completo, com os quatro branches, os READMEs congelados de cada etapa e os relatórios brutos de cada execução, está em: