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

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

¡Hola, Habr! En Badoo estamos trabajando activamente en el rendimiento de PHP, ya que tenemos un sistema bastante grande en este lenguaje y la cuestión del rendimiento es una cuestión de ahorro de dinero. Hace más de diez años creamos PHP-FPM para ello, que al principio consistía en un conjunto de parches para PHP y más tarde se incluyó en la entrega oficial.

En los últimos años, PHP ha avanzado considerablemente: el recolector de basura ha mejorado, se ha elevado el nivel de estabilidad: hoy en día es posible escribir demonios y scripts de larga duración en PHP sin mayores problemas. Esto ha permitido a Spiral Scout ir más allá: RoadRunner, a diferencia de PHP-FPM, no limpia la memoria entre solicitudes, lo que proporciona una ganancia adicional en rendimiento (aunque este enfoque complica el proceso de desarrollo). Actualmente estamos experimentando con esta herramienta, pero aún no tenemos resultados que compartir. Para hacer la espera más amena, publicamos la traducción del anuncio de RoadRunner de Spiral Scout.

El enfoque del artículo nos resulta cercano: al abordar nuestras tareas, también utilizamos con frecuencia la combinación de PHP y Go, obteniendo ventajas de ambos lenguajes y sin renunciar a uno en favor del otro.

¡Disfruta!

En los últimos diez años hemos creado aplicaciones tanto para empresas de la lista Fortune 500, como para negocios con una audiencia de no más de 500 usuarios. Durante todo este tiempo, nuestros ingenieros han desarrollado el backend principalmente en PHP. Pero hace dos años, algo afectó no solo el rendimiento de nuestros productos, sino también su escalabilidad: incorporamos Golang (Go) a nuestro stack tecnológico.

Casi de inmediato descubrimos que Go nos permite crear aplicaciones más grandes con un aumento de rendimiento de hasta 40 veces. Con él, pudimos expandir los productos existentes desarrollados en PHP, mejorándolos gracias a la combinación de las ventajas de ambos lenguajes.

Contaremos 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 capaz de aliviar parte de los problemas asociados con el modelo de 'muerte' de PHP.

Su entorno cotidiano de desarrollo en PHP

Antes de hablar sobre cómo Go puede revitalizar el modelo de 'muerte' de PHP, veamos su entorno estándar de desarrollo en PHP.

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

Veamos cómo PHP-FPM ejecuta el código de la aplicación. Cuando llega una solicitud, PHP-FPM inicializa un proceso hijo 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 puedes obtener un nuevo conjunto de datos de entrada de una manera: limpiando la memoria del proceso e inicializándolo de nuevo.

Este modelo de ejecución tiene muchas ventajas. No necesitas preocuparte demasiado por el consumo de memoria, todos los procesos están completamente aislados, y si uno de ellos 'muere', se recreará automáticamente sin afectar a los demás procesos. Sin embargo, este enfoque también tiene desventajas que se hacen evidentes al intentar escalar la aplicación.

Desventajas e ineficiencia del entorno PHP común

Si te dedicas al desarrollo profesional en PHP, sabes que lo primero que debes hacer al iniciar un nuevo proyecto es elegir un marco. Este consiste en bibliotecas para inyección de dependencias, ORM, traducciones y plantillas. Y, por supuesto, todos los datos de entrada del usuario se pueden convenientemente colocar en un solo objeto (Symfony/HttpFoundation o PSR-7). ¡Los frameworks son geniales!

Pero todo tiene su precio. En cualquier marco de nivel empresarial, para procesar una simple solicitud de usuario o realizar una consulta a la base de datos tendrás que cargar, como mínimo, decenas de archivos, crear numerosas clases y analizar múltiples configuraciones. Pero lo peor es que tras cada tarea completada tendrás que reiniciar todo: todo el código que acabas de inicializar se vuelve inútil, ya no podrás procesar otra solicitud con él. Dile esto a cualquier programador que utilice otro lenguaje, y verás la incredulidad en su rostro.

Los ingenieros de PHP han estado buscando durante años formas de solucionar este problema, utilizando técnicas bien pensadas de "carga perezosa", microframeworks, bibliotecas optimizadas, caché, etc. Pero, al final, siempre terminan teniendo que reiniciar toda la aplicación y comenzar de nuevo, una y otra vez. (Nota del traductor: parte de este problema se resolverá con la llegada de preload PHP 7.4)

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

