Desarrollo de aplicaciones híbridas PHP / Go con RoadRunner

La aplicación PHP clásica es de un solo subproceso, carga pesada (a menos, por supuesto, que escriba en microframes) y la muerte inevitable del proceso después de cada solicitud ... Dicha aplicación es pesada y lenta, pero podemos darle una segunda vida por hibridación. Para acelerar, demonizamos y optimizamos las pérdidas de memoria para lograr un mejor rendimiento, presentaremos nuestro propio servidor de aplicaciones PHP Golang RoadRunner para agregar flexibilidad: simplifique el código PHP, expanda la pila y comparta la responsabilidad entre el servidor y la aplicación. En esencia, haremos que nuestra aplicación funcione como si la estuviéramos escribiendo en Java u otro idioma.

Gracias a la hibridación, una aplicación previamente lenta dejó de sufrir 502 errores bajo carga, el tiempo de respuesta promedio a las solicitudes disminuyó, el rendimiento aumentó y la implementación y el ensamblaje se hicieron más fáciles debido a la unificación de la aplicación y a la eliminación de enlaces innecesarios en forma de nginx + php-fpm.


Anton Titov ( Lachezis ) es CTO y cofundador de SpiralScout LLC con 12 años de experiencia activa en desarrollo comercial en PHP. En los últimos años, ha estado implementando activamente Golang en la pila de desarrollo de la compañía. Anton habló sobre un ejemplo en PHP Rusia 2019 .

Ciclo de vida de la aplicación PHP


Esquemáticamente, un dispositivo de aplicación abstracta con un cierto marco se ve así.



Cuando enviamos una solicitud a un proceso, sucede:

  • inicialización del proyecto;
  • cargar bibliotecas, marcos y ORM compartidos;
  • cargar bibliotecas requeridas para un proyecto específico;
  • enrutamiento;
  • solicitud de enrutamiento a un controlador específico;
  • Generación de respuesta.

Este es el principio de funcionamiento de una aplicación clásica de subproceso único con un único punto de entrada, que después de cada ejecución se destruye por completo o se borra su estado. Todo el código se descarga de la memoria, el trabajador se borra o simplemente restablece su estado.

Carga perezosa


La forma estándar y fácil de acelerar es la implementación del sistema de carga diferida o las bibliotecas de carga a pedido.



Con Lazy-loading solicitamos solo el código necesario.

Al acceder a un controlador específico, solo las bibliotecas necesarias se cargarán en la memoria, se procesarán y luego se descargarán. Esto le permite reducir el tiempo promedio de respuesta del proyecto y facilitar enormemente el proceso de trabajo en el servidor. En todos los marcos que estamos utilizando actualmente, se implementa el principio de carga diferida.

Cálculos frecuentes de caché


El método es más complicado y se usa activamente, por ejemplo, en el marco de Symfony, los motores de plantillas, los esquemas ORM y el enrutamiento. Esto no es almacenamiento en caché como memcached o Redis para datos de usuario. Este sistema calienta partes del código de antemano . En la primera solicitud, el sistema genera un código o un archivo de caché, y en solicitudes posteriores, estos cálculos, necesarios, por ejemplo, para compilar una plantilla, ya no se realizarán.



El almacenamiento en caché acelera significativamente la aplicación , pero al mismo tiempo la complica . Por ejemplo, hay problemas al invalidar el caché y actualizar la aplicación. No confunda el caché del usuario con el caché de la aplicación; en uno, los datos cambian con el tiempo, en el otro solo cuando se actualiza el código.

Procesamiento de solicitudes


Cuando se recibe una solicitud de un servidor PHP-FPM externo, el punto de entrada de la solicitud y la inicialización coincidirán.

Resulta que la solicitud del cliente es el estado de nuestro proceso.

La única forma de cambiar este estado es destruir completamente al trabajador y comenzar de nuevo con una nueva solicitud.



Este es un modelo clásico de un solo hilo con sus ventajas.

  • Todos los trabajadores al final de la solicitud mueren.
  • Pérdidas de memoria, condición de carrera, puntos muertos no son inherentes a PHP. No te preocupes por eso.
  • El código es simple: escribimos, procesamos la solicitud, morimos y seguimos adelante.

Por otro lado, para cada solicitud, cargamos completamente el marco, todas las bibliotecas, realizamos algunos cálculos, volvemos a compilar las plantillas. Con cada solicitud en un círculo producimos muchas manipulaciones y trabajos innecesarios.

Cómo funciona en el servidor


Lo más probable es que funcionen un montón de nginx y PHP. Nginx funcionará como un proxy inverso: proporcione a los usuarios parte de las estadísticas y delegue parte de las solicitudes al administrador de procesos PHP PHP-FPM a continuación. El gerente ya plantea un trabajador separado para la solicitud y la procesa. Después de eso, el trabajador es destruido o despedido. A continuación, se crea un nuevo trabajador para la siguiente solicitud.



Tal modelo funciona de manera estable: la aplicación es casi imposible de eliminar. Pero con cargas pesadas, la cantidad de trabajo para inicializar y destruir trabajadores afecta el rendimiento del sistema, porque incluso para una simple solicitud GET, a menudo tenemos que extraer un montón de dependencias y volver a aumentar la conexión de la base de datos.

Acelerando la aplicación


¿Cómo acelerar la aplicación clásica después de introducir caché y carga diferida? ¿Qué otras opciones hay?

Dirígete al lenguaje en sí .

  • Utiliza OPCache. ¿Creo que nadie está ejecutando PHP en producción sin OPCache habilitado?
  • Espere RFC: precarga . Le permite precargar un conjunto de archivos en una máquina virtual.
  • JIT : acelera seriamente la aplicación en tareas vinculadas a la CPU. Desafortunadamente, con las tareas relacionadas con las bases de datos, no ayudará mucho.

Usa alternativas . Por ejemplo, la máquina virtual HHVM de Facebook. Ejecuta código en un entorno más optimizado. Desafortunadamente, HHVM no es totalmente compatible con la sintaxis de PHP. Como alternativa, los compiladores kPHP de VK o PeachPie, que convierte completamente el código a .NET C #, son una alternativa.

Reescribir completamente a otro idioma. Esta es una opción radical: elimine completamente la carga de código entre solicitudes.

Puede almacenar completamente el estado de la aplicación en la memoria , usar esta memoria activamente para el trabajo y olvidarse del concepto de un trabajador moribundo y borrar completamente la aplicación entre solicitudes.

Para lograr esto, movemos el punto de entrada, que solía estar junto con el punto de inicialización, profundamente en la aplicación.

Transferencia de punto de entrada - demonización


Esto está creando un bucle infinito en la aplicación: solicitud entrante, ejecútela a través del marco, genere una respuesta para el usuario. Este es un ahorro importante: todo el arranque, toda la inicialización del marco se realiza solo una vez, y luego la aplicación procesa varias solicitudes.



Adaptamos la aplicación


Curiosamente, podemos centrarnos en optimizar solo esa parte de la aplicación que se ejecutará en tiempo de ejecución : controladores, lógica de negocios. En este caso, puede abandonar el modelo de carga diferida. Formaremos parte del proyecto bootstrapping desde el principio, en el momento de la inicialización. Cálculos preliminares: enrutamiento, plantillas, configuraciones, esquemas ORM inflarán la inicialización, pero en el futuro ahorrarán tiempo de procesamiento para una solicitud específica.



No recomiendo compilar plantillas al descargar un trabajador, pero descargar, por ejemplo, todas las configuraciones es útil.

Compara modelos


Compare los modelos demonizados (izquierda) y clásicos.



El modelo demonizado lleva más tiempo desde el momento en que se creó el proceso hasta el momento en que se devuelve la respuesta al usuario. La aplicación clásica está optimizada para una rápida creación, procesamiento y destrucción.

Sin embargo, todas las solicitudes posteriores después de calentar el código son mucho más rápidas. El marco, la aplicación, el contenedor ya está en la memoria y listo para aceptar solicitudes y responder rápidamente.

Problemas del modelo de larga vida.


A pesar de las ventajas, el modelo tiene un conjunto de limitaciones.

Fugas de memoria. La aplicación permanece en la memoria durante mucho tiempo, y si usa las "curvas" de la biblioteca, las dependencias incorrectas o los estados globales, la memoria comenzará a perder. En algún momento, aparecerá un error fatal que romperá la solicitud del usuario.

