RoadRunner: PHP no está hecho para morir, o Golang al rescate

RoadRunner: PHP no está hecho para morir, o Golang al rescate

¡Hola, Habr! Estamos activos en Badoo trabajando en el rendimiento de PHP, ya que tenemos un sistema bastante grande en este lenguaje y el tema del rendimiento es un tema de ahorro de dinero. Hace más de diez años, creamos PHP-FPM para esto, que en un principio era un conjunto de parches para PHP, y luego entró en la distribución oficial.

En los últimos años, PHP ha progresado mucho: el recolector de basura ha mejorado, el nivel de estabilidad ha aumentado; hoy en día, puede escribir demonios y scripts de larga duración en PHP sin ningún problema. Esto permitió a Spiral Scout ir más allá: RoadRunner, a diferencia de PHP-FPM, no limpia la memoria entre solicitudes, lo que brinda una ganancia de rendimiento adicional (aunque este enfoque complica el proceso de desarrollo). Actualmente estamos experimentando con esta herramienta, pero aún no tenemos ningún resultado para compartir. Para que esperarlos sea más divertido, publicamos la traducción del anuncio de RoadRunner de Spiral Scout.

El enfoque del artículo está cerca de nosotros: al resolver nuestros problemas, también usamos con mayor frecuencia un montón de PHP y Go, obteniendo los beneficios de ambos lenguajes y no abandonando uno a favor del otro.

¡Disfruta!

En los últimos diez años, hemos creado aplicaciones para empresas de la lista Fortune 500, y para empresas con una audiencia de no más de 500 usuarios. Durante todo este tiempo, nuestros ingenieros han estado desarrollando el backend principalmente en PHP. Pero hace dos años, algo tuvo un gran impacto no solo en el rendimiento de nuestros productos, sino también en su escalabilidad: introdujimos Golang (Go) en nuestra pila de tecnología.

Casi de inmediato, descubrimos que Go nos permitía crear aplicaciones más grandes con mejoras de rendimiento de hasta 40 veces. Con él pudimos ampliar productos existentes escritos en PHP, mejorándolos al combinar las ventajas de ambos lenguajes.

Le diremos cómo la combinación de Go y PHP ayuda a resolver problemas reales de desarrollo y cómo se ha convertido en una herramienta para nosotros que puede eliminar algunos de los problemas asociados con Modelo moribundo de PHP.

Tu entorno de desarrollo PHP diario

Antes de hablar sobre cómo puede usar Go para revivir el modelo de extinción de PHP, echemos un vistazo a su entorno de desarrollo de PHP predeterminado.

En la mayoría de los casos, ejecuta su aplicación usando una combinación del servidor web nginx y el servidor PHP-FPM. El primero sirve archivos estáticos y redirige solicitudes específicas a PHP-FPM, mientras que el propio PHP-FPM ejecuta código PHP. Es posible que esté utilizando la combinación menos popular de Apache y mod_php. Pero aunque funciona un poco diferente, los principios son los mismos.

Echemos un vistazo a cómo PHP-FPM ejecuta el código de la aplicación. Cuando llega una solicitud, PHP-FPM inicializa un proceso secundario de PHP y pasa los detalles de la solicitud como parte de su estado (_GET, _POST, _SERVER, etc.).

El estado no puede cambiar durante la ejecución del script PHP, por lo que solo hay una forma de obtener un nuevo conjunto de datos de entrada: borrando la memoria del proceso y reiniciándola.

Este modelo de ejecución tiene muchas ventajas. No tienes que preocuparte demasiado por el consumo de memoria, todos los procesos están completamente aislados, y si uno de ellos "muere", se recreará automáticamente y no afectará al resto de procesos. Pero este enfoque también tiene desventajas que aparecen al intentar escalar la aplicación.

Desventajas e ineficiencias del entorno PHP regular

Si es un desarrollador profesional de PHP, entonces sabe dónde comenzar un nuevo proyecto, con la elección de un marco. Consiste en bibliotecas de inyección de dependencia, ORM, traducciones y plantillas. Y, por supuesto, todas las entradas del usuario se pueden poner cómodamente en un objeto (Symfony/HttpFoundation o PSR-7). ¡Los marcos son geniales!

Pero todo tiene su precio. En cualquier marco de nivel empresarial, para procesar una solicitud de usuario simple o acceder a una base de datos, deberá cargar al menos docenas de archivos, crear numerosas clases y analizar varias configuraciones. Pero lo peor es que después de completar cada tarea, deberá reiniciar todo y comenzar de nuevo: todo el código que acaba de iniciar se vuelve inútil, con su ayuda ya no procesará otra solicitud. Dile esto a cualquier programador que escriba en algún otro lenguaje, y verás desconcierto en su rostro.

Los ingenieros de PHP han estado buscando formas de resolver este problema durante años, utilizando técnicas inteligentes de carga diferida, microframeworks, bibliotecas optimizadas, caché, etc. Pero al final, aún debe restablecer toda la aplicación y comenzar de nuevo, una y otra vez. . (Nota del traductor: este problema se resolverá parcialmente con la llegada de precarga en PHP 7.4)

¿Puede PHP con Go sobrevivir a más de una solicitud?

Es posible escribir secuencias de comandos PHP que duren más de unos pocos minutos (hasta horas o días): por ejemplo, tareas cron, analizadores CSV, interruptores de cola. Todos trabajan según el mismo escenario: recuperan una tarea, la ejecutan y esperan la siguiente. El código reside en la memoria todo el tiempo, ahorrando preciosos milisegundos ya que se requieren muchos pasos adicionales para cargar el marco y la aplicación.

Pero desarrollar scripts duraderos no es fácil. Cualquier error mata por completo el proceso, el diagnóstico de fugas de memoria es exasperante y la depuración F5 ya no es posible.

La situación ha mejorado con el lanzamiento de PHP 7: ha aparecido un recolector de basura confiable, se ha vuelto más fácil manejar los errores y las extensiones del kernel ahora son a prueba de fugas. Es cierto que los ingenieros aún deben tener cuidado con la memoria y estar al tanto de los problemas de estado en el código (¿hay algún lenguaje que pueda ignorar estas cosas?). Aún así, PHP 7 tiene menos sorpresas reservadas para nosotros.

¿Es posible tomar el modelo de trabajar con scripts PHP de larga duración, adaptarlo a tareas más triviales como procesar solicitudes HTTP y, por lo tanto, deshacerse de la necesidad de cargar todo desde cero con cada solicitud?

Para resolver este problema, primero necesitábamos implementar una aplicación de servidor que pudiera aceptar solicitudes HTTP y redirigirlas una por una al trabajador de PHP sin matarlo cada vez.

Sabíamos que podíamos escribir un servidor web en PHP puro (PHP-PM) o usando una extensión C (Swoole). Y aunque cada método tiene sus propios méritos, ambas opciones no nos convenían, queríamos algo más. Necesitábamos algo más que un servidor web: esperábamos obtener una solución que pudiera salvarnos de los problemas asociados con un "comienzo difícil" en PHP, que al mismo tiempo pudiera adaptarse y extenderse fácilmente para aplicaciones específicas. Es decir, necesitábamos un servidor de aplicaciones.

¿Go puede ayudar con esto? Sabíamos que podía porque el lenguaje compila aplicaciones en archivos binarios únicos; es multiplataforma; utiliza su propio modelo de procesamiento paralelo muy elegante (concurrencia) y una biblioteca para trabajar con HTTP; y finalmente, miles de bibliotecas e integraciones de código abierto estarán disponibles para nosotros.

Las dificultades de combinar dos lenguajes de programación

En primer lugar, era necesario determinar cómo dos o más aplicaciones se comunicarán entre sí.

Por ejemplo, usando excelente biblioteca Alex Palaestras, fue posible compartir memoria entre procesos PHP y Go (similar a mod_php en Apache). Pero esta biblioteca tiene características que limitan su uso para resolver nuestro problema.

Decidimos utilizar un enfoque diferente y más común: generar interacción entre procesos a través de sockets/tuberías. Este enfoque ha demostrado ser confiable durante las últimas décadas y ha sido bien optimizado a nivel del sistema operativo.

Para empezar, creamos un protocolo binario simple para intercambiar datos entre procesos y manejar errores de transmisión. En su forma más simple, este tipo de protocolo es similar a cadena de red с encabezado de paquete de tamaño fijo (en nuestro caso 17 bytes), que contiene información sobre el tipo de paquete, su tamaño y una máscara binaria para comprobar la integridad de los datos.

En el lado de PHP usamos función de paquete, y en el lado Go, la biblioteca codificación / binario.

Nos pareció que un protocolo no era suficiente, y agregamos la capacidad de llamar servicios net/rpc go directamente desde PHP. Más tarde, esto nos ayudó mucho en el desarrollo, ya que podíamos integrar fácilmente las bibliotecas Go en las aplicaciones PHP. El resultado de este trabajo se puede ver, por ejemplo, en nuestro otro producto de código abierto gorila.

Distribuir tareas entre varios trabajadores de PHP

