PHP asincrónico y la historia de una bicicleta

Después del lanzamiento de PHP7, se hizo posible escribir aplicaciones de larga duración a un costo relativamente bajo. Para los programadores, se han puesto a disposición proyectos como prooph , broadway , tactician , messenger , cuyos autores toman la solución a los problemas más comunes. Pero, ¿qué pasa si das un pequeño paso adelante y profundizas en la pregunta?


Intentemos averiguar el destino de otra bicicleta, que le permite implementar la aplicación Publicar / Suscribir.


Para comenzar, trataremos de revisar brevemente las tendencias actuales en el mundo de PHP, así como un breve vistazo a la operación asincrónica.


PHP creado para morir


Durante mucho tiempo, PHP se utilizó principalmente en el flujo de trabajo de solicitud / respuesta. Desde el punto de vista de los desarrolladores, esto es bastante conveniente, porque no hay necesidad de preocuparse por las pérdidas de memoria, monitorear las conexiones.


Todas las consultas se ejecutarán de manera aislada, se liberarán los recursos utilizados y las conexiones, por ejemplo, a la base de datos se cerrarán cuando se complete el proceso.


Como ejemplo, puede tomar una aplicación CRUD regular escrita sobre la base del marco de Symfony. Para leer de la base de datos y devolver JSON, es necesario realizar una serie de pasos (para ahorrar espacio y tiempo, excluir los pasos para generar / ejecutar códigos de operación):


  • Análisis de la configuración;
  • Compilación de contenedores;
  • Solicitar enrutamiento
  • Cumplimiento;
  • Representando el resultado.

Como en el caso de PHP (que usa aceleradores), el marco utiliza activamente el almacenamiento en caché (algunas tareas no se completarán en la próxima solicitud), así como la inicialización retrasada. A partir de la versión 7.4, la precarga estará disponible, lo que optimizará aún más la inicialización de la aplicación.


Sin embargo, no es posible eliminar por completo todos los costos generales para la inicialización.


Ayudemos a PHP a sobrevivir


La solución al problema parece bastante simple: si ejecuta la aplicación cada vez que es demasiado costosa, debe inicializarla una vez y luego pasarle las solicitudes, controlando su ejecución.


Hay proyectos en el ecosistema PHP como php-pm y RoadRunner . Ambos conceptualmente hacen lo mismo:


  • Se crea un proceso principal que actúa como supervisor;
  • Se crea un grupo de procesos secundarios;
  • Cuando se recibe una solicitud, el maestro recupera el proceso del grupo y le pasa la solicitud. El cliente está pendiente en este momento;
  • Una vez que se completa la tarea, el maestro devuelve el resultado al cliente y el proceso secundario se envía de vuelta al grupo.

Si algún proceso secundario muere, el supervisor lo crea nuevamente y lo agrega al grupo. Creamos un demonio desde nuestra aplicación con un único propósito: eliminar la sobrecarga de inicialización, aumentando significativamente la velocidad de procesamiento de solicitudes. Esta es la forma más indolora de aumentar la productividad, pero no la única.


Nota:
Muchos ejemplos de la serie "tomar ReactPHP y acelerar Laravel N veces" caminan por la red. Es importante comprender la diferencia entre demonizar (y, como resultado, ahorrar tiempo en el arranque de la aplicación) y la multitarea.
Cuando use php-pm o roadrunner, su código no se bloqueará. Simplemente ahorra tiempo en la inicialización.
La comparación de php-pm, roadrunner y ReactPHP / Amp / Swoole es incorrecta por definición.

PHP y E / S

La interacción con E / S en PHP se ejecuta por defecto en modo de bloqueo. Esto significa que si ejecutamos una solicitud para actualizar la información en la tabla, el flujo de ejecución se detendrá esperando una respuesta de la base de datos. Cuantas más llamadas estén en proceso de procesar la solicitud, más tiempo estarán inactivos los recursos del servidor. De hecho, en el proceso de procesamiento de la solicitud, tenemos que ir a la base de datos varias veces, escribir algo en el registro y devolver el resultado al cliente, al final, también una operación de bloqueo.