El problema se resuelve de dos maneras.

  • Escriba código preciso, use bibliotecas comprobadas.
  • Monitorear activamente a los trabajadores. Si sospecha que hay pérdida de memoria dentro del proceso, cámbielo proactivamente a un análogo con un límite inferior, es decir, simplemente a una nueva copia que aún no haya logrado acumular memoria sin limpiar.

Fugas de datos . Por ejemplo, si durante una solicitud entrante guardamos al usuario actual del sistema en alguna variable global y olvidamos restablecer esta variable después de la solicitud, entonces existe la posibilidad de que el siguiente usuario del sistema obtenga accidentalmente acceso a datos que no debería ver.

El problema se resuelve a nivel de arquitectura de la aplicación.

  • No almacene un usuario activo en un contexto global. Todos los datos que son específicos del contexto de la solicitud se descartan y se borran antes de la próxima solicitud.
  • Maneje los datos de la sesión con cuidado. Sesiones en PHP: con el enfoque clásico, este es un objeto global. Envuélvalo correctamente para que, en una solicitud posterior, se restablezca.

Gestión de recursos .

  • Supervisar las conexiones a la base de datos. Si la aplicación se bloquea en la memoria durante un mes o dos, entonces la conexión abierta probablemente se cerrará dentro de este tiempo: la base de datos se volverá a instalar, se reiniciará o el firewall restablecerá la conexión. En el nivel de código, considere volver a conectar, o después de cada solicitud, restablecer la conexión y volver a subirla en la próxima solicitud.
  • Evite el bloqueo de archivos de larga duración. Si su trabajador escribe alguna información en un archivo, no hay problema. Pero si este archivo está abierto y tiene un bloqueo, entonces ningún otro proceso en su sistema tendrá acceso hasta que se libere el bloqueo.


Explore el modelo de larga vida


Considere un modelo de trabajador de larga duración, demonizando una aplicación, y explore formas de implementarla.

Enfoque sin bloqueo


Utilizamos PHP asincrónico: cargamos la aplicación una vez en la memoria y procesamos las solicitudes HTTP entrantes dentro de la aplicación. Ahora la aplicación y el servidor son un solo proceso . Cuando llega la solicitud, creamos una rutina por separado o, en el bucle de eventos, damos una promesa, la procesamos y se la damos al usuario.



La ventaja innegable del enfoque es el máximo rendimiento. También es posible utilizar herramientas interesantes, por ejemplo, configurar WebSocket directamente en su aplicación .

Sin embargo, el enfoque aumenta significativamente la complejidad del desarrollo . Es necesario instalar ELDO, recuerde que no todos los controladores de bases de datos serán compatibles y que la biblioteca PDO está excluida.

Para resolver problemas en el caso de demonización con un enfoque sin bloqueo, puede utilizar herramientas conocidas: ReactPHP , amphp y Swoole , un desarrollo interesante en forma de una extensión C. Estas herramientas funcionan rápidamente, tienen una buena comunidad y buena documentación.

Enfoque de bloqueo


No levantamos corutinas dentro de la aplicación, sino que lo hacemos desde el exterior.



Simplemente recogemos algunos procesos de aplicación , como haría PHP-FPM. En lugar de transmitir estas solicitudes en forma de un estado de proceso, las entregamos desde el exterior en forma de un protocolo o mensaje.

Escribimos el mismo código de subproceso único que conocemos, utilizamos las mismas bibliotecas y el mismo PDO. Todo el trabajo duro de trabajar con sockets, HTTP y otras herramientas se realiza fuera de la aplicación PHP .

De los inconvenientes: debemos monitorear la memoria y recordar que la comunicación entre dos procesos diferentes no es gratuita , pero necesitamos transferir datos. Esto creará una ligera sobrecarga.

Para resolver el problema, ya existe una herramienta PHP-RM escrita en PHP. En la biblioteca ReactPHP, tiene integración con varios marcos . Sin embargo, PHP-PM es muy lento, pierde memoria a nivel del servidor y bajo carga no muestra tanto crecimiento como PHP-FRM.

Escribimos nuestro servidor de aplicaciones


Escribimos nuestro servidor de aplicaciones , que es similar a PHP-RM, pero hay más funcionalidades. ¿Qué queríamos del servidor?

Combinar con marcos existentes. Nos gustaría tener una integración flexible con casi todos los marcos en el mercado. No tengo ganas de escribir una herramienta que funcione solo en un caso particular en particular.

