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).
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
| Branch | O que foi adicionado / alterado |
|---|---|
feature/01-direct-api-unstable | Comunicaçã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-counter | Contador 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-degradation | Leitura 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-worker | Escrita 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ão | Alternativa Considerada | Por que foi Escolhida? | Trade-off Aceito |
|---|---|---|---|
| Gatilho de Ativação Dinâmica em 50% da Cota | Manter o Cache/Degradação ativo em 100% do tempo | Preserva 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ções | Bloquear escritas ou enviar gravações síncronas durante a degradação | Evita 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 HTTP | Permite 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 recentes | Garante 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 carga | Permite 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.
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.
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.
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}$).
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 t | Estado do Serviço | Total Req | Req Normal | Req Degradado | Uso de CPU (%) | Uso de RAM (MiB) | Uso de Memória Redis (MB) |
|---|---|---|---|---|---|---|---|
| 10s | NORMAL | 22 | 22 | 0 | 3.00% | 37.50 MiB | 0.85 MB |
| 60s | NORMAL | 30 | 30 | 0 | 5.20% | 42.30 MiB | 1.12 MB |
| 70s | DEGRADADO | 32 | 0 | 32 | 6.80% | 44.80 MiB | 1.18 MB |
| 120s | DEGRADADO | 40 | 0 | 40 | 10.40% | 52.80 MiB | 1.38 MB |
| 180s | DEGRADADO (Pico) | 80 | 0 | 80 | 15.40% | 60.50 MiB | 1.45 MB |
| 200s | DEGRADADO | 67 | 0 | 67 | 13.53% | 57.63 MiB | 1.42 MB |
| 240s | DEGRADADO | 40 | 0 | 40 | 9.80% | 51.90 MiB | 1.36 MB |
| 270s | NORMAL (Recuperado) | 25 | 25 | 0 | 5.10% | 42.00 MiB | 1.15 MB |
| 300s | NORMAL | 10 | 10 | 0 | 2.10% | 37.50 MiB | 0.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 429eHTTP 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: