RoadRunner: PHP não foi feito para morrer, ou Golang para o resgate

RoadRunner: PHP não foi feito para morrer, ou Golang para o resgate

Oi, Habr! Somos ativos no Badoo trabalhando no desempenho do PHP, já que temos um sistema bastante grande nessa linguagem e o problema de desempenho é uma questão de economia de dinheiro. Há mais de dez anos, criamos o PHP-FPM para isso, que a princípio era um conjunto de patches para PHP e depois entrou na distribuição oficial.

Nos últimos anos, o PHP fez grandes progressos: o coletor de lixo melhorou, o nível de estabilidade aumentou - hoje você pode escrever daemons e scripts de longa duração em PHP sem problemas. Isso permitiu que o Spiral Scout fosse além: o RoadRunner, ao contrário do PHP-FPM, não limpa a memória entre as solicitações, o que dá um ganho adicional de desempenho (embora essa abordagem complique o processo de desenvolvimento). No momento, estamos experimentando essa ferramenta, mas ainda não temos nenhum resultado para compartilhar. Para tornar a espera por eles mais divertida, publicamos a tradução do anúncio RoadRunner do Spiral Scout.

A abordagem do artigo está próxima de nós: ao resolver nossos problemas, também costumamos usar um monte de PHP e Go, obtendo os benefícios de ambas as linguagens e não abandonando uma em favor da outra.

Divirta-se!

Nos últimos dez anos, criamos aplicativos para empresas da lista Fortune 500, e para empresas com um público de até 500 usuários. Durante todo esse tempo, nossos engenheiros desenvolveram o back-end principalmente em PHP. Mas há dois anos, algo teve um grande impacto não apenas no desempenho de nossos produtos, mas também em sua escalabilidade - introduzimos o Golang (Go) em nossa pilha de tecnologia.

Quase imediatamente, descobrimos que o Go nos permitia criar aplicativos maiores com melhorias de desempenho de até 40 vezes. Com ele, pudemos estender nossos produtos PHP existentes, aprimorando-os combinando os benefícios de ambas as linguagens.

Contaremos como a combinação de Go e PHP ajuda a resolver problemas reais de desenvolvimento e como se tornou uma ferramenta para nós que pode nos livrar de alguns dos problemas associados ao PHP morrendo modelo.

Seu ambiente diário de desenvolvimento PHP

Antes de falarmos sobre como você pode usar o Go para reviver o modelo PHP que está morrendo, vamos dar uma olhada em seu ambiente de desenvolvimento PHP padrão.

Na maioria dos casos, você executa seu aplicativo usando uma combinação do servidor web nginx e do servidor PHP-FPM. O primeiro serve arquivos estáticos e redireciona solicitações específicas para o PHP-FPM, enquanto o próprio PHP-FPM executa o código PHP. Você pode estar usando a combinação menos popular de Apache e mod_php. Mas embora funcione um pouco diferente, os princípios são os mesmos.

Vamos dar uma olhada em como o PHP-FPM executa o código do aplicativo. Quando uma solicitação chega, o PHP-FPM inicializa um processo filho do PHP e passa os detalhes da solicitação como parte de seu estado (_GET, _POST, _SERVER, etc.).

O estado não pode mudar durante a execução de um script PHP, portanto, a única maneira de obter um novo conjunto de dados de entrada é limpando a memória do processo e inicializando-o novamente.

Este modelo de execução tem muitas vantagens. Você não precisa se preocupar muito com o consumo de memória, todos os processos são completamente isolados, e se um deles "morrer", ele será recriado automaticamente e não afetará os demais processos. Mas essa abordagem também apresenta desvantagens que aparecem ao tentar dimensionar o aplicativo.

Desvantagens e ineficiências de um ambiente PHP regular

Se você é um desenvolvedor PHP profissional, sabe por onde começar um novo projeto - com a escolha de uma estrutura. Consiste em bibliotecas de injeção de dependência, ORMs, traduções e modelos. E, é claro, todas as entradas do usuário podem ser convenientemente colocadas em um único objeto (Symfony/HttpFoundation ou PSR-7). Quadros são legais!

Mas tudo tem seu preço. Em qualquer framework de nível empresarial, para processar uma simples requisição de usuário ou acesso a um banco de dados, você terá que carregar pelo menos dezenas de arquivos, criar inúmeras classes e analisar diversas configurações. Mas o pior é que após a conclusão de cada tarefa, você precisará redefinir tudo e começar de novo: todo o código que você acabou de iniciar torna-se inútil, com a ajuda dele você não processará mais outra solicitação. Diga isso a qualquer programador que escreva em alguma outra linguagem e você verá a perplexidade em seu rosto.