Diferentes procesos para servidor y aplicación . Posibilidad de un reinicio en caliente, de modo que cuando se desarrolle localmente, presione F5 y vea el nuevo código actualizado, así como poder expandirlos individualmente.

Alta velocidad y estabilidad . Aún así, estamos escribiendo un servidor HTTP.

Fácil extensibilidad . Queremos usar el servidor no solo como un servidor HTTP, sino también para escenarios individuales como un servidor de cola o un servidor gRPC.

Trabaje fuera de la caja siempre que sea ​​posible: Windows, Linux, CPU ARM.

Capacidad para escribir extensiones multiproceso muy rápidas específicas para nuestra aplicación.

Como ya entendió, escribiremos en Golang.

Servidor RoadRunner


Para crear un servidor PHP, debe resolver 4 problemas principales:

  • Establecer comunicación entre los procesos de Golang y PHP.
  • Gestión de procesos: creación, destrucción, seguimiento de trabajadores.
  • Equilibrio de tareas: distribución eficiente de tareas a los trabajadores. Dado que estamos implementando un sistema que bloquea a un trabajador individual para una tarea entrante específica en particular, es importante crear un sistema que diga rápidamente que el proceso ha terminado el trabajo y está listo para aceptar la siguiente tarea.
  • Pila HTTP: envío de datos de solicitud HTTP al trabajador. Es una tarea simple escribir un punto entrante al que el usuario envía una solicitud, que se pasa a PHP y se devuelve.

Variantes de interacción entre procesos.


Primero, solucionemos el problema de comunicación entre los procesos de Golang y PHP. Tenemos varias formas

Incrustar: incrustar un intérprete PHP directamente en Golang. Esto es posible, pero requiere un ensamblado PHP personalizado, una configuración compleja y un proceso común para el servidor y PHP. Como en go-php , por ejemplo, donde el intérprete PHP está integrado en Golang.

Memoria compartida: el uso del espacio de memoria compartida, donde los procesos comparten este espacio . Se necesita un trabajo minucioso. Al intercambiar datos, tendrá que sincronizar el estado manualmente y la cantidad de errores que pueden ocurrir es bastante grande. La memoria compartida también depende del sistema operativo.

Escribiendo su protocolo de transporte - Goridge


Seguimos un camino simple que se utiliza en casi todas las soluciones en sistemas Linux: utilizamos el protocolo de transporte. Está escrito en la parte superior de los TUBOS estándar y ENCHUFES UNIX / TCP .

Tiene la capacidad de transferir datos en ambas direcciones, detectar errores y también etiquetar solicitudes y colocar encabezados. Un matiz importante para nosotros es la capacidad de implementar el protocolo sin dependencias tanto del lado de PHP como de Golang, sin extensiones C en un lenguaje puro.

Como con cualquier protocolo, la base es un paquete de datos. En nuestro caso, el paquete tiene un encabezado fijo de 17 bytes.



El primer byte se asigna para determinar el tipo de paquete. Esto puede ser una secuencia o una bandera que indica el tipo de serialización de datos. Luego, dos veces empaquetamos el tamaño de los datos en Little Endian y Big Endian. Utilizamos este legado para detectar errores de transmisión. Si vemos que el tamaño de los datos empaquetados en dos pedidos diferentes no coincide, lo más probable es que se haya producido un error de transferencia de datos. Luego se transmiten los datos.



En la tercera versión del paquete, eliminaremos ese legado, presentaremos un enfoque más clásico con una suma de comprobación y también agregaremos la capacidad de usar este protocolo con procesos PHP asíncronos.

Para implementar el protocolo en Golang y PHP, utilizamos herramientas estándar.

En Golang: bibliotecas de codificación / binarias y bibliotecas io y net para trabajar con canalizaciones estándar y sockets UNIX / TCP.

En PHP: la función familiar para trabajar con paquetes de datos binarios / desempaquetar y las extensiones de flujos y sockets para tuberías y sockets.

Un efecto secundario interesante surgió durante la implementación. Lo integramos con la biblioteca estándar de Golang net / rpc, que nos permite llamar al código de servicio de Golang directamente en la aplicación.

Escribimos un servicio:

//  sample type  struct{} // Hi returns greeting message. func (a *App) Hi(name string, r *string) error { *r = fmt.Sprintf("ll, %s!", name) return nil } 

