O aplicativo PHP clássico é um carregamento pesado de thread único (a menos que você escreva em microframes) e a morte inevitável do processo após cada solicitação ... Esse aplicativo é pesado e lento, mas podemos dar uma segunda vida por hibridização. Para acelerar - demonizamos e otimizamos o vazamento de memória para obter melhor desempenho - apresentaremos nosso próprio servidor de aplicativos PHP Golang RoadRunner para adicionar flexibilidade - simplificar o código PHP, expandir a pilha e compartilhar responsabilidades entre o servidor e o aplicativo. Em essência, faremos nosso aplicativo funcionar como se o estivéssemos escrevendo em Java ou em outra linguagem.
Graças à hibridização, um aplicativo anteriormente lento parou de sofrer 502 erros sob carga, o tempo médio de resposta às solicitações diminuiu, a produtividade aumentou e a implantação e montagem ficaram mais fáceis devido à unificação do aplicativo e à eliminação de ligações desnecessárias na forma de nginx + php-fpm.
Anton Titov (
Lachezis ) é CTO e co-fundador da SpiralScout LLC, com 12 anos de experiência ativa em desenvolvimento comercial em PHP. Nos últimos anos, ele implementou ativamente a Golang na pilha de desenvolvimento da empresa. Anton falou sobre um exemplo no
PHP Russia 2019 .
Ciclo de vida de aplicativos PHP
Esquematicamente, um dispositivo de aplicativo abstrato com uma certa estrutura se parece com isso.

Quando enviamos uma solicitação para um processo, acontece:
- inicialização do projeto;
- carregando bibliotecas compartilhadas, estruturas e ORMs;
- carregando bibliotecas necessárias para um projeto específico;
- roteamento;
- solicitação de roteamento para um controlador específico;
- geração de resposta.
Esse é o princípio de operação de um
aplicativo clássico
de encadeamento único com um único ponto de entrada, que após cada execução é completamente destruído ou limpa seu estado. Todo o código é descarregado da memória, o trabalhador é limpo ou simplesmente redefine seu estado.
Carregamento lento
A maneira padrão e fácil de acelerar é a implementação do
sistema de carregamento lento ou das bibliotecas de carregamento sob demanda.

Com o carregamento lento, solicitamos apenas o código necessário.
Ao acessar um controlador específico, apenas as bibliotecas necessárias serão carregadas na memória, processadas e depois descarregadas. Isso permite
reduzir o tempo médio de resposta do projeto e facilitar muito o processo de trabalho no servidor. Em todas as estruturas que estamos usando atualmente, o princípio do carregamento lento é implementado.
Cálculos frequentes de cache
O método é mais complicado e usado ativamente, por exemplo, na estrutura Symfony, mecanismos de modelo, esquemas ORM e roteamento. Isso não está armazenando em cache como memcached ou Redis para dados do usuário. Este sistema
aquece partes do código antecipadamente . Na primeira solicitação, o sistema gera um código ou arquivo de cache e, nas solicitações subsequentes, esses cálculos, necessários, por exemplo, para compilar um modelo, não serão mais executados.

O armazenamento em cache
acelera significativamente
o aplicativo , mas ao mesmo tempo o
complica . Por exemplo, há problemas com a invalidação do cache e a atualização do aplicativo. Não confunda o cache do usuário com o cache do aplicativo - em um, os dados mudam com o tempo, no outro somente quando o código é atualizado.
Processamento de solicitação
Quando uma solicitação é recebida de um servidor PHP-FPM externo, o ponto de entrada e a inicialização da solicitação coincidem.
Acontece que a solicitação do cliente é o estado do nosso processo.
A única maneira de alterar esse estado é destruir completamente o trabalhador e começar de novo com uma nova solicitação.

