Elegir una tecnología de colas con Postgres
(adriano.fyi)- Antes de incorporar un sistema de colas aparte, si puedes construir una cola de trabajos en segundo plano usando solo Postgres, que ya utilizas, puedes aprovechar mucho la simplicidad operativa y la familiaridad del equipo
- Desde Postgres 9.5,
NOTIFY/LISTENyFOR UPDATE SKIP LOCKEDresuelven en conjunto tanto la notificación de nuevos trabajos como la prevención de procesamiento duplicado entre workers - Redis, Kafka, RabbitMQ y Amazon SQS son opciones poderosas, pero una nueva dependencia añade modos de fallo y costo de aprendizaje a los entornos de desarrollo, pruebas y operación
- Las colas con Postgres tampoco son una solución universal: el push/pop puede ser más lento que en Redis, y pueden aparecer costos operativos como una base de datos o servidor de colas aparte y
VACUUMmás frecuentes - Es más seguro abstraer el código de trabajos en segundo plano para que no quede acoplado al backend de cola, y evaluar otras tecnologías de colas solo cuando la tecnología conocida no cumpla con los requisitos
Cómo funciona una cola con Postgres
- La tecnología de colas con Postgres se compone de dos elementos
- pub/sub para anunciar y recibir nuevos trabajos
- bloqueo de filas para evitar que varios workers procesen el mismo trabajo
- Ambas funciones vienen incluidas desde Postgres 9.5, lanzado en 2016
- Si usas
NOTIFYyLISTENjuntos, puedes añadir pub/sub a la aplicación FOR UPDATE SKIP LOCKEDtoma un bloqueo de fila sobre los registros que cumplen la condición y omite los que ya están bloqueados- Si se aplica a registros de trabajos, se puede crear una consulta de procesamiento de colas como
SELECT * FROM jobs ORDER BY created_at FOR UPDATE SKIP LOCKED LIMIT 1
- Si se aplica a registros de trabajos, se puede crear una consulta de procesamiento de colas como
SKIP LOCKEDofrece una vista “inconsistente” de los datos, pero esa característica es útil en una cola- Los trabajos que ya se están procesando no son visibles para otros workers debido al bloqueo de fila
- Este comportamiento permite la exclusión mutua distribuida
- Si
NOTIFYavisa a los workers que están enLISTENsobre nuevos trabajos, se puede construir tanto procesamiento por lotes periódico como procesamiento en tiempo real
Cómo Redis se volvió la opción por defecto para trabajos en segundo plano
- Decidir cómo procesar trabajos de larga duración es una elección tecnológica con varios trade-offs
- Las opciones más usadas de colas y message brokers son las siguientes
- Redis: almacén de datos en memoria y backend de muchas librerías de trabajos en segundo plano
- Apache Kafka: plataforma distribuida de event streaming mantenida por Apache Foundation
- RabbitMQ: presentado como un message broker ampliamente desplegado
- Amazon SQS: SaaS de Amazon que ofrece colas altamente escalables
- En el topic de background jobs de GitHub, las 5 librerías más populares están basadas en Redis
- Redis almacena datos en memoria, por lo que inserciones y consultas son rápidas, y ofrece una API de pub/sub y estructuras de datos como
listyset, que encajan bien para implementar colas - La escalabilidad de Redis hace que muchos desarrolladores lo tomen como opción por defecto, y las opciones por defecto tienen mucho peso al elegir tecnología
Criterios a evaluar antes que la escalabilidad
- La obsesión del sector tecnológico con el “scale” puede dejar en segundo plano la simplicidad, la facilidad de mantenimiento y la reducción de la carga cognitiva para desarrolladores
- Muchas aplicaciones no requieren una escala al nivel de Google, Facebook o Uber
- Al iniciar un proyecto o negocio, en lugar de optimizar solo para escalabilidad desde el principio, conviene priorizar estos criterios
- una tecnología que el equipo conozca bien
- una tecnología suficientemente buena que cumpla con las necesidades de los usuarios
- la solución que mejor encaje con la capacidad del equipo y requiera menos esfuerzo
- Postgres también escala bien en la práctica, pero no es el sistema especializado de colas que más escala para este caso de uso
- Como software de propósito general, Postgres puede manejar bastantes tareas razonablemente bien, y las colas son una de ellas
Decidir con una “tecnología aburrida”
- Al elegir una tecnología de colas, la pregunta más importante es qué tecnología ya usas y entiendes bien
- Una tecnología que ya utilizas y conoces bien añade menos carga al stack de software
- Si ya usas una base de datos relacional y esa base de datos es Postgres, puedes considerar primero una cola con Postgres antes que otro software
- La “tecnología aburrida” no es una lista fija, sino que depende de las tecnologías que ya están en uso
- Para aplicaciones centradas en mensajería, RabbitMQ puede ser la tecnología aburrida
- Para aplicaciones centradas en caché, Redis puede ser la tecnología aburrida
- Para aplicaciones con muchos datos relacionales, Postgres puede ser la tecnología aburrida
- Incorporar Redis, Kafka, RabbitMQ o SQS solo para trabajos en segundo plano puede salir caro
- Se añade una nueva dependencia del sistema a los entornos de desarrollo, pruebas y producción
- En el futuro, Developers, DBAs y SREs tendrán que conocer los modos de fallo y la configuración del nuevo sistema
- Se necesita conocimiento para recuperación ante fallos, diagnóstico de problemas y monitoreo de rendimiento
- Los unknown unknowns que el equipo todavía no conoce siguen siendo un riesgo
El costo de una cola con Postgres y cómo elegir
- La tecnología aburrida no es todopoderosa, y Postgres no es la excepción
- Al elegir una cola con Postgres, obtienes ventajas como familiaridad, modos de fallo conocidos y distribución de costos, pero puedes pagar estos costos
- El push/pop de una cola con Postgres es bastante más lento que en Redis
- En lugar de una sola base de datos relacional, podrías necesitar una base de datos de aplicación y otra para la cola
- Puede ser necesario un servidor de base de datos aparte para escalar los trabajos en segundo plano de forma independiente
- Puede hacer falta
VACUUMmás frecuente, con el consiguiente costo de rendimiento
- Ninguno de Postgres, Redis, Kafka, RabbitMQ o SQS debería convertirse automáticamente en la opción por defecto
- Elegir tecnología sigue siendo un trade-off continuo y debe decidirse según el equipo y los requisitos de la aplicación
- En caso de duda, puede aplicarse este criterio: “considerar alternativas solo cuando se haya demostrado que la tecnología aburrida no cumple con los requisitos”
Una arquitectura de trabajos en segundo plano que permita cambiar de cola
- El código que procesa trabajos en segundo plano no debería depender de una tecnología de cola específica
- A medida que la aplicación crece, podrían añadirse tecnologías como memcached o Redis según sea necesario, y con el tiempo también podrían convertirse en “tecnología aburrida” para el equipo
- Si abstraes la tecnología de colas, los usuarios pueden elegir la cola adecuada para cada trabajo
- Entre las librerías de trabajos en segundo plano más populares en GitHub, salvo Hangfire, ninguna ofrece una vía de escape hacia una tecnología de cola distinta de Redis
- En una estructura así, cambiar de cola obliga a reescribir el código de la aplicación
- Neoq es una librería para Go creada con esa filosofía
- El backend de cola puede ser en memoria, Postgres o Redis
- El usuario puede inicializar otro backend de cola sin cambiar el código de la aplicación
- Las implementaciones en memoria y Postgres son de primera clase, y la implementación de Redis usa asynq
- Redis, Kafka, RabbitMQ y SQS pueden ser tecnologías excelentes, pero no siempre son la cola adecuada para ese trabajo ni tienen siempre la complejidad apropiada
1 comentarios
Opiniones de Hacker News
Hasta ahora he creado tres sistemas de trabajos distribuidos, y la regla práctica que recomiendo desde hace años es “diseña para 10 veces la escala actual”
Si tienes 70 solicitudes por segundo, piensa en 700; si necesitas 20 servidores de trabajos batch, toma como referencia 200. Incluso una startup que crece 100% al año estará a 8 veces su tamaño en 3 años, así que hay tiempo para reescribir mientras crece
En el primer sistema que hice evité SQL por “escalabilidad”, pero sufrí mucho al encontrar muchos casos excepcionales que requerían integridad transaccional
Los dos sistemas de trabajos distribuidos que hice después usan PostgreSQL como coordinador y están armados, literalmente, alrededor de
SELECT FOR UPDATE SKIP LOCKED. Uno controla normalmente 350 workers, y el otro aplica prioridades complejas a miles de trabajosAmbos aguantarán sin problemas hasta llegar a ingresos anuales de varios millones de dólares, y por ejemplo el de 350 workers podría escalar hasta unas 2,000 CPU con solo algunos ajustes
La tecnología de escala masiva parece bastante barata hasta que te das cuenta de que necesitas transacciones. Pero desde ese momento, imitar semántica transaccional sobre un almacenamiento de consistencia eventual se vuelve una pesadilla de ingeniería
Así que hay que sentarse y hacer los cálculos. Si la empresa factura 100 millones de dólares al año, qué tan grande tendría que ser el sistema distribuido, si es fácil conseguir una instancia de PostgreSQL que maneje esa carga, o si alcanza con sharding por cliente; y, si es posible, vale la pena considerar seriamente PostgreSQL. Cien cosas pequeñas se vuelven más fáciles
Las soluciones SQL suelen ser más simples, consumen menos recursos de cómputo y son más fáciles de operar
Eso sí, para hacerlas funcionar hay que conocer la base de datos lo suficiente como para saber que existen funciones como
SELECT FOR UPDATE SKIP LOCKED. Hoy muchos ingenieros crecen en entornos donde ORMs pesados bloquean las verdaderas capacidades del DBMS, así que este conocimiento se está volviendo bastante raroEl sistema debe soportar la carga pico, y si no conoces el pico, debes dejar margen de seguridad o incorporar una forma de descartar trabajos o posponerlos cuando sea necesario
Todo es una concesión: hay que optimizar solo lo que necesita optimización, y la capacidad de decidir qué es eso es una característica de un buen ingeniero
Si esto es nuevo para ti, acabas de justificar una semana leyendo HN en horario laboral; vale la pena escribirlo en un papel y pegarlo en tu tarjeta del cajero automático
En varios proyectos elegí un enfoque más simple: usar tablas comunes y
SELECT FOR UPDATE SKIP LOCKED, algo que funciona de inmediato en los ORM/frameworks DSL de consultas de todos los lenguajeshttps://www.pgcasts.com/episodes/the-skip-locked-feature-in-...
No es “web scale”, pero en mi experiencia escala fácilmente hasta miles de trabajos en segundo plano
Usé este método sin problemas incluso para trabajos en una organización grande. No requiere despliegues especiales ni infraestructura nueva: basta con levantar algunos threads worker dentro de la app. También se puede tener un thread que reinicie trabajos abandonados
Pero en 3 años eso nunca ocurrió realmente; todo estaba dentro de
try/catch, así que si fallaba lo volvía a meter en la cola, y la app Java también era muy establedelete from taskwhere task_id in( select task_idfrom taskorder by random() -- use tablesample for better performancefor updateskip lockedlimit 1)returning task_id, task_type, params::jsonb as params[1] https://taylor.town/pg-task
Dices “miles de trabajos en segundo plano”, pero las colas normalmente deberían analizarse según la ley de Little, hablando de tasas como la entrada promedio de trabajos por segundo y la duración promedio de cada trabajo. El número bruto por sí solo no significa mucho
Al principio, incluso un
UPDATE ... SETingenuo puede servir, pero toma demasiados locks. Puedes usar una subconsultaSELECTdentro deUPDATE, o hacer más eficiente el lock de extracción conSELECT FOR UPDATE SKIP LOCKED, pero al final las consultas de extracción pueden bloquear los locks de las demás y detener la colaEn ese momento puedes apagar la entrada para darle aire a la DB, pero la entrada perdida causa pérdida de datos, y normalmente el problema central es que las extracciones se bloquean entre sí
También podrías shardear de emergencia la tabla de trabajos para evitar locks, pero es frágil al distribuirla entre varios workers y puede terminar en pérdida de datos. También podrías descartar una parte aleatoria de los trabajos, pero obviamente eso es pérdida de datos
Estas opciones son extremadamente estresantes en producción, y la recuperación también es muy difícil si no rediseñas desde cero. Salvo que tengas solo unos pocos clientes y estés seguro de que siempre estarás en el orden de decenas de trabajos por segundo, después de vivir esto en carne propia prefiero elegir una tecnología de colas real antes que una base de datos, siempre que sea posible
A menudo veo críticas del estilo “los ingenieros copian la infraestructura de FAANG para verse cool, aunque no tenga nada que ver con sus necesidades”, pero creo que en gran parte es un problema de conocimiento y documentación
Si quieres seguir a FAANG o a otras startups y crear una arquitectura basada en colas infinitamente escalable, hay decenas de guías, tutoriales y white papers de alta calidad que te permiten configurar en unas horas soluciones escalables como Redis o SQS
El costo de mantenimiento es más alto, pero puedes empezar copiando y pegando comandos, código y configuraciones de fuentes con buena reputación
En cambio, si quieres usar
NOTIFYde PostgreSQL y buscas “SQLALchemy notify listen postgres”, solo aparecen algunas preguntas de Stack Overflow sin respuesta y algún GitHub Gist sin contextoDe verdad me gustaría probar este enfoque en un proyecto paralelo, pero no tengo margen para dedicar 2 o 3 días a resolverlo por mi cuenta
Por fuera parece una elección entre “simple pero no escala, o sea usar solo PostgreSQL” y “complejo pero escala, o sea Redis/SQS, etc.”; entonces queda como si yo, cegado por la tecnología llamativa y FAANG, eligiera una escalabilidad que no necesito
Pero si también consideramos las guías y los materiales disponibles, las opciones reales se parecen más a “complejo y con escalabilidad impredecible, porque no conozco la implementación ni las trampas” frente a “simple y escalable, la forma en que todos lo hacen en la práctica”. Así, la elección del ingeniero que sigue a FAANG se ve mucho más razonable
NOTIFY, basta con leer https://www.postgresql.org/docs/current/sql-notify.html, sin necesitar habilidades de búsquedaPor ejemplo, si usas PostgreSQL con baja carga, las migraciones de esquema, agregar nuevas restricciones, el análisis, etc., se vuelven casi triviales
Si usas SQS, Cassandra, etc., ganas escalabilidad/disponibilidad, pero cambiar las cosas cuando descubres que el diseño original no encaja lleva mucho más tiempo. Es lo que pasa cuando el negocio pide agregar una restricción como “los usuarios de tipo foo no deben combinar simultáneamente el valor bar”
Se puede implementar sin PostgreSQL, pero no es fácil ni simple, sobre todo si hay que cambiarlo
Por eso, para mantener la flexibilidad, creo que conviene usar PostgreSQL, y cuando sabes que algo no va a cambiar, usar otra cosa encima de PostgreSQL. Claro que en ese caso se suma overhead de infraestructura y mantenimiento
Al final siempre es un trade-off, y hay que saber qué intercambiar, cuándo y por qué
No creo que sea ideal que todo el mundo implemente por su cuenta, dentro de la aplicación, un worker de trabajos en segundo plano a medida, ya sea con un backend PostgreSQL u otra cola
En los trabajos en segundo plano hay muchas sutilezas y detalles de implementación fáciles de hacer mal, así que por lo general conviene que se encargue una librería o framework dedicado y más completo
Si todas las aplicaciones Rails no tuvieran Sidekiq/Active Jobs y cada una implementara sus propios workers en segundo plano a medida, es muy probable que las apps Rails tuvieran una reputación mucho peor en cuanto a confiabilidad
Basta con elegir la solución que dé el mayor beneficio sin alejarse demasiado del objetivo principal
He visto una startup donde los desarrolladores pasaban alrededor del 80% del tiempo peleando con herramientas e infraestructura. Tenían 3 meses de runway, y ahora al final de esa pista solo quedó un gran agujero. Todavía me da escalofríos pensarlo
Siento que falta suficiente respeto por la complejidad
Al usar PostgreSQL como bus de publicación/suscripción con
LISTEN/NOTIFY, la mayor desventaja es queLISTENes una funcionalidad de sesión y, por lo tanto, no encaja con el pooling de conexiones a nivel de sentenciaSi se usa este enfoque, conviene usar bloqueos consultivos. Otros bloqueos explícitos imponen una carga mayor a la base de datos, pero los bloqueos consultivos fueron diseñados intencionalmente para ser muy livianos
Un ejemplo de implementación que me gusta es que, que fue porteado a varios lenguajes
NOTIFY/LISTENes que, cuando hay problemas, es difícil de diagnosticarHace poco también tuvimos que dejar de usarlo por un problema en el que, después de un tiempo, todos los
NOTIFY/LISTENse detenían y solo se resolvía reiniciando la base de datos https://dba.stackexchange.com/questions/325104/error-could-n...Skype usó PostgreSQL como cola hace años, junto con un pequeño plugin, para procesar todos sus CDR. No sé si todavía lo usan, pero para estándares de hace 10 años era “escala web” y, mientras en internet se discutía que usar una base de datos como cola era un antipatrón, simplemente funcionaba bien.
Tener transacciones es bastante conveniente.
https://wiki.postgresql.org/wiki/SkyTools
Como lo usé bastante en el trabajo, también di varias charlas sobre este tema en Sydpy. Si ya operas y das soporte a PostgreSQL correctamente, es útil.
Dicho eso, hoy probablemente usaría una cola dedicada, y evitaría RabbitMQ a toda costa.
Incluso si te preocupa el vendor lock-in, las operaciones básicas de una cola son tan reducidas —prácticamente push/pop— que también es relativamente fácil escribirlo de forma que puedas migrar si hace falta.
Si metiste también la cola en la base de datos solo porque ya estabas operando una base de datos, esa lógica es un arma de doble filo. No es motivo de celebración, pero ahora el desastre de la cola puede arrastrar también a tu almacén principal de datos.
Una de las mayores ventajas de usar PostgreSQL como cola de aplicaciones es que todos los trabajos asíncronos programados se benefician de la transaccionalidad.
Por ejemplo, si tienes un cambio complejo de backend después de un registro de usuario, que envía un correo, y después de insertar el trabajo de envío de email falla una parte posterior de la misma transacción y se hace rollback, ese trabajo de email nunca entra en la cola.
En el caso del email, si esperas la confirmación de envío y falla, tienes que elegir entre hacer fallar la transacción y aceptar la posibilidad de duplicar el email, o seguir adelante y aceptar la posibilidad de perderlo.
La gran ventaja aparece en rutas de código que modifican la DB de forma asíncrona. Como el consumo del trabajo y la actualización de la DB están dentro de la misma transacción, puedes manejar una semántica de exactamente una vez de forma completamente transaccional.
En PostgreSQL tengo una tabla de log transaccional que contiene el evento disparado y el
pg_current_xact_id()de esa transacción. También se podría usar elxminintegrado de la fila, pero hay que preocuparse por el wraparound de transacciones. Al insertar en esta fila se genera unNOTIFY.Un proceso en segundo plano se ejecuta repetidamente y toma de la tabla de transacciones todas las filas con ID de transacción entre el
xminde la ejecución anterior y elpg_snapshot_xmin(pg_current_snapshot())actual. Mapea esos eventos a trabajos, los envía a SQS, registra elxminy luego espera el siguienteNOTIFYconLISTEN.Con una cola en PostgreSQL, es cierto que si insertas el trabajo de envío de email y después falla la transacción, el email no entra en la cola.
El código de base de datos que de verdad debe estar separado merece dividirse en transacciones distintas. Por otro lado, si el usuario no fue creado en la DB quizá tampoco quieras enviarle el email de registro, así que depende del caso.
Con una cola basada en Redis, o prácticamente con cualquier otra cola, enseguida te encuentras con situaciones donde se ejecuta un trabajo que asume que existe un registro en la base de datos antes de que la transacción haya hecho commit. El código para arreglar eso suele volverse incómodo y complejo.
Lo que me gusta de Kafka es que simplemente es un log solo de anexado, y los clientes básicamente solo mantienen offsets.
Conceptualmente es muy simple de entender, persistente y bastante resistente a fallos, porque puedes volver a cualquier offset y leer de nuevo.
Lamentablemente, Kafka trae suficiente complejidad por su naturaleza distribuida como para que, en la mayoría de los casos de uso, termine no valiendo la pena.
Personalmente me gustaría que existiera algo parecido pero más fácil de operar. En un solo nodo podría manejar cientos, quizá miles de eventos por segundo, y sin la complejidad distribuida sería bastante bueno.
En teoría también se podría hacer con PostgreSQL: simplemente no borrar nunca las filas. Tal vez esa sea la respuesta.
Si no necesitas todas las funciones como grupos de consumidores o particiones, la configuración de los consumidores también sería mucho más simple.
En Windmill usamos exactamente este enfoque. Windmill es una alternativa open source a Retool y una herramienta más cercana a un Airflow moderno, y corremos benchmarks todos los días.
Si ejecutas un worker de Windmill y PostgreSQL en contenedores sobre una instancia razonable de GitHub CI, el benchmark procesa 1,200 trabajos por segundo. Al agregar workers, escala de forma estable hasta 5,000 trabajos por segundo.
Estamos evaluando usar Citus para superar la barrera de 5,000 trabajos por segundo en una instancia multitenant.
https://github.com/windmill-labs/windmill/tree/benchmarks
Cuando estábamos en el orden de unos 10 mensajes por segundo, usamos PostgreSQL para algunas colas y escaló bastante. Pero, siendo sinceros, configurar SQS u otro stack de colas en AWS, GCP o Azure es demasiado fácil; están hechos para ese propósito y traen cosas como DLQ integradas.
No veo por qué elegiría este camino teniendo que preocuparme de que ese sistema se caiga y termine afectando también el estado del resto de la DB.
Me gusta el principio de “usa la herramienta más simple”, pero a veces parece que los ingenieros se van demasiado lejos y terminan quedándose con la herramienta más tonta, con advertencias ambiguas, aunque las alternativas mainstream sean relativamente baratas y simples.
Si guardas el estado del trabajo en la DB, puedes consultarlo cómodamente. Aunque no muestre el estado real con total precisión, ayuda al analizar incidentes en operación. En especial porque la mayoría de las colas de trabajos simplemente eliminan los registros procesados.
Y si haces que todos los trabajos en segundo plano sean idempotentes, un enfoque como “enviar un mensaje a la cola de trabajos para que procese este trabajo” casi siempre es seguro.
Si dependes solo de una cola de mensajes, muchas veces es difícil saber qué está pasando cuando aparecen problemas de rendimiento. Por ejemplo, RabbitMQ puede informarte el tamaño de la cola, pero tiene pocas o muy limitadas capacidades para inspeccionar los datos internos.
Se puede prever que cierto software nunca excederá las capacidades de PostgreSQL y, aun si las excede, se puede diseñar para que sea fácil migrarlo a otro servicio.
Y quizá quieras construir de forma simple, dentro de PostgreSQL, un sistema de trabajos con transacciones.
Usamos Oban de Elixir para procesar de cientos de miles a millones de trabajos por día dentro de PostgreSQL.
Tener semántica transaccional alrededor de los trabajos en segundo plano —por ejemplo, programar un email solo cuando una transacción tuvo éxito— es tremendamente conveniente.
Hay que ajustar un poco
autovacuum, pero después de dejarlo en orden nos funcionó muy bien.