Se llama una pequeña cantidad de código desde la aplicación:

 <?php use Spiral\Goridge; require "vendor/autoload.php"; $rpc = new Goridge\RPC( new Goridge\SocketRelay("127.0.0.1", 6001) ); echo $rpc->call("App.Hi", "Antony"); 

PHP Process Manager


La siguiente parte del servidor es la gestión de los trabajadores de PHP.


Worker es un proceso PHP que observamos constantemente desde Golang. Recopilamos el registro de sus errores en el archivo STDERR, nos comunicamos con el trabajador a través del protocolo de transporte Goridge y recopilamos estadísticas sobre el consumo de memoria, la ejecución de tareas y el bloqueo.

La implementación es simple: esta es la funcionalidad estándar de os / exec, runtime, sync, atomic. Para crear trabajadores usamos Worker Factory .


¿Por qué la fábrica de trabajadores? Porque queremos comunicarnos tanto en tuberías estándar como en enchufes. En este caso, el proceso de inicialización es ligeramente diferente. Al crear un trabajador que se comunica por canalización, podemos crearlo de inmediato y enviar datos directamente. En el caso de los sockets, debe crear un trabajador, esperar hasta que llegue al sistema, hacer un apretón de manos PID y solo entonces continuar trabajando.

Balanceador de tareas


La tercera parte del servidor es la más importante para el rendimiento.

Para la implementación, utilizamos la funcionalidad estándar de Golang, un canal protegido . En particular, creamos varios trabajadores y los colocamos en este canal como una pila LIFO.

Cuando recibimos tareas del usuario, enviamos una solicitud a la pila LIFO y solicitamos emitir el primer trabajador libre. Si el trabajador no puede ser asignado por un cierto período de tiempo, el usuario recibe un error del tipo "Error de tiempo de espera". Si el trabajador está asignado, se obtiene de la pila, se bloquea, después de lo cual recibe la tarea del usuario.

Una vez procesada la tarea, la respuesta se devuelve al usuario y el trabajador se encuentra al final de la pila. Está listo para realizar la siguiente tarea nuevamente.

Si se produce un error, el usuario recibirá un error, ya que el trabajador será destruido. Pedimos a Worker Pool y Worker Factory que creen un proceso idéntico y lo reemplacen en la pila. Esto permite que el sistema funcione incluso en caso de errores fatales simplemente recreando a los trabajadores por analogía con PHP-FPM.


Como resultado, resultó implementar un pequeño sistema que funciona muy rápidamente: 200 ns para la asignación de trabajadores . Es capaz de funcionar incluso en caso de errores fatales. Cada trabajador en un punto en el tiempo procesa solo una tarea, lo que nos permite utilizar el enfoque clásico de bloqueo .

Monitoreo proactivo


Una parte separada tanto del administrador de procesos como del equilibrador de tareas es el sistema de monitoreo proactivo.


Este es un sistema que una vez por segundo sondea a los trabajadores y monitorea los indicadores: analiza la cantidad de memoria que consumen, la cantidad de memoria que contienen, si están inactivos. Además del seguimiento, el sistema supervisa las pérdidas de memoria. Si el trabajador excede un cierto límite, lo veremos y lo eliminaremos cuidadosamente del sistema antes de que ocurra una fuga fatal.

Pila HTTP


La última y simple parte.

Cómo se implementa:

  • plantea un punto HTTP en el lado de Golang;
  • recibimos una solicitud;
  • convertir al formato PSR-7;
  • enviar la solicitud al primer trabajador libre;
  • Desempaquete la solicitud en un objeto PSR-7;
  • procesamos
  • Generamos la respuesta.

Para la implementación, utilizamos la biblioteca estándar de Golang NET / HTTP . Esta es una famosa biblioteca con muchas extensiones. Capaz de funcionar tanto sobre HTTPS como sobre el protocolo HTTP / 2.

En el lado de PHP, utilizamos el estándar PSR-7 . Es un marco independiente con muchas extensiones y Middlewares. El diseño del PSR-7 es inmutable , lo que encaja bien con el concepto de aplicaciones de larga duración y evita errores de consulta globales.

Ambas estructuras en Golang y PSR-7 son similares, lo que ahorró significativamente tiempo para asignar una solicitud de un idioma a otro.

