
Olá, Habr! Nós no Badoo estamos ativamente , uma vez que temos um sistema grande nesta linguagem e a questão da performance é uma questão de economia. Mais de dez anos atrás, criamos o PHP-FPM, que inicialmente era um conjunto de patches para PHP e depois integrou a versão oficial.
Nos últimos anos, o PHP avançou muito: o coletor de lixo melhorou, o nível de estabilidade aumentou - hoje é possível escrever demons e scripts de longa duração em PHP sem muitos problemas. Isso permitiu que a Spiral Scout fosse mais além: RoadRunner, ao contrário do PHP-FPM, não limpa a memória entre as requisições, proporcionando um ganho adicional em performance (embora essa abordagem complique o processo de desenvolvimento). Estamos experimentando essa ferramenta, mas ainda não temos resultados para compartilhar. Para tornar a espera mais divertida, publicamos a tradução do anúncio do RoadRunner da Spiral Scout.
A abordagem do artigo ressoa conosco: ao resolver nossos problemas, também usamos frequentemente a combinação de PHP e Go, obtendo benefícios de ambas as linguagens sem abrir mão de uma em favor da outra.
Aproveite!
Nos últimos dez anos, criamos aplicativos tanto para empresas da lista , quanto para negócios com uma audiência de não mais que 500 usuários. Durante todo esse tempo, nossos engenheiros desenvolveram o backend predominantemente em PHP. Mas há dois anos, algo teve um grande impacto não só na performance de nossos produtos, mas também na escalabilidade deles - introduzimos o Golang (Go) em nossa pilha tecnológica.
Quase imediatamente, descobrimos que Go nos permite criar aplicativos maiores com um aumento de performance de até 40 vezes. Com ele, conseguimos expandir produtos existentes escritos em PHP, melhorando-os com a combinação de vantagens de ambas as linguagens.
Vamos falar sobre como a combinação de Go e PHP ajuda a resolver problemas reais de desenvolvimento e como se tornou uma ferramenta capaz de aliviar alguns problemas relacionados ao .
Seu ambiente diário de desenvolvimento PHP
Antes de falar sobre como o Go pode reviver o modelo 'morrendo' do PHP, vamos olhar para o seu ambiente padrão de desenvolvimento PHP.
Na maioria dos casos, você executa o aplicativo usando uma combinação do servidor web nginx e do servidor PHP-FPM. O primeiro gerencia arquivos estáticos e redireciona solicitações específicas para o PHP-FPM, que por sua vez executa o código PHP. Talvez você esteja usando uma combinação menos comum do Apache com mod_php. Mas, embora funcione de maneira um pouco diferente, os princípios são os mesmos.
Vamos considerar como o PHP-FPM executa o código do aplicativo. Quando chega uma solicitação, o PHP-FPM inicializa um processo PHP filho e transmite 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, para obter um novo conjunto de dados de entrada, há apenas uma maneira: limpando a memória do processo e inicializando-o novamente.
Esse 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 totalmente isolados, e se um deles "morrer", ele será automaticamente recriado, o que não afetará os outros processos. Mas esse método também tem desvantagens, que se tornam evidentes ao tentar escalar o aplicativo.
Desvantagens e ineficiências do ambiente PHP convencional
Se você é um desenvolvedor PHP profissional, sabe por onde começar um novo projeto - pela escolha do framework. Ele consiste em bibliotecas para injeção de dependência, ORMs, traduções e templates. E, claro, todos os dados de entrada do usuário podem ser facilmente agrupados em um único objeto (Symfony/HttpFoundation ou PSR-7). Frameworks são incríveis!
Mas tudo tem seu preço. Em qualquer framework de nível empresarial, para lidar com uma simples solicitação de usuário ou chamada ao banco de dados, você terá que carregar pelo menos dezenas de arquivos, criar várias classes e analisar várias configurações. Mas o pior de tudo é que, após a execução de cada tarefa, você precisará reiniciar tudo: todo o código que você acabou de inicializar se torna inútil, e com ele você não pode mais processar outra solicitação. Diga isso a qualquer programador que escreve em outra linguagem, e você verá a confusão em seu rosto.
Engenheiros PHP buscaram maneiras de resolver esse problema por anos, utilizando metodologias pensadas de 'lazy' loading, micro frameworks, bibliotecas otimizadas, cache, etc. Mas, no final das contas, ainda é necessário reiniciar toda a aplicação e recomeçar, repetidamente. (Nota do tradutor: parte desse problema será resolvida com a chegada do no PHP 7.4)
O PHP pode, com a ajuda do Go, lidar com mais de uma solicitação?
É possível escrever scripts PHP que durem mais do que alguns minutos (até horas ou dias): por exemplo, tarefas cron, parsers de CSV, processadores de filas. Todos funcionam de maneira semelhante: extraem uma tarefa, executam-na, aguardam a próxima. O código permanece na memória, economizando preciosos milissegundos, pois carregar frameworks e aplicações exige muitas ações adicionais.
Porém, 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 não é mais possível usar o debug do F5.
A situação melhorou com o lançamento do PHP 7: um coletor de lixo confiável foi introduzido, o tratamento de erros ficou mais fácil, e as extensões do núcleo agora estão protegidas contra vazamentos. No entanto, os engenheiros ainda precisam manusear a memória com cuidado e lembrar dos problemas de estado no código (existe alguma linguagem onde isso não seja uma preocupação?). Ainda assim, no PHP 7, encontramos menos surpresas.
É possível pegar o modelo de trabalho com scripts PHP de longa duração, adaptá-lo para tarefas mais triviais como o tratamento de solicitações HTTP, e assim eliminar a necessidade de carregar tudo novamente a cada solicitação?
Para resolver essa tarefa, primeiro era necessário implementar uma aplicação servidor que pudesse aceitar solicitações HTTP e redirecioná-las uma a uma para o trabalhador PHP, sem encerrá-lo a cada vez.
Sabíamos que poderíamos criar um servidor web em PHP puro (PHP-PM) ou usando uma extensão em C (Swoole). E embora cada método tenha suas vantagens, nenhuma das opções nos satisfazia — queríamos algo mais. Precisávamos não apenas de um servidor web — esperávamos uma solução capaz de nos livrar dos problemas relacionados ao 'alto custo de inicialização' em PHP, que ao mesmo tempo pudesse ser facilmente adaptada e expandida para aplicações específicas. Ou seja, precisávamos de um servidor de aplicativos.
O Go pode ajudar nisso? Sabíamos que sim, porque essa linguagem compila aplicativos em arquivos binários únicos; é multiplataforma; usa um modelo de concorrência muito elegante e uma biblioteca para trabalho com HTTP; e, finalmente, teremos acesso a milhares de bibliotecas e integrações open-source.
Dificuldades em combinar duas linguagens de programação
Primeiro, era necessário definir como dois ou mais aplicativos se comunicariam uns com os outros.
Por exemplo, usando de Alex Paleytress, era possível implementar o compartilhamento de memória entre os processos PHP e Go (semelhante ao mod_php no Apache). Mas essa biblioteca possui características que limitam seu uso para resolver nossa tarefa.
Decidimos usar uma abordagem diferente e mais comum: construir a interação entre processos através de sockets/pipes. Essa abordagem se mostrou confiável nas últimas décadas e foi bem otimizada no nível do sistema operacional.
Primeiro, criamos um protocolo binário simples para troca de dados entre processos e tratamento de erros de transmissão. Em sua forma mais simples, um protocolo desse tipo é semelhante a com (no nosso caso, 17 bytes), que contém informações sobre o tipo de pacote, seu tamanho e uma máscara binária para verificação da integridade dos dados.
Do lado do PHP, usamos , enquanto no lado do Go — a biblioteca.
Achamos que apenas um protocolo não era suficiente — e adicionamos a possibilidade de invocar. 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 outro de nossos produtos open-source.
Distribuição de tarefas entre vários PHP workers
Após a implementação do mecanismo de interação, começamos a pensar na forma mais eficiente de passar tarefas para processos PHP. Quando uma tarefa chega, o servidor de aplicações deve selecionar um worker livre para executá-la. Se o worker/processo finalizou com erro ou ‘morreu’, nós o removemos e criamos um novo em seu lugar. Se o worker/processo foi bem-sucedido, o devolvemos ao pool de workers disponíveis para tarefas.

