RoadRunner: PHP non está construído para morrer, nin Golang para o rescate

RoadRunner: PHP non está construído para morrer, nin Golang para o rescate

Ola Habr! Estamos activos en Badoo traballando no rendemento de PHP, xa que temos un sistema bastante grande neste idioma e o problema de rendemento é un problema de aforro. Hai máis de dez anos, creamos para iso PHP-FPM, que nun principio era un conxunto de parches para PHP, e despois entrou na distribución oficial.

Nos últimos anos, PHP fixo grandes progresos: o colector de lixo mellorou, o nivel de estabilidade aumentou; hoxe en día podes escribir daemons e scripts de longa duración en PHP sen ningún problema. Isto permitiu a Spiral Scout ir máis aló: RoadRunner, a diferenza de PHP-FPM, non limpa a memoria entre as solicitudes, o que proporciona unha ganancia de rendemento adicional (aínda que este enfoque complica o proceso de desenvolvemento). Actualmente estamos experimentando con esta ferramenta, pero aínda non temos ningún resultado que compartir. Para que agardalos sexa máis divertido, publicamos a tradución do anuncio RoadRunner de Spiral Scout.

O enfoque do artigo está preto de nós: á hora de resolver os nosos problemas, tamén usamos moitas veces PHP e Go, obtendo os beneficios de ambos os idiomas e non abandonando un en favor do outro.

Divírtete!

Nos últimos dez anos, creamos aplicacións para empresas da lista Fortune 500, e para empresas cunha audiencia de non máis de 500 usuarios. Durante todo este tempo, os nosos enxeñeiros estiveron desenvolvendo o backend principalmente en PHP. Pero hai dous anos, algo tivo un gran impacto non só no rendemento dos nosos produtos, senón tamén na súa escalabilidade: introducimos Golang (Go) na nosa pila tecnolóxica.

Case de inmediato, descubrimos que Go nos permitía crear aplicacións máis grandes con melloras de rendemento de ata 40 veces. Con el, puidemos ampliar os produtos existentes escritos en PHP, mellorándoos combinando as vantaxes de ambos os idiomas.

Contarémosche como a combinación de Go e PHP axuda a resolver problemas reais de desenvolvemento e como se converteu nunha ferramenta para nós que pode desfacernos dalgúns dos problemas asociados con Modelo moribundo PHP.

O teu entorno de desenvolvemento PHP diario

Antes de falar sobre como podes usar Go para revivir o modelo de extinción de PHP, imos dar unha ollada ao teu ambiente de desenvolvemento PHP predeterminado.

Na maioría dos casos, executa a súa aplicación usando unha combinación do servidor web nginx e o servidor PHP-FPM. O primeiro serve ficheiros estáticos e redirixe solicitudes específicas a PHP-FPM, mentres que PHP-FPM executa o código PHP. Podes estar usando a combinación menos popular de Apache e mod_php. Pero aínda que funciona un pouco diferente, os principios son os mesmos.

Vexamos como PHP-FPM executa o código da aplicación. Cando chega unha solicitude, PHP-FPM inicializa un proceso fillo PHP e pasa os detalles da solicitude como parte do seu estado (_GET, _POST, _SERVER, etc.).

O estado non pode cambiar durante a execución do script PHP, polo que só hai un xeito de obter un novo conxunto de datos de entrada: borrando a memoria do proceso e reiniciándoa.

Este modelo de execución ten moitas vantaxes. Non tes que preocuparte demasiado polo consumo de memoria, todos os procesos están completamente illados, e se un deles "morre", recrearase automaticamente e non afectará ao resto dos procesos. Pero este enfoque tamén ten desvantaxes que aparecen cando se intenta escalar a aplicación.

Desvantaxes e ineficiencias dun entorno PHP normal

Se es un programador profesional de PHP, entón sabes por onde comezar un novo proxecto - coa elección dun marco. Consta de bibliotecas de inxección de dependencias, ORM, traducións e modelos. E, por suposto, todas as entradas do usuario pódense colocar convenientemente nun mesmo obxecto (Symfony/HttpFoundation ou PSR-7). Os marcos son xeniais!

Pero todo ten o seu prezo. En calquera marco empresarial, para procesar unha simple solicitude de usuario ou acceder a unha base de datos, terás que cargar polo menos decenas de ficheiros, crear numerosas clases e analizar varias configuracións. Pero o peor é que despois de completar cada tarefa, terás que restablecer todo e comezar de novo: todo o código que acabas de iniciar vólvese inservible, coa súa axuda xa non procesarás outra solicitude. Cóntao a calquera programador que escriba noutro idioma, e verás o desconcerto no seu rostro.