Imagine que es un operador de centro de llamadas y necesita llamar a 50 clientes en una hora.
Marcas el primer número, y allí está ocupado (el suscriptor discute por teléfono la última serie del Juego de Tronos y en qué consiste la serie).
Y ahora estás sentado y tratando de alcanzarlo antes de la victoria. El tiempo pasa, el cambio está llegando a su fin. Después de perder 40 minutos tratando de llegar al primer suscriptor, perdió la oportunidad de contactar a otros y, naturalmente, recibió del jefe.
Pero puede hacerlo de otra manera: no espere hasta que el primer suscriptor esté libre y, tan pronto como escuche un pitido, cuelgue y comience a marcar el siguiente número. Puedes volver al primero un poco más tarde.
Con este enfoque, las posibilidades de llamar al número máximo de personas aumentan considerablemente, y la velocidad de su trabajo no descansa en la tarea más lenta.

El código que no bloquea el hilo de ejecución (no usa llamadas de bloqueo de E / S, así como funciones como sleep() ), se llama asíncrono.


Volvamos a nuestra aplicación Symfony CRUD. Es casi imposible que funcione en modo asíncrono debido a la abundancia del uso de funciones de bloqueo: todos funcionan con configuraciones, cachés, registros, representación de la respuesta, interacción con la base de datos.


Pero todas estas son convenciones, intentemos lanzar Symfony y usar Amp , que proporciona una implementación de Event Loop (incluyendo una serie de carpetas), Promesas y Corutinas, como una guinda para resolver nuestro problema.


Promise es una forma de organizar el código asincrónico. Por ejemplo, necesitamos acceder a algún recurso http.


Creamos un objeto de solicitud y lo pasamos al transporte, que Promise nos devuelve con el estado actual. Hay tres estados posibles:


  • Éxito: nuestra solicitud se completó con éxito;
  • Error: durante la ejecución de la solicitud, algo salió mal (por ejemplo, el servidor devolvió una respuesta 500);
  • En espera: el procesamiento de solicitudes aún no ha comenzado.

Cada Promesa tiene un método (en el ejemplo, Promise es analizado por Amp ): onResolve() , en el que se pasa una función de devolución de llamada con dos argumentos


 $promise->onResolve( static function(?/Throwable $throwable, $result): void { if(null !== $throwable) { /**   */ return; } /**  */ } ); 

Después de recibir Promise, surge la pregunta: ¿quién supervisará su estado y nos notificará el cambio de estado?


Para esto, se usa Event Loop.


En esencia, un bucle de eventos es un programador que supervisa la ejecución. Tan pronto como se complete la tarea (no importa cómo), se llamará al invocable que pasamos a Promise.


En cuanto a los matices, recomendaría leer un artículo de Nikita Popov: Multitarea cooperativa utilizando corutinas . Ayudará a aportar algo de claridad sobre lo que está sucediendo y dónde están los generadores.


Armados con nuevos conocimientos, intentemos volver a nuestra tarea de renderización JSON.
Un ejemplo de procesamiento de una solicitud HTTP entrante usando amphp / http-server .
Tan pronto como recibamos la solicitud, se realiza una lectura asincrónica de la base de datos (obtenemos Promesa) y, una vez completada, el usuario recibirá el codiciado JSON, formado sobre la base de los datos recibidos.


Si necesitamos escuchar un puerto de varios procesos, podemos mirar hacia amphp / cluster

La principal diferencia es que un solo proceso puede atender varias solicitudes a la vez debido al hecho de que el hilo de ejecución no está bloqueado. El cliente recibirá su respuesta cuando se complete la lectura de la base de datos, y si bien no hay respuesta, puede comenzar a atender la siguiente solicitud.


El maravilloso mundo del PHP asincrónico