Este é um modelo clássico de thread único com suas vantagens.
- Todos os trabalhadores no final da solicitação morrem.
- Vazamentos de memória, condição de corrida, conflitos não são inerentes ao PHP. Você não pode se preocupar com isso.
- O código é simples: escrevemos, processamos a solicitação, morremos e seguimos em frente.
Por outro lado, para cada solicitação, carregamos completamente a estrutura, todas as bibliotecas, realizamos alguns cálculos, recompilamos os modelos. Com cada solicitação em um círculo, produzimos muitas manipulações e trabalho desnecessário.
Como funciona no servidor
Muito provavelmente, um monte de nginx e PHP funcionará. O Nginx funcionará como um proxy reverso: dê aos usuários parte das estatísticas e delegue parte das solicitações ao gerenciador de processos PHP PHP-FPM abaixo. O gerente já cria um trabalhador separado para a solicitação e a processa. Depois disso, o trabalhador é destruído ou liberado. Em seguida, um novo trabalhador é criado para a próxima solicitação.

Esse modelo funciona de forma estável - o aplicativo é quase impossível de matar. Mas, sob cargas pesadas, a quantidade de trabalho para inicializar e destruir trabalhadores afeta o desempenho do sistema, porque mesmo para uma simples solicitação GET, muitas vezes precisamos puxar várias dependências e aumentar novamente a conexão com o banco de dados.
Acelerando o aplicativo
Como acelerar o aplicativo clássico após a introdução do cache e do carregamento lento? Que outras opções existem?
Vire para o próprio idioma .
- Use o OPCache. Eu acho que ninguém está executando o PHP na produção sem o OPCache ativado?
- Aguarde RFC: pré-carregando . Ele permite pré-carregar um conjunto de arquivos em uma máquina virtual.
- JIT - acelera seriamente o aplicativo em tarefas associadas à CPU. Infelizmente, com tarefas relacionadas a bancos de dados, isso não ajudará muito.
Use alternativas . Por exemplo, a máquina virtual HHVM do Facebook. Ele executa o código em um ambiente mais otimizado. Infelizmente, o HHVM não é totalmente compatível com a sintaxe do PHP. Como alternativa, os compiladores kPHP do VK ou PeachPie, que convertem completamente o código em .NET C #, são uma alternativa.
Reescreva totalmente para outro idioma. Esta é uma opção radical - elimine completamente o carregamento de código entre solicitações.
Você pode
armazenar completamente
o estado do aplicativo na memória , usá-lo ativamente para trabalhar e esquecer o conceito de trabalhador que está morrendo e limpar completamente o aplicativo entre solicitações.
Para conseguir isso, movemos o ponto de entrada, que costumava estar junto com o ponto de inicialização, para dentro do aplicativo.
Transferindo ponto de entrada - demonização
Isso está criando um loop infinito no aplicativo: solicitação de entrada - execute-a na estrutura - gere uma resposta para o usuário. Esta é uma economia séria - toda a inicialização, toda a inicialização da estrutura é realizada apenas uma vez e, em seguida, várias solicitações são processadas pelo aplicativo.

Nós adaptamos a aplicação
Curiosamente, podemos nos concentrar em otimizar apenas a parte do aplicativo que será executada
em tempo de execução : controladores, lógica de negócios. Nesse caso, você pode abandonar o modelo de carregamento lento. Iremos participar do projeto de bootstrap desde o início - no momento da inicialização. Cálculos preliminares: roteamento, modelos, configurações, esquemas ORM inflarão a inicialização, mas no futuro eles economizarão tempo de processamento para uma solicitação específica.

Não recomendo compilar modelos ao baixar um trabalhador, mas o download, por exemplo, de todas as configurações é útil.
Comparar modelos
Compare os modelos demonizados (esquerda) e clássicos.

