Preload em aplicações Laravel
guias novidades
O Preload foi introduzido ao core do PHP na versão 7.4 e pode trazer ganhos significativos de performance e redução do consumo de memória em aplicações Laravel. Neste post vamos entender o que é esta feature, como ativá-la e configurá-la em aplicações Laravel.
Como o PHP é interpretado
Antes de falar especificamente de preloading é interessante entender como o PHP é interpretado. Este assunto já foi muito bem abordado neste excelente artigo do @nawarian. Ao explicar como o JIT (just in time compiler) funciona também há uma explicação de como o código PHP é interpretado e suas etapas, especificamente no diagrama de fluxo abaixo podemos ver o passo a passo do que acontece quando rodamos um código PHP.
A parte que nos interessa neste artigo está entre o passo de compilação e transformação em opcodes pois é nesta etapa que o preloading irá agir!
Basicamente todo código PHP vira opcode em algum momento e para se transformar em opcode rola todo este fluxo apresentado. Mas e se fosse possível “pular" algumas destas etapas?
Olá, meu nome é Preload
Preload, como o nome já sugere, é a técnica implementada para fazer um pré-carregamento de alguma coisa, no nosso caso, scripts PHP.
Esta técnica nos ajuda -e muito- em alguns cenários principalmente onde uma grande quantia de código PHP reside (alô, ouvi frameworks?). Basicamente o que acontece é que o preload se encarrega de carregar uma série de arquivos PHP de uma lista pré-definida no momento em que o servidor PHP é iniciado, mas como isso ajuda em algo?
Quando uma requisição é enviada ao servidor, a primeira coisa que será feita é verificar se aquele item solicitado já está armazenado no opcache:
Caso o item solicitado já esteja no opcache todo o resto do contexto é “pulado" e o código é executado de forma direta. Com o preload habilitado e configurado, este processo de gerar opcodes e armazenar no opcache é feito em tempo de inicialização do servidor PHP e não mais em tempo de execução, desta forma todo código que estiver definido nas configurações do preload serão executados “direto" e isso pode gerar ganhos consideráveis tanto em tempo de requisição quanto consumo de recursos.
Como habilitar o Preload
Para a demonstração o ambiente utilizado contará com Ubuntu 20, PHP 7.4 FPM e Nginx.
Habilitar o preloading é relativamente simples! Basta ter acesso/permissão para mexer nas configurações do PHP, ter o opcache ativo e passar um script no qual será feito o preloading dos arquivos PHP.
Para que seja possível fazer o preloading de scripts PHP, é necessário que o script passado nas configurações (conforme vamos fazer já já) faça o require
do script que gostaríamos que que fosse pré-carregado ou então utilizando a função opcache_compile_file()
.
Um ponto muito importante a ser ressaltado é que ao trabalhar com orientação a objetos é comum termos muitos use statements
, herança, traits etc. Isso influencia na forma como devemos fazer o preloading, não basta gerar o opcache apenas de uma classe específica e não gerar das suas dependências, herança e tudo mais. É por isso que no script para fazer o preloading do Laravel vamos utilizar o require_once
pois quando um arquivo é utilizado com esta diretiva, todas suas dependências (use statements) são resolvidas e também serão carregadas para o opcache.
Preload com Laravel
Antes de prosseguir com o preloading é importante observar que o Laravel em específico tem algumas considerações importantes que precisam ser feitas. Não é possível pré-carregar todo o framework pois algumas classes precisam que o bootstrap da aplicação seja feito para que possam ser resolvidas pelo container de injeção de dependência (DI Container), este é o caso das classes Cache
, LogManager
, o namespace Testing
e a classe UploadedFile
. Por isso vamos ignorar essas classes e fazer o preloading do restante. Finalmente: vamos ao script que realizará o preload!
Neste ótimo artigo, especificamente na seção indicada (“preloading in practice") o autor criou uma classe excelente para realizar o preloading de arquivos e/ou todo diretório de arquivos que forem indicados no construtor ou no método paths()
, como não precisamos reinventar a roda vamos utilizar esta classe e adicionar os itens que queremos e os que não queremos que sejam carregados no preload.
Vamos fazer o preloading do framework como um todo, dos controllers, dos arquivos de bootstrap da aplicação, dos arquivos de configuração e do entrypoint da aplicação. Não é possível fazer o pré-carregamento de todos os arquivos do namespace do Carbon
mas algumas das classes mais utilizadas são possíveis então também vamos fazer o preloading destas classes de maneira individual.
A classe Preloader
pode ser encontrada de forma completa no github, para o código de exemplo não ficar gigantesco vou mostrar apenas a utilização da classe.
// laravel-preload.php
$dir = __DIR__;
(new Preloader())
->paths("{$dir}/vendor/laravel", "{$dir}/app/Http/Controllers",
"{$dir}/bootstrap", "{$dir}/config", "{$dir}/public/index.php",
'/home/jose/Studies/palestra-summit-otimizada/vendor/nesbot/carbon/src/Carbon/CarbonImmutable.php',
'/home/jose/Studies/palestra-summit-otimizada/vendor/nesbot/carbon/src/Carbon/Translator.php',
'/home/jose/Studies/palestra-summit-otimizada/vendor/nesbot/carbon/src/Carbon/CarbonPeriod.php',
'/home/jose/Studies/palestra-summit-otimizada/vendor/nesbot/carbon/src/Carbon/CarbonInterval.php',)
->ignore(
\Illuminate\Filesystem\Cache::class,
\Illuminate\Log\LogManager::class,
\Illuminate\Http\Testing\File::class,
\Illuminate\Http\UploadedFile::class,
'Illuminate\Foundation\Testing',
)
->load();
Agora que já definimos quais arquivos queremos e quais não queremos fazer o preloading, vamos atualizar a configuração do PHP-FPM passando o caminho do script que criamos:
// /etc/php/7.4/fpm/conf.d/10-opcache.ini
; configuration for php opcache module
; priority=10
zend_extension=opcache.so
opcache.preload=/home/jose/Studies/php-summit-2020/laravel-preload.php
opcache.preload_user=www-data
O caminho onde as configurações do PHP-FPM são encontradas pode variar de acordo com a forma que o PHP foi instalado ou sistema operacional, no ubuntu com a instalação padrão deve estar em/etc/php/7.4/fpm
.
A diretiva que precisamos utilizar é a opcache.preload
e esta diretiva recebe o caminho do script de onde será feito o carregamento dos arquivos que queremos fazer o preload conforme exemplo acima. Quando o PHP-FPM for reiniciado, este script será executado e as classes irão passar pelo processo de interpretação do PHP e será gerado o opcode e este posteriormente será armazenado no opcache.
Outra diretiva necessária é a opcache.preload_user
, o valor passado para esta diretiva será o usuário utilizado na execução do preload, geralmente é o mesmo que está rodando o webserver (apache ou nginx) que no nosso caso é o nginx, logo, o usuário padrão é o www-data
.
Vamos reiniciar o PHP-FPM:
sudo service php7.4-fpm restart
// ou
sudo systemctl restart php7.4-fpm
Ao verificar os logs gerados pelo processo do PHP podemos verificar como foi o andamento do script, abaixo segue um trecho:
…
jan 03 15:01:27 jose-uotz php-fpm7.4[167700]: [Preloader] Preloaded `Illuminate\Broadcasting\InteractsWithSockets`
jan 03 15:01:27 jose-uotz php-fpm7.4[167700]: [Preloader] Preloaded `Illuminate\Broadcasting\Broadcasters\RedisBroadcaster`
jan 03 15:01:27 jose-uotz php-fpm7.4[167700]: [Preloader] Preloaded `Illuminate\Broadcasting\Broadcasters\PusherBroadcaster`
jan 03 15:01:27 jose-uotz php-fpm7.4[167700]: [Preloader] Preloaded `Illuminate\Broadcasting\Broadcasters\Broadcaster`
jan 03 15:01:27 jose-uotz php-fpm7.4[167700]: [Preloader] Preloaded `Illuminate\Broadcasting\Broadcasters\UsePusherChannelConventions`
jan 03 15:01:27 jose-uotz php-fpm7.4[167700]: [Preloader] Preloaded `Illuminate\Broadcasting\Broadcasters\LogBroadcaster`
jan 03 15:01:27 jose-uotz php-fpm7.4[167700]: [Preloader] Preloaded `Illuminate\Broadcasting\Broadcasters\NullBroadcaster`
jan 03 15:01:27 jose-uotz php-fpm7.4[167700]: [Preloader] Preloaded `Illuminate\Config\Repository`
jan 03 15:01:27 jose-uotz php-fpm7.4[167700]: [Preloader] Preloaded `Laravel\Tinker\TinkerServiceProvider`
jan 03 15:01:27 jose-uotz php-fpm7.4[167700]: [Preloader] Preloaded `Laravel\Tinker\ClassAliasAutoloader`
jan 03 15:01:27 jose-uotz php-fpm7.4[167700]: [Preloader] Preloaded `Laravel\Tinker\Console\TinkerCommand`
jan 03 15:01:27 jose-uotz php-fpm7.4[167700]: [Preloader] Preloaded `Laravel\Tinker\TinkerCaster`
jan 03 15:01:27 jose-uotz php-fpm7.4[167700]: [Preloader] Preloaded `App\Http\Controllers\PostController`
jan 03 15:01:27 jose-uotz php-fpm7.4[167700]: [Preloader] Preloaded `App\Http\Controllers\CommentsController`
jan 03 15:01:27 jose-uotz php-fpm7.4[167700]: [Preloader] Preloaded `App\Http\Controllers\Controller`
jan 03 15:01:27 jose-uotz php-fpm7.4[167700]: [Preloader] Preloaded 855 classes
jan 03 15:01:27 jose-uotz systemd[1]: Started The PHP 7.4 FastCGI Process Manager.
-- Subject: A start job for unit php7.4-fpm.service has finished successfully
-- Defined-By: systemd
-- Support: http://www.ubuntu.com/support
--
-- A start job for unit php7.4-fpm.service has finished successfully.
--
-- The job identifier is 19455.
Pronto, preload habilitado e já foi feita a “mágica"!
É possível verificar quais arquivos estão no opcache
através da função opcache_get_status()
. Apenas a fim de matar a curiosidade vou deixar um dd(opcache_get_status(true))
ao final do arquivo index.php
do Laravel, desta forma qualquer requisição que fizermos irá nos mostrar o status do opcache:
array:8 [▼
"opcache_enabled" => true
"cache_full" => false
"restart_pending" => false
"restart_in_progress" => false
"memory_usage" => array:4 [▶]
"interned_strings_usage" => array:4 [▶]
"opcache_statistics" => array:13 [▶]
"scripts" => array:463 [▶]
]
No último índice chamado “scripts" estão os scripts importados para o opcache e tivemos 463 arquivos importados.
Benchmark
Em setembro de 2020 tive a oportunidade de apresentar uma palestra sobre otimização de aplicações Laravel no PHP Community Summit e lá falei sobre o preload. Você pode conferir os slides da palestra onde podemos ver vários exemplos de benchmark realizados. Abaixo segue o exemplo de um mesmo método sendo executado com preloading desativado à esquerda e ativado na direita:
Houve um ganho bastante significativo no consumo de memória, foram necessários aproximadamente ~66% menos memória para executar a mesma tarefa apenas por habilitar o preload. A maior parte dos ganhos, conforme evidenciado no print acima, foi no bootstrap do framework onde foi necessário utilizar menos da metade da memória quando o preload está ativado e as classes estão no opcache.
Conclusão
O preload é bastante simples de ser configurado e com pouco esforço podemos ter ganhos bem expressivos quanto ao consumo de recursos e algum ganho com relação ao tempo de resposta. Em conjunto com outras técnicas como caching e as próprias otimizações que o Laravel já faz por padrão somos capazes de obter excelentes ganhos de performance!
Vale lembrar que no exemplo foi feito o preload do framework e dos arquivos pasta Controllers
mas somos livres para adicionar/remover quaisquer arquivos que sejam necessários. Provavelmente você irá querer fazer o preloading dos seus services
, repositories
etc. Aqui cabe tirar um tempo para “brincar" com esta ferramenta e verificar o que faz sentido no seu contexto. Ative o preload, faça testes e descubra a melhor configuração para sua aplicação!