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



Olá Habr! Nós do Badoo estamos trabalhando ativamente no desempenho do PHP , pois temos um sistema bastante grande nessa linguagem e a questão do desempenho é uma questão de economizar dinheiro. Mais de dez anos atrás, criamos para este PHP-FPM, que primeiro foi um conjunto de correções para PHP, e depois fomos entregues oficialmente.

Nos últimos anos, o PHP fez grandes progressos: o coletor de lixo melhorou, o nível de estabilidade melhorou - hoje em PHP você pode escrever demônios e scripts de longa duração sem problemas especiais. Isso permitiu ao Spiral Scout ir além: o RoadRunner, ao contrário do PHP-FPM, não limpa a memória entre solicitações, o que fornece um ganho de desempenho adicional (embora essa abordagem complique o processo de desenvolvimento). Agora estamos experimentando essa ferramenta, mas ainda não temos resultados que possam ser compartilhados. Esperar por eles foi mais divertido, publicamos a tradução do anúncio do RoadRunner da 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 vantagens de ambas as linguagens e não abandonando uma em favor da outra.

Aproveite!



Nos últimos dez anos, criamos aplicativos para empresas da Fortune 500 e para empresas com uma audiência de mais de 500 usuários. Todo esse tempo, nossos engenheiros desenvolveram o back-end principalmente em PHP. Mas há dois anos, algo influenciou bastante não apenas o desempenho de nossos produtos, mas também sua escalabilidade - introduzimos o Golang (Go) em nossa pilha de tecnologias.

Quase imediatamente, descobrimos que o Go nos permite criar aplicativos maiores com desempenho até 40 vezes maior. Com isso, conseguimos expandir os produtos existentes escritos em PHP, aprimorando-os através de uma combinação das vantagens de ambas as linguagens.

Mostraremos como a combinação Go e PHP ajuda a resolver problemas reais de desenvolvimento e como se tornou para nós uma ferramenta que pode aliviar parte dos problemas associados ao modelo de "morte" do PHP .

Seu ambiente diário de desenvolvimento PHP


Antes de falarmos sobre como o Go pode animar o modelo de "morte" do PHP, vamos ver o seu ambiente de desenvolvimento PHP padrão.

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

Considere como o PHP-FPM executa o código do aplicativo. Quando uma solicitação chega, o PHP-FPM inicializa um processo PHP filho 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 do script PHP, portanto, você pode obter um novo conjunto de dados de entrada de apenas uma maneira: limpando a memória do processo e inicializando-a 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 isso não afetará os outros processos. Mas essa abordagem também tem desvantagens que aparecem ao tentar dimensionar o aplicativo.

Desvantagens e ineficiências de um ambiente PHP regular


Se você está envolvido em desenvolvimento profissional em PHP, sabe por onde começar um novo projeto, com a escolha de uma estrutura. É uma biblioteca para injeção de dependência, ORMs, traduções e modelos. E, é claro, toda a entrada do usuário pode ser convenientemente colocada em um objeto (Symfony / HttpFoundation ou PSR-7). Frameworks são legais!

Mas tudo tem um preço. Em qualquer estrutura de nível empresarial, para processar uma solicitação simples do usuário ou acessar o banco de dados, você deverá baixar pelo menos dezenas de arquivos, criar várias classes e analisar várias configurações. Mas a pior parte é que, após concluir cada tarefa, você precisará redefinir tudo e começar de novo: todo o código que você acabou de iniciar se torna inútil, e com isso você não processará mais outra solicitação. Diga a qualquer programador que escrever em qualquer outro idioma sobre isso e você verá perplexidade em seu rosto.

Por anos, os engenheiros de PHP têm procurado maneiras de resolver esse problema, usando métodos bem pensados ​​de carregamento lento, microframes, bibliotecas otimizadas, cache etc. Mas, no final, você ainda precisa redefinir o aplicativo inteiro e começar de novo e de novo. (Nota do tradutor: esse problema será parcialmente resolvido com o advento do pré-carregamento no PHP 7.4)

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


Você pode escrever scripts PHP que durarão mais do que alguns minutos (até horas ou dias): por exemplo, tarefas cron, analisadores CSV, disjuntores de filas. Todos eles trabalham de acordo com um cenário: eles extraem a tarefa, concluem, esperam pelo próximo. O código está constantemente na memória, economizando milissegundos preciosos, pois são necessárias muitas etapas adicionais para baixar a estrutura e o aplicativo.

Mas desenvolver scripts de longa duração não é tão simples. Qualquer erro mata completamente o processo, o diagnóstico de vazamentos de memória é irritante e a depuração usando 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 estão protegidas contra vazamentos. É verdade que os engenheiros ainda precisam lidar com a memória com cuidado e lembrar-se dos problemas de estado no código (existe um idioma no qual você pode ignorar essas coisas?). E, no entanto, no PHP 7, há menos surpresas.

É possível usar um modelo para trabalhar com scripts PHP de longa duração, adaptá-lo para tarefas mais triviais, como processar solicitações HTTP e, assim, eliminar a necessidade de baixar tudo do zero a cada solicitação?

Para resolver esse problema, primeiro foi necessário implementar um aplicativo de servidor capaz de aceitar solicitações HTTP e redirecioná-las uma a uma para o trabalhador PHP, sem matá-lo sempre.