Para iniciar el servidor se requiere un enlace mínimo :

 http: address: 0.0.0.0:8080 workers: command: "php psr-worker.php" pool: numWorkers: 4 

Además, desde la versión 1.3.0 se puede omitir la última parte de la configuración.

Descargue el archivo binario del servidor, póngalo en el contenedor Docker o en la carpeta del proyecto. Alternativamente, a nivel mundial, escribimos un pequeño archivo de configuración que describe qué pod vamos a escuchar, qué trabajador es el punto de entrada y cuántos se requieren.

En el lado de PHP, escribimos un ciclo primario que recibe una solicitud PSR-7, la procesa y devuelve una respuesta o un error al servidor.

 while ($req = $psr7->acceptRequest()) { try { $resp = new \Zend\Diactoros\Response(); $resp->getBody()->write("hello world"); $psr7->respond($resp); } catch (\Throwable $e) { $psr7->getWorker()->error((string)$e); } } 

Asamblea Para implementar el servidor, elegimos una arquitectura con un enfoque de componentes. Esto permite ensamblar el servidor para las necesidades del proyecto, agregando o eliminando piezas individuales según los requisitos de la aplicación.

 func main() { rr.Container.Register(env.ID, &env.Service{}) rr.Container.Register(rpc.ID, &rpc.Service{}) rr.Container.Register(http.ID, &http.Service{}) rr.Container.Register(static.ID, &static.Service{}) rr.Container.Register(limit.ID, &limit.Service{} // you can register additional commands using cmd.CLI rr.Execute() } 

Casos de uso


Considere las opciones para usar el servidor y modificar la estructura. Para comenzar, considere la canalización clásica: el trabajo del servidor con las solicitudes.

Modularidad


El servidor recibe la solicitud a un punto HTTP y la pasa a través de un conjunto de Middleware, que están escritos en Golang. Una solicitud entrante se convierte en una tarea que el trabajador comprende. El servidor entrega la tarea al trabajador y la devuelve.



Al mismo tiempo, el trabajador, utilizando el protocolo Goridge, se comunica con el servidor, monitorea su estado y le transfiere datos.

Middleware en Golang: Autorización


Esta es la primera cosa que hacer. En nuestra aplicación, escribimos Middleware para autorizar al usuario por token JWT . Middleware está escrito de la misma manera para cualquier otro tipo de autorización. Una implementación muy banal y simple es escribir Rate-Limiter o Circuit-Breaker.



La autorización es rápida . Si la solicitud no es válida, simplemente no la envíe a la aplicación PHP y no desperdicie recursos en el procesamiento de tareas inútiles.

Monitoreo


El segundo caso de uso. Podemos integrar el sistema de monitoreo directamente en Golang Middleware. Por ejemplo, Prometheus, para recopilar estadísticas sobre la velocidad de los puntos de respuesta, la cantidad de errores.



También puede combinar el monitoreo con métricas específicas de la aplicación (disponible como estándar con 1.4.5). Por ejemplo, podemos enviar el número de solicitudes a la base de datos o el número de solicitudes específicas procesadas al servidor Golang, y luego a Prometheus.

Rastreo distribuido y registro


Escribimos Middleware con un administrador de procesos. En particular, podemos conectarnos al sistema en tiempo real para monitorear registros y recopilar todos los registros en una base de datos central , lo cual es útil al escribir aplicaciones distribuidas.



También podemos etiquetar solicitudes , darles una identificación específica y pasar esta identificación a todos los servicios posteriores o sistemas de comunicación entre ellos. Como resultado, podemos construir un rastreo distribuido y ver cómo funcionan los registros de la aplicación.

Grabe su historial de consultas


Este es un pequeño módulo que registra todas las solicitudes entrantes y las almacena en una base de datos externa. El módulo le permite realizar solicitudes de repetición en el proyecto e implementar un sistema de prueba automático, un sistema de prueba de carga o simplemente verificar el funcionamiento de la API.



¿Cómo implementamos el módulo?

Procesamos parte de las solicitudes de Golang . Escribimos Middleware en Golang y podemos enviar parte de las solicitudes a Handler, que también está escrito en Golang. Si algún punto de la aplicación es preocupante en términos de rendimiento, lo reescribimos en Golang y arrastramos la pila de un idioma a otro.



Estamos escribiendo un servidor WebSocket . Implementar un servidor WebSocket o un servidor de notificaciones push se está convirtiendo en una tarea trivial.

  • Servicio de Golang a nivel de servidor.
  • Para la comunicación usamos Goridge.
  • Capa de servicio delgada en PHP.
  • Implementamos el servidor de notificaciones.

Recibimos una solicitud y generamos una conexión WebSocket. Si la aplicación necesita enviar algún tipo de notificación al usuario, lanza este mensaje a través del protocolo RPC al servidor WebSocket.



Gestiona tu entorno PHP. Al crear un grupo de trabajadores, RoadRunner tiene control total sobre el estado de las variables de entorno y le permite cambiarlas a su gusto. Si estamos escribiendo una aplicación distribuida grande, podemos usar una sola fuente de datos de configuración y conectarla como un sistema para configurar el entorno. Si planteamos un conjunto de servicios, todos estos servicios afectarán a un solo sistema, se configurarán y luego funcionarán. Esto puede simplificar enormemente la implementación, así como deshacerse de los archivos .env.



Curiosamente, las variables env que están disponibles dentro del trabajador no son globales dentro del sistema. Esto mejora ligeramente la seguridad del contenedor.

Integración de la biblioteca de Golang en PHP


Utilizamos esta opción en el sitio web oficial de RoadRunner . Esta es una integración de una base de datos casi completa con la búsqueda de texto completo BleveSearch dentro del servidor.



Indexamos las páginas de documentación: las colocamos en Bolt DB, después de lo cual realizamos una búsqueda de texto completo sin una base de datos real como MySQL y sin un clúster de búsqueda como Elasticsearch. El resultado fue un pequeño proyecto donde parte de la funcionalidad está en PHP, pero la búsqueda está en Golang.

Implementando Funciones Lambda


Puede ir más allá y deshacerse por completo de la capa HTTP. En este caso, implementar, por ejemplo, las funciones de Lambda es una tarea simple.



Para la implementación, utilizamos el tiempo de ejecución estándar de AWS para la función Lambda. Escribimos un pequeño enlace, cortamos completamente los servidores HTTP y enviamos los datos en formato binario a los trabajadores. También tenemos acceso a la configuración del entorno, que nos permite escribir funciones que se configuran directamente desde el panel de administración de Amazon.

Los trabajadores están en memoria durante toda la vida del proceso, y la función Lambda después de la solicitud inicial permanece en la memoria durante 15 minutos. En este momento, el código no se carga y responde rápidamente. En las pruebas sintéticas, recibimos hasta 0.5 ms por cada solicitud entrante .

gRPC para PHP


La opción más difícil es reemplazar la capa HTTP con la capa gRPC. Este paquete está disponible en GitHub .


Podemos enviar por proxy todas las solicitudes de Protobuf entrantes a una aplicación PHP subordinada, allí se pueden desempaquetar, procesar y responder. Podemos escribir código tanto en PHP como en Golang, combinando y transfiriendo funcionalidad de una pila a otra. El servicio es compatible con Middleware. Tanto la aplicación independiente como junto con HTTP pueden funcionar.

Servidor de colas


La última y más interesante opción es la implementación del servidor de colas .


En el lado de PHP, todo lo que hacemos es obtener una carga binaria, desempacarla, hacer el trabajo y decirle al servidor sobre el éxito. En el lado de Golang, estamos totalmente comprometidos en la gestión de las conexiones con los corredores. Puede ser RabbitMQ, Amazon SQS o Beanstalk.

En el lado de Golang, implementamos el " cierre agraciado" de los trabajadores. Podemos esperar bellamente la implementación de la "conexión duradera": si se pierde la conexión con el intermediario, el servidor espera un momento utilizando la "estrategia de retroceso", levanta la conexión y la aplicación ni siquiera lo nota.

Podemos procesar estas solicitudes en PHP y Golang, y ponerlas en cola en ambos lados:

  • desde PHP a través del protocolo Goridge Goridge RPC;
  • de Golang: comunicación con la biblioteca SDK.

Si la carga útil cae, entonces no cae todo el Consumidor, sino solo un proceso separado. El sistema lo eleva inmediatamente, la tarea se envía al siguiente trabajador. Esto le permite realizar tareas sin parar.

Implementamos uno de los corredores directamente en la memoria del servidor y utilizamos la funcionalidad Golang. Esto nos permite escribir una aplicación usando colas antes de elegir la pila final. Levantamos la aplicación localmente, la iniciamos y tenemos colas que funcionan en la memoria y se comportan de la misma manera que se comportarían en RabbitMQ, Amazon SQS o Beanstalk.

Cuando se usan dos idiomas en un paquete híbrido, vale la pena recordar cómo separarlos.

Dominios de dominio separados


Golang es un lenguaje multiproceso y rápido que es adecuado para escribir lógica de infraestructura y supervisión de usuarios y lógica de autorización.

También es útil para implementar controladores personalizados para acceder a las fuentes de datos; estas son colas, por ejemplo, Kafka, Cassandra.

PHP es un gran lenguaje para escribir lógica de negocios.

Este es un buen sistema para renderizar HTML, ORM y trabajar con la base de datos.

Herramienta de comparación


Hace varios meses en Habré comparó PHP-FPM, PHP-PM, React-PHP, Roadrunner y otras herramientas. El punto de referencia se llevó a cabo en un proyecto con Symfony 4 real.

RoadRunner bajo carga muestra buenos resultados y está por delante de todos los servidores. En comparación con PHP-FPM, el rendimiento es 6-8 veces más.


En el mismo punto de referencia, RoadRunner no perdió una sola solicitud, todo se resolvió al 100%. Desafortunadamente, React-PHP perdió 8–9 solicitudes bajo cargas, esto es inaceptable. Nos gustaría que el servidor no se bloquee y funcione de manera estable.


Desde la publicación de RoadRunner en acceso público en GitHub, hemos recibido más de 30,000 instalaciones. La comunidad nos ha ayudado a escribir un conjunto específico de extensiones, mejoras y creemos que la solución tiene derecho a la vida.

RoadRunner es bueno si desea acelerar significativamente la aplicación, pero aún no está listo para saltar a PHP asincrónico . Este es un compromiso que requerirá una cierta cantidad de esfuerzo, pero no tan significativo como una reescritura completa de la base del código.

Tome RoadRunner si desea tener más control sobre el ciclo de vida de PHP , si no hay suficientes capacidades de PHP, por ejemplo, para el sistema de colas o Kafka, y cuando su popular biblioteca de Golang resuelve su problema, que no está en PHP, y la escritura lleva tiempo, que usted tampoco tiene.

Resumen


Lo que obtuvimos al escribir este servidor y usarlo en nuestra infraestructura de producción.

  • Aumentaron la velocidad de reacción de los puntos de aplicación en 4 veces en comparación con PHP-FPM.
  • Se deshizo completamente de 502 errores bajo cargas . En las cargas máximas, el servidor solo espera un poco más y responde como si no hubiera cargas.
  • Después de optimizar las pérdidas de memoria, los trabajadores permanecen en la memoria hasta por 2 meses . Esto ayuda al escribir aplicaciones distribuidas, ya que todas las solicitudes entre servicios ya están en caché a nivel de socket.
  • Usamos Keep-Alive. Esto acelera significativamente la comunicación entre un sistema distribuido.
  • Dentro de la infraestructura real, colocamos todo en el Alpine Docker en Kubernetes . El sistema de implementación y construcción del proyecto ahora es más fácil. Todo lo que se requiere es construir una compilación RoadRunner personalizada para el proyecto, ponerla en el proyecto Docker, completar la imagen de Docker y luego subir tranquilamente nuestro pod a Kubernetes.
  • Según el tiempo real de uno de los proyectos a puntos individuales que no tienen acceso a la base de datos, el tiempo de respuesta promedio es de 0,33 ms .

La próxima conferencia profesional para desarrolladores PHP PHP Rusia solo el próximo año. Por ahora, ofrecemos lo siguiente:

  • Preste atención a GolangConf si está interesado en la parte Go y desea conocer más detalles o escuchar argumentos a favor de cambiar a este idioma. Si está listo para compartir su experiencia, envíe resúmenes .
  • Participe en HighLoad ++ en Moscú, si todo es importante para usted que está asociado con un alto rendimiento, envíe un informe antes del 7 de septiembre o reserve una entrada.
  • Suscríbase al boletín y al canal de telegramas para recibir una invitación a PHP Rusia 2020 antes que otros.

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


All Articles