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 n
eventos 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=1
y 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 SELECT
consultas 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.
fuente
ANALYZE
de 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.select null
en 500 conexiones. Apuesto a que encontrará que el grupo de conexiones no es el problema allí.Respuestas:
Flujo de entrada
No está claro si sus 1000 eventos / segundo representan picos o si es una carga continua:
Solución propuesta
Intuitivamente, en ambos casos, optaría por una secuencia de eventos basada en Kafka :
Esto es altamente escalable en todos los niveles:
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í:
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):
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.
fuente
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?Use multiple databases
idea, 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?Supongo que necesita explorar más cuidadosamente un enfoque que ha rechazado
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).
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).
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.
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.
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.Si entiendo correctamente, el flujo actual es:
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.
fuente
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.
fuente
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.
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.
fuente
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.
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.
fuente
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
fuente