1 puntos por GN⁺ 1 시간 전 | 1 comentarios | Compartir por WhatsApp
  • El bloqueo exclusivo global de Postgres LISTEN/NOTIFY limita el rendimiento de una implementación simple, pero si se almacenan las notificaciones en búfer y se envían por lotes, un solo servidor puede procesar hasta 60 mil escrituras de streams por segundo
  • Una transacción que llama a NOTIFY mantiene el bloqueo global hasta que terminan el commit y fsync() para garantizar el orden de commit de las notificaciones; esto serializa los commits e impide aprovechar el commit grupal
  • La implementación inicial, que llamaba a NOTIFY mediante un trigger en cada escritura de la tabla de streams, ofrecía baja latencia, pero se atascaba en 2,900 escrituras por segundo sin usar plenamente CPU, memoria ni IOPS
  • Si se toma la tabla de la base de datos, y no las notificaciones, como fuente de verdad, y las notificaciones acumuladas en memoria se envían periódicamente en una sola transacción, se puede reducir mucho la cantidad de veces que se adquiere el bloqueo
  • La posible pérdida de notificaciones del búfer por una falla del proceso se compensa con polling de baja frecuencia; incluso con lecturas concurrentes, se logra una latencia de 15 a 100 ms y 20 veces más rendimiento que antes

Streams de baja latencia implementados con Postgres

  • Un stream basado en Postgres guarda cada fragmento del stream como una fila nueva en la tabla streams; un token de respuesta de un LLM también puede ser un fragmento
  • Del lado de lectura, no se sabe cuándo llegará el siguiente fragmento, por lo que es difícil esperar de forma eficiente solo con consultas simples
  • El polling periódico, si el intervalo es largo, aumenta la latencia en usos interactivos como el chat en línea; si es corto, muchos pollers concurrentes pueden saturar la base de datos
  • Con LISTEN/NOTIFY, el proceso de lectura puede esperar bloqueado y despertarse de inmediato con una notificación de que se escribió un nuevo fragmento, evitando polling innecesario

El cuello de botella de enviar NOTIFY en cada escritura

  • En la implementación inicial, cada vez que se registraba un nuevo fragmento en la tabla streams, un trigger ejecutaba una función que enviaba un NOTIFY, y el proceso de lectura esperaba la notificación para luego leer el nuevo fragmento
  • Se logró corrección y baja latencia, pero incluso con una base de datos Postgres grande no se pudo sostener más de 2,900 escrituras de streams por segundo
  • Mientras ocurría el cuello de botella, el uso de CPU, memoria e IOPS no aumentaba de forma notable; la causa era el bloqueo global en la ruta de commit de NOTIFY

Bloqueo global para garantizar el orden de commit

  • Una transacción que llama a NOTIFY adquiere un bloqueo exclusivo global al iniciar el commit y no lo libera hasta que queda completamente confirmado y su contenido se escribe en disco con fsync()
  • Postgres garantiza que las notificaciones se entreguen en el orden de commit de las transacciones y guarda todas las notificaciones salientes en una cola interna global que debe coincidir exactamente con ese orden
  • Agregar notificaciones a la cola también debe hacerse de forma transaccional como parte del commit, pero como cada transacción tarda distinto en confirmar, no se puede determinar el orden antes de que termine el commit
  • El bloqueo global serializa el commit de las transacciones que incluyen notificaciones para fijar el orden de antemano y hacer que también se agreguen a la cola interna de notificaciones en ese mismo orden

Cómo la serialización de commits limita el rendimiento

  • Como todas las escrituras de streams llaman a NOTIFY mediante un trigger, cada transacción de escritura mantiene el bloqueo global durante todo el commit y el flush a disco
  • Al confirmarse las transacciones una por una, no se puede aprovechar el commit grupal de Postgres, que procesa varias transacciones con un solo fsync()
  • El rendimiento no puede superar la velocidad a la que Postgres confirma transacciones individuales, y las tareas quedan esperando el bloqueo, por lo que tampoco se usan plenamente la CPU ni el disco
  • El parche que se incluirá en Postgres 19 no elimina el bloqueo global, así que no resuelve este cuello de botella
    • En cambio, optimiza casos limitados donde hay muchos canales de notificación y cada listener espera solo un canal específico

Búfer de notificaciones y envío por lotes

  • En muchos usos de LISTEN/NOTIFY, incluidos los streams, las notificaciones no son la fuente de verdad, sino solo una señal para revisar la tabla donde están guardados los datos reales
  • En esta estructura, las notificaciones en sí no necesitan tener un orden global perfecto ni durabilidad completa, por lo que pueden almacenarse en memoria y enviarse periódicamente dentro de una única transacción por lote
  • El bloqueo global se adquiere solo al vaciar el búfer, no en cada escritura individual del stream
  • Las escrituras individuales avanzan rápido al desacoplarse del envío de notificaciones en segundo plano, y se puede aumentar el rendimiento aprovechando optimizaciones de Postgres como el commit grupal

Polling de baja frecuencia para compensar notificaciones perdidas

  • Si el proceso se detiene mientras las notificaciones siguen en memoria, esas notificaciones pueden no entregarse
  • El proceso de lectura espera notificaciones y, al mismo tiempo, consulta periódicamente la base de datos para comprobar si hay datos de stream registrados sin notificación
  • Como este polling es solo un mecanismo auxiliar para recuperar notificaciones perdidas, puede ejecutarse con baja frecuencia y su impacto en el rendimiento no es grande

Rendimiento y latencia

  • La implementación optimizada procesó hasta 60 mil escrituras de streams por segundo en un entorno con procesos de lectura concurrentes, 20 veces más rendimiento que la implementación inicial
  • Incluso con mayor rendimiento, la latencia se mantuvo en el rango de 15 a 100 ms
  • En el rendimiento máximo, la CPU de Postgres se utilizó por completo, lo que muestra que se alcanzó la saturación real de la propia base de datos, no contención por bloqueos
  • El código completo del benchmark está disponible en dbos-postgres-benchmark

1 comentarios

 
GN⁺ 1 시간 전
Comentarios en Hacker News
  • La escalabilidad es una escala continua, no algo binario. 60 mil eventos por segundo pueden ser 100 mil veces más de lo necesario para un sistema y 100 mil veces menos para otro. Un error común de los desarrolladores que señalaría incluso más que la optimización prematura es elegir una tecnología con características de escalabilidad inadecuadas
    Una tecnología demasiado pequeña falla claramente cuando supera sus límites, pero una tecnología excesivamente escalable también trae carga operativa y restricciones. Introducir una tecnología así en un sistema pequeño donde un modelo más rico podría reducir mucho el esfuerzo de desarrollo también es una mala elección
    Los límites de LISTEN/NOTIFY son lo bastante bajos como para tener cuidado, así que conviene calcular la carga máxima pesimista y aun así dejar al menos un margen de 10x, pero para muchos proyectos es suficiente. Por su integración con la base de datos, disponibilidad y la ventaja de no requerir operar un servicio aparte, no es una opción que deba descartarse automáticamente; incluso los 2 mil eventos por segundo mencionados antes son una cifra grande para un sistema que procesa un mensaje por segundo

    • Aunque es una objeción menor a un buen artículo, casi no existen sistemas que procesen 6 mil millones de solicitudes por segundo, y aun si existieran, probablemente usarían herramientas hechas a medida para ese propósito
    • Es mejor el problema de haber estimado menos de 60 mil por segundo y luego saltar de 20 mil a 200 mil, que haber construido para 1 millón por segundo y que la carga real sea de 20 mil. En el primer caso, el éxito inesperado ayuda a pagar medidas temporales y costos de escalado; en el segundo, uno queda atado a una estructura de costos alta y a una inversión anticipada
      Conviene diseñar con un margen razonable sobre la escala realmente esperada y elegir algo más allá de eso solo cuando la escalabilidad adicional sea prácticamente gratis. Si se puede comprar hardware más grande por unos miles de dólares o si las opciones son equivalentes salvo por la escalabilidad, entonces sí conviene elegir la más grande
    • En este caso, sí puede considerarse escalable porque llega hasta el límite del hardware. El cuello de botella no está en la base de datos ni en la E/S, sino en el hardware, y al rendimiento máximo el CPU de Postgres se usa por completo, lo que muestra saturación de la propia base de datos y no contención
  • Tuvimos mucho éxito combinando LISTEN/NOTIFY con un broker de suscripciones GraphQL en Rust. Había decenas de miles de suscripciones, pero solo había una conexión LISTEN por host, 3 o 4 en total
    Enviábamos todos los cambios a cada host, y el host administraba las suscripciones reales de los usuarios y decidía qué publicar. Si cambias cientos de hosts de Ruby o Node por unos pocos hosts en Rust, la arquitectura puede simplificarse mucho, y un enfoque que suele considerarse no escalable puede funcionar bastante bien

    • Me gustaría saber si hay alguna librería de GraphQL recomendable para Rust
  • Cuando trabajaba como CTO, procesábamos unas 100 mil operaciones al día en todos los servicios, luego crecimos a millones y finalmente a decenas de millones. En ese proceso, un ingeniero construyó una cola sobre la semántica de LISTEN/NOTIFY para aprovechar la consistencia fuerte con el modelo de datos. No era difícil de entender y además eliminaba la necesidad de una capa separada de almacenamiento y entrega, así que en ese momento parecía razonable
    Pero al escalar esa funcionalidad hecha en casa, tuvimos que esquivar el funcionamiento interno de PostgreSQL y se volvió muy incómodo; deberíamos haber migrado antes a otro sistema. Tampoco escalaba bien: en RDS empezó a haber mucha contención de disco difícil de diagnosticar, y el VACUUM de esa tabla era una pesadilla. Como una cola familiar estaba implementada de forma poco familiar con funciones internas de PostgreSQL, otros ingenieros le tenían miedo y evitaban depurarla o asumir su propiedad
    La lección principal, aparte de detalles como esquema e índices, es elegir siempre primero tecnología simple y predecible. Si no se necesita una consistencia de datos extremadamente fuerte, es mejor usar una cola con un contrato de API simple, como SQS o Redis queues, y adaptar el resto a eso, aunque implique un componente más en la infraestructura. Cuanto menos responsabilidad mecánica recaiga sobre un único almacén central de datos, mejor

    • Al final puede resumirse en: “si lo usas sin entender cómo funciona, no escala”. LISTEN/NOTIFY básico no escala, pero el artículo original sí encontró una forma real de escalarlo, así que no hace falta que cada equipo vuelva a resolverlo
      A medida que madura el ecosistema de sistemas distribuidos, entendemos mejor qué puede hacer cada componente. Es más sano empezar con pocos componentes e ir agregando más cuando de verdad hagan falta
    • Rara vez un componente nuevo pesa más que los demás factores, pero operar la cola de forma independiente de otros componentes arquitectónicos tiene una gran ventaja. Las colas están hechas precisamente para almacenar y luego entregar, así que separar su ciclo de vida también permite aislar otros sistemas durante actualizaciones, análisis de fallas y ventanas de parcheo
    • Esto parece más un problema de gestión, y no estoy de acuerdo con la conclusión de que haya que agregar un nuevo componente a la infraestructura. No tiene sentido añadir un nuevo nodo de red solo porque los desarrolladores no quieran hacerse cargo de una parte del stack; simplemente hay que asignarles esa responsabilidad
  • Me sigue gustando DBOS, que aprovecha bien Postgres y ahora también SQLite. Se puede introducir en un stack CRUD existente casi sin esfuerzo
    Una vez que empiezas a usar workflows durables, sigues encontrando lugares donde aplicarlos. Últimamente estoy experimentando con ver cada email como un workflow durable, donde el usuario, la contraparte, el agente y herramientas como GitHub o Attio participan en el flujo por turnos
    https://housecat.com/blog/gmail-durable-workflows-sandbox-vm

  • Este tipo de artículos suele ser el resultado de que cada quien evalúa de forma independiente su propio problema, entendimiento y solución. Es discutible llamar falta de pericia a esperar cierto rendimiento solo porque venía en la configuración por defecto de una herramienta; todos seguimos aprendiendo a través del fracaso
    El hecho de que en el experimento se haya usado un servidor de base de datos de 96 núcleos y 384 GB de RAM (https://github.com/dbos-inc/dbos-postgres-benchmark/blob/mai...) es muy importante y debería haberse dicho claramente. La base de datos puede escalar verticalmente, pero eso también tiene límites. Desde dónde y quién se conecta también afecta el rendimiento y la latencia total
    60 mil por segundo puede parecer mucho, pero lo que realmente tumba sistemas en producción son los picos repentinos de tráfico más que el tráfico normal. A menos que seas una gran empresa, no empezarías con un servidor tan grande. Si además incluyes réplicas de lectura y redundancia entre regiones, un solo clúster de base de datos de producción cuesta más de 100 mil dólares

  • Parece ser el artículo relacionado: Postgres LISTEN/NOTIFY does not scale - https://news.ycombinator.com/item?id=44490510 - julio de 2025, 321 comentarios

  • Parece que falta la parte más importante del artículo: cómo asignar offsets o números de secuencia que permitan rastrear hasta dónde leyó el consumidor y consultar los mensajes nuevos por una ruta alternativa. Hay varias formas, pero no es fácil resolverlo sin complejidad ni contención por bloqueos, y si se implementa mal también puede generar condiciones de carrera con el consumidor. Normalmente el autor bloquearía una sola fila de una tabla de estado o algo similar para asignar el siguiente número del tema de eventos
    Me pregunto cuál sería la mejor forma. Una herramienta que lea captura de datos de cambios (CDC) y escriba los números de evento asignados en otra tabla podría servir, pero puede aumentar la latencia, y ese procesador de CDC también tendría que hacer NOTIFY
    Del lado del consumidor, también hay casos de uso donde el procesamiento por lotes puede aumentar mucho el rendimiento. En ese caso, se puede ejecutar el consumidor en bucle sin LISTEN/NOTIFY, procesando cada vez todos los mensajes nuevos no procesados, y guardar el último número de secuencia entre iteraciones

  • Recuerdo que en la primera versión que soportó LISTEN/NOTIFY la implementación de bloqueos no era buena y había problemas de rendimiento. El artículo anterior que aquí se critica también lo corrigió en una fe de erratas justo después del primer párrafo
    Si la corrección es del 8 de mayo, entonces en el artículo del 24 de julio habría que reconocer que el famoso texto que decía que esta función no escala quizá no fue escrito de mala fe ni necesariamente estaba equivocado para ese momento

    • Si se refiere a la optimización que entrará en Postgres 19, el original también la menciona. Ese parche (https://github.com/postgres/postgres/commit/282b1cde9dedf456...) no elimina el bloqueo global ni resuelve el cuello de botella observado
      En cambio, optimiza el caso más limitado donde hay muchos canales de notificación y cada receptor espera solo un canal específico
  • La última vez que lo revisé, LISTEN/NOTIFY tenía un límite de 8,000 bytes para los datos de la notificación, así que claramente había un aspecto en el que no escalaba. Si los datos no se pueden guardar como filas y solo pasar el ID, sería difícil usarlo
    Los eventos de un juego web eran datos efímeros que describían cambios de estado, así que no había razón para guardarlos en la base de datos, y además podían superar los 8,000 bytes, por lo que no servía para ese caso de uso

    • Si implementara un sistema de notificaciones escalable, pondría un límite al tamaño de las notificaciones. Así se mantiene el tamaño de los mensajes en O(1) y se puede enfocar el escalado en la cantidad de notificaciones; los mensajes arbitrariamente grandes pueden frenar el rendimiento y también indicar que se está usando mal el sistema de notificaciones
    • Me pregunto si no sería posible enviar mensajes que hagan referencia al estado cambiado aunque no apunten a una fila específica
  • El artículo trata la contención por bloqueos de la cola global, pero parece no mencionar otro problema de una cola global de tamaño fijo. Un solo receptor lento en un canal podía bloquear las escrituras de todos los canales. Al menos hace algunos años ese tipo de falla era posible, aunque quizá eso ya cambió