Se pueden escribir scripts de PHP que duren más que unos pocos minutos (hasta horas o días): por ejemplo, tareas cron, analizadores de CSV, procesadores de colas. Todos ellos funcionan siguiendo un único patrón: extraen una tarea, la realizan, y esperan la siguiente. El código permanece constantemente en memoria, ahorrando preciosos milisegundos, ya que se requiere realizar muchas acciones adicionales para cargar el framework y la aplicación.

Pero desarrollar scripts de larga duración no es tan sencillo. Cualquier error destruye completamente el proceso, diagnosticar fugas de memoria vuelve loco, y ya no se puede usar la depuración con F5.

La situación ha mejorado con el lanzamiento de PHP 7: ahora hay un recolector de basura confiable, es más fácil manejar errores, y las extensiones del núcleo están protegidas contra fugas. Sin embargo, los ingenieros aún deben tener cuidado con la memoria y estar atentos a los problemas de estado en el código (¿acaso existe un lenguaje en el que no haya que prestar atención a estas cosas?). Aún así, en PHP 7 nos esperan menos sorpresas.

¿Se puede tomar el modelo de trabajo con scripts de PHP de larga duración, adaptarlo a tareas más triviales como el procesamiento de solicitudes HTTP y así eliminar la necesidad de cargar todo desde cero en cada solicitud?

Para resolver esta tarea, primero era necesario implementar una aplicación del lado del servidor capaz de recibir solicitudes HTTP y redirigirlas una por una a un trabajador de PHP, sin matarlo cada vez.

Sabíamos que podríamos escribir un servidor web en PHP puro (PHP-PM) o utilizando una extensión en C (Swoole). Y aunque cada método tiene sus ventajas, ninguno de los dos nos satisfacía; queríamos algo más. Necesitábamos no solo un servidor web, sino una solución capaz de liberarnos de los problemas asociados con el 'arranque pesado' en PHP, que además se pudiera adaptar y ampliar fácilmente para aplicaciones específicas. Es decir, necesitábamos un servidor de aplicaciones.

¿Puede Go ayudar en esto? Sabíamos que sí, porque este lenguaje compila aplicaciones en archivos binarios individuales; es multiplataforma; utiliza su propio y elegante modelo de concurrencia y una biblioteca para trabajar con HTTP; y, por último, tendríamos acceso a miles de bibliotecas e integraciones de código abierto.

Dificultades de integrar dos lenguajes de programación

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

Por ejemplo, a través de una excelente biblioteca de Alex Palastras que permitía la memoria compartida entre procesos PHP y Go (similar a mod_php en Apache). Pero esta biblioteca tiene particularidades que limitan su uso para resolver nuestra tarea.

Decidimos utilizar otro enfoque, más común: construir la interacción entre procesos a través de sockets/pipelines. Este enfoque ha demostrado su fiabilidad en las últimas décadas y ha sido bien optimizado a nivel del sistema operativo.

Para comenzar, creamos un protocolo binario simple para el intercambio de datos entre procesos y el manejo de errores de transmisión. En su forma más simple, un protocolo de este tipo se asemeja a netstring con con una cabecera 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 verificar la integridad de los datos.

En la parte de PHP utilizamos la función pack, y en la parte de Go, la biblioteca encoding/binary.

Un solo protocolo nos pareció insuficiente, y añadimos la capacidad de llamar a servicios Go net/rpc directamente desde PHP. Más adelante, esto nos ayudó mucho en el desarrollo, ya que podíamos integrar fácilmente bibliotecas de Go en aplicaciones PHP. El resultado de este trabajo se puede ver, por ejemplo, en otro de nuestros productos de código abierto Goridge.

Distribución de tareas entre varios trabajadores PHP

Después de implementar el mecanismo de interacción, comenzamos a pensar en cómo transmitir tareas a los procesos PHP de la manera más eficiente. Cuando llega una tarea, el servidor de aplicaciones debe seleccionar un trabajador libre para su ejecución. Si el trabajador/proceso ha terminado con un error o "ha fallado", lo eliminamos y creamos uno nuevo en su lugar. Pero si el trabajador/proceso ha completado su trabajo con éxito, lo devolvemos al grupo de trabajadores disponibles para realizar tareas.

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

