Guardar eventos de alta frecuencia en una base de datos restringida por límite de conexión

13

Tenemos una situación en la que tengo que lidiar con una afluencia masiva de eventos que llegan a nuestro servidor, en promedio, alrededor de 1000 eventos por segundo (el pico podría ser de ~ 2000).

El problema

Nuestro sistema está alojado en Heroku y utiliza un Heroku Postgres DB , que permite un máximo de 500 conexiones DB. Utilizamos la agrupación de conexiones para conectar desde el servidor a la base de datos.

Los eventos llegan más rápido de lo que la agrupación de conexiones de DB puede manejar

El problema que tenemos es que los eventos son más rápidos de lo que puede manejar el grupo de conexiones. En el momento en que una conexión finaliza el viaje de ida y vuelta de la red desde el servidor a la base de datos, para que pueda volver a liberarse en la agrupación, entran más de n de eventos adicionales.

Eventualmente, los eventos se acumulan, en espera de ser guardados y debido a que no hay conexiones disponibles en el grupo, expiran y todo el sistema se vuelve inoperante.

Hemos resuelto la emergencia al emitir los eventos ofensivos de alta frecuencia a un ritmo más lento de los clientes, pero aún queremos saber cómo manejar estos escenarios en caso de que necesitemos manejar esos eventos de alta frecuencia.

Restricciones

Otros clientes pueden querer leer eventos al mismo tiempo

Otros clientes solicitan continuamente leer todos los eventos con una clave en particular, incluso si aún no están guardados en la base de datos.

Un cliente puede consultar GET api/v1/events?clientId=1 y obtener todos los eventos enviados por el cliente 1, incluso si esos eventos todavía no se han guardado en la base de datos.

¿Hay ejemplos de "aula" sobre cómo lidiar con esto?

Posibles soluciones

Encolar los eventos en nuestro servidor

Podríamos poner en cola los eventos en el servidor (con la cola teniendo una concurrencia máxima de 400 para que el grupo de conexiones no se agote).

Esto es mala idea porque:

  • Se consumirá la memoria del servidor disponible. Los eventos en cola apilados consumirán enormes cantidades de RAM.
  • Nuestros servidores se reinician una vez cada 24 horas . Este es un límite máximo impuesto por Heroku. El servidor puede reiniciarse mientras los eventos se ponen en cola, lo que hace que perdamos los eventos en cola.
  • Introduce el estado en el servidor, dañando así la escalabilidad. Si tenemos una configuración de varios servidores y un cliente desea leer todos los eventos en cola + guardados, no sabremos en qué servidor se encuentran los eventos en cola.

Usar una cola de mensajes separada

Supongo que podríamos usar una cola de mensajes, (como RabbitMQ ?), donde bombeamos los mensajes en él y en el otro Al final hay otro servidor que solo se ocupa de guardar los eventos en la base de datos.

No estoy seguro de si las colas de mensajes permiten consultar eventos en cola (que aún no se guardaron), por lo que si otro cliente quiere leer los mensajes de otro cliente, puedo obtener los mensajes guardados de la base de datos y los mensajes pendientes. de la cola y concatenarlos juntos para que pueda enviarlos de vuelta al cliente de solicitud de lectura.

Utilice varias bases de datos, cada una de ellas guarda una parte de los mensajes con un servidor central de coordinación de base de datos para gestionarlos

Otra solución que hemos implementado es utilizar varias bases de datos, con un "coordinador de DB / equilibrador de carga" central. Al recibir un evento se este coordinador elegiría una de las bases de datos para escribir el mensaje. Esto debería permitirnos utilizar múltiples bases de datos Heroku, por lo tanto, aumentar el límite de conexión a 500 x número de bases de datos.

En una consulta de lectura, este coordinador podría emitir consultas SELECT a cada base de datos, fusionar todos los resultados y enviarlos de vuelta al cliente que solicitó la lectura.

Esto es mala idea porque:

  • Esta idea suena como ... ejem ... ¿sobre ingeniería? Sería una pesadilla para gestionar también (copias de seguridad, etc.). Es complicado de construir y mantener, y a menos que sea absolutamente necesario, suena como una violación de KISS .
  • Se sacrifica Consistencia . Hacer transacciones a través de múltiples bases de datos es un no-go si vamos con esta idea.
pregunta Nik Kyriakides 22.09.2018 - 07:34

7 respuestas

9

flujo de entrada

No está claro si sus 1000 eventos / segundo representan picos o si es una carga continua:

  • si es un pico, podría usar una cola de mensajes como búfer para distribuir la carga en el servidor de base de datos durante más tiempo;
  • si se trata de una carga constante, la cola de mensajes por sí sola no es suficiente, ya que el servidor DB nunca podrá ponerse al día. Entonces deberías pensar en una base de datos distribuida.

Solución propuesta

Intuitivamente, en ambos casos, me gustaría ir a una Kafka flujo de eventos:

  • Todos los eventos se publican sistemáticamente en un kafka topic
  • Un consumidor se suscribiría a los eventos y los almacenaría en la base de datos.
  • Un procesador de consultas manejará las solicitudes de los clientes y consultará la base de datos.

Esto es altamente escalable en todos los niveles:

  • Si el servidor DB es el cuello de botella, simplemente agregue varios consumidores. Cada uno podría suscribirse al tema y escribir en un servidor de base de datos diferente. Sin embargo, si la distribución se produce aleatoriamente en los servidores de base de datos, el procesador de consultas no podrá predecir el servidor de base de datos que tomará y tendrá que consultar varios servidores de base de datos. Esto podría llevar a un nuevo cuello de botella en el lado de la consulta.
  • Por lo tanto, se podría anticipar el esquema de distribución de la base de datos organizando el flujo de eventos en varios temas (por ejemplo, utilizando grupos de claves o propiedades, para particionar la base de datos según una lógica predecible).
  • Si un servidor de mensajes no es suficiente para manejar una creciente inundación de eventos de entrada, puede agregar kafka particitions para distribuir los temas de kafka en varios servidores físicos.

Ofreciendo eventos a los clientes que aún no están escritos en la base de datos

Desea que sus clientes puedan acceder también a la información que todavía está en la tubería y que aún no se ha escrito en la base de datos. Esto es un poco más delicado.

Opción 1: usar un caché para complementar las consultas de db

No he analizado en profundidad, pero la primera idea que se me ocurre es hacer que los procesadores de consultas sean consumidores de los temas kafka, pero en un kafka consumer group . El procesador de solicitudes recibiría todos los mensajes que recibirá el escritor de la base de datos, pero de manera independiente. Entonces podría mantenerlos en un caché local. Las consultas se ejecutarían en DB + caché (+ eliminación de duplicados).

El diseño se vería así:

Laescalabilidaddeestacapadeconsultasepodríalograragregandomásprocesadoresdeconsulta(cadaunoensupropiogrupodeconsumidores).

Opción2:diseñarunaAPIdual

UnmejorenfoqueenmihumildeopiniónseríaofrecerunaAPIdual(useelmecanismodelgrupodeconsumidoresseparado):

  • unaAPIdeconsultaparaaccederaeventosenlabasededatosy/orealizaranálisis
  • unaAPIdetransmisiónquesoloreenvíamensajesdirectamentedesdeeltema

Laventaja,esquedejasqueelclientedecidaquéeslointeresante.Estopodríaevitarquefusionesistemáticamentelosdatosdelabasededatoscondatosreciéncobrados,cuandoelclientesoloestáinteresadoennuevoseventosentrantes.Siladelicadafusiónentreeventosnuevosyarchivadosesrealmentenecesaria,entonceselclientetendríaqueorganizarla.

Variantes

Propusekafkaporqueestádiseñadopara volúmenes muy altos con mensajes persistentes para que pueda reiniciar los servidores si es necesario.

Podrías construir una arquitectura similar con RabbitMQ. Sin embargo, si necesita colas persistentes, puede disminuir el rendimiento . Además, que yo sepa, la única forma de lograr el consumo paralelo de los mismos mensajes por parte de varios lectores (por ejemplo, writer + cache) con RabbitMQ es clonar las colas . Por lo tanto, una mayor escalabilidad podría tener un precio más alto.

    
respondido por el Christophe 22.09.2018 - 20:16
11

Mi opinión es que necesita explorar más detenidamente un enfoque que haya rechazado

  • Encolar los eventos en nuestro servidor

Mi sugerencia sería comenzar a leer los diversos artículos publicados sobre la arquitectura LMAX . Lograron que los lotes de gran volumen funcionen para su caso de uso, y es posible que sus compromisos se parezcan más a los suyos.

Además, es posible que desee ver si puede eliminar las lecturas, idealmente, le gustaría poder escalarlas independientemente de las escrituras. Eso puede significar buscar en CQRS (comando de segregación de responsabilidad de consulta).

  

El servidor puede reiniciarse mientras los eventos se ponen en cola, lo que hace que perdamos los eventos en cola.

En un sistema distribuido, creo que puedes estar bastante seguro de que los mensajes se perderán. Es posible que pueda mitigar parte del impacto de eso al ser juicioso acerca de las barreras de secuencia (por ejemplo, asegurarse de que la escritura en el almacenamiento duradero ocurra, antes de que el evento se comparta fuera del sistema).

  • Utilice varias bases de datos, cada una de ellas guarda una parte de los mensajes con un servidor central de coordinación de base de datos para administrarlos

Tal vez: sería más probable que observara los límites de su negocio para ver si hay lugares naturales para compartir los datos.

  

¿Hay casos en los que perder datos es una compensación aceptable?

Bueno, supongo que podría haber, pero no es a eso a donde iba. El punto es que el diseño debería haber incorporado la robustez necesaria para progresar ante la pérdida de mensajes.

Lo que a menudo se ve es un modelo basado en extracción con notificaciones. El proveedor escribe los mensajes en un almacén duradero ordenado. El consumidor extrae los mensajes de la tienda, rastreando su propia marca de agua. Las notificaciones push se utilizan como un dispositivo de reducción de la latencia, pero si se pierde la notificación, el mensaje aún se obtiene (eventualmente) porque el consumidor está siguiendo un horario regular (la diferencia es que si se recibe la notificación, la extracción se produce antes ).

Consulte Mensajería confiable sin transacciones distribuidas, por Udi Dahan (ya referenciado por Andy ) y Data Polyglot de Greg Young.

    
respondido por el VoiceOfUnreason 22.09.2018 - 14:21
6

Si entiendo correctamente, el flujo actual es:

  1. Recepción y evento (¿Supongo que a través de HTTP?)
  2. Solicitar una conexión de la agrupación.
  3. Inserte el evento en la base de datos
  4. Libere la conexión al grupo.

Si es así, creo que el primer cambio en el diseño sería dejar de tener el código de manejo uniforme para devolver las conexiones al grupo en cada evento. En su lugar, cree un grupo de subprocesos / procesos de inserción que sea 1 a 1 con el número de conexiones de base de datos. Cada uno tendrá una conexión DB dedicada.

Al usar algún tipo de cola concurrente, estos subprocesos obtienen mensajes de la cola concurrente e insertan. En teoría, nunca deben devolver la conexión a la agrupación o solicitar una nueva, pero es posible que tenga que construir un manejo en caso de que la conexión se estropee. Podría ser más fácil eliminar el proceso / hilo e iniciar uno nuevo.

Esto debería eliminar efectivamente la sobrecarga del grupo de conexiones. Por supuesto, deberá poder realizar eventos de al menos 1000 / conexiones por segundo en cada conexión. Es posible que desee probar diferentes números de conexiones, ya que tener 500 conexiones trabajando en las mismas tablas podría crear contienda en la base de datos, pero esa es una pregunta completamente diferente. Otra cosa a considerar es el uso de inserciones por lotes, es decir, cada hilo extrae una serie de mensajes y los inserta a la vez. Además, evite tener varias conexiones que intenten actualizar las mismas filas.

    
respondido por el JimmyJames 24.09.2018 - 16:39
5

Suposiciones

Voy a asumir que la carga que describe es constante, ya que ese es el escenario más difícil de resolver.

También asumiré que tiene alguna forma de ejecutar cargas de trabajo de larga ejecución desencadenadas fuera del proceso de su aplicación web.

Solución

Suponiendo que haya identificado correctamente su cuello de botella (latencia entre su proceso y la base de datos de Postgres), ese es el problema principal por resolver. La solución debe tener en cuenta su restricción de coherencia con otros clientes que desean leer los eventos tan pronto como sea posible después de que se reciban.

Para resolver el problema de la latencia, debe trabajar de una manera que minimice la cantidad de latencia incurrida por evento que se almacenará. Esto es lo que debe lograr si no está dispuesto o no puede cambiar hardware . Dado que estás en los servicios de PaaS y no tienes control sobre el hardware o la red, la única forma de reducir la latencia por evento será con algún tipo de escritura de eventos por lotes.

Deberá almacenar una cola de eventos localmente que se desechan y se escriben periódicamente en su base de datos, ya sea una vez que alcanza un tamaño determinado o después de un período de tiempo transcurrido. Un proceso deberá supervisar esta cola para desencadenar la descarga al almacén. Debería haber muchos ejemplos en torno a cómo administrar una cola concurrente que se vacíe periódicamente en el idioma de su elección: Aquí hay un ejemplo en C # , del popular sumidero de lotes periódico de la biblioteca de registro de Serilog.

Esta respuesta de SO describe la forma más rápida de vaciar datos en Postgres, aunque requeriría que la tienda almacene en la cola la cola. disco, y es probable que haya un problema que resolver allí cuando su disco desaparece al reiniciar en Heroku.

Restricción

Otra respuesta ya ha mencionado CQRS , y ese es el enfoque correcto para resolver la restricción. Desea hidratar los modelos de lectura a medida que se procesa cada evento: un patrón de mediador puede ayudar a encapsular un evento y distribuirlo a varios manejadores en proceso. Por lo tanto, un controlador puede agregar el evento a su modelo de lectura que está en la memoria y los clientes pueden consultar, y otro controlador puede ser responsable de poner en cola el evento para su escritura final por lotes.

El beneficio clave de CQRS es que desacopla sus modelos conceptuales de lectura y escritura, lo que es una forma elegante de decir que escribe en un modelo y que lee de otro modelo totalmente diferente. Para obtener los beneficios de escalabilidad de CQRS, generalmente querrá asegurarse de que cada modelo se almacene por separado de manera óptima para sus patrones de uso. En este caso, podemos usar un modelo de lectura agregado, por ejemplo, un caché Redis, o simplemente en memoria, para garantizar que nuestras lecturas sean rápidas y consistentes, mientras que todavía usamos nuestra base de datos transaccional para escribir nuestros datos.

    
respondido por el Andrew Best 24.09.2018 - 12:17
3
  

Los eventos llegan más rápido de lo que la agrupación de conexiones de DB puede manejar

Este es un problema si cada proceso necesita una conexión de base de datos. El sistema debe estar diseñado para que tenga un grupo de trabajadores donde cada trabajador solo necesite una conexión de base de datos y cada trabajador pueda procesar múltiples eventos.

La cola de mensajes se puede usar con ese diseño, necesita productores de mensajes que envíen eventos a la cola de mensajes y los trabajadores (consumidores) procesan los mensajes de la cola.

  

Otros clientes pueden querer leer eventos al mismo tiempo

Esta restricción solo es posible si los eventos se almacenan en la base de datos sin ningún procesamiento (eventos sin procesar). Si los eventos se procesan antes de almacenarlos en la base de datos, la única forma de obtenerlos es desde la base de datos.

Si los clientes solo desean consultar eventos sin procesar, yo sugeriría usar un motor de búsqueda como Elastic Search. Incluso obtendrás la API de consulta / búsqueda de forma gratuita.

Dado que parece importante consultar los eventos antes de guardarlos en la base de datos, una solución simple como Elastic Search debería funcionar. Básicamente, simplemente almacena todos los eventos en él y no duplica los mismos datos al copiarlos en la base de datos.

Scaling Elastic Search es fácil, pero incluso con una configuración básica tiene un alto rendimiento.

Cuando necesite procesamiento, su proceso puede obtener los eventos de ES, procesarlos y almacenarlos en la base de datos. No sé cuál es el nivel de rendimiento que necesita de este procesamiento, pero estaría completamente separado de consultar los eventos de ES. De todos modos, no debería tener problemas de conexión, ya que puede tener un número fijo de trabajadores y cada uno con una conexión de base de datos.

    
respondido por el imel96 24.09.2018 - 11:53
2

Los eventos de 1k o 2k (5KB) por segundo no son tanto para una base de datos si tienen un esquema adecuado y un motor de almacenamiento. Como lo sugiere @eddyce, un maestro con uno o más esclavos puede separar las consultas de lectura de las escrituras de confirmación. Usar menos conexiones de base de datos le proporcionará un mejor rendimiento general.

  

Otros clientes pueden querer leer eventos al mismo tiempo

Para estas solicitudes, también tendrían que leer desde la db maestra, ya que habría un retraso de replicación para los esclavos leídos.

He usado (Percona) MySQL con el motor TokuDB para escrituras de muy alto volumen. También hay un motor MyRocks basado en LSMtrees que es bueno para escribir cargas. Tanto para estos motores como para PostgreSQL probablemente hay configuraciones para el aislamiento de las transacciones, así como para cometer un comportamiento de sincronización que puede aumentar dramáticamente la capacidad de escritura. En el pasado, aceptamos hasta 1s datos perdidos que se informaron al cliente de db como confirmados. En otros casos, había SSD con respaldo de batería para evitar pérdidas.

Se afirma que Amazon RDS Aurora en la versión de MySQL tiene un rendimiento de escritura 6x más alto con replicación de costo cero (similar a los esclavos que comparten un sistema de archivos con el maestro). El sabor Aurora PostgreSQL también tiene un mecanismo de replicación avanzado diferente.

    
respondido por el karmakaze 08.10.2018 - 01:22
1

Soltaría Heroku todos juntos, es decir, soltaría un enfoque centralizado: las escrituras múltiples que alcanzan el pico máximo de la conexión de la agrupación es una de las razones principales por las que se inventaron los clusters db, principalmente porque no cargar los db (s) de escritura con solicitudes de lectura que pueden ser realizadas por otros db en el clúster, probaría con una topología maestro-esclavo, además, como ya mencionó alguien más, tener sus propias instalaciones de db permitiría ajuste todo el sistema para asegurarse de que el tiempo de propagación de consultas se maneje correctamente.

Buena suerte

    
respondido por el Edoardo 25.09.2018 - 00:45

Lea otras preguntas en las etiquetas