Bloques de construcción de aplicaciones distribuidas. Segunda aproximación

anuncio

Colegas, a mediados del verano planeo publicar otra serie de artículos sobre el diseño de sistemas de colas: "El experimento VTrade", un intento de escribir un marco para los sistemas comerciales. La serie examinará la teoría y la práctica de la construcción de una bolsa, una subasta y una tienda. Al final del artículo te invito a votar por los temas que más te interesen.

Bloques de construcción de aplicaciones distribuidas. Segunda aproximación

Este es el último artículo de la serie sobre aplicaciones reactivas distribuidas en Erlang/Elixir. EN primer artículo puedes encontrar los fundamentos teóricos de la arquitectura reactiva. Segundo artículo ilustra los patrones y mecanismos básicos para construir tales sistemas.

Hoy plantearemos cuestiones de desarrollo del código base y proyectos en general.

organización de servicios

En la vida real, al desarrollar un servicio, a menudo es necesario combinar varios patrones de interacción en un controlador. Por ejemplo, el servicio de usuarios, que resuelve el problema de gestionar los perfiles de usuario del proyecto, debe responder a las solicitudes req-resp e informar sobre las actualizaciones del perfil a través de pub-sub. Este caso es bastante simple: detrás de la mensajería hay un controlador que implementa la lógica del servicio y publica actualizaciones.

La situación se vuelve más complicada cuando necesitamos implementar un servicio distribuido tolerante a fallas. Imaginemos que los requisitos para los usuarios han cambiado:

  1. ahora el servicio debería procesar solicitudes en 5 nodos del clúster,
  2. ser capaz de realizar tareas de procesamiento en segundo plano,
  3. y también poder administrar dinámicamente listas de suscripción para actualizaciones de perfiles.

Nota: No consideramos la cuestión del almacenamiento consistente y la replicación de datos. Supongamos que estos problemas se resolvieron antes y que el sistema ya tiene una capa de almacenamiento confiable y escalable, y que los controladores tienen mecanismos para interactuar con ella.

La descripción formal del servicio a los usuarios se ha vuelto más complicada. Desde el punto de vista de un programador, los cambios son mínimos debido al uso de la mensajería. Para satisfacer el primer requisito, necesitamos configurar el equilibrio en el punto de intercambio req-resp.

El requisito de procesar tareas en segundo plano ocurre con frecuencia. En el caso de los usuarios, esto podría consistir en comprobar documentos de usuario, procesar archivos multimedia descargados o sincronizar datos con las redes sociales. redes. Estas tareas deben distribuirse de alguna manera dentro del clúster y monitorearse el progreso de la ejecución. Por lo tanto, tenemos dos opciones de solución: usar la plantilla de distribución de tareas del artículo anterior o, si no nos conviene, escribir un programador de tareas personalizado que administre el grupo de procesadores de la forma que necesitemos.

El punto 3 requiere la extensión de plantilla pub-sub. Y para la implementación, después de crear un punto de intercambio pub-sub, debemos iniciar adicionalmente el controlador de este punto dentro de nuestro servicio. Por lo tanto, es como si estuviéramos trasladando la lógica para procesar suscripciones y bajas desde la capa de mensajería a la implementación de los usuarios.

Como resultado, la descomposición del problema mostró que para cumplir con los requisitos, necesitamos iniciar 5 instancias del servicio en diferentes nodos y crear una entidad adicional: un controlador pub-sub, responsable de la suscripción.
Para ejecutar 5 controladores, no es necesario cambiar el código de servicio. La única acción adicional es establecer reglas de equilibrio en el punto de intercambio, de lo que hablaremos un poco más adelante.
También existe una complejidad adicional: el controlador pub-sub y el programador de tareas personalizado deben funcionar en una sola copia. Nuevamente el servicio de mensajería, como fundamental, debe proporcionar un mecanismo para seleccionar un líder.

Elección del líder

En los sistemas distribuidos, la elección de líder es el procedimiento para designar un proceso único responsable de programar el procesamiento distribuido de alguna carga.

En sistemas que no son propensos a la centralización, se utilizan algoritmos universales y basados ​​en consenso, como paxos o raft.
Dado que la mensajería es un intermediario y un elemento central, conoce a todos los controladores de servicios: los líderes candidatos. La mensajería puede nombrar un líder sin votar.

Después de iniciar y conectarse al punto de intercambio, todos los servicios reciben un mensaje del sistema #'$leader'{exchange = ?EXCHANGE, pid = LeaderPid, servers = Servers}. Si LeaderPid coincide con pid proceso actual, se le nombra líder y la lista Servers Incluye todos los nodos y sus parámetros.
En el momento en que aparece uno nuevo y se desconecta un nodo del clúster en funcionamiento, todos los controladores de servicio reciben #'$slave_up'{exchange = ?EXCHANGE, pid = SlavePid, options = SlaveOpts} и #'$slave_down'{exchange = ?EXCHANGE, pid = SlavePid, options = SlaveOpts} respectivamente.

De esta manera, todos los componentes están al tanto de todos los cambios y se garantiza que el clúster tendrá un líder en un momento dado.

Mediadores

Para implementar procesos complejos de procesamiento distribuido, así como en problemas de optimización de una arquitectura existente, es conveniente utilizar intermediarios.
Para no cambiar el código del servicio y resolver, por ejemplo, problemas de procesamiento adicional, enrutamiento o registro de mensajes, puede habilitar un controlador proxy antes del servicio, que realizará todo el trabajo adicional.

Un ejemplo clásico de optimización pub-sub es una aplicación distribuida con un núcleo empresarial que genera eventos de actualización, como cambios de precios en el mercado, y una capa de acceso: N servidores que proporcionan una API websocket para clientes web.
Si lo decides de frente, el servicio al cliente se verá así:

  • el cliente establece conexiones con la plataforma. Del lado del servidor que finaliza el tráfico, se inicia un proceso para dar servicio a esta conexión.
  • En el contexto del proceso de servicio, se produce la autorización y suscripción a actualizaciones. El proceso llama al método de suscripción para temas.
  • Una vez que se genera un evento en el kernel, se entrega a los procesos que dan servicio a las conexiones.

Imaginemos que tenemos 50000 suscriptores al tema “noticias”. Los suscriptores se distribuyen uniformemente en 5 servidores. Como resultado, cada actualización que llegue al punto de intercambio se replicará 50000 veces: 10000 veces en cada servidor, según el número de suscriptores que tenga. No es un plan muy eficaz, ¿verdad?
Para mejorar la situación, introduzcamos un proxy que tenga el mismo nombre que el punto de intercambio. El registrador de nombres global debe poder devolver el proceso más cercano por nombre, esto es importante.

Iniciemos este proxy en los servidores de la capa de acceso, y todos nuestros procesos que sirven a la API de websocket se suscribirán a él, y no al punto de intercambio pub-sub original en el kernel. El proxy se suscribe al núcleo solo en el caso de una suscripción única y replica el mensaje entrante a todos sus suscriptores.
Como resultado, se enviarán 5 mensajes entre el kernel y los servidores de acceso, en lugar de 50000.

Enrutamiento y equilibrio

Req-Resp

En la implementación de mensajería actual, existen 7 estrategias de distribución de solicitudes:

  • default. La solicitud se envía a todos los controladores.
  • round-robin. Las solicitudes se enumeran y distribuyen cíclicamente entre los controladores.
  • consensus. Los controladores que atienden el servicio se dividen en líderes y esclavos. Las solicitudes se envían únicamente al líder.
  • consensus & round-robin. El grupo tiene un líder, pero las solicitudes se distribuyen entre todos los miembros.
  • sticky. La función hash se calcula y asigna a un controlador específico. Las solicitudes posteriores con esta firma van al mismo administrador.
  • sticky-fun. Al inicializar el punto de intercambio, la función de cálculo hash para sticky equilibrio.
  • fun. Similar a Sticky-Fun, solo que usted puede redirigirlo, rechazarlo o preprocesarlo adicionalmente.

La estrategia de distribución se establece cuando se inicializa el punto de intercambio.

Además de equilibrar, la mensajería le permite etiquetar entidades. Veamos los tipos de etiquetas en el sistema:

  • Etiqueta de conexión. Le permite comprender a través de qué conexión surgieron los eventos. Se utiliza cuando un proceso de controlador se conecta al mismo punto de intercambio, pero con diferentes claves de enrutamiento.
  • Etiqueta de servicio. Le permite combinar controladores en grupos para un servicio y ampliar las capacidades de enrutamiento y equilibrio. Para el patrón req-resp, el enrutamiento es lineal. Enviamos una solicitud al punto de cambio y luego este la pasa al servicio. Pero si necesitamos dividir los controladores en grupos lógicos, entonces la división se realiza mediante etiquetas. Al especificar una etiqueta, la solicitud se enviará a un grupo específico de controladores.
  • Solicitar etiqueta. Le permite distinguir entre respuestas. Dado que nuestro sistema es asíncrono, para procesar las respuestas del servicio debemos poder especificar una RequestTag al enviar una solicitud. A partir de él podremos entender la respuesta a la que nos llegó la solicitud.

pub-sub

Para pub-sub todo es un poco más sencillo. Disponemos de un punto de intercambio al que se publican los mensajes. El punto de intercambio distribuye mensajes entre los suscriptores que se han suscrito a las claves de enrutamiento que necesitan (podemos decir que esto es análogo a los temas).

Escalabilidad y tolerancia a fallos

La escalabilidad del sistema en su conjunto depende del grado de escalabilidad de las capas y componentes del sistema:

  • Los servicios se escalan agregando nodos adicionales al clúster con controladores para este servicio. Durante la operación de prueba, puede elegir la política de equilibrio óptima.
  • El servicio de mensajería en sí dentro de un clúster separado generalmente se escala moviendo puntos de intercambio particularmente cargados a nodos separados del clúster o agregando procesos proxy a áreas particularmente cargadas del clúster.
  • La escalabilidad de todo el sistema como característica depende de la flexibilidad de la arquitectura y de la capacidad de combinar grupos individuales en una entidad lógica común.

