Todo lo que quería saber sobre el procesamiento de consultas, pero no quería preguntar

¿Qué es un servicio de red? Este es un programa que acepta solicitudes entrantes a través de la red y las procesa, posiblemente devolviendo respuestas.


Hay muchos aspectos en los que los servicios de red difieren entre sí. En este artículo, me centro en cómo manejar las solicitudes entrantes.


Elegir un método de procesamiento de solicitudes tiene consecuencias de largo alcance. ¿Cómo hacer un servicio de chat con 100.000 conexiones simultáneas? ¿Qué enfoque tomar para extraer datos de una secuencia de archivos mal estructurados? La elección incorrecta conducirá a una pérdida de tiempo y energía.


El artículo analiza enfoques como un conjunto de procesos / subprocesos, procesamiento orientado a eventos, patrón de media sincronización / mitad de sincronización y muchos otros. Se dan numerosos ejemplos, se consideran los pros y los contras de los enfoques, se consideran sus características y aplicaciones.


Introduccion


El tema de los métodos de procesamiento de consultas no es nuevo, vea, por ejemplo: uno , dos . Sin embargo, la mayoría de los artículos lo consideran solo parcialmente. Este artículo está destinado a llenar los vacíos y proporcionar una presentación coherente del problema.


Se considerarán los siguientes enfoques:


  • procesamiento secuencial
  • proceso de solicitud
  • flujo de solicitud
  • proceso / grupo de subprocesos
  • procesamiento orientado a eventos (patrón de reactor)
  • patrón de media sincronización / mitad asíncrono
  • procesamiento de transportador

Cabe señalar que un servicio que procesa solicitudes no es necesariamente un servicio de red. Este puede ser un servicio que recibe nuevas tareas de la base de datos o la cola de tareas. En este artículo, se entiende por servicios de red, pero debe comprender que los enfoques considerados tienen un alcance más amplio.


TL; DR


Al final del artículo hay una lista con una breve descripción de cada enfoque.


Procesamiento secuencial


Una aplicación consta de un solo hilo en un solo proceso. Todas las solicitudes se procesan solo secuencialmente. No hay paralelismo. Si varias solicitudes llegan al servicio al mismo tiempo, una de ellas se procesa, el resto se pone en cola.


Además, este enfoque es fácil de implementar. No hay cerraduras y competencia por los recursos. La desventaja obvia es la incapacidad de escalar con un gran número de clientes.


Proceso de solicitud


Una aplicación consiste en un proceso central que acepta solicitudes entrantes y flujos de trabajo. Para cada nueva solicitud, el proceso principal crea un flujo de trabajo que procesa la solicitud. Escalar por el número de solicitudes es simple: cada solicitud tiene su propio proceso.


No hay nada complicado en esta arquitectura, pero tiene los problemas limitaciones :


  • El proceso consume muchos recursos.
    Intente crear 10,000 conexiones simultáneas a PostgreSQL RDBMS y observe el resultado.
  • Los procesos no tienen memoria compartida (predeterminado). Si necesita acceso a datos compartidos o un caché compartido, tendrá que mapear la memoria compartida (llamando a linux mmap, munmap) o usar almacenamiento externo (memcahed, redis)

Estos problemas no se detienen de ninguna manera. A continuación se mostrará cómo se gestionan en PostgeSQL RDBMS.


Pros de esta arquitectura:


  • La caída de uno de los procesos no afectará a los demás. Por ejemplo, un error de procesamiento de un caso raro no descartará toda la aplicación, solo la solicitud procesada sufrirá
  • Diferenciación de derechos de acceso a nivel del sistema operativo. Dado que el proceso es la esencia del sistema operativo, puede utilizar sus mecanismos estándar para delimitar los derechos de acceso a los recursos del sistema operativo
  • Puede cambiar el proceso de ejecución sobre la marcha. Por ejemplo, si se utiliza un script separado para procesar una solicitud, para reemplazar el algoritmo de procesamiento, es suficiente cambiar el script. Un ejemplo será considerado a continuación.
  • Máquinas multinúcleo utilizadas eficientemente

Ejemplos:


  • PostgreSQL RDBMS crea un nuevo proceso para cada nueva conexión. La memoria compartida se usa para trabajar con datos generales. PostgreSQL puede manejar el alto consumo de recursos de los procesos de muchas maneras diferentes. Si hay pocos clientes (un stand dedicado para analistas), entonces no hay tal problema. Si hay una sola aplicación que accede a la base de datos, puede crear un grupo de conexiones de base de datos en el nivel de la aplicación. Si hay muchas aplicaciones, puede usar pgbouncer
  • sshd escucha las solicitudes entrantes en el puerto 22 y la bifurcación en cada conexión. Cada conexión ssh es una bifurcación del demonio sshd que recibe y ejecuta los comandos del usuario en secuencia. Gracias a esta arquitectura, los recursos del sistema operativo se utilizan para diferenciar los derechos de acceso.
  • Un ejemplo de nuestra propia práctica. Hay una secuencia de archivos no estructurados de los que necesita obtener metadatos. El proceso de servicio principal distribuye archivos entre los procesos del controlador. Cada proceso de controlador es un script que toma una ruta de archivo como parámetro. El procesamiento de archivos ocurre en un proceso separado, por lo tanto, debido a un error de procesamiento, el servicio completo no se bloquea. Para actualizar el algoritmo de procesamiento, es suficiente cambiar los scripts de procesamiento sin detener el servicio.

En general, debo decir que este enfoque tiene sus ventajas, que determinan su alcance, pero la escalabilidad es muy limitada.


Solicitar flujo


Este enfoque es muy parecido al anterior. La diferencia es que se utilizan hilos en lugar de procesos. Esto le permite usar la memoria compartida fuera de la caja. Sin embargo, las otras ventajas del enfoque anterior ya no se pueden usar, mientras que el consumo de recursos también será alto.


Pros:


  • Memoria compartida lista para usar
  • Facilidad de implementación
  • Uso eficiente de CPU multi-core

Contras:


  • Una secuencia consume muchos recursos. En sistemas operativos tipo Unix, un subproceso consume casi tantos recursos como un proceso.

Un ejemplo de uso es MySQL. Pero debe tenerse en cuenta que MySQL utiliza un enfoque mixto, por lo que este ejemplo se discutirá en la siguiente sección.


Proceso / grupo de subprocesos


Los flujos (procesos) crean costosos y largos. Para no desperdiciar recursos, puede usar el mismo hilo repetidamente. Habiendo limitado adicionalmente el número máximo de hilos, obtenemos un grupo de hilos (procesos). Ahora el hilo principal acepta solicitudes entrantes y las pone en cola. Los flujos de trabajo toman solicitudes de la cola y las procesan. Este enfoque se puede tomar como la escala natural del procesamiento secuencial de solicitudes: cada subproceso de trabajo solo puede procesar flujos secuencialmente, agruparlos le permite procesar solicitudes en paralelo. Si cada transmisión puede manejar 1000 rps, entonces 5 transmisiones manejarán la carga cerca de 5000 rps (sujeto a una competencia mínima por los recursos compartidos).


La agrupación se puede crear de antemano al inicio del servicio o formarse gradualmente. Usar un grupo de subprocesos es más común ya que le permite aplicar memoria compartida.


El tamaño del grupo de subprocesos no tiene que ser limitado. Un servicio puede usar subprocesos libres del grupo y, si no hay ninguno, crear un nuevo subproceso. Después de procesar la solicitud, el subproceso se une al grupo y espera la siguiente solicitud. Esta opción es una combinación de un enfoque de subprocesos bajo solicitud y un grupo de subprocesos. Un ejemplo se dará a continuación.


Pros:


  • el uso de muchos núcleos de CPU
  • reducción de costos para crear un hilo / proceso

Contras:


  • Escalabilidad limitada en el número de clientes concurrentes. El uso del grupo nos permite reutilizar el mismo subproceso varias veces sin costos de recursos adicionales, sin embargo, no resuelve el problema fundamental de una gran cantidad de recursos consumidos por el subproceso / proceso. La creación de un servicio de chat que pueda soportar 100,000 conexiones simultáneas utilizando este enfoque fallará.
  • La escalabilidad está limitada por los recursos compartidos, por ejemplo, si los subprocesos usan memoria compartida ajustando el acceso a ella mediante semáforos / mutexes. Esta es una limitación de todos los enfoques que utilizan recursos compartidos.

Ejemplos:


  1. Aplicación Python que se ejecuta con uWSGI y nginx. El proceso principal de uWSGI recibe solicitudes entrantes de nginx y las distribuye entre los procesos de Python del intérprete que procesan las solicitudes. La aplicación se puede escribir en cualquier marco compatible con uWSGI: Django, Flask, etc.
  2. MySQL usa un grupo de subprocesos: cada nueva conexión es procesada por uno de los subprocesos libres del grupo. Si no hay hilos libres, MySQL crea un nuevo hilo. El tamaño del grupo de subprocesos libres y el número máximo de subprocesos (conexiones) están limitados por la configuración.

Quizás este sea uno de los enfoques más comunes para construir servicios de red, si no el más común. Le permite escalar bien, alcanzando grandes rps. La principal limitación del enfoque es el número de conexiones de red procesadas simultáneamente. De hecho, este enfoque funciona bien solo si las solicitudes son cortas o pocos clientes.


Procesamiento orientado a eventos (patrón de reactor)


Dos paradigmas, sincrónico y asincrónico, son eternos competidores entre sí. Hasta ahora, solo se han discutido enfoques sincrónicos, pero sería un error ignorar el enfoque asincrónico. El procesamiento de solicitudes orientado a eventos o reactivo es un enfoque en el que cada operación de E / S se realiza de forma asíncrona y, al final de la operación, se llama a un controlador. Como regla general, el procesamiento de cada solicitud consta de muchas llamadas asíncronas seguidas de la ejecución de controladores. En cualquier momento, una aplicación de un solo subproceso ejecuta el código de un solo controlador, pero la ejecución de los controladores de varias solicitudes se alterna entre sí, lo que le permite procesar simultáneamente (pseudo-paralelo) muchas solicitudes paralelas.


Una discusión completa de este enfoque está más allá del alcance de este artículo. Para una mirada más profunda, puede recomendar Reactor (Reactor) , ¿Cuál es el secreto de la velocidad de NodeJS? , Dentro de NGINX . Aquí nos limitamos a considerar los pros y los contras de este enfoque.


Pros:


  • Escalado efectivo por rps y el número de conexiones simultáneas. Un servicio reactivo puede procesar simultáneamente una gran cantidad de conexiones (decenas de miles) si la mayoría de las conexiones están esperando que se complete la E / S

Contras:


  • La complejidad del desarrollo. La programación en estilo asíncrono es más difícil que en síncrono. La lógica del procesamiento de solicitudes es más compleja, la depuración también es más difícil que en el código síncrono.
  • Errores que conducen al bloqueo de todo el servicio. Si el idioma o el tiempo de ejecución no están diseñados originalmente para el procesamiento asíncrono, entonces una sola operación sincrónica puede bloquear todo el servicio, negando la posibilidad de escalar.
  • Difícil de escalar a través de los núcleos de la CPU. Este enfoque supone un solo subproceso en un solo proceso, por lo que no puede usar múltiples núcleos de CPU al mismo tiempo. Cabe señalar que hay formas de evitar esta limitación.
  • Corolario del párrafo anterior: este enfoque no escala bien para solicitudes que requieren CPU. El número de rps para este enfoque es inversamente proporcional al número de operaciones de CPU requeridas para procesar cada solicitud. Exigir solicitudes de CPU niega las ventajas de este enfoque.

Ejemplos:


  1. Node.js utiliza el patrón de reactor listo para usar. Para obtener más detalles, consulte ¿Cuál es el secreto de la velocidad de NodeJS?
  2. nginx: los procesos de trabajo de nginx utilizan el patrón del reactor para procesar solicitudes en paralelo. Ver Inside NGINX para más detalles.
  3. Programa C / C ++ que usa directamente herramientas del sistema operativo (epoll en Linux, IOCP en Windows, kqueue en FreeBSD), o usa el marco (libev, libevent, libuv, etc.).

Media sincronización / mitad asíncrona


El nombre se toma de POSA: Patrones para objetos concurrentes y en red . En el original, este patrón se interpreta de manera muy amplia, pero para los propósitos de este artículo, entenderé este patrón de manera algo más estrecha. Half sync / half async es un enfoque de procesamiento de solicitudes que utiliza un flujo de control ligero (hilo verde) para cada solicitud. Un programa consta de uno o más subprocesos a nivel del sistema operativo, sin embargo, el sistema de ejecución del programa admite subprocesos verdes que el sistema operativo no ve y no puede controlar.


Algunos ejemplos para hacer la consideración más específica:


  1. Servicio en idioma Go. El lenguaje Go admite muchos hilos de ejecución livianos: goroutine. El programa utiliza uno o más subprocesos del sistema operativo, pero el programador opera con goroutines, que se distribuyen de manera transparente entre los subprocesos del sistema operativo para utilizar CPU de múltiples núcleos.
  2. Servicio de Python con biblioteca gevent. La biblioteca gevent permite al programador usar hilos verdes en el nivel de la biblioteca. Todo el programa se ejecuta en un solo hilo del sistema operativo.

En esencia, este enfoque está diseñado para combinar el alto rendimiento del enfoque asincrónico con la simplicidad de la programación de código síncrono.


Usando este enfoque, a pesar de la ilusión de sincronismo, el programa funcionará de forma asíncrona: el sistema de ejecución del programa controlará el bucle de eventos y cada operación "sincrónica" en realidad será asíncrona. Cuando se llama a una operación de este tipo, el sistema de ejecución llamará a la operación asincrónica utilizando herramientas del sistema operativo y registrará un controlador de finalización de la operación. Cuando se completa la operación asincrónica, el sistema de ejecución llamará al controlador registrado previamente, que continuará ejecutando el programa en el punto de invocación de la operación "sincrónica".


Como resultado, el enfoque de media sincronización / mitad asincrónica contiene algunas ventajas y algunas desventajas del enfoque asincrónico. El volumen del artículo no nos permite considerar este enfoque en detalle. Para aquellos interesados, le aconsejo que lea el capítulo del mismo nombre en el libro POSA: Patrones para objetos concurrentes y en red .


El enfoque de media sincronización / mitad asincrónica en sí introduce una nueva entidad de "flujo verde": un flujo de control ligero a nivel del programa o del sistema de ejecución de la biblioteca. Qué hacer con hilos verdes es una elección del programador. Puede usar un grupo de hilos verdes, puede crear un nuevo hilo verde para cada nueva solicitud. La diferencia en comparación con los subprocesos / procesos del sistema operativo es que los subprocesos verdes son mucho más baratos: consumen mucha menos RAM y se crean mucho más rápido. Esto le permite crear una gran cantidad de hilos verdes, por ejemplo, cientos de miles en el idioma Go. Una cantidad tan grande justifica el uso del enfoque verde de flujo a pedido.


Pros:


  • Se escala bien en rps y la cantidad de conexiones simultáneas
  • El código es más fácil de escribir y depurar en comparación con el enfoque asincrónico

Contras:


  • Dado que la ejecución de operaciones es realmente asíncrona, los errores de programación son posibles cuando una sola operación síncrona bloquea todo el proceso. Esto se siente especialmente en lenguajes donde este enfoque se implementa mediante una biblioteca, por ejemplo Python.
  • La opacidad del programa. Cuando se utilizan subprocesos o procesos del sistema operativo, el algoritmo de ejecución del programa es claro: cada subproceso / proceso realiza operaciones en la secuencia en la que están escritos en el código. Utilizando el enfoque de media sincronización / mitad asincrónica, las operaciones que se escriben secuencialmente en el código pueden alternar de manera impredecible con operaciones que procesan solicitudes concurrentes.
  • No apto para sistemas en tiempo real. El procesamiento asíncrono de solicitudes complica enormemente la provisión de garantías para el tiempo de procesamiento de cada solicitud individual. Esto es una consecuencia del párrafo anterior.

Dependiendo de la implementación, este enfoque escala bien en todos los núcleos de CPU (Golang) o no escala en absoluto (Python).
Este enfoque, además de asíncrono, le permite manejar una gran cantidad de conexiones simultáneas. Pero programar un servicio usando este enfoque es más fácil porque El código está escrito en un estilo sincrónico.


Procesamiento del transportador


Como su nombre lo indica, en este enfoque, las solicitudes se procesan por canalización. El proceso de procesamiento consta de varios subprocesos del sistema operativo dispuestos en una cadena. Cada hilo es un enlace en la cadena; realiza un cierto subconjunto de las operaciones necesarias para procesar la solicitud. Cada solicitud pasa secuencialmente a través de todos los enlaces de la cadena, y diferentes enlaces en cada momento procesan diferentes solicitudes.


Pros:


  • Este enfoque escala bien en rps. Cuantos más enlaces en la cadena, más solicitudes se procesan por segundo.
  • El uso de múltiples subprocesos le permite escalar bien en los núcleos de la CPU.

Contras:


  • No todas las categorías de consulta son adecuadas para este enfoque. Por ejemplo, organizar encuestas largas utilizando este enfoque será difícil e inconveniente.
  • La complejidad de la implementación y la depuración. Batir el procesamiento secuencial para que la productividad sea alta puede ser difícil. La depuración de un programa en el que cada solicitud se procesa secuencialmente en varios subprocesos paralelos es más difícil que el procesamiento secuencial.

Ejemplos:


  1. Un ejemplo interesante de procesamiento de transportadores se describió en el informe highload 2018 La evolución de la arquitectura del sistema de negociación y compensación de la Bolsa de Moscú

La canalización se usa ampliamente, pero la mayoría de las veces los enlaces son componentes individuales en procesos independientes que intercambian mensajes, por ejemplo, a través de una cola de mensajes o una base de datos.


Resumen


Un breve resumen de los enfoques considerados:


  • Procesamiento sincrónico.
    Un enfoque simple, pero muy limitado en escalabilidad, tanto en rps como en el número de conexiones simultáneas. No permite el uso de múltiples núcleos de CPU simultáneamente.
  • Un nuevo proceso para cada solicitud.
    . , . . ( , ).
  • .
    , , . , .
  • /.
    /. . rps . . .
  • - (reactor ).
    rps . - , . CPU
  • Half sync/half async.
    rps . CPU (Golang) (Python). , () . reactor , , reactor .
  • .
    , . (, long polling ).

, .


: ? , ?


Referencias


  1. Artículos relacionados:
  2. - :
  3. :
  4. Half sync/half async:
  5. :
  6. :

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


All Articles