Os enxeñeiros de PHP levan anos buscando formas de resolver este problema, utilizando técnicas intelixentes de carga preguiceira, microframeworks, bibliotecas optimizadas, caché, etc. Pero ao final, aínda tes que restablecer toda a aplicación e comezar de novo, unha e outra vez. . (Nota do tradutor: este problema resolverase parcialmente coa chegada de precarga en PHP 7.4)

Pode PHP con Go sobrevivir a máis dunha solicitude?

É posible escribir scripts PHP que vivan máis duns poucos minutos (ata horas ou días): por exemplo, tarefas cron, analizadores CSV, queue breakers. Todos funcionan segundo o mesmo escenario: recuperan unha tarefa, execútana e agardan á seguinte. O código reside na memoria todo o tempo, aforrando milisegundos preciosos xa que son necesarios moitos pasos adicionais para cargar o marco e a aplicación.

Pero desenvolver guións de longa duración non é doado. Calquera erro mata o proceso por completo, o diagnóstico de fugas de memoria é irritante e a depuración de F5 xa non é posible.

A situación mellorou co lanzamento de PHP 7: apareceu un colector de lixo fiable, fíxose máis fácil xestionar os erros e as extensións do núcleo agora están a proba de fugas. É certo que os enxeñeiros aínda teñen que ter coidado coa memoria e estar atentos aos problemas de estado no código (hai algunha linguaxe que poida ignorar estas cousas?). Aínda así, PHP 7 ten menos sorpresas reservadas para nós.

É posible tomar o modelo de traballar con scripts PHP de longa duración, adaptalo a tarefas máis triviais como procesar solicitudes HTTP e, así, desfacerse da necesidade de cargar todo desde cero con cada solicitude?

Para resolver este problema, primeiro necesitabamos implementar unha aplicación de servidor que puidese aceptar solicitudes HTTP e redirixilas unha a unha ao traballador PHP sen matalo cada vez.

Sabiamos que podíamos escribir un servidor web en PHP puro (PHP-PM) ou usando unha extensión C (Swoole). E aínda que cada método ten os seus propios méritos, ambas opcións non nos conviñan: queriamos algo máis. Necesitabamos algo máis que un servidor web: esperabamos conseguir unha solución que nos salvase dos problemas asociados a un "inicio difícil" en PHP, que ao mesmo tempo podería adaptarse e ampliarse facilmente para aplicacións específicas. É dicir, necesitabamos un servidor de aplicacións.

Go pode axudar con isto? Sabiamos que podería porque a linguaxe compila aplicacións en binarios únicos; é multiplataforma; usa o seu propio modelo de procesamento paralelo, moi elegante (concurrencia) e unha biblioteca para traballar con HTTP; e, finalmente, miles de bibliotecas e integracións de código aberto estarán dispoñibles para nós.

As dificultades de combinar dúas linguaxes de programación

En primeiro lugar, foi necesario determinar como se comunicarán dúas ou máis aplicacións entre si.

Por exemplo, usando excelente biblioteca Alex Palaestras, foi posible compartir memoria entre procesos PHP e Go (semellante a mod_php en Apache). Pero esta biblioteca ten características que limitan o seu uso para resolver o noso problema.

Decidimos utilizar un enfoque diferente e máis común: construír a interacción entre procesos a través de sockets / canalizacións. Este enfoque demostrou ser fiable nas últimas décadas e optimizouse ben a nivel do sistema operativo.

Para comezar, creamos un protocolo binario sinxelo para intercambiar datos entre procesos e xestionar erros de transmisión. Na súa forma máis sinxela, este tipo de protocolo é semellante a cadea de rede с cabeceira de paquete de tamaño fixo (no noso caso 17 bytes), que contén información sobre o tipo de paquete, o seu tamaño e unha máscara binaria para comprobar a integridade dos datos.

No lado de PHP usamos función de paquete, e no lado Go, a biblioteca codificación/binario.

Pareceunos que un protocolo non era suficiente, e engadimos a posibilidade de chamar net/rpc go servizos directamente desde PHP. Máis tarde, isto axudounos moito no desenvolvemento, xa que puidemos integrar facilmente as bibliotecas Go en aplicacións PHP. O resultado deste traballo pódese ver, por exemplo, no noso outro produto de código aberto Goridge.

Distribución de tarefas entre varios traballadores PHP