Descargo de responsabilidad
El PHP asincrónico se considera en el contexto de los exóticos y no se considera algo saludable / normal. Básicamente, esperarán risas al estilo de "take GO / Kotlin, a tonto", etc. No diría que estas personas están equivocadas, pero ...

Hay una serie de proyectos que ayudan a escribir código PHP sin bloqueo. En el marco del artículo, no analizaré completamente todos los pros y los contras, pero trataré de examinar cada uno de ellos superficialmente.


Swoole

Un marco asincrónico escrito en contraste con los demás en C y entregado como una extensión a PHP. Posee quizás los mejores indicadores de rendimiento en este momento.


Hay una implementación de canales, corutina y otras cosas sabrosas, pero tiene 1 gran inconveniente: la documentación. Aunque está parcialmente en inglés, en mi opinión no es muy detallado, y la API en sí no es muy obvia.


En cuanto a la comunidad, tampoco todo es simple e inequívoco. Personalmente, no conozco a una sola persona viva que use Swoole en la batalla. Quizás supere mis miedos y migre a él, pero esto no sucederá en un futuro cercano.


A las desventajas, también puede agregar que contribuir con el proyecto (usando la solicitud de extracción) con cualquier cambio también es difícil si no conoce C en el nivel adecuado.


Trabajador

Si pierde velocidad con respecto a su competidor (hablando de Swoole), entonces no es muy notable y la diferencia en varios escenarios puede ser descuidada.


Tiene integración con ReactPHP, que a su vez expande el número de implementaciones de problemas de infraestructura. Para ahorrar espacio, describiré las desventajas junto con ReactPHP.


ReactPHP

Las ventajas incluyen una comunidad bastante grande y una gran cantidad de ejemplos. Los contras comienzan a aparecer en el proceso de uso: este es el concepto de Promesa.
Si necesita realizar varias operaciones asincrónicas, el código se convierte en una papelera interminable de llamadas (aquí hay un ejemplo de una conexión simple a RabbiqMQ sin crear intercambio / cola y sus carpetas).


Con un poco de refinamiento con un archivo (considerado la norma), puede obtener una implementación de la rutina, que ayudará a deshacerse de Promise hell.


Sin el proyecto recoilphp / recoil, usar ReactPHP, en mi opinión, no es posible en una aplicación sensata.


Además, además de todo lo demás, uno tiene la sensación de que su desarrollo se ha ralentizado mucho. No es suficiente, por ejemplo, el trabajo normal con PostgreSQL.


Amplificador

En mi opinión, la mejor de las opciones que existen en este momento.
Además de la Promesa habitual, hay una implementación de Coroutine, que facilita enormemente el proceso de desarrollo y el código se ve más familiar para los programadores de PHP.


Los desarrolladores complementan y mejoran constantemente el proyecto, con la retroalimentación tampoco hay problemas.


Desafortunadamente, con todas las ventajas del marco, la comunidad es relativamente pequeña, pero al mismo tiempo hay implementaciones, por ejemplo, trabajando con PostgreSQL, así como todas las cosas básicas (sistema de archivos, cliente http, DNS, etc.).


Todavía no entiendo muy bien el destino del proyecto ext-async, pero los chicos continúan con él. Lo que saldrá de esto en la tercera versión, el tiempo lo dirá.


Empezando


Entonces, resolvimos un poco la parte teórica, es hora de pasar a practicar y llenar los baches.


Primero, formalizamos un poco los requisitos:


  • Mensajería asincrónica (el concepto de message sí puede dividirse en 2 tipos)
    • command : indica la necesidad de completar la tarea. No devuelve un resultado (al menos en el caso de comunicación asincrónica);
    • event : informa cualquier cambio de estado (por ejemplo, como resultado de un comando).
  • Formato sin bloqueo para trabajar con E / S;
  • La capacidad de aumentar fácilmente el número de procesadores;
  • Capacidad para escribir manejadores de mensajes en cualquier idioma.

Cualquier mensaje es inherentemente una estructura simple y compartida solo por la semántica. El nombramiento de mensajes es extremadamente importante desde el punto de vista de entender el tipo y el propósito (aunque este punto se ignora en el ejemplo).