Sabíamos que poderíamos escrever um servidor da Web em PHP puro (PHP-PM) ou usando a extensão C (Swoole). E embora cada método tenha suas próprias vantagens, ambas as opções não nos agradam - eu queria algo mais. Não era apenas um servidor da Web que era necessário - esperávamos obter uma solução que pudesse nos salvar dos problemas associados a um "começo difícil" em PHP, que ao mesmo tempo pudesse ser facilmente adaptado e expandido para aplicativos específicos. Ou seja, precisávamos de um servidor de aplicativos.

A Go pode ajudar com isso? Sabíamos que sim, porque essa linguagem compila aplicativos em arquivos binários únicos; é multiplataforma; usa seu próprio modelo de concorrência muito elegante 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.

Dificuldades na combinação de duas linguagens de programação


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

Por exemplo, com a ajuda da excelente biblioteca de Alex Palaestras, foi possível implementar o compartilhamento de memória pelos 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: criar interação entre processos através de soquetes / pipelines. Essa abordagem nas últimas décadas provou ser confiável 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. Na sua forma mais simples, um protocolo desse tipo é semelhante ao netstring com um 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 a função pack , e no lado do Go, a biblioteca de codificação / binária .

Um protocolo não foi suficiente para nós - e adicionamos a capacidade de chamar serviços go net / rpc diretamente do PHP . Mais tarde, nos ajudou bastante no desenvolvimento, pois pudemos integrar facilmente as bibliotecas Go nos aplicativos PHP. O resultado deste trabalho pode ser visto, por exemplo, em nosso outro produto de código aberto Goridge .

Distribuição de tarefas entre vários trabalhadores de PHP


Após a implementação do mecanismo de interação, começamos a pensar em como melhor transferir tarefas para processos PHP. Quando uma tarefa chega, o servidor de aplicativos deve escolher um trabalhador livre para concluí-la. Se o trabalhador / processo terminou com um erro ou "morreu", nos livramos dele e criamos um novo em troca. E se o trabalhador / processo funcionou com sucesso, retornamos ao conjunto de trabalhadores disponíveis para concluir as tarefas.



Usamos um canal em buffer para armazenar o pool de trabalhadores ativos; para remover inesperadamente trabalhadores "mortos" do pool, adicionamos um mecanismo para rastrear erros e estado dos trabalhadores.

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

Para que nosso aplicativo comece a trabalhar como servidor da Web, tive que escolher um padrão PHP confiável para apresentar qualquer solicitação HTTP recebida. No nosso caso, simplesmente convertemos a solicitação net / http de Ir para o formato PSR-7 para que seja compatível com a maioria das estruturas PHP disponíveis hoje.

Como o PSR-7 é considerado imutável (alguém dirá que tecnicamente não é), os desenvolvedores precisam escrever aplicativos que, em princípio, não tratam a solicitação como uma entidade global. Isso vai bem com o conceito de processos PHP de longa duração. Nossa implementação final, que ainda não recebeu um nome, era assim:



Apresentando o RoadRunner - um servidor de aplicativos PHP de alto desempenho


Nossa primeira tarefa de teste foi um back-end da API, que periodicamente causava explosões imprevisíveis de solicitações (com muito mais frequência do que o habitual). Embora na maioria dos casos houvesse recursos nginx suficientes, encontramos regularmente um erro 502, porque não conseguimos equilibrar o sistema com rapidez suficiente para o aumento esperado na carga.

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

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

Como o RoadRunner pode melhorar sua pilha de desenvolvimento


O uso do RoadRunner nos permitiu usar o Middleware net / http no lado Go para realizar a verificação JWT antes que a solicitação entre no PHP, além de processar WebSockets e estados agregados globalmente no Prometheus.

Graças ao RPC interno, você pode abrir a API de qualquer biblioteca Go para PHP sem escrever wrappers de extensão. Mais importante, o RoadRunner pode implantar novos servidores que não sejam HTTP. Os exemplos incluem a execução de manipuladores do AWS Lambda em PHP, a criação de resolvedores de filas robustos e até a adição de gRPC aos nossos aplicativos.

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

Conclusão


Alguns ainda são cativados pela noção desatualizada do PHP como uma linguagem lenta e complicada, adequada apenas para escrever plugins para WordPress. Essas pessoas podem até dizer que o PHP tem essa limitação: quando o aplicativo se torna grande o suficiente, você precisa escolher uma linguagem mais "madura" e reescrever a base de código que se acumulou ao longo de muitos anos.

Eu gostaria de responder a tudo isso: pense novamente. Acreditamos que somente você mesmo define restrições para o PHP. Você pode passar a vida inteira mudando de um idioma para outro, tentando encontrar a combinação perfeita com suas necessidades, ou pode começar a perceber os idiomas como ferramentas. As falhas aparentes de uma linguagem como PHP podem realmente ser as razões 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 um único idioma.

Depois de trabalhar com um monte de Go e PHP, podemos dizer que os amamos. Não planejamos sacrificar um em favor do outro - pelo contrário, procuraremos maneiras de extrair ainda mais benefícios dessa pilha dupla.

UPD: Bem-vindo ao criador do RoadRunner e co-autor do artigo original - Lachezis

Source: https://habr.com/ru/post/pt434272/


All Articles