Despois de implementar o mecanismo de interacción, comezamos a pensar na forma máis eficiente de transferir tarefas aos procesos PHP. Cando chega unha tarefa, o servidor de aplicacións debe escoller un traballador libre para executala. Se un traballador/proceso sae cun erro ou "morre", desfarámonos del e creamos un novo para substituílo. E se o traballador/proceso completouse con éxito, devolvémolo ao grupo de traballadores dispoñibles para realizar tarefas.

RoadRunner: PHP non está construído para morrer, nin Golang para o rescate

Para almacenar o grupo de traballadores activos, utilizamos canle almacenado en búfer, para eliminar traballadores "mortos" inesperadamente do grupo, engadimos un mecanismo para rastrexar erros e estados dos traballadores.

Como resultado, obtivemos un servidor PHP capaz de procesar calquera solicitude presentada en forma binaria.

Para que a nosa aplicación comezase a funcionar como servidor web, tivemos que escoller un estándar PHP fiable para representar as solicitudes HTTP entrantes. No noso caso, só transformar net/http solicitude de Ir ao formato PSR-7para que sexa compatible coa maioría dos frameworks PHP dispoñibles na actualidade.

Dado que PSR-7 considérase inmutable (algúns dirían que tecnicamente non o é), os desenvolvedores teñen que escribir aplicacións que non traten a solicitude como unha entidade global en principio. Isto encaixa moi ben co concepto de procesos PHP de longa duración. A nosa implementación final, que aínda non foi nomeada, quedou así:

RoadRunner: PHP non está construído para morrer, nin Golang para o rescate

Presentación de RoadRunner - servidor de aplicacións PHP de alto rendemento

A nosa primeira tarefa de proba foi un backend de API, que periódicamente irrompe en solicitudes imprevisibles (moito máis frecuente do habitual). Aínda que nginx foi suficiente na maioría dos casos, atopámonos regularmente con erros 502 porque non puidemos equilibrar o sistema o suficientemente rápido para o aumento esperado da carga.

Para substituír esta solución, implantamos o noso primeiro servidor de aplicacións PHP/Go a principios de 2018. E inmediatamente obtivo un efecto incrible! Non só eliminamos completamente o erro 502, senón que puidemos reducir o número de servidores en dous terzos, aforrando moito diñeiro e pílulas de dor de cabeza para enxeñeiros e xestores de produtos.

A mediados do ano, melloramos a nosa solución, publicámola en GitHub baixo a licenza MIT e puxémola nome Roadrunner, destacando así a súa incrible velocidade e eficiencia.

Como RoadRunner pode mellorar a túa pila de desenvolvemento

Aplicación Roadrunner permitiunos usar Middleware net/http no lado Ir para realizar a verificación JWT antes de que a solicitude chegue a PHP, así como xestionar WebSockets e o estado agregado globalmente en Prometheus.

Grazas ao RPC integrado, pode abrir a API de calquera biblioteca Go para PHP sen escribir envoltorios de extensións. Máis importante aínda, con RoadRunner pode implantar novos servidores non HTTP. Os exemplos inclúen a execución de controladores en PHP AWS Lambda, creando separadores de colas fiables e incluso engadindo gRPC ás nosas aplicacións.

Coa axuda das comunidades PHP e Go, melloramos a estabilidade da solución, aumentamos o rendemento das aplicacións ata 40 veces nalgunhas probas, melloramos as ferramentas de depuración, implementamos a integración co framework Symfony e engadimos soporte para HTTPS, HTTP/2, complementos e PSR-17.

Conclusión

Algunhas persoas aínda están atrapadas na noción obsoleta de PHP como unha linguaxe lenta e difícil de manexar que só é boa para escribir complementos para WordPress. Estas persoas incluso poden dicir que PHP ten tal limitación: cando a aplicación se fai o suficientemente grande, tes que escoller unha linguaxe máis "madura" e reescribir o código base acumulado durante moitos anos.

A todo isto quero responder: pensa de novo. Cremos que só vostede establece restricións para PHP. Podes pasar a túa vida enteira facendo a transición dunha lingua a outra, intentando atopar a combinación perfecta para as túas necesidades, ou podes comezar a pensar nas linguas como ferramentas. Os supostos fallos dunha linguaxe como PHP poden ser realmente a razón do seu éxito. E se o combinas con outro idioma como Go, crearás produtos moito máis potentes que se estiveses limitado a usar calquera idioma.

Despois de traballar con Go e PHP, podemos dicir que nos encantan. Non pensamos sacrificar un polo outro; pola contra, buscaremos formas de obter aínda máis valor desta pila dual.

UPD: damos a benvida ao creador de RoadRunner e ao coautor do artigo orixinal - Lachesis

Fonte: www.habr.com

Engadir un comentario