O modelo demonizado leva mais tempo desde o momento em que o processo foi criado até o momento em que a resposta é retornada ao usuário. O aplicativo clássico é otimizado para criação, processamento e destruição rápidos.
No entanto, todas as solicitações subsequentes após o aquecimento do código são muito mais rápidas. A estrutura, o aplicativo e o contêiner já estão na memória e prontos para aceitar solicitações e responder rapidamente.
Problemas do modelo de longa duração
Apesar das vantagens, o modelo possui um conjunto de limitações.
Vazamentos de memória. O aplicativo fica na memória por muito tempo e, se você usar as "curvas" da biblioteca, as dependências incorretas ou os estados globais - a memória começará a vazar. Em algum momento, será exibido um erro fatal que interromperá a solicitação do usuário.
O problema é resolvido de duas maneiras.
- Escreva um código preciso, use bibliotecas comprovadas.
- Monitore ativamente os trabalhadores. Se você suspeitar que a memória está vazando dentro do processo, altere-a proativamente para um analógico com um limite inferior, ou seja, simplesmente para uma nova cópia que ainda não conseguiu acumular memória não limpa.
Vazamentos de dados . Por exemplo, se durante uma solicitação recebida salvarmos o usuário atual do sistema em alguma variável global e esquecermos de redefinir essa variável após a solicitação, é possível que o próximo usuário do sistema obtenha acesso acidental a dados que ele não deve ver.
O problema foi resolvido no nível da arquitetura do aplicativo.
- Não armazene um usuário ativo em um contexto global. Todos os dados específicos do contexto da solicitação são descartados e limpos antes da próxima solicitação.
- Manuseie os dados da sessão com cuidado. Sessões em PHP - com a abordagem clássica, este é um objeto global. Enrole-o corretamente para que, mediante solicitação subsequente, seja redefinido.
Gerenciamento de recursos .
- Monitore as conexões com o banco de dados. Se o aplicativo ficar na memória por um mês ou dois, a conexão aberta provavelmente será fechada dentro desse período: o banco de dados será reinstalado, reiniciado ou o firewall redefinirá a conexão. No nível do código, considere reconectar ou, após cada solicitação, redefinir a conexão e aumentá-la novamente na próxima solicitação.
- Evite bloqueio de arquivo de longa duração. Se o seu trabalhador gravar algumas informações em um arquivo, não há problema. Mas se esse arquivo estiver aberto e tiver um bloqueio, nenhum outro processo no seu sistema terá acesso a ele até que o bloqueio seja liberado.
Explore o modelo de longa duração
Considere um modelo de trabalhador de longa duração - demonizando um aplicativo - e explore maneiras de implementá-lo.
Abordagem sem bloqueio
Usamos PHP assíncrono - carregamos o aplicativo uma vez na memória e processamos solicitações HTTP recebidas dentro do aplicativo. Agora, o
aplicativo e o servidor são um processo . Quando a solicitação chega, criamos uma rotina separada ou, no loop de eventos, prometemos, processamos e entregamos ao usuário.

A vantagem inegável da abordagem é o desempenho máximo. Também é possível usar ferramentas interessantes, por exemplo,
configurar o WebSocket diretamente no seu aplicativo .
No entanto, a abordagem
aumenta significativamente
a complexidade do desenvolvimento . É necessário instalar o ELDO, lembre-se de que nem todos os drivers de banco de dados serão suportados e a biblioteca PDO está excluída.
Para resolver problemas no caso de demonização com uma abordagem sem bloqueio, você pode usar ferramentas conhecidas:
ReactPHP ,
amphp e
Swoole - um desenvolvimento interessante na forma de uma extensão C. Essas ferramentas funcionam rapidamente, possuem uma boa comunidade e boa documentação.
Abordagem de bloqueio
Nós não elevamos corotinas dentro do aplicativo, mas fazemos de fora.