Para obtener una lista de requisitos, una implementación simple del patrón Publicar / Suscribir es la más adecuada.
Para garantizar la ejecución distribuida, utilizaremos RabbitMQ como intermediario de mensajes.


El prototipo fue escrito usando ReactPHP , Bunny y DoctrineDBAL .
Un lector atento puede haber notado que Dbal usa llamadas de bloqueo pdo / mysqli internamente, pero en la etapa actual esto no era particularmente importante, ya que tenía que entender lo que debería suceder al final.


Uno de los problemas fue la falta de bibliotecas para trabajar con PostgreSQL. Hay algunos borradores, pero esto no es suficiente para el trabajo completo (más sobre esto a continuación).


Después de una breve investigación, ReactPHP se eliminó a favor de Amp, ya que es relativamente simple y se desarrolla de manera muy activa.


RabbitMQ transport

Pero con todas las ventajas de Amp, hubo 1 problema: Amp no tiene un controlador para RabbitMQ ( Bunny solo es compatible con ReactPHP).


En teoría, Amp te permite usar Promise de un competidor. Parece que todo debería ser simple, pero ReactPHP usa Event Loop para trabajar con sockets en la biblioteca.
En un momento dado, obviamente, no se pudieron iniciar dos bucles de eventos diferentes, por lo que no pude usar la función adapt () .


Desafortunadamente, la calidad del código en bunny dejó mucho que desear y no fue posible reemplazar adecuadamente una implementación con otra. Para no detener el trabajo, se decidió reescribir un poco la biblioteca para que funcione con Amp y no conduzca a bloquear el flujo de ejecución.


Esta adaptación parecía muy aterradora, todo el tiempo me daba mucha vergüenza, pero lo más importante, funcionó. Bueno, dado que no hay nada más permanente que temporal, el adaptador se anticipó a una persona que no es demasiado perezosa para involucrarse en la implementación del controlador.


Y tal hombre fue encontrado. El proyecto PHPinnacle , entre otras cosas, proporciona una implementación de un adaptador adaptado para Amp.


El nombre del autor es Anton Shabovta, quien hablará sobre php asíncrono en el marco de PHP Rusia y sobre el desarrollo de controladores para PHP fwdays .

PostgreSQL

La segunda característica del trabajo es la interacción con la base de datos. En las condiciones de PHP "tradicional", todo es simple: tenemos una conexión y todas las solicitudes se ejecutan secuencialmente.


En el caso de la ejecución asincrónica, debemos poder ejecutar simultáneamente varias solicitudes (por ejemplo, 3 transacciones). Para poder hacer esto, se requiere una implementación de grupo de conexión.


El mecanismo de trabajo es bastante simple:


  • abrimos N conexiones al inicio (o inicialización retrasada, no el punto);
  • si es necesario, tomamos la conexión del grupo, asegurando que nadie más pueda usarla;
  • Ejecutamos la solicitud y destruimos la conexión o la devolvemos al grupo (preferido).

En primer lugar, nos permite iniciar varias transacciones a la vez, y en segundo lugar, acelera el trabajo debido a la presencia de conexiones ya abiertas. Amp tiene un componente amphp / postgres . Él se encarga de las conexiones: monitorea su número, vida útil y todo esto sin bloquear el flujo de ejecución.


Por cierto, cuando utilice, por ejemplo, ReactPHP, deberá implementarlo usted mismo si desea trabajar con una base de datos.


Mutex

Para un funcionamiento efectivo y, lo más importante, adecuado de la aplicación, es necesario implementar algo similar a los mutexes. Podemos distinguir 3 escenarios para su uso:


  • Dentro del marco de un proceso, un mecanismo simple en memoria es adecuado sin ningún excedente;
  • Si queremos proporcionar el bloqueo en varios procesos, entonces podemos usar el sistema de archivos (por supuesto, en modo sin bloqueo);
  • Si en el contexto de varios servidores, ya necesita pensar en algo como Zookeeper.

Se necesitan mutexes para resolver problemas de condición de carrera . Después de todo, no sabemos (y no podemos saber) en qué orden se realizarán nuestras tareas, pero, sin embargo, debemos garantizar la integridad de los datos.


Registro / Contextos

Para iniciar sesión, Monolog ya se ha convertido en estándar, pero con algunas advertencias: no podemos utilizar los controladores integrados, ya que conducirán a bloqueos.
Para escribir en stdOut, puede tomar amphp / log , o escribir un mensaje simple enviándolo a Graylog.


Dado que en un momento dado, podemos procesar muchas tareas, y al grabar registros, debe comprender en qué contexto se escriben los datos. Durante los experimentos, se decidió hacer trace_id ( rastreo distribuido ). La conclusión es que toda la cadena de llamadas debe ir acompañada de un identificador de transferencia que pueda rastrearse. Además, en el momento de recibir el mensaje, package_id genera package_id , que indica exactamente el mensaje recibido.


Por lo tanto, usando ambos identificadores, podemos rastrear fácilmente a qué se refiere un registro en particular. La cuestión es que en PHP tradicional todos los registros que obtenemos en el registro están principalmente en el orden en que fueron escritos. En el caso de ejecución asincrónica, no hay patrón en el orden de las entradas.


Terminando

Otro de los matices del desarrollo asincrónico es controlar el cierre de nuestro demonio. Si acaba de matar el proceso, entonces no se completarán todas las tareas que están en progreso y se perderán los datos. En el enfoque habitual, también existe un problema, pero no es tan grande, porque solo se realiza una tarea a la vez.


Para completar la ejecución correctamente, necesitamos:


  • Darse de baja de la cola. En otras palabras, hacer imposible recibir nuevos mensajes;
  • Complete todas las tareas restantes (espere a resolver las promesas);
  • Y solo después de eso termina el guión.

Fugas, depuración

Contrariamente a la creencia popular, en PHP moderno no es tan simple enfrentar situaciones en las que ocurre una pérdida de memoria. Es necesario hacer algo absolutamente malo.


Sin embargo, una vez enfrentado a esto, pero debido al descuido banal. Durante la implementación de heartbeat, se agregó un nuevo temporizador cada 40 segundos para consultar la conexión. No es difícil adivinar que después de un tiempo el uso de la memoria comenzó a aumentar progresivamente.


Además, entre otras cosas, escribió un observador simple que opcionalmente comenzará cada 10 minutos y llamará a gc_collect_cycles () y gc_mem_caches () .
Pero el inicio forzado del recolector de basura no es algo necesario y fundamental.


Para ver constantemente el uso de la memoria, se agregó un MemoryUsageProcessor estándar al registro .


Si tiene la idea de que Event Loop se está bloqueando con algo, esto también se puede verificar fácilmente: solo conecte LoopBlockWatcher .


, production . .



: php-service-bus , Message Based .


, :


 composer create-project php-service-bus/skeleton pub-sub-example cd pub-sub-example docker-compose up --build -d 

, , .


/bin/consumer , .
/src 3 : Ping ; Pong : ; PingService : , .
PingService , 2 :


  /** @CommandHandler() */ public function handle(Ping $command, KernelContext $context): Promise { return $context->delivery(new Pong()); } /** @EventListener() */ public function whenPong(Pong $event, KernelContext $context): void { $context->logContextMessage('Pong message received'); } 

  • handle ( 1 ). @CommandHandler ;
    • Promise , RabbitMQ ( delivery() ). , RabbitMQ .
  • whenPongPong . . @EventListener ;
    , — . , , , . php-service-bus , , .

2 : , ( ) . , , (, ).


Ping , Pong . .


, RabbitMQ:


 tools/ping 

, php-service-bus , Message based .


Ping\Pong, — , , Hello, world .


, .


- , , , Saga pattern (Process manager) .



, symfony/messenger .


, , .

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


All Articles