Os engenheiros do PHP têm procurado maneiras de resolver esse problema há anos, usando técnicas inteligentes de carregamento lento, microframeworks, bibliotecas otimizadas, cache etc. . (Nota do tradutor: este problema será parcialmente resolvido com o advento do pré-carga no PHP 7.4)

O PHP com Go pode sobreviver a mais de uma solicitação?

É possível escrever scripts PHP que duram mais do que alguns minutos (até horas ou dias): por exemplo, tarefas cron, analisadores CSV, quebra de fila. Todos trabalham de acordo com o mesmo cenário: recuperam uma tarefa, executam-na e aguardam a próxima. O código reside na memória o tempo todo, economizando milissegundos preciosos, pois há muitas etapas adicionais necessárias para carregar a estrutura e o aplicativo.

Mas desenvolver scripts de longa duração não é fácil. Qualquer erro mata completamente o processo, diagnosticar vazamentos de memória é irritante e a depuração F5 não é mais possível.

A situação melhorou com o lançamento do PHP 7: um coletor de lixo confiável apareceu, ficou mais fácil lidar com erros e as extensões do kernel agora são à prova de vazamentos. É verdade que os engenheiros ainda precisam ter cuidado com a memória e estar cientes dos problemas de estado no código (existe uma linguagem que pode ignorar essas coisas?). Ainda assim, o PHP 7 tem menos surpresas reservadas para nós.

É possível pegar o modelo de trabalho com scripts PHP de longa duração, adaptá-lo a tarefas mais triviais como processar solicitações HTTP e, assim, livrar-se da necessidade de carregar tudo do zero a cada solicitação?

Para resolver esse problema, primeiro precisamos implementar um aplicativo de servidor que pudesse aceitar solicitações HTTP e redirecioná-las uma a uma para o PHP worker sem eliminá-lo todas as vezes.

Sabíamos que poderíamos escrever um servidor web em PHP puro (PHP-PM) ou usando uma extensão C (Swoole). E embora cada método tenha seus próprios méritos, ambas as opções não nos agradaram - queríamos algo mais. Precisávamos de mais do que apenas um servidor web - esperávamos obter uma solução que nos livrasse dos problemas associados a um “início difícil” em PHP, que ao mesmo tempo pudesse ser facilmente adaptado e estendido para aplicações específicas. Ou seja, precisávamos de um servidor de aplicativos.

Go pode ajudar com isso? Sabíamos que sim porque a linguagem compila aplicativos em binários únicos; é multiplataforma; usa seu próprio modelo de processamento paralelo muito elegante (simultaneidade) e uma biblioteca para trabalhar com HTTP; e, finalmente, milhares de bibliotecas e integrações de código aberto estarão disponíveis para nós.

As dificuldades de combinar duas linguagens de programação

Antes de tudo, era necessário determinar como dois ou mais aplicativos se comunicariam entre si.

Por exemplo, usando excelente biblioteca Alex Palaestras, foi possível compartilhar memória entre os processos PHP e Go (semelhante ao mod_php no Apache). Mas esta biblioteca possui recursos que limitam seu uso para resolver nosso problema.

Decidimos usar uma abordagem diferente e mais comum: construir a interação entre os processos por meio de soquetes/pipelines. Essa abordagem provou ser confiável nas últimas décadas e foi bem otimizada no nível do sistema operacional.

Para começar, criamos um protocolo binário simples para troca de dados entre processos e tratamento de erros de transmissão. Em sua forma mais simples, esse tipo de protocolo é semelhante ao rede с cabeçalho de pacote de tamanho fixo (no nosso caso 17 bytes), que contém informações sobre o tipo de pacote, seu tamanho e uma máscara binária para verificar a integridade dos dados.

No lado do PHP, usamos função de pacote, e no lado Go, a biblioteca codificação / binário.

Pareceu-nos que um protocolo não era suficiente - e adicionamos a capacidade de chamar serviços net/rpc go diretamente do PHP. Mais tarde, isso nos ajudou muito no desenvolvimento, pois pudemos integrar facilmente bibliotecas Go em aplicativos PHP. O resultado desse trabalho pode ser visto, por exemplo, em nosso outro produto de código aberto Goridge.

Distribuindo tarefas entre vários PHP workers

Depois de implementar o mecanismo de interação, começamos a pensar na maneira mais eficiente de transferir tarefas para processos PHP. Quando uma tarefa chega, o servidor de aplicação deve escolher um trabalhador livre para executá-la. Se um trabalhador/processo sai com um erro ou "morre", nos livramos dele e criamos um novo para substituí-lo. E se o trabalhador/processo for concluído com sucesso, nós o retornamos ao pool de trabalhadores disponíveis para executar tarefas.