Nós apenas
escolhemos alguns processos de aplicação , como o PHP-FPM faria. Em vez de transmitir essas solicitações na forma de um estado do processo, as entregamos de fora na forma de um protocolo ou mensagem.
Escrevemos o mesmo
código de thread único que conhecemos, usamos as mesmas bibliotecas e o mesmo PDO. Todo o trabalho duro de trabalhar com soquetes, HTTP e outras ferramentas é feito
fora do aplicativo PHP .
Dos pontos negativos: devemos
monitorar a memória e lembrar que a
comunicação entre dois processos diferentes não é livre , mas precisamos transferir dados. Isso criará uma ligeira sobrecarga.
Para resolver o problema, já existe uma
ferramenta PHP-RM escrita em PHP. Na biblioteca ReactPHP, possui
integração com várias estruturas . No entanto, o PHP-PM é muito
lento, vaza memória no nível do servidor e, sob carga, não mostra tanto crescimento quanto o PHP-FRM.
Escrevemos nosso servidor de aplicativos
Nós escrevemos
nosso servidor de aplicativos , que é semelhante ao PHP-RM, mas há mais funcionalidades. O que queríamos do servidor?
Combine com estruturas existentes. Gostaríamos de ter uma integração flexível com quase todas as estruturas do mercado. Não tenho vontade de escrever uma ferramenta que funcione apenas em um caso específico.
Diferentes processos para servidor e aplicativo . Possibilidade de uma reinicialização a quente, para que, ao desenvolver localmente, pressione F5 e veja o novo código atualizado, além de poder expandi-los individualmente.
Alta velocidade e estabilidade . Ainda assim, estamos escrevendo um servidor HTTP.
Fácil extensibilidade . Queremos usar o servidor não apenas como um servidor HTTP, mas também para cenários individuais como um servidor de filas ou um servidor gRPC.
Trabalhe imediatamente, sempre que possível: Windows, Linux, CPU ARM.
Capacidade de escrever
extensões multithread muito
rápidas, específicas para a nossa aplicação.
Como você já entendeu, vamos escrever em Golang.
Servidor RoadRunner
Para criar um servidor PHP, você precisa resolver 4 problemas principais:
- Estabelecer comunicação entre os processos Golang e PHP.
- Gerenciamento de processos: criação, destruição, monitoramento de trabalhadores.
- Balanceamento de tarefas - distribuição eficiente de tarefas aos trabalhadores. Como estamos implementando um sistema que bloqueia um trabalhador individual para uma tarefa de entrada específica específica, é importante criar um sistema que diga rapidamente que o processo terminou o trabalho e está pronto para aceitar a próxima tarefa.
- Pilha HTTP - enviando dados da solicitação HTTP para o trabalhador. É uma tarefa simples escrever um ponto de entrada para o qual o usuário envia uma solicitação, que é passada ao PHP e retornada.
Variantes de interação entre processos
Primeiro, vamos resolver o problema de comunicação entre os processos Golang e PHP. Temos várias maneiras.
Incorporação: incorporação de um intérprete PHP diretamente no Golang. Isso é possível, mas requer um assembly PHP personalizado, configuração complexa e um processo comum para o servidor e o PHP. Como no
go-php , por exemplo, onde o interpretador PHP é integrado ao Golang.
Memória compartilhada - O uso do espaço de memória compartilhada, onde os processos compartilham esse espaço . É preciso um trabalho meticuloso. Ao trocar dados, você precisará sincronizar o estado manualmente e a quantidade de erros que podem ocorrer é bastante grande. A memória compartilhada também depende do sistema operacional.
Escrevendo seu protocolo de transporte - Goridge
Seguimos um caminho simples que é usado em quase todas as soluções em sistemas Linux - usamos o protocolo de transporte. Está
escrito em cima dos PIPES e UNIX / TCP padrão .
Ele tem a capacidade de transferir dados em ambas as direções, detectar erros e também marcar solicitações e colocar cabeçalhos nelas. Uma nuance importante para nós é a capacidade de implementar o protocolo sem dependências do lado do PHP e Golang - sem extensões C em linguagem pura.
Como em qualquer protocolo, a base é um pacote de dados. No nosso caso, o pacote possui um cabeçalho fixo de 17 bytes.

O primeiro byte é alocado para determinar o tipo de pacote. Pode ser um fluxo ou um sinalizador que indica o tipo de serialização de dados. Em seguida, duas vezes empacotamos o tamanho dos dados em Little Endian e Big Endian. Usamos esse legado para detectar erros de transmissão. Se percebermos que o tamanho dos dados compactados em dois pedidos diferentes não corresponde, provavelmente ocorreu um erro de transferência de dados. Então os dados são transmitidos.

Na terceira versão do pacote, nos livraremos desse legado, apresentaremos uma abordagem mais clássica com uma soma de verificação e também adicionaremos a capacidade de usar esse protocolo com processos PHP assíncronos.
Para implementar o protocolo em Golang e PHP, usamos ferramentas padrão.
No Golang: bibliotecas de codificação / binárias e io e net para trabalhar com pipes padrão e soquetes UNIX / TCP.
No PHP: a função familiar para trabalhar com pacote / descompactação de dados binários e os fluxos de extensões e soquetes para tubos e soquetes.
Um
efeito colateral interessante surgiu durante a implementação. Nós o integramos à biblioteca líquida / rpc Golang padrão, que nos permite chamar o código de serviço da Golang diretamente no aplicativo.
Escrevemos um serviço:
Com uma pequena quantidade de código, chamamos de aplicativo:
<?php use Spiral\Goridge; require "vendor/autoload.php"; $rpc = new Goridge\RPC( new Goridge\SocketRelay("127.0.0.1", 6001) ); echo $rpc->call("App.Hi", "Antony");
Gerenciador de processos PHP
A próxima parte do servidor é o gerenciamento de trabalhadores PHP.