Para armazenar o pool de workers ativos, utilizamos, e para eliminar workers que inesperadamente ‘morreram’, adicionamos um mecanismo de monitoramento de erros e estados dos workers.
Como resultado, obtivemos um servidor PHP funcional, capaz de processar quaisquer solicitações apresentadas em formato binário.
Para que nossa aplicação começasse a funcionar como um servidor web, foi necessário escolher um padrão PHP confiável para representar quaisquer solicitações HTTP recebidas. No nosso caso, nós apenas a solicitação net/http de Go para o formato, a fim de garantir compatibilidade com a maioria dos frameworks PHP disponíveis atualmente.
Como o PSR-7 é considerado imutável (alguém diria que tecnicamente não é), os desenvolvedores precisam escrever aplicações que, em princípio, não tratam a solicitação como uma entidade global. Isso combina perfeitamente com a ideia de processos PHP de longa duração. Nossa implementação final, que ainda não recebeu um nome, foi a seguinte:

Apresentamos o RoadRunner —
Nossa primeira tarefa de teste foi um backend de API, no qual picos de solicitações surgiam periodicamente de forma imprevisível (muito mais frequentemente do que o normal). Embora nas maiores partes dos casos as capacidades do nginx fossem suficientes, frequentemente encontramos o erro 502, pois não conseguíamos balancear o sistema rapidamente o suficiente para o aumento esperado da carga.
Para substituir essa solução, no início de 2018, implementamos nosso primeiro servidor de aplicações PHP/Go. E imediatamente obtivemos um efeito incrível! Não apenas eliminamos completamente o erro 502, como também conseguimos reduzir em dois terços o número de servidores, economizando uma fortuna e evitando dores de cabeça para engenheiros e gerentes de produtos.
No meio do ano, aprimoramos nossa solução, publicamos no GitHub sob a licença MIT e a chamamos de , destacando assim sua incrível velocidade e eficiência.
Como o RoadRunner pode melhorar seu stack de desenvolvimento
Aplicação nos permitiu utilizar Middleware net/http no Go para realizar a verificação JWT antes que a solicitação chegue ao PHP, além de gerenciar WebSockets e agregar estados globalmente no Prometheus.
Com o RPC embutido, é possível abrir APIs de quaisquer bibliotecas Go para PHP sem a necessidade de escrever extensões wrapper. O que é ainda mais importante, com o RoadRunner é possível implantar novos servidores que não são baseados em HTTP. Exemplos incluem a execução de manipuladores em PHP, criação de analisadores de filas confiáveis e até mesmo a adição de em nossas aplicações.
Com as comunidades PHP e Go, aumentamos a estabilidade da solução, em alguns testes aumentamos o desempenho das aplicações em até 40 vezes, aprimoramos as ferramentas de depuração, implementamos a integração com o framework Symfony e adicionamos suporte a HTTPS, HTTP/2, plugins e PSR-17.
Conclusão
Alguns ainda estão presos à ideia ultrapassada de que PHP é uma linguagem lenta e pesada, adequada apenas para escrita de plugins para WordPress. Essas pessoas podem até dizer que o PHP tem uma limitação: quando a aplicação se torna grande o suficiente, é necessário escolher uma linguagem mais 'madura' e reescrever a base de código acumulada ao longo de muitos anos.
A isso, gostaríamos de responder: pense novamente. Acreditamos que apenas você mesmo impõe algumas limitações ao PHP. Você pode passar a vida toda alternando de uma linguagem para outra, tentando encontrar a combinação perfeita para suas necessidades, ou pode começar a ver as linguagens como ferramentas. Supostos defeitos da linguagem como o PHP podem, na verdade, ser as razões para seu sucesso. E, se você combiná-la com outra linguagem como Go, criará produtos muito mais poderosos do que se ficasse limitado ao uso de uma única linguagem.
Trabalhando com a combinação de Go e PHP, podemos afirmar que nos apaixonamos por ambas. Não planejamos sacrificar uma em favor da outra — ao contrário, estaremos em busca de maneiras de extrair ainda mais benefícios desse stack duplo.
UPD: damos as boas-vindas ao criador do RoadRunner e co-autor do artigo original —
Fonte: habr.com