Para almacenar el grupo de trabajadores activos utilizamos canal bufferizado, para eliminar de el grupo a los trabajadores "muertos" inesperadamente, añadimos un mecanismo de seguimiento de errores y estados de los trabajadores.

Como resultado, obtuvimos un servidor PHP operativo capaz de manejar cualquier solicitud presentada en formato binario.

Para que nuestra aplicación comenzara a funcionar como servidor web, tuvimos que elegir un estándar PHP confiable para representar cualquier solicitud HTTP entrante. En nuestro caso, simplemente convertimos una solicitud net/http de Go en el formato PSR-7, para que fuera compatible con la mayoría de los frameworks PHP disponibles hoy en día.

Dado que PSR-7 se considera inmutable (alguien podría decir que técnicamente no es así), los desarrolladores deben escribir aplicaciones que, en principio, no manejan la solicitud como una entidad global. Esto combina muy bien con el concepto de procesos PHP de larga duración. Nuestra implementación final, que aún no ha recibido un nombre, se veía así:

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

Presentamos a RoadRunner — un servidor de aplicaciones PHP de alto rendimiento

Nuestra primera tarea de prueba fue un backend API, en el que surgieron picos de solicitudes de forma impredecible (mucho más frecuentemente de lo habitual). Aunque en la mayoría de los casos las capacidades de nginx eran suficientes, nos enfrentamos regularmente al error 502, porque no podíamos equilibrar el sistema lo suficientemente rápido ante el aumento esperado de carga.

Para reemplazar esta solución, a principios de 2018 desplegamos nuestro primer servidor de aplicaciones PHP/Go. ¡Y obtuvimos un efecto increíble! No solo eliminamos completamente el error 502, sino que también logramos reducir el número de servidores en dos tercios, ahorrando un montón de dinero y evitando dolores de cabeza para los ingenieros y gerentes de producto.

A mediados de año, mejoramos nuestra solución, la publicamos en GitHub bajo la licencia MIT y la llamamos RoadRunner, destacando así su increíble velocidad y eficiencia.

Cómo RoadRunner puede mejorar tu pila de desarrollo

Aplicación RoadRunner nos permitió utilizar Middleware net/http en el lado de Go para realizar la verificación JWT incluso antes de que la solicitud llegue a PHP, así como para manejar WebSockets y agregar estados globalmente en Prometheus.

Gracias a la RPC incorporada, es posible abrir la API de cualquier biblioteca de Go para PHP sin necesidad de escribir extensiones envolventes. Lo que es aún más importante, con RoadRunner se pueden desplegar nuevos servidores que no son solo HTTP. Ejemplos incluyen lanzar en PHP controladores AWS Lambda, crear parseadores de colas confiables e incluso agregar gRPC a nuestras aplicaciones.

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

Conclusión

Algunos todavía están atrapados en la antigua noción de PHP como un lenguaje lento y voluminoso, adecuado únicamente para escribir complementos para WordPress. Estas personas incluso pueden decir que PHP tiene una limitación: cuando una aplicación se vuelve lo suficientemente grande, hay que elegir un lenguaje más 'maduro' y reescribir la base de código adquirida durante muchos años.

Todo esto merece una respuesta: piénsalo de nuevo. Creemos que solo tú mismo estableces limitaciones para PHP. Puedes pasar toda tu vida cambiando de un lenguaje a otro, tratando de encontrar la combinación perfecta para tus necesidades, o puedes comenzar a ver los lenguajes como herramientas. Las supuestas desventajas de un lenguaje como PHP en realidad pueden ser la razón de su éxito. Y si lo combinamos con otro lenguaje como Go, crearás productos mucho más poderosos que si te limitaras a usar solo un lenguaje.

Trabajando con la combinación de Go y PHP, podemos afirmar que nos hemos enamorado de ellos. No planeamos sacrificar uno en favor del otro; al contrario, buscaremos formas de extraer aún más beneficios de esta doble pila.

UPD: damos la bienvenida al creador de RoadRunner y coautor del artículo original — Lachezis

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster