RoadRunner: PHP no está hecho para morir, o Golang 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 ahorrar dinero. Hace más de diez años, creamos para este PHP-FPM, que primero fue un conjunto de parches para PHP, y luego entró en la entrega oficial.

En los últimos años, PHP ha avanzado mucho: el recolector de basura ha mejorado, el nivel de estabilidad ha mejorado: hoy en PHP puede escribir demonios y scripts de larga duración sin ningún problema especial. Esto permitió a Spiral Scout ir más allá: RoadRunner, a diferencia de PHP-FPM, no borra la memoria entre solicitudes, lo que proporciona una ganancia de rendimiento adicional (aunque este enfoque complica el proceso de desarrollo). Ahora estamos experimentando con esta herramienta, pero aún no tenemos resultados que puedan compartirse. Esperarlos fue más divertido, publicamos la traducción del anuncio del RoadRunner de Spiral Scout.

El enfoque del artículo es cercano a nosotros: al resolver nuestros problemas, también usamos a menudo un montón de PHP y Go, obteniendo ventajas de ambos lenguajes y no abandonamos uno en favor del otro.

¡A disfrutar!



En los últimos diez años, hemos creado aplicaciones para compañías de Fortune 500 y para negocios con una audiencia de no más de 500 usuarios. Todo este tiempo, nuestros ingenieros desarrollaron el back-end principalmente en PHP. Pero hace dos años, algo influyó enormemente no solo en el rendimiento de nuestros productos, sino también en su escalabilidad: presentamos Golang (Go) en nuestra pila de tecnología.

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

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

Su entorno de desarrollo PHP diario


Antes de hablar sobre cómo Go puede animar el modelo "moribundo" de PHP, veamos su entorno de desarrollo PHP estándar.

En la mayoría de los casos, inicia la aplicación utilizando 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, y el propio PHP-FPM ejecuta el código PHP. Quizás esté utilizando un paquete menos popular de Apache y mod_php. Pero aunque funciona un poco diferente, los principios son los mismos.

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

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

Desventajas e ineficiencias de un entorno PHP normal


Si está involucrado en el desarrollo profesional en PHP, entonces sabe dónde comenzar un nuevo proyecto, con la elección de un marco. Es una biblioteca para inyección de dependencias, ORM, traducciones y plantillas. Y, por supuesto, todas las entradas del usuario se pueden colocar convenientemente en un objeto (Symfony / HttpFoundation o PSR-7). Los marcos son geniales!

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

Durante años, los ingenieros de PHP han estado buscando formas de resolver este problema, utilizando métodos bien pensados ​​de carga diferida, microframes, bibliotecas optimizadas, caché, etc. Pero al final, aún tiene que restablecer toda la aplicación y comenzar una y otra vez. (Nota del traductor: este problema se resolverá parcialmente con la llegada de la precarga en PHP 7.4)

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


Puede escribir scripts PHP que durarán más de unos pocos minutos (hasta horas o días): por ejemplo, tareas cron, analizadores CSV, interruptores de cola. Todos funcionan de acuerdo con un escenario: extraen la tarea, la completan, esperan la siguiente. El código está constantemente en la memoria, ahorrando preciosos milisegundos, ya que se requieren muchos pasos adicionales para descargar el marco y la aplicación.

Pero desarrollar scripts de larga duración no es tan simple. Cualquier error mata por completo el proceso, el diagnóstico de pérdidas de memoria es irritante y ya no es posible la depuración con F5.

La situación mejoró con el lanzamiento de PHP 7: apareció un recolector de basura confiable, se hizo más fácil manejar los errores y las extensiones del núcleo ahora están protegidas contra fugas. Es cierto que los ingenieros aún necesitan manejar con cuidado la memoria y recordar los problemas de estado en el código (¿hay algún idioma en el que pueda ignorar estas cosas?). Y, sin embargo, en PHP 7, hay menos sorpresas.

¿Es posible tomar un modelo para trabajar con scripts PHP de larga duración, adaptarlo para tareas más triviales como procesar solicitudes HTTP y así eliminar la necesidad de descargar todo desde cero con cada solicitud?

Para resolver este problema, primero fue necesario implementar una aplicación de servidor capaz de aceptar solicitudes HTTP y redirigirlas una por una al trabajador PHP, sin matarlo cada vez.

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

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