RoadRunner: PHP não foi feito para morrer, ou Golang para o resgate

Para armazenar o pool de trabalhadores ativos, usamos canal em buffer, para remover trabalhadores inesperadamente “mortos” do pool, adicionamos um mecanismo para rastrear erros e estados de trabalhadores.

Como resultado, obtivemos um servidor PHP funcional capaz de processar qualquer solicitação apresentada em formato binário.

Para que nosso aplicativo começasse a funcionar como um servidor web, tivemos que escolher um padrão PHP confiável para representar qualquer solicitação HTTP recebida. No nosso caso, apenas transformar solicitação net/http de Ir para o formato PSR-7para que seja compatível com a maioria dos frameworks PHP disponíveis hoje.

Como o PSR-7 é considerado imutável (alguns diriam que tecnicamente não é), os desenvolvedores precisam escrever aplicativos que não tratem a solicitação como uma entidade global em princípio. Isso se encaixa perfeitamente com o conceito de processos PHP de longa duração. Nossa implementação final, que ainda não foi nomeada, ficou assim:

RoadRunner: PHP não foi feito para morrer, ou Golang para o resgate

Apresentando o RoadRunner - servidor de aplicativos PHP de alto desempenho

Nossa primeira tarefa de teste foi um back-end de API, que periodicamente explode de forma imprevisível (com muito mais frequência do que o normal). Embora o nginx fosse suficiente na maioria dos casos, encontramos regularmente erros 502 porque não conseguimos balancear o sistema com rapidez suficiente para o aumento esperado na carga.

Para substituir essa solução, implantamos nosso primeiro servidor de aplicativos PHP/Go no início de 2018. E imediatamente obteve um efeito incrível! Não apenas nos livramos completamente do erro 502, mas também reduzimos o número de servidores em dois terços, economizando muito dinheiro e dores de cabeça para engenheiros e gerentes de produto.

Em meados do ano, melhoramos nossa solução, publicamos no GitHub sob a licença do MIT e a nomeamos RoadRunner, enfatizando assim sua incrível velocidade e eficiência.

Como o RoadRunner pode melhorar sua pilha de desenvolvimento

Aplicação RoadRunner nos permitiu usar o Middleware net/http no lado Go para executar a verificação JWT antes que a solicitação chegasse ao PHP, bem como lidar com WebSockets e agregar estado globalmente no Prometheus.

Graças ao RPC integrado, você pode abrir a API de qualquer biblioteca Go para PHP sem escrever wrappers de extensão. Mais importante, com RoadRunner você pode implantar novos servidores não HTTP. Os exemplos incluem a execução de manipuladores em PHP AWS Lambda, criando separadores de fila confiáveis ​​e até adicionando gRPC às nossas aplicações.

Com a ajuda das comunidades PHP e Go, melhoramos a estabilidade da solução, aumentamos o desempenho do aplicativo em até 40 vezes em alguns testes, aprimoramos as ferramentas de depuração, implementamos a integração com o framework Symfony e adicionamos suporte para HTTPS, HTTP/2, plug-ins e PSR-17.

Conclusão

Algumas pessoas ainda estão presas à noção antiquada de PHP como uma linguagem lenta e pesada, boa apenas para escrever plugins para WordPress. Essas pessoas podem até dizer que o PHP tem uma limitação: quando o aplicativo fica grande o suficiente, você precisa escolher uma linguagem mais “madura” e reescrever a base de código acumulada ao longo de muitos anos.

A tudo isso quero responder: pense novamente. Acreditamos que apenas você define quaisquer restrições para o PHP. Você pode passar a vida inteira fazendo a transição de um idioma para outro, tentando encontrar a combinação perfeita para suas necessidades, ou pode começar a pensar nos idiomas como ferramentas. As supostas falhas de uma linguagem como PHP podem, na verdade, ser a razão de seu sucesso. E se você combiná-lo com outro idioma como o Go, criará produtos muito mais poderosos do que se estivesse limitado a usar qualquer idioma.

Tendo trabalhado com um monte de Go e PHP, podemos dizer que os amamos. Não planejamos sacrificar um pelo outro - pelo contrário, buscaremos maneiras de obter ainda mais valor dessa pilha dupla.

UPD: damos as boas-vindas ao criador do RoadRunner e ao co-autor do artigo original - Lachesis

Fonte: habr.com

Adicionar um comentário