Después de implementar el mecanismo de interacción, comenzamos a pensar en la forma más eficiente de transferir tareas a procesos PHP. Cuando llega una tarea, el servidor de aplicaciones debe elegir un trabajador libre para ejecutarla. Si un trabajador/proceso sale con un error o "muere", lo eliminamos y creamos uno nuevo para reemplazarlo. Y si el trabajador/proceso se completó con éxito, lo devolvemos al grupo de trabajadores disponibles para realizar tareas.

RoadRunner: PHP no está hecho para morir, o Golang al rescate

Para almacenar el grupo de trabajadores activos, usamos canal almacenado en búfer, para eliminar trabajadores inesperadamente "muertos" del grupo, agregamos un mecanismo para rastrear errores y estados de los trabajadores.

Como resultado, obtuvimos un servidor PHP en funcionamiento capaz de procesar cualquier solicitud presentada en forma binaria.

Para que nuestra aplicación comenzara a funcionar como un servidor web, tuvimos que elegir un estándar PHP confiable para representar cualquier solicitud HTTP entrante. En nuestro caso, solo transformar solicitud net/http de Ir a formato PSR-7para que sea compatible con la mayoría de los marcos PHP disponibles en la actualidad.

Debido a que PSR-7 se considera inmutable (algunos dirían que técnicamente no lo es), los desarrolladores deben escribir aplicaciones que, en principio, no traten la solicitud como una entidad global. Esto encaja muy bien con el concepto de procesos PHP de larga duración. Nuestra implementación final, que aún no tiene nombre, se veía así:

RoadRunner: PHP no está hecho para morir, o Golang al rescate

Presentamos RoadRunner - servidor de aplicaciones PHP de alto rendimiento

Nuestra primera tarea de prueba fue un backend de API, que se dispara periódicamente de forma impredecible (mucho más a menudo de lo habitual). Aunque nginx fue suficiente en la mayoría de los casos, encontramos errores 502 con regularidad porque no pudimos equilibrar el sistema lo suficientemente rápido para el aumento de carga esperado.

Para reemplazar esta solución, implementamos nuestro primer servidor de aplicaciones PHP/Go a principios de 2018. ¡E inmediatamente obtuve un efecto increíble! No solo nos deshicimos del error 502 por completo, sino que pudimos reducir la cantidad de servidores en dos tercios, ahorrando mucho dinero y pastillas para el dolor de cabeza para ingenieros y gerentes de producto.

A mediados de año, habíamos mejorado nuestra solución, la publicamos en GitHub bajo la licencia MIT y la llamamos Avance, destacando así su increíble rapidez y eficacia.

Cómo RoadRunner puede mejorar su pila de desarrollo

solicitud Avance nos permitió usar Middleware net/http en el lado de Go para realizar la verificación de JWT antes de que la solicitud llegue a PHP, así como manejar WebSockets y el estado agregado globalmente en Prometheus.

Gracias al RPC incorporado, puede abrir la API de cualquier biblioteca de Go para PHP sin escribir envoltorios de extensión. Más importante aún, con RoadRunner puede implementar nuevos servidores que no sean HTTP. Los ejemplos incluyen la ejecución de controladores en PHP AWS Lambda, creando interruptores de cola confiables e incluso agregando gRPC a nuestras aplicaciones.

Con la ayuda de las comunidades de PHP y Go, mejoramos la estabilidad de la solución, aumentamos el rendimiento de la aplicación hasta 40 veces en algunas pruebas, mejoramos las herramientas de depuración, implementamos la integración con el marco Symfony y agregamos soporte para HTTPS, HTTP/2, complementos y PSR-17.

Conclusión

Algunas personas todavía están atrapadas en la noción anticuada de PHP como un lenguaje lento y difícil de manejar que solo es bueno para escribir complementos para WordPress. Estas personas podrían incluso decir que PHP tiene tal limitación: cuando la aplicación crece lo suficiente, debe elegir un lenguaje más "maduro" y reescribir la base de código acumulada durante muchos años.

A todo esto quiero responder: piénsalo de nuevo. Creemos que solo usted establece restricciones para PHP. Puede pasar toda su vida haciendo la transición de un idioma a otro, tratando de encontrar la combinación perfecta para sus necesidades, o puede comenzar a pensar en los idiomas como herramientas. Los supuestos defectos de un lenguaje como PHP en realidad pueden ser la razón de su éxito. Y si lo combina con otro idioma como Go, creará productos mucho más potentes que si estuviera limitado a usar un solo idioma.

Habiendo trabajado con un montón de Go y PHP, podemos decir que nos encantan. No planeamos sacrificar uno por el otro; al contrario, buscaremos formas de obtener aún más valor de esta doble pila.

UPD: damos la bienvenida al creador de RoadRunner y coautor del artículo original - Láquesis

Fuente: habr.com

Añadir un comentario