El éxito de un proyecto a menudo depende de la simplicidad y la velocidad de escalado. La mensajería en su versión actual crece junto con la aplicación. Incluso si nos falta un grupo de 50 a 60 máquinas, podemos recurrir a la federación. Desafortunadamente, el tema de la federación está fuera del alcance de este artículo.

Reserva

Al analizar el equilibrio de carga, ya analizamos la redundancia de los controladores de servicios. Sin embargo, la mensajería también debe reservarse. En caso de que un nodo o una máquina falle, la mensajería debería recuperarse automáticamente y en el menor tiempo posible.

En mis proyectos utilizo nodos adicionales que recogen la carga en caso de caída. Erlang tiene una implementación de modo distribuido estándar para aplicaciones OTP. El modo distribuido realiza la recuperación en caso de falla iniciando la aplicación fallida en otro nodo iniciado previamente. El proceso es transparente; después de una falla, la aplicación se mueve automáticamente al nodo de conmutación por error. Puedes leer más sobre esta funcionalidad. aquí.

Rendimiento

Intentemos al menos comparar aproximadamente el rendimiento de Rabbitmq y nuestra mensajería personalizada.
encontré resultados oficiales Pruebas de Rabbitmq del equipo de OpenStack.

En el párrafo 6.14.1.2.1.2.2. El documento original muestra el resultado del RPC CAST:
Bloques de construcción de aplicaciones distribuidas. Segunda aproximación

No realizaremos ninguna configuración adicional en el kernel del sistema operativo ni en la VM erlang por adelantado. Condiciones para la prueba:

  • erl opta: +A1 +sbtu.
  • La prueba dentro de un único nodo erlang se ejecuta en una computadora portátil con un i7 antiguo en versión móvil.
  • Las pruebas de cluster se realizan en servidores con red 10G.
  • El código se ejecuta en contenedores acoplables. Red en modo NAT.

Código de prueba:

req_resp_bench(_) ->
  W = perftest:comprehensive(10000,
    fun() ->
      messaging:request(?EXCHANGE, default, ping, self()),
      receive
        #'$msg'{message = pong} -> ok
      after 5000 ->
        throw(timeout)
      end
    end
  ),
  true = lists:any(fun(E) -> E >= 30000 end, W),
  ok.

Script 1: La prueba se ejecuta en una computadora portátil con una versión móvil i7 antigua. La prueba, la mensajería y el servicio se ejecutan en un nodo en un contenedor Docker:

Sequential 10000 cycles in ~0 seconds (26987 cycles/s)
Sequential 20000 cycles in ~1 seconds (26915 cycles/s)
Sequential 100000 cycles in ~4 seconds (26957 cycles/s)
Parallel 2 100000 cycles in ~2 seconds (44240 cycles/s)
Parallel 4 100000 cycles in ~2 seconds (53459 cycles/s)
Parallel 10 100000 cycles in ~2 seconds (52283 cycles/s)
Parallel 100 100000 cycles in ~3 seconds (49317 cycles/s)

Script 2: 3 nodos ejecutándose en diferentes máquinas bajo Docker (NAT).

Sequential 10000 cycles in ~1 seconds (8684 cycles/s)
Sequential 20000 cycles in ~2 seconds (8424 cycles/s)
Sequential 100000 cycles in ~12 seconds (8655 cycles/s)
Parallel 2 100000 cycles in ~7 seconds (15160 cycles/s)
Parallel 4 100000 cycles in ~5 seconds (19133 cycles/s)
Parallel 10 100000 cycles in ~4 seconds (24399 cycles/s)
Parallel 100 100000 cycles in ~3 seconds (34517 cycles/s)

En todos los casos, la utilización de la CPU no superó el 250%.

resultados

Espero que este ciclo no parezca un volcado de mente y que mi experiencia sea de verdadero beneficio tanto para los investigadores de sistemas distribuidos como para los profesionales que están comenzando a construir arquitecturas distribuidas para sus sistemas comerciales y están mirando Erlang/Elixir con interés. , pero tengo dudas si vale la pena...

Galleria @chuttersnap

Solo los usuarios registrados pueden participar en la encuesta. Registrarsepor favor

¿Qué temas debería cubrir con más detalle como parte de la serie VTrade Experiment?

  • Teoría: Mercados, órdenes y su timing: DAY, GTD, GTC, IOC, FOK, MOO, MOC, LOO, LOC

  • Libro de pedidos. Teoría y práctica de la implementación de un libro con agrupaciones.

  • Visualización de operaciones: ticks, barras, resoluciones. Cómo almacenar y cómo pegar.

  • Oficina administrativa. Planificación y desarrollo. Seguimiento de empleados e investigación de incidentes.

  • API. Averigüemos qué interfaces se necesitan y cómo implementarlas.

  • Almacenamiento de información: PostgreSQL, Timescale, Tarantool en sistemas comerciales

  • Reactividad en los sistemas comerciales.

  • Otro. escribiré en los comentarios

6 usuarios votaron. 4 usuarios se abstuvieron.

Fuente: habr.com

Añadir un comentario