Dificultades para combinar dos lenguajes de programación.


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

Por ejemplo, con la ayuda de la excelente biblioteca de Alex Palaestras, fue posible implementar el intercambio de memoria mediante los 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 usar un enfoque diferente y más común: crear interacción entre procesos a través de sockets / tuberías. Este enfoque en las últimas décadas ha demostrado ser confiable y ha sido optimizado a nivel de 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, un protocolo de este tipo es similar a la cadena de red con un 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 verificar la integridad de los datos.

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

Un protocolo no fue suficiente para nosotros, y agregamos la capacidad de llamar a net / rpc Go-services directamente desde PHP . Más tarde, nos ayudó mucho en el desarrollo, ya que pudimos integrar fácilmente las bibliotecas Go en aplicaciones PHP. El resultado de este trabajo se puede ver, por ejemplo, en nuestro otro producto 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 transferir mejor las tareas a los procesos de PHP. Cuando llega una tarea, el servidor de aplicaciones debe elegir un trabajador libre para completarla. Si el trabajador / proceso finalizó con un error o "murió", lo eliminamos y creamos uno nuevo a cambio. Y si el trabajador / proceso funcionó con éxito, lo devolvemos al grupo de trabajadores disponibles para completar las tareas.



Utilizamos un canal protegido para almacenar el grupo de trabajadores activos; para eliminar del grupo a los trabajadores inesperadamente "muertos", agregamos un mecanismo para rastrear errores y el estado 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 servidor web, tuve que elegir un estándar PHP confiable para presentar cualquier solicitud HTTP entrante. En nuestro caso, simplemente convertimos la solicitud net / http de Go al formato PSR-7 para que sea compatible con la mayoría de los frameworks PHP disponibles en la actualidad.

Dado que PSR-7 se considera inmutable (alguien dirá que técnicamente no lo es), los desarrolladores tienen que escribir aplicaciones que, en principio, no manejan la solicitud como una entidad global. Esto va 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í:



Presentación de RoadRunner: un servidor de aplicaciones PHP de alto rendimiento


Nuestra primera tarea de prueba fue un back-end API, que periódicamente causaba ráfagas de solicitudes impredecibles (mucho más a menudo de lo habitual). Aunque en la mayoría de los casos había suficientes funciones nginx, encontramos regularmente un error 502, porque no podíamos equilibrar el sistema lo suficientemente rápido para el aumento esperado de la carga.

Para reemplazar esta solución, a principios de 2018, implementamos nuestro primer servidor de aplicaciones PHP / Go. ¡Y de inmediato obtuvo un efecto increíble! No solo nos deshicimos por completo del error 502, sino que también pudimos reducir la cantidad de servidores en dos tercios, ahorrando una tonelada de dinero y pastillas para dolores de cabeza para 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 , enfatizando su increíble velocidad y eficiencia.

Cómo RoadRunner puede mejorar su pila de desarrollo


El uso de RoadRunner nos permitió usar Middleware net / http en el lado Go para llevar a cabo la verificación JWT antes de que la solicitud llegue a PHP, así como para procesar WebSockets y estados globales agregados en Prometheus.

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

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

Conclusión


Algunos todavía están cautivados por la noción obsoleta de PHP como un lenguaje lento y engorroso, adecuado solo para escribir complementos para WordPress. Estas personas incluso pueden decir que PHP tiene esa limitación: cuando la aplicación se vuelve lo suficientemente grande, debe elegir un lenguaje más "maduro" y reescribir la base de código que se ha acumulado durante muchos años.

Me gustaría responder a todo esto: piense de nuevo. Creemos que solo usted mismo establece restricciones para PHP. Puede pasar toda su vida cambiando de un idioma a otro, tratando de encontrar la combinación perfecta con sus necesidades, o puede comenzar a percibir los idiomas como herramientas. Los defectos aparentes de un lenguaje como PHP pueden ser las razones 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.

Después de trabajar con un montón de Go y PHP, podemos decir que los amamos. No planeamos sacrificar uno en favor del otro; por el contrario, buscaremos formas de obtener aún más beneficios de esta doble pila.

UPD: Bienvenido al creador de RoadRunner y coautor del artículo original - Lachezis

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


All Articles