Buzeli
buzeliSoluções Digitais
SRE

168.551 requisições/dia saturando o PHP-FPM: como resolvemos com nginx srcache + Valkey via stunnel

Publicado em 31 de março de 2026

168.551 requisições/dia saturando PHP-FPM — resolvido com nginx srcache + Valkey via stunnel na OCI

O problema: ISR cascade em escala

O portal usava Next.js com ISR (Incremental Static Regeneration) para servir um glossário com dezenas de milhares de termos. A configuração de revalidate: 60 instruía o Next.js a regenerar cada página a cada 60 segundos — o que, com 36.508 páginas no glossário, criava um ciclo contínuo e ininterrupto de chamadas para a API WordPress interna.

O padrão de acesso ao endpoint interno era assim:

Copiar
GET /wp-json/api/v1/glossary?per_page=100&page=1
GET /wp-json/api/v1/glossary?per_page=100&page=2
...
GET /wp-json/api/v1/glossary?per_page=100&page=36508

Com revalidate: 60, esse ciclo completo de 36.508 requisições se repetia a cada minuto. Em dias de pico, o volume chegou a 168.551 requisições por dia só para esse endpoint. Com 150–201 workers PHP-FPM disponíveis, o socket retornava Resource temporarily unavailable em menos de 60 segundos de cada ciclo. Os bursts duravam 1–2 minutos e voltavam a cada 60–90 minutos.

p-queue com concurrency 10 foi tentado antes e não resolveu: com 36k páginas e revalidate 60s, a fila nunca esvazia antes do próximo ciclo começar. O volume total de requests não cai — apenas o pico de concorrência é suavizado.

A solução: nginx srcache + Valkey via stunnel

A arquitetura de cache foi montada em três camadas, todas rodando no mesmo host (VM na OCI):

Next.js ISR → nginx (OpenResty) com srcache → stunnel (127.0.0.1:6379) → OCI Cache Valkey (TLS)

O nginx com o módulo srcache intercepta as requisições para /wp-json/ antes de chegarem ao PHP-FPM. Se a resposta estiver no Valkey, retorna diretamente — PHP não é chamado. Se for miss, o PHP é chamado, a resposta é armazenada no Valkey, e as próximas requisições idênticas são servidas do cache.

Por que stunnel?

O OCI Cache exige conexão TLS. Os módulos redis_pass e redis2_pass do OpenResty não suportam TLS nativamente — eles fazem conexão plaintext ao Redis. A solução é o stunnel rodando como proxy local: o nginx conecta em 127.0.0.1:6379 (plaintext), o stunnel encripta e repassa para o FQDN do OCI Cache na porta 6379 via TLS.

Configuração do stunnel

Copiar
/etc/stunnel/redis-oci.conf
[redis-oci]
client  = yes
accept  = 127.0.0.1:6379
connect = <oci-cache-fqdn>.redis.sa-saopaulo-1.oci.oraclecloud.com:6379
verifyChain = no

Ativando como serviço systemd persistente:

Copiar
systemctl enable stunnel@redis-oci --now

Teste de conectividade antes de configurar o nginx:

Copiar
redis-cli -h 127.0.0.1 -p 6379 PING  # deve retornar PONG

Configuração do nginx srcache (OpenResty)

O ponto crítico é usar redis.conf em vez de wpfc.conf. A diferença fundamental:

wpfc.conf (fastcgi_cache): tem skip para $query_string — requisições com ?per_page=100&page=N NÃO são cacheadas.

redis.conf (srcache): sem skip para query string — cada ?per_page=100&page=N é cacheada individualmente. Exatamente o comportamento necessário para ISR.

O bloco de configuração srcache para /wp-json/:

Copiar
location ^~ /wp-json/ {
    set $key          "$scheme$host$request_uri";
    set $escaped_key  $key;

Copiar
    srcache_fetch_skip             $skip_cache;
    srcache_store_skip             $skip_cache;
    srcache_response_cache_control off;
    srcache_fetch GET  /redis-fetch $key;
    srcache_store PUT  /redis-store key=$escaped_key;

Copiar
    more_set_headers 'X-SRCache-Fetch-Status $srcache_fetch_status';
    more_set_headers 'X-SRCache-Store-Status $srcache_store_status';

Copiar
    fastcgi_pass php;
    include fastcgi_params;
}

O upstream Redis aponta para o stunnel local:

Copiar
upstream redis {
    server 127.0.0.1:6379;
    keepalive 512;
}

Armadilha crítica: try_files quebra tudo

A primeira versão do bloco /wp-json/ usava try_files $uri $uri/ /index.php?$args — padrão comum em configs WordPress. Isso causava redirect 301 para / em todas as requisições GET para /wp-json/: a chain era try_files → redirect interno /index.php → location = /index.php { return 301 /; } definido no redis.conf.

Nunca use try_files em locations que não servem arquivos estáticos. Para /wp-json/, use fastcgi_pass diretamente.

Armadilha: testar cache com curl -sI

Durante a validação, testar com curl -sI (HEAD request) sempre retorna X-SRCache-Store-Status: BYPASS — sem body na resposta, o srcache não armazena nada. O teste correto é com GET:

Copiar
# Primeira requisição: MISS (PHP chamado, resposta armazenada)
curl -s https://portal.exemplo.com/wp-json/api/v1/glossary?per_page=100&page=1 -o /dev/null -w '%{http_code} %header{X-SRCache-Fetch-Status}'
# 200 MISS

Copiar
# Segunda requisição: HIT (Valkey, PHP não chamado)
# 200 HIT

WP Redis Object Cache no mesmo banco

O WordPress também foi configurado com Redis Object Cache (plugin) apontando para o mesmo Valkey via stunnel, usando o database 0 — o mesmo do nginx srcache.

Copiar
// wp-config.php
define( 'WP_REDIS_HOST',     '127.0.0.1' );
define( 'WP_REDIS_PORT',     6379 );
define( 'WP_REDIS_DATABASE', 0 );

A decisão de compartilhar o database 0 foi intencional: em situações de emergência, um FLUSHDB via plugin WP Redis limpa tanto o object cache quanto o cache do nginx — comportamento desejável quando há necessidade de invalidação completa imediata.

Configuração de rede na OCI

A VM WordPress e o cluster OCI Cache estão em subnets diferentes. Foi necessário adicionar regras de ingresso na Security List do cluster cache liberando TCP 6379 a partir das subnets da VM:

Copiar
# Subnet da VM (pública): 10.1.0.0/24
# Subnet do OCI Cache (privada): 10.1.1.0/24
# Regra adicionada: Ingress TCP 6379 de 10.1.0.0/24

Importante: sempre use o FQDN do OCI Cache, nunca o IP privado. IPs de serviços gerenciados OCI podem mudar. O FQDN é atualizado automaticamente pelo serviço.

O resultado

Após a configuração, o cache hit rate atingiu 93% no primeiro dia. O PHP-FPM parou de saturar. As 168.551 requisições/dia continuam chegando ao nginx — mas 93% delas são respondidas diretamente pelo Valkey, sem tocar o PHP.

X-SRCache-Fetch-Status: HIT → Valkey respondeu, PHP não foi chamado

X-SRCache-Fetch-Status: MISS → primeira requisição, PHP chamado, resposta cacheada

X-SRCache-Store-Status: BYPASS → request com cookie de autenticação, não cacheado (comportamento correto)

O fix definitivo do problema é de código — aumentar o revalidate no Next.js de 60s para 3600s reduziria o volume de ~168k para ~36k requisições/dia. Mas enquanto o time de desenvolvimento implementa, a infra absorve a carga sem degradação.

Resumo da stack

OpenResty (nginx + srcache + redis2 modules) — intercepta requisições /wp-json/ antes do PHP

stunnel — proxy TLS local (127.0.0.1:6379 → OCI Cache FQDN:6379)

OCI Cache (Valkey 7.2) — cluster gerenciado Redis-compatible, $19/mês, região sa-saopaulo-1

WP Redis Object Cache — database 0, mesmo cluster, invalidação unificada

Custo total de infra de cache: $19/mês. Economia em PHP-FPM e CPU: não mensurável diretamente, mas evitou escalonamento horizontal do cluster OKE que estava sendo considerado antes da solução.