Worker é um processo PHP que constantemente observamos na Golang. Coletamos o log de seus erros no arquivo STDERR, nos comunicamos com o trabalhador através do protocolo de transporte Goridge e coletamos estatísticas sobre consumo de memória, execução de tarefas e bloqueio.
A implementação é simples - essa é a funcionalidade padrão de os / exec, runtime, sync, atomic. Para criar trabalhadores, usamos
Worker Factory .

Por que fábrica de trabalhadores? Porque queremos nos comunicar tanto em tubos padrão quanto em soquetes. Nesse caso, o processo de inicialização é um pouco diferente. Ao criar um trabalhador que se comunica por canal, podemos criá-lo imediatamente e enviar dados diretamente. No caso de soquetes, você precisa criar um trabalhador, aguardar até que ele atinja o sistema, fazer um aperto de mão PID e só então continuar trabalhando.
Balanceador de tarefas
A terceira parte do servidor é a mais importante para o desempenho.
Para implementação, usamos a funcionalidade padrão Golang - um
canal em buffer . Em particular, criamos vários trabalhadores e os colocamos neste canal como uma pilha LIFO.

Ao receber tarefas do usuário, enviamos uma solicitação para a pilha LIFO e solicitamos que o primeiro trabalhador livre seja emitido. Se o trabalhador não puder ser alocado por um determinado período de tempo, o usuário receberá um erro do tipo "Erro de tempo limite". Se o trabalhador estiver alocado - ele sai da pilha, é bloqueado, após o que recebe a tarefa do usuário.

Depois que a tarefa é processada, a resposta é retornada ao usuário e o trabalhador fica no final da pilha. Ele está pronto para executar a próxima tarefa novamente.

Se ocorrer um erro, o usuário receberá um erro, pois o trabalhador será destruído. Solicitamos ao Worker Pool e Worker Factory para criar um processo idêntico e substituí-lo na pilha. Isso permite que o sistema funcione mesmo em caso de erros fatais, simplesmente recriando trabalhadores por analogia com o PHP-FPM.

Como resultado, acabou implementando um pequeno sistema que funciona muito rapidamente -
200 ns para alocação de trabalhadores . É capaz de funcionar mesmo em caso de erros fatais. Cada trabalhador, em determinado momento, processa apenas uma tarefa, o que nos permite usar a
abordagem clássica de bloqueio .
Monitoramento proativo
Uma parte separada do gerenciador de processos e do balanceador de tarefas é o sistema de monitoramento proativo.

Este é um sistema que, uma vez por segundo, pesquisa trabalhadores e monitora indicadores: analisa quanta memória eles consomem, quanto estão, se estão inativos. Além do rastreamento, o sistema monitora vazamentos de memória. Se o trabalhador exceder um determinado limite, nós o veremos e o removeremos cuidadosamente do sistema antes que ocorra um vazamento fatal.
Pilha HTTP
A última e simples parte.
Como é implementado:- gera um ponto HTTP no lado Golang;
- nós recebemos uma solicitação;
- converter para o formato PSR-7;
- envie a solicitação ao primeiro trabalhador livre;
- Descompacte a solicitação em um objeto PSR-7;
- nós processamos;
- nós geramos a resposta.
Para implementação, usamos a
biblioteca padrão
Golang NET / HTTP . Esta é uma biblioteca famosa com muitas extensões. Capaz de trabalhar tanto em HTTPS quanto em protocolo HTTP / 2.
No lado do PHP, usamos o padrão PSR-7
. É uma
estrutura independente com muitas extensões e Middlewares. O PSR-7 é
imutável em design , que se adapta bem ao conceito de aplicativos de longa duração e evita erros globais de consulta.
Ambas as estruturas no Golang e no PSR-7 são semelhantes, o que economizou significativamente tempo para mapear uma solicitação de um idioma para outro.
Para iniciar o servidor, é necessária uma
ligação mínima :
http: address: 0.0.0.0:8080 workers: command: "php psr-worker.php" pool: numWorkers: 4
Além disso, a partir da versão 1.3.0, a última parte da configuração pode ser omitida.
Faça o download do arquivo binário do servidor, coloque-o no contêiner do Docker ou na pasta do projeto. Como alternativa, globalmente, escrevemos um pequeno arquivo de configuração que descreve qual pod vamos ouvir, qual trabalhador é o ponto de entrada e quantos são necessários.
No lado do PHP, escrevemos um loop primário que recebe uma solicitação PSR-7, a processa e retorna uma resposta ou um erro ao servidor.
while ($req = $psr7->acceptRequest()) { try { $resp = new \Zend\Diactoros\Response(); $resp->getBody()->write("hello world"); $psr7->respond($resp); } catch (\Throwable $e) { $psr7->getWorker()->error((string)$e); } }
Assembléia Para implementar o servidor, escolhemos uma arquitetura com uma abordagem de componente. Isso possibilita montar o servidor para as necessidades do projeto, adicionando ou removendo peças individuais, dependendo dos requisitos do aplicativo.
func main() { rr.Container.Register(env.ID, &env.Service{}) rr.Container.Register(rpc.ID, &rpc.Service{}) rr.Container.Register(http.ID, &http.Service{}) rr.Container.Register(static.ID, &static.Service{}) rr.Container.Register(limit.ID, &limit.Service{}
Casos de uso
Considere as opções para usar o servidor e modificar a estrutura. Para começar, considere o pipeline clássico - o trabalho do servidor com solicitações.
Modularidade
O servidor recebe a solicitação para um ponto HTTP e a passa por um conjunto de Middleware, escrito em Golang. Uma solicitação de entrada é convertida em uma tarefa que o trabalhador entende. O servidor entrega a tarefa ao trabalhador e a devolve.

Ao mesmo tempo, o trabalhador, usando o protocolo Goridge, se comunica com o servidor, monitora seu status e transfere dados para ele.
Middleware em Golang: autorização
Esta é a primeira coisa a fazer. Em nosso aplicativo, criamos o Middleware para
autorizar o usuário pelo token JWT . O middleware é escrito da mesma maneira para qualquer outro tipo de autorização. Uma implementação muito banal e simples é escrever limitador de taxa ou disjuntor.
A autorização é rápida . Se a solicitação não for válida - simplesmente não a envie para o aplicativo PHP e não desperdice recursos no processamento de tarefas inúteis.
Monitoramento
O segundo caso de uso. Podemos integrar o sistema de monitoramento diretamente no Golang Middleware. Por exemplo, Prometheus, para coletar estatísticas sobre a velocidade dos pontos de resposta, o número de erros.

Você também pode
combinar o monitoramento com métricas específicas do aplicativo (disponível como padrão na 1.4.5). Por exemplo, podemos enviar o número de solicitações para o banco de dados ou o número de solicitações específicas processadas para o servidor Golang e, em seguida, para o Prometheus.
Rastreamento e registro distribuídos
Escrevemos Middleware com um gerenciador de processos. Em particular, podemos nos conectar ao sistema em tempo real para monitorar logs e
coletar todos os logs em um banco de dados central , o que é útil ao escrever aplicativos distribuídos.

Também podemos
marcar solicitações , fornecer um ID específico e transmitir esse ID a todos os serviços ou sistemas de comunicação downstream entre eles. Como resultado, podemos criar um
rastreamento distribuído e ver como os logs do aplicativo vão.
Registre seu histórico de consultas
Este é um pequeno módulo que registra todas as solicitações recebidas e as armazena em um banco de dados externo. O módulo permite fazer solicitações de reprodução no projeto e implementar um sistema de teste automático, um sistema de teste de carga ou apenas verificar a operação da API.

Como implementamos o módulo?
Processamos parte dos pedidos de Golang . Escrevemos Middleware em Golang e podemos enviar parte das solicitações para Handler, que também está escrito em Golang. Se algum ponto do aplicativo é preocupante em termos de desempenho, reescrevemos para Golang e arrastamos a pilha de um idioma para outro.
Estamos escrevendo um servidor WebSocket . A implementação de um servidor WebSocket ou servidor de notificação por push está se tornando uma tarefa trivial.
- Serviço Golang no nível do servidor.
- Para comunicação, usamos Goridge.
- Camada de serviço fina em PHP.
- Implementamos o servidor de notificação.
Recebemos uma solicitação e aumentamos uma conexão WebSocket. Se o aplicativo precisar enviar algum tipo de notificação ao usuário, ele lançará essa mensagem através do protocolo RPC para o servidor WebSocket.
Gerencie seu ambiente PHP. Ao criar um pool de trabalhadores, o RoadRunner tem controle total sobre o estado das variáveis de ambiente e permite que você as altere conforme desejar. Se estivermos escrevendo um aplicativo distribuído grande, podemos usar uma única fonte de dados de configuração e conectá-los como um sistema para configurar o ambiente. Se criarmos um conjunto de serviços, todos esses serviços baterão em um único sistema, configurarão e funcionarão. Isso pode simplificar bastante a implantação e livrar-se dos arquivos .env.

Curiosamente, as variáveis env que estão disponíveis dentro do trabalhador não são globais no sistema. Isso melhora um pouco a segurança do contêiner.
Integração da biblioteca Golang em PHP
Usamos essa opção no site oficial do
RoadRunner . Esta é uma integração de um banco de dados quase completo
com a pesquisa de texto completo BleveSearch dentro do servidor.

Nós indexamos as páginas da documentação: as colocamos no Bolt DB, após o qual realizamos uma pesquisa de texto completo sem um banco de dados real como o MySQL e sem um cluster de pesquisa como o Elasticsearch. O resultado foi um pequeno projeto em que parte da funcionalidade está em PHP, mas a pesquisa está em Golang.
Implementando funções Lambda
Você pode ir além e
se livrar completamente da camada HTTP. Nesse caso, implementar, por exemplo, funções Lambda é uma tarefa simples.

Para implementação, usamos o
tempo de
execução padrão da
AWS para a função Lambda. Escrevemos uma pequena ligação, cortamos completamente os servidores HTTP e enviamos os dados em formato binário para os trabalhadores. Também temos acesso às configurações do ambiente, o que nos permite escrever funções configuradas diretamente no painel de administração da Amazon.
Os trabalhadores ficam na memória durante toda a vida útil do processo e a função Lambda após a solicitação inicial permanece na memória por 15 minutos. No momento, o código não carrega e responde rapidamente. Em testes sintéticos, recebemos até
0,5 ms por uma solicitação recebida .
gRPC para PHP
A opção mais difícil é substituir a camada HTTP pela camada gRPC. Este
pacote está disponível no GitHub .

Podemos proxy completamente todas as solicitações Protobuf recebidas para um aplicativo PHP subordinado, onde elas podem ser descompactadas, processadas e respondidas novamente. Podemos escrever código no PHP e no Golang, combinando e transferindo funcionalidades de uma pilha para outra. O serviço suporta Middleware. O aplicativo independente e em conjunto com o HTTP podem funcionar.
Servidor de fila
A última e mais interessante opção é a implementação do
servidor de filas .

No lado do PHP, tudo o que fazemos é obter uma carga binária, descompactá-la, fazer o trabalho e informar o servidor sobre o sucesso. No lado Golang, estamos totalmente engajados no gerenciamento de conexões com corretores. Pode ser RabbitMQ, Amazon SQS ou Beanstalk.
No lado de Golang, implementamos o "
desligamento gracioso" dos trabalhadores. Podemos esperar pela implementação da “conexão durável” - se a conexão com o broker for perdida, o servidor esperará um pouco usando a “estratégia de retirada”, ela eleva a conexão e o aplicativo nem percebe.
Podemos processar essas solicitações no PHP e Golang e colocá-las na fila dos dois lados:
- do PHP através do protocolo Goridge Goridge RPC;
- de Golang - comunicando-se com a biblioteca SDK.
Se a carga útil cair, nem todo o consumidor cairá, mas apenas um processo separado. O sistema gera imediatamente, a tarefa é enviada para o próximo trabalhador. Isso permite que você execute tarefas sem parar.
Implementamos um dos corretores diretamente na memória do servidor e usamos a funcionalidade Golang. Isso nos permite escrever um aplicativo usando filas antes de escolher a pilha final. Levantamos o aplicativo localmente, iniciamos e temos filas que funcionam na memória e se comportam da mesma maneira que se comportariam no RabbitMQ, Amazon SQS ou Beanstalk.
Ao usar dois idiomas em um pacote híbrido, vale lembrar como separá-los.
Domínios separados
Golang é uma linguagem rápida e multiencadeada, adequada para escrever lógica de infraestrutura e lógica de monitoramento e autorização de usuário.
Também é útil para
implementar drivers personalizados para acessar fontes de dados - são filas, por exemplo, Kafka, Cassandra.
PHP é uma ótima linguagem para escrever lógica de negócios.
Este é um bom sistema para renderização HTML, ORM e trabalho com o banco de dados.
Comparação de ferramentas
Há alguns meses,
Habré comparou PHP-FPM, PHP-PM, React-PHP, Roadrunner e outras ferramentas. O benchmark foi realizado em um projeto com o Symfony real 4.
O RoadRunner sob carga mostra bons resultados e está à frente de todos os servidores. Comparado ao PHP-FPM, o desempenho é de 6 a 8 vezes mais.

Na mesma referência, o RoadRunner não perdeu um único pedido, tudo estava 100% resolvido. Infelizmente, o React-PHP perdeu 8 a 9 solicitações sob cargas - isso é inaceitável. Gostaríamos que o servidor não travasse e trabalhasse de forma estável.

Desde a publicação do RoadRunner no acesso público ao GitHub, recebemos mais de 30.000 instalações. A comunidade nos ajudou a escrever um conjunto específico de extensões, melhorias e acredita que a solução tem direito à vida.
O RoadRunner é bom se você deseja
acelerar significativamente o aplicativo, mas ainda não está pronto para entrar no PHP assíncrono . Esse é um compromisso que exigirá uma certa quantidade de esforço, mas não tão significativo quanto uma reescrita completa da base de código.
Tome o RoadRunner se você deseja ter
mais controle sobre o ciclo de vida do PHP ,
se não houver recursos PHP suficientes, por exemplo, para o sistema de filas ou o Kafka, e quando sua popular biblioteca Golang resolver seu problema, que não está em PHP, e a escrita leva tempo, o que você também não tem.
Sumário
O que conseguimos ao escrever este servidor e usá-lo em nossa infraestrutura de produção.
- Eles aumentaram a velocidade de reação dos pontos de aplicação em 4 vezes em comparação com o PHP-FPM.
- Livre-se completamente de 502 erros sob cargas . Em picos de carga, o servidor espera um pouco mais e responde como se não houvesse cargas.
- Após otimizar o vazamento de memória, os funcionários ficam na memória por até 2 meses . Isso ajuda ao escrever aplicativos distribuídos, pois todas as solicitações entre serviços já estão armazenadas em cache no nível do soquete.
- Usamos o Keep-Alive. Isso acelera significativamente a comunicação entre um sistema distribuído.
- Dentro da infraestrutura real, colocamos tudo no Alpine Docker em Kubernetes . O sistema de implantação e construção do projeto agora é mais fácil. Tudo o que é necessário é criar uma versão personalizada do RoadRunner para o projeto, colocá-la no projeto Docker, preencher a imagem do Docker e fazer o upload calmo de nosso pod para o Kubernetes.
- De acordo com o tempo real de um dos projetos para pontos individuais que não têm acesso ao banco de dados, o tempo médio de resposta é de 0,33 ms .
A próxima conferência profissional para desenvolvedores de PHP PHP Rússia apenas no próximo ano. Por enquanto, oferecemos o seguinte:
- Preste atenção no GolangConf se você estiver interessado na parte Go e quiser saber mais detalhes ou ouvir argumentos a favor de mudar para esse idioma. Se você estiver pronto para compartilhar sua experiência, envie resumos .
- Participe do HighLoad ++ em Moscou, se tudo que for importante para você associado ao alto desempenho, envie um relatório antes de 7 de setembro ou reserve um ingresso.
- Assine o boletim informativo e o canal de telegrama para receber um convite para o PHP Russia 2020 mais cedo do que outros.