Corrigi o PHP e nada mudou: OPcache, deploy e o arquivo que você já trocou
Entenda por que o OPcache continua servindo código antigo depois do deploy, como configurar desenvolvimento e produção e como provar que a primeira requisição já vê a versão nova.
Você corrige o cálculo do frete em Frete.php, faz o deploy, abre o carrinho e o valor errado continua lá. Limpa o cache do navegador, abre em aba anônima, confere o arquivo no servidor: está certo. O PHP está executando uma versão que não existe mais no disco. É o OPcache servindo o compilado antigo, e ele vai continuar fazendo isso até você mandar parar.
A resposta curta: em desenvolvimento, deixe o OPcache conferir a data dos arquivos a cada requisição. Em produção, desligue essa conferência para ganhar desempenho e faça do recarregamento do PHP-FPM um passo explícito do deploy, seguido de uma verificação que prova que o código novo está sendo servido.
Por que o PHP serve um arquivo que você já trocou
A cada requisição, o PHP precisaria ler o arquivo, analisar e compilar para opcodes antes de executar. O OPcache guarda esse resultado compilado em memória compartilhada entre os processos do PHP-FPM. Na requisição seguinte, ele pula a leitura e a compilação. Esse é o ganho, e ele é grande.
O custo é decidir quando o compilado ficou velho. Duas diretivas controlam isso:
opcache.validate_timestamps: se ligada (o padrão), o OPcache confere a data de modificação do arquivo de tempos em tempos. Se desligada, ele nunca confere; o compilado vale até o cache ser limpo.opcache.revalidate_freq: com a conferência ligada, de quantos em quantos segundos ela acontece. O padrão é 2. Com 60, uma correção pode levar até um minuto para aparecer.
Há uma terceira camada que engana em deploy por link simbólico. O PHP mantém um cache de caminhos resolvidos (realpath_cache), e o OPcache identifica scripts pelo caminho real. Se o deploy troca o link /var/www/atual de releases/v41 para releases/v42, processos que já resolveram o caminho continuam apontando para a pasta antiga até o cache de caminhos expirar ou o processo ser reciclado. O arquivo novo existe; o PHP só não está olhando para ele.
APCu não é cache de opcode
Muito material antigo mistura os nomes. O APC original fazia cache de opcode e de dados. Quando o OPcache passou a vir junto com o PHP, o APC saiu de cena e o APCu ficou só com a parte de dados do usuário (apcu_store, apcu_fetch). Pacotes de compatibilidade como o apcu_bc serviam para código antigo que chamava as funções apc_*, numa época de PHP 7. Não instale extensão antiga de Windows só porque o zip ainda está disponível para download: extensão compilada precisa bater com a versão exata do PHP, e uma versão abandonada não recebe correção.
Como configurar e fazer deploy sem fantasma
- Descubra o que está valendo no PHP que atende a web, não no terminal. O PHP da linha de comando tem configuração e cache próprios.
- Em desenvolvimento, use
opcache.validate_timestamps=1eopcache.revalidate_freq=0: toda requisição confere a data, e a correção aparece na hora. - Em produção, use
opcache.validate_timestamps=0e trate o deploy como o único momento em que o código muda. - No deploy, recarregue o PHP-FPM depois de trocar os arquivos ou o link. O reload recria os processos e, com eles, o OPcache.
- No Nginx com deploy por link, passe o caminho real ao PHP com
$realpath_root, para que cada requisição resolva o link de novo. - Prove a versão servida com uma rota que devolve a revisão do código, e falhe o deploy se ela não bater.
A configuração por ambiente
; desenvolvimento (por exemplo, conf.d/99-dev.ini)
opcache.enable=1
opcache.validate_timestamps=1
opcache.revalidate_freq=0
; produção (por exemplo, conf.d/99-prod.ini)
opcache.enable=1
opcache.validate_timestamps=0
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
Os dois últimos valores de produção são ponto de partida, não regra: ajuste olhando o uso real. Se o cache encher, o OPcache para de guardar scripts novos e o desempenho cai sem erro aparente. A função opcache_get_status(false) mostra uso de memória e se o cache está cheio; consulte-a de dentro do PHP-FPM, por uma rota protegida, porque rodada no terminal ela mostra o cache do PHP de linha de comando, que é outro.
O Nginx com deploy por link simbólico
location ~ [.]php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
Com $realpath_root, o Nginx resolve o link antes de passar o caminho ao PHP. Depois da troca do link, o PHP recebe /var/www/releases/v42/..., um caminho que nunca viu, e compila do zero. O caminho do socket varia por distribuição e versão; use o que o seu pool define.
O deploy que prova a versão
Na loja do exemplo, o deploy grava um arquivo PHP com a revisão do Git. Como esse arquivo também passa pelo OPcache, a rota /versao só devolve a revisão nova se o PHP estiver de fato executando o código novo. É um teste honesto.
#!/usr/bin/env bash
set -euo pipefail
REV=$(git rev-parse --short HEAD)
DEST=/var/www/releases/$REV
mkdir -p "$DEST"
git archive HEAD | tar -x -C "$DEST"
printf '<?php return %s;' "'$REV'" > "$DEST/config/revisao.php"
ln -sfn "$DEST" /var/www/atual
sudo systemctl reload php8.3-fpm
SERVIDA=$(curl -fsS https://loja.exemplo.com.br/versao)
if [ "$SERVIDA" != "$REV" ]; then
echo "Deploy falhou: servindo $SERVIDA, esperado $REV" >&2
exit 1
fi
echo "Servindo $REV"
E a rota, em public/versao.php:
<?php
header('Content-Type: text/plain; charset=utf-8');
header('Cache-Control: no-store');
echo require __DIR__ . '/../config/revisao.php';
O nome do serviço do PHP-FPM depende da distribuição e da versão instalada. Se você usa containers, a troca de imagem já começa com cache vazio, e a verificação da revisão continua valendo para pegar o container antigo que não saiu. Vale ensaiar a volta também, como mostra o post sobre rollback ensaiado: voltar o link sem recarregar o PHP-FPM serve, de novo, o código errado.
O cache não é lugar para esconder erro
Às vezes o sistema "funciona" só porque o OPcache ainda tem um arquivo que foi apagado do disco, ou um include que quebrou depois. Quando o cache é limpo, o erro aparece, e parece que o reload causou o problema. Se recarregar o PHP-FPM quebra o site, o problema já estava no disco. Rode os testes e um php -l nos arquivos alterados antes do deploy, não depois.
Erros comuns
- Usar
validate_timestamps=0em desenvolvimento: nenhuma alteração aparece, e você passa a tarde duvidando do editor. - Chamar
opcache_reset()pelo terminal: isso limpa o cache do PHP de linha de comando, não o do PHP-FPM que atende a web. - Trocar o link do deploy sem recarregar o PHP-FPM: processos continuam resolvendo a pasta antiga e servem a versão anterior por tempo indeterminado.
- Usar
$document_rootcom deploy por link: o PHP recebe sempre o mesmo caminho, e o cache de caminhos aponta para o release anterior. - Confundir APCu com OPcache: instalar ou limpar o APCu não muda nada no código compilado.
- Deploy sem verificação da versão servida: o script termina com sucesso enquanto o site entrega o código de ontem.
Checklist
- Você sabe o valor de
opcache.validate_timestampsno PHP-FPM de cada ambiente. - Desenvolvimento confere a data a cada requisição (
revalidate_freq=0). - O deploy de produção recarrega o PHP-FPM depois de trocar arquivos ou link.
- Com deploy por link, o Nginx usa
$realpath_root. - Existe uma rota que devolve a revisão servida, e o deploy falha se ela não bater.
- O status do OPcache é consultado pelo PHP-FPM, não pelo terminal.
- Nenhuma extensão foi instalada de pacote antigo sem conferir a versão do PHP.
Faça o teste no próximo deploy: logo depois de terminar, a primeira requisição à sua aplicação devolve a revisão que você acabou de publicar, e como você sabe disso?
Referência: https://windows.php.net/downloads/pecl/releases/apcu_bc/1.0.5/