Guardar eventos de alta frecuencia en una base de datos restringida de 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, a aproximadamente 1000 eventos por segundo, en promedio (el pico podría ser ~ 2000).

El problema

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

Los eventos llegan más rápido de lo que el grupo de conexiones DB puede manejar

El problema que tenemos es que los eventos llegan más rápido de lo que el grupo de conexiones puede manejar. En el momento en que una conexión ha finalizado el viaje de ida y vuelta de la red desde el servidor a la base de datos, para que pueda liberarse de nuevo al grupo, más que neventos adicionales entran.

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

Hemos resuelto la emergencia emitiendo los eventos ofensivos de alta frecuencia a un ritmo más lento por parte de los clientes, pero aún queremos saber cómo manejar estos escenarios en el 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 particular, incluso si aún no están guardados en la base de datos.

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

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

Soluciones posibles

Poner en cola los eventos en nuestro servidor

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

Esta es una mala idea porque:

  • Se comerá la memoria del servidor disponible. Los eventos en cola apilados consumirán grandes cantidades de RAM.
  • Nuestros servidores se reinician una vez cada 24 horas . Este es un límite duro impuesto por Heroku. El servidor puede reiniciarse mientras los eventos están en cola, lo que nos hace perder los eventos en cola.
  • Introduce el estado en el servidor, lo que perjudica la escalabilidad. Si tenemos una configuración de servidores múltiples y un cliente quiere leer todos los eventos en cola + guardados, no sabremos en qué servidor viven los eventos en cola.

Use una cola de mensajes separada

Supongo que podríamos usar una cola de mensajes (¿como RabbitMQ ?), Donde bombeamos los mensajes y en el otro extremo 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 desea 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.

Use múltiples bases de datos, cada una de las cuales guarda una parte de los mensajes con un servidor central coordinador de DB para administrarlos

Sin embargo, otra solución que tenemos es utilizar múltiples bases de datos, con un "coordinador de DB / equilibrador de carga" central. Al recibir un evento, este coordinador elegiría una de las bases de datos para escribir el mensaje. Esto debería permitirnos usar múltiples bases de datos Heroku, aumentando así el límite de conexión a 500 x número de bases de datos.

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

Esta es una mala idea porque:

  • Esta idea suena como ... ejem ... ¿sobre ingeniería? Sería una pesadilla para administrar 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 .
  • Sacrifica la consistencia . Hacer transacciones a través de múltiples bases de datos es imposible si seguimos con esta idea.
Nik Kyriakides
fuente
3
¿Dónde está tu cuello de botella? Está mencionando su grupo de conexiones, pero eso solo influye en el paralelismo, no en la velocidad por inserción. Si tiene 500 conexiones y, por ejemplo, 2000QPS, esto debería funcionar bien si cada consulta se completa dentro de 250 ms, lo cual es un tiempo muy largo. ¿Por qué está por encima de 15 ms? También tenga en cuenta que al usar un PaaS está renunciando a importantes oportunidades de optimización, como escalar el hardware de la base de datos o usar réplicas de lectura para reducir la carga en la base de datos primaria. Heroku no vale la pena a menos que la implementación sea su mayor problema.
amon
@amon El cuello de botella es de hecho el conjunto de conexiones. Me he ocupado ANALYZEde las consultas y no son un problema. También construí un prototipo para probar la hipótesis del grupo de conexiones y verifiqué que este es realmente el problema. La base de datos y el servidor en sí viven en diferentes máquinas, de ahí la latencia. Además, no queremos renunciar a Heroku a menos que sea absolutamente necesario, no preocuparnos por las implementaciones es una gran ventaja para nosotros.
Nik Kyriakides
1
Dicho esto, entiendo que hay micro optimizaciones que podría hacer que me ayudarán a resolver el problema actual . Me pregunto si hay una solución arquitectónica escalable para mi problema.
Nik Kyriakides
3
¿Cómo verificó exactamente que el problema es el grupo de conexiones? @amon es correcto en sus cálculos. Intente emitir select nullen 500 conexiones. Apuesto a que encontrará que el grupo de conexiones no es el problema allí.
usr
1
Si seleccionar nulo es problemático, entonces probablemente tenga razón. Aunque sería interesante dónde se pasa todo ese tiempo. Ninguna red es tan lenta.
usr

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, puede usar una cola de mensajes como búfer para distribuir la carga en el servidor de base de datos durante más tiempo;
  • Si es una carga constante, la cola de mensajes por sí sola no es suficiente, porque el servidor de base de datos nunca podrá ponerse al día. Entonces deberías pensar en una base de datos distribuida.

Solución propuesta

Intuitivamente, en ambos casos, optaría por una secuencia de eventos basada en Kafka :

  • Todos los eventos se publican sistemáticamente sobre un tema kafka.
  • 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 de base de datos 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 bases de datos, el procesador de consultas no podrá predecir el servidor de bases de datos que tendrá que tomar y tendrá que consultar varios servidores de bases de datos. Esto podría conducir a un nuevo cuello de botella en el lado de la consulta.
  • Por lo tanto, el esquema de distribución de la base de datos podría anticiparse organizando el flujo de eventos en varios temas (por ejemplo, utilizando grupos de claves o propiedades, para dividir la base de datos de acuerdo con una lógica predecible).
  • Si un servidor de mensajes no es suficiente para manejar una creciente avalancha de eventos de entrada, puede agregar particiones kafka para distribuir temas kafka en varios servidores físicos.

Ofrecer eventos aún no escritos en la base de datos a los clientes

Desea que sus clientes puedan tener acceso también a la información que aún está en trámite y que aún no está escrita en la base de datos. Esto es un poco más delicado.

Opción 1: uso de un caché para complementar las consultas 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 de kafka, pero en un grupo de consumidores de kafka diferente . El procesador de solicitudes recibiría todos los mensajes que recibirá el escritor de DB, pero de forma independiente. Luego 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í:

ingrese la descripción de la imagen aquí

La escalabilidad de esta capa de consulta podría lograrse agregando más procesadores de consultas (cada uno en su propio grupo de consumidores).

Opción 2: diseñar una API dual

Un mejor enfoque en mi humilde opinión sería ofrecer una API dual (usar el mecanismo del grupo de consumidores por separado):

  • una API de consulta para acceder a eventos en la base de datos y / o realizar análisis
  • una API de transmisión que solo reenvía mensajes directamente desde el tema

La ventaja es que dejas que el cliente decida qué es interesante. Esto podría evitar que combine sistemáticamente los datos de la base de datos con datos recién cobrados, cuando el cliente solo está interesado en nuevos eventos entrantes. Si la delicada fusión entre eventos nuevos y archivados es realmente necesaria, entonces el cliente tendría que organizarla.

Variantes

Propuse kafka porque está diseñado para 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, escritor + caché) con RabbitMQ es clonar las colas . Por lo tanto, una mayor escalabilidad podría tener un precio más alto.

Christophe
fuente
Estelar; ¿Qué quieres decir con a distributed database (for example using a specialization of the server by group of keys)? ¿También por qué Kafka en lugar de RabbitMQ? ¿Hay alguna razón particular para elegir una sobre la otra?
Nik Kyriakides
@NicholasKyriakides ¡Gracias! 1) Simplemente estaba pensando en varios servidores de bases de datos independientes, pero con un esquema de partición claro (clave, geografía, etc.) que podría usarse para enviar los comandos de manera efectiva. 2) Intuitivamente , tal vez porque Kafka está diseñado para un rendimiento muy alto con mensajes persistentes, ¿necesita reiniciar sus servidores?). No estoy seguro de que RabbitMQ sea tan flexible para los escenarios distribuidos, y las colas persistentes disminuyen el rendimiento
Christophe
Para 1) Entonces, esto es bastante similar a mi Use multiple databasesidea, pero usted dice que no debería distribuir los mensajes de forma aleatoria (o por turnos) a cada una de las bases de datos. ¿Derecho?
Nik Kyriakides
Si. Mi primer pensamiento sería no optar por una distribución aleatoria porque podría aumentar la carga de procesamiento de las consultas (es decir, la consulta de ambos DB múltiples la mayor parte del tiempo). También podría considerar los motores de DB distribuidos (por ejemplo, ¿Encender?). Pero hacer una elección informada requeriría una buena comprensión de los patrones de uso de la base de datos (qué más hay en la base de datos, con qué frecuencia se consulta, qué tipo de consultas, existen restricciones transaccionales más allá de los eventos individuales, etc.).
Christophe
3
Solo quiero decir que, aunque kafka puede proporcionar un rendimiento muy alto, probablemente esté más allá de las necesidades de la mayoría de las personas. Descubrí que tratar con kafka y su API fue un gran error para nosotros. RabbitMQ no se queda atrás y tiene una interfaz que esperarías de un MQ
imel96
11

Supongo que necesita explorar más cuidadosamente un enfoque que ha rechazado

  • Poner en cola los eventos en nuestro servidor

Mi sugerencia sería comenzar a leer los diversos artículos publicados sobre la arquitectura LMAX . Se las arreglaron para hacer que el procesamiento por lotes de gran volumen funcione para su caso de uso, y es posible que sus compensaciones se parezcan más a las de ellos.

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 (segregación de responsabilidad de consulta de comando).

El servidor puede reiniciarse mientras los eventos están en cola, lo que nos hace perder 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 siendo prudente acerca de las barreras de secuencia (por ejemplo, asegurando que la escritura en el almacenamiento duradero ocurra antes de que el evento se comparta fuera del sistema).

  • Use múltiples bases de datos, cada una de las cuales guarda una parte de los mensajes con un servidor central coordinador de DB para administrarlos

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

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

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

Lo que a menudo parece es un modelo basado en extracción con notificaciones. El proveedor escribe los mensajes en una tienda duradera ordenada. El consumidor saca los mensajes de la tienda, rastreando su propia marca de límite superior. Las notificaciones push se usan como un dispositivo de reducción de latencia, pero si la notificación se pierde, el mensaje aún se recupera (eventualmente) porque el consumidor está haciendo un cronograma regular (la diferencia es que si se recibe la notificación, la extracción ocurre antes) )

Consulte Mensajes confiables sin transacciones distribuidas, de Udi Dahan (ya referenciado por Andy ) y Polyglot Data de Greg Young.

VoiceOfUnreason
fuente
In a distributed system, I think you can be pretty confident that messages are going to get lost. De Verdad? ¿Hay casos en los que perder datos es una compensación aceptable? Tenía la impresión de que perder datos = falla.
Nik Kyriakides
1
@NicholasKyriakides, generalmente no es aceptable, por lo tanto, OP sugirió la posibilidad de escribir en una tienda duradera antes de emitir el evento. Consulte este artículo y este video de Udi Dahan donde aborda el problema con más detalle.
Andy
6

Si entiendo correctamente, el flujo actual es:

  1. Recibir y evento (supongo a través de HTTP?)
  2. Solicitar una conexión de la piscina.
  3. Insertar el evento en la base de datos
  4. Libere la conexión a la piscina.

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

Usando algún tipo de cola concurrente, luego hace que estos hilos extraigan mensajes de la cola concurrente y los inserten. En teoría, nunca necesitan devolver la conexión al grupo o solicitar una nueva, pero es posible que deba incorporar el manejo en caso de que la conexión se dañe. Puede ser más fácil matar el hilo / proceso e iniciar uno nuevo.

Esto debería eliminar efectivamente la sobrecarga del grupo de conexiones. Por supuesto, deberá poder empujar al menos 1000 / eventos de 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 una contención en la base de datos, pero esa es una pregunta completamente diferente. Otra cosa a considerar es el uso de insertos por lotes, es decir, cada hilo extrae una cantidad de mensajes y los empuja todos a la vez. Además, evite que varias conexiones intenten actualizar las mismas filas.

JimmyJames
fuente
5

Supuestos

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

También voy a suponer que tiene alguna forma de ejecutar cargas de trabajo activadas y de larga duración fuera del proceso de su aplicación web.

Solución

Suponiendo que ha identificado correctamente su cuello de botella (latencia entre su proceso y la base de datos de Postgres), ese es el problema principal a 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 recibirlos.

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 clave que debe lograr si no está dispuesto o no puede cambiar el hardware . Dado que usted está en los servicios de PaaS y no tiene control sobre el hardware o la red, la única forma de reducir la latencia por evento será con algún tipo de escritura por lotes de eventos.

Deberá almacenar una cola de eventos localmente que se vacíe y escriba periódicamente en su base de datos, ya sea una vez que alcance un tamaño determinado o después de un período de tiempo transcurrido. Un proceso necesitará monitorear esta cola para activar el vaciado a la tienda. Debería haber muchos ejemplos sobre cómo administrar una cola simultánea que se vacía periódicamente en el idioma de su elección: Aquí hay un ejemplo en C # , del sumidero de lotes periódico de la popular biblioteca de registro Serilog.

Esta respuesta SO describe la forma más rápida de vaciar datos en Postgres, aunque requeriría que su lote almacene la cola en el disco, y es probable que se resuelva un problema allí cuando su disco desaparezca 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 múltiples controladores en proceso. Por lo tanto, un controlador puede agregar el evento a su modelo de lectura que está en la memoria que los clientes pueden consultar, y otro controlador puede ser responsable de poner en cola el evento para su eventual escritura por lotes.

El beneficio clave de CQRS es que desacoplas tus modelos conceptuales de lectura y escritura, que es una forma elegante de decir que escribes en un modelo y lees de otro modelo totalmente diferente. Para obtener beneficios de escalabilidad de CQRS, generalmente desea asegurarse de que cada modelo se almacene por separado de una manera que sea óptima para sus patrones de uso. En este caso, podemos usar un modelo de lectura agregada, 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.

Andrew Best
fuente
3

Los eventos llegan más rápido de lo que el grupo de conexiones 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 que envíen eventos a la cola de mensajes y los trabajadores (consumidores) procesen los mensajes desde la cola.

Otros clientes pueden querer leer eventos al mismo tiempo

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

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

Dado que parece que consultar eventos antes de que se guarden en la base de datos es importante para usted, una solución simple como Elastic Search debería funcionar. Básicamente, solo almacena todos los eventos en él y no duplica los mismos datos copiándolos en la base de datos.

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

Cuando necesita 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 sería completamente diferente de consultar los eventos desde ES. No debería tener problemas de conexión de todos modos, ya que puede tener un número fijo de trabajadores y cada uno con una conexión de base de datos.

imel96
fuente
2

1k o 2k eventos (5KB) por segundo no es tanto para una base de datos si tiene un esquema y un motor de almacenamiento adecuados. Según lo sugerido por @eddyce, un maestro con uno o más esclavos puede separar las consultas de lectura de las escrituras de confirmación. El uso de menos conexiones DB le dará un mejor rendimiento general.

Otros clientes pueden querer leer eventos al mismo tiempo

Para estas solicitudes, también tendrían que leer del db maestro, ya que habría un retraso de replicación para los esclavos de lectura.

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 cargas de escritura. Para estos dos motores y probablemente también para PostgreSQL, hay configuraciones para el aislamiento de transacciones y el comportamiento de sincronización de confirmación que puede aumentar drásticamente la capacidad de escritura. En el pasado, aceptamos hasta 1s de datos perdidos que se informaron al cliente db como confirmados. En otros casos, había SSD respaldados por batería para evitar pérdidas.

Se afirma que Amazon RDS Aurora en el estilo MySQL tiene un rendimiento de escritura 6 veces mayor 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.

karmakaze
fuente
TBH cualquier base de datos bien administrada en hardware suficiente debería ser capaz de hacer frente a esta carga. El problema de OP no parece ser el rendimiento de la base de datos sino la latencia de la conexión; Supongo que Heroku como proveedor de PaaS les está vendiendo una instancia de Postgres en una región diferente de AWS.
amon
1

Soltaría heroku todos juntos, es decir, abandonaría un enfoque centralizado: las escrituras múltiples que alcanzan el máximo de la conexión de grupo máxima es una de las razones principales por las que se inventan los clústeres de db, principalmente porque no se carga la escritura db (s) con solicitudes de lectura que pueden ser realizadas por otros db's en el clúster, intentaría con una topología maestro-esclavo, además, como alguien más ya mencionó, tener sus propias instalaciones de db haría posible ajustar todo sistema para asegurarse de que el tiempo de propagación de consultas se manejará correctamente.

Buena suerte

Edoardo
fuente