1 puntos por GN⁺ 4 시간 전 | 1 comentarios | Compartir por WhatsApp
  • ClickHouse Managed Postgres opera de forma predeterminada una flota multiproceso proporcional a la cantidad de núcleos, para evitar la limitación de que PgBouncer, al ser de un solo hilo, quede atado a un único núcleo de CPU
  • Todos los procesos se enlazan al mismo puerto con so_reuseport y el kernel distribuye las conexiones nuevas, por lo que para los clientes se ve como un único endpoint
  • Las solicitudes de cancelación de Postgres, que llegan por una conexión separada, pueden llegar a un proceso que no posee la sesión; por eso se reenvían al verdadero dueño de la sesión mediante peering entre procesos
  • En una c7i.4xlarge de 16 vCPU, un solo proceso alcanzó un pico de alrededor de 87,000 TPS, mientras que una flota de 16 procesos llegó a cerca de 336,000 TPS, registrando aproximadamente 4 veces más throughput
  • Con pocas conexiones, un solo proceso puede ser similar o un poco más rápido, pero con alta concurrencia un núcleo se vuelve el cuello de botella; por eso hay que repartir el presupuesto de conexiones entre toda la flota para escalar el throughput y el límite de conexiones sin sobreconectar Postgres

El cuello de botella de CPU que crea un solo proceso

  • PgBouncer es de un solo hilo, por lo que un proceso usa solo un núcleo, independientemente de la cantidad de CPU del sistema
  • Incluso en un servidor de 16 vCPU, el trabajo de connection pooling se concentra en un solo núcleo y los otros 15 núcleos quedan inactivos
    • Por eso, antes de que se agote la capacidad de procesamiento de Postgres, el pooler limita el throughput total
  • ClickHouse Managed Postgres opera una flota de procesos que define la cantidad de procesos PgBouncer en proporción a la cantidad de núcleos disponibles

Distribución de conexiones con so_reuseport

  • Todos los procesos de la flota activan so_reuseport y se enlazan al mismo puerto
  • El kernel distribuye las conexiones entrantes entre los procesos, de modo que los clientes se conectan a un único endpoint y no necesitan saber que detrás operan varios PgBouncer
  • La documentación de PgBouncer también presenta so_reuseport como una forma de aprovechar varios núcleos con múltiples procesos de un solo hilo

Peering entre procesos para garantizar la cancelación de consultas

  • Las solicitudes de cancelación de Postgres llegan junto con una clave de cancelación a través de una nueva conexión, separada de la conexión de la consulta en ejecución
  • Al usar so_reuseport, el kernel puede entregar la nueva conexión de cancelación a un proceso distinto del que posee esa sesión
    • Si el proceso que recibe la solicitud no conoce esa consulta, la cancelación no se ejecuta
  • Si los procesos PgBouncer hacen peering entre sí, una solicitud que llegó al proceso equivocado puede reenviarse al proceso que realmente posee la sesión
  • De esta forma, sin importar qué proceso reciba la solicitud, la cancelación de consultas funciona correctamente en toda la flota

Pooling de transacciones y presupuesto de conexiones

  • El pooling opera en modo transacción, y la conexión al servidor vuelve al pool en cuanto se confirma la transacción
  • max_client_conn y max_db_connections se dividen por la cantidad de procesos y se asignan a cada proceso
  • Al dividir el presupuesto de conexiones, se puede aumentar el límite agregado de conexiones manteniendo la cantidad total de conexiones de la flota dentro de un límite seguro para Postgres
  • Un solo proceso rechaza los clientes nuevos que superan su propio max_client_conn con el siguiente error
    • FATAL: no more connections allowed (max_client_conn)

Benchmark en el mismo hardware

  • Ambas configuraciones se midieron en el mismo entorno de AWS EC2
    • Servidor del pooler: c7i.4xlarge de 16 vCPU
    • Postgres: servidor separado
    • Generador de carga: pgbench en un tercer servidor
    • Carga de trabajo: solo lectura (select-only), modo de pooling de transacciones
  • La comparación fue entre un solo proceso de PgBouncer y una flota de 16 procesos, con el mismo tipo de instancia, Postgres y carga de trabajo
  • Se aumentaron las conexiones de clientes de 8 a 256 y se midieron el throughput y el uso de CPU del servidor de 16 núcleos
Clientes TPS de un solo proceso CPU del servidor con un solo proceso TPS de la flota CPU del servidor con flota
8 8,910 0.8% 6,450 2.9%
32 54,203 5.2% 64,244 12.3%
64 86,570 8.3% 219,439 31.9%
128 83,463 8.1% 320,547 45.9%
256 76,893 7.7% 336,469 48.9%

Resultados de throughput y uso de CPU

  • El proceso único alcanzó un pico de alrededor de 87,000 TPS y luego, a medida que aumentó la carga, su rendimiento bajó hasta unos 77,000 TPS con 256 clientes
  • La flota de 16 procesos aumentó hasta cerca de 336,000 TPS al aprovechar más núcleos, mostrando aproximadamente 4 veces más throughput que el proceso único
  • En mediciones con pidstat, un solo proceso PgBouncer usó alrededor del 97% de CPU durante la carga, ocupando por completo un núcleo, pero el uso total del servidor de 16 vCPU se mantuvo por debajo del 10%
  • La flota distribuyó el trabajo por todo el servidor y usó unos 8 núcleos, y aún conservaba capacidad adicional cuando Postgres y el generador de carga llegaron a su límite
  • En una medición manteniendo 256 clientes, el servidor con proceso único usó de forma sostenida alrededor de 9% de CPU, mientras que el servidor con flota usó cerca de 52% de CPU
  • En la medición externa de EC2 CloudWatch, la CPUUtilization promedio de la instancia con proceso único fue de alrededor de 16%, y la de la flota, de alrededor de 60%
    • Las cifras de CloudWatch son un poco más altas que las mediciones internas del guest, pero la diferencia es la misma: un solo PgBouncer no puede aprovechar la mayor parte de una máquina de 16 vCPU

Elección de configuración según la concurrencia

  • Cuando hay pocas conexiones, no hay trabajo para paralelizar y las conexiones de la flota se distribuyen entre varios procesos, por lo que un solo proceso puede ser suficiente o incluso un poco más rápido
    • Con 8 clientes, el proceso único registró 8,910 TPS y la flota 6,450 TPS
  • Cuando aumenta la concurrencia, el único núcleo ocupado por el proceso único se convierte en una pared para el throughput, y la brecha de rendimiento frente a la flota crece
  • En entornos donde Postgres llega al límite antes que PgBouncer, un solo proceso también es un valor predeterminado adecuado
  • Cuando el pooler empieza a limitar el throughput, hay que combinar una flota de procesos ajustada a la cantidad de núcleos, uso compartido de puerto basado en so_reuseport y peering entre procesos para eliminar el cuello de botella
  • Todos los servidores de ClickHouse Managed Postgres ofrecen esta configuración de forma predeterminada

1 comentarios

 
GN⁺ 4 시간 전
Comentarios en Hacker News
  • Solo usen https://github.com/yandex/odyssey. Odyssey es un PgBouncer escalable

    • Primero ajustamos bien y usamos PgBouncer, que es lo más probado y encaja de forma natural con PostgreSQL, y funciona muy bien, incluso resolviendo problemas antiguos como el soporte de sentencias preparadas.
      También tenemos muchos clientes que escalaron las conexiones de PostgreSQL a más de 10 mil, y en el futuro planeamos evaluar alternativas como Odyssey o pgdog. Aun así, personalmente no me gusta la idea de tener más de 10 mil conexiones a PostgreSQL, y creo que con unos pocos cientos ya se puede escalar lo suficiente
    • Como dato curioso relacionado, ClickHouse también fue desarrollado originalmente por Yandex
    • Yandex es una empresa rusa, así que creo que es mejor no usarlo
  • Si operas PgBouncer en Kubernetes, es fácil levantar varios procesos en una sola máquina y también distribuirlos entre varias máquinas.
    Esto es especialmente útil porque Azure a veces provoca fallas secuenciales en todo el conjunto de servidores durante el mantenimiento de VMs

    • Me pregunto qué impacto tiene en el rendimiento agregar un salto de red más entre PgBouncer y PostgreSQL. Supongo que la diferencia será pequeña, pero valdría la pena comprobarlo
  • Entiendo el concepto de peering del que hablan aquí, pero nunca lo he usado en PostgreSQL.
    A) ¿Hay algún modo o configuración en PostgreSQL para montar esto fácilmente? Me imagino una estructura donde las solicitudes de cancelación se van pasando secuencialmente a los peers hasta que no den error, o donde la solicitud de cancelación incluye metadatos para que el proceso que la recibió por error pueda identificar el proceso correcto.
    B) Si todos los procesos de PostgreSQL reciben solicitudes del cliente con so_reuseport, parecería que para la comunicación entre peers haría falta algún mecanismo aparte de comunicación entre procesos (IPC); ¿qué mecanismo se usa en la práctica?

  • Estoy usando pgdog y encaja muy bien con lo que necesito

    • Pgdog fue creado para resolver las limitaciones de PgBouncer con las que Instacart se topó hace mucho, y para ofrecer una base más sólida para escalar PostgreSQL horizontalmente, es decir, sharding
    • Más adelante también planeamos evaluar ofrecer pgdog. La razón por la que elegimos PgBouncer está explicada con más detalle en https://news.ycombinator.com/item?id=48873867
    • Últimamente han ido apareciendo por turnos proyectos de proxy inverso para PostgreSQL. Aquí se puede ver por qué se creó Pgdog: https://news.ycombinator.com/item?id=48819308
  • Me pregunto por qué PgBouncer tiene que preocuparse incluso por las solicitudes de cancelación. ¿No bastaría con reenviarlas tal cual a PostgreSQL, y si PostgreSQL devuelve un error en vez de una respuesta normal a la consulta cancelada, que lo maneje el PgBouncer encargado de esa conexión?

    • La cancelación de consultas se envía por separado de la conexión original de la consulta, así que PgBouncer tiene que mandar la solicitud al servidor y conexión correctos.
      PgBouncer le da al cliente un PID y un valor secreto de cancelación falsos pero rastreables, y cuando vuelve una solicitud de cancelación, busca el PID y valor secreto del servidor/proceso real para reenviarlos. Además, puede darse la situación de que el cliente crea que la consulta sigue ejecutándose, pero PgBouncer ya sabe que terminó y que la conexión fue reutilizada, así que debe verificar si esa conexión y PID todavía están ejecutando esa consulta
  • ¿Esto es para controlar el acceso al servidor PostgreSQL mediante un pool de conexiones desde microservicios? Si fuera un backend monolítico, no parecería necesario.
    La mayoría de los frameworks decentes de backend ya traen pool de conexiones integrado, y el 98% de los casos de uso donde no se necesitan ni se recomiendan microservicios se resuelven con eso

    • Si en cada host la aplicación backend corre como un único binario grande, sí. Pero si el backend está compuesto por muchos procesos fork que no comparten estado, entonces claramente sí hace falta
  • PgBouncer es un software excelente. Lo usamos mucho y simplifica bastante la operación de bases de datos

  • Acabo de enterarme de so_reuseport y me parece interesante. La clave de la configuración parece ser esto y el peering; ¿el peering viene integrado en PgBouncer y es fácil de configurar?

  • ¿También se puede usar peering en Kubernetes? En este entorno no haría falta reutilizar puertos, así que también me pregunto si cada Pod termina funcionando de forma independiente con su propio pool de conexiones

  • Ahora tengo 46 años, y todavía recuerdo lo impactado que quedé a los 23 cuando vi el modelo de conexiones pesadas de PostgreSQL. ¿Todavía no ha mejorado desde entonces?

    • Sí ha mejorado bastante, y ahora maneja miles de conexiones razonablemente bien: https://techcommunity.microsoft.com/blog/adforpostgresql/imp...
      Aun así, si no usas un pool de conexiones, PostgreSQL tiene que hacer fork de procesos, así que crear una conexión nueva siempre cuesta varias decenas de milisegundos o más. Las aplicaciones escritas sin pool de conexiones tampoco son ideales, pero son bastante comunes.
      Los frameworks de aplicación también cambiaron, y las arquitecturas serverless pueden generar decenas de miles de conexiones, que es donde PostgreSQL empieza a tener problemas. Personalmente no me gustan más de unos pocos cientos de conexiones, pero hoy en día es una situación totalmente realista
    • En esa época, muchas empresas apenas empezaban a encontrar una respuesta más o menos útil al modelo de hilos que proponía Java. Era una forma de extender el modelo de Windows, e incluso Solaris sufría para manejar Java, HP-UX estaba peor, y no recuerdo si SGI estaba mejor que HP.
      Para lograr compatibilidad con Java, muchas empresas tuvieron que cambiar mucho su manera de manejar la concurrencia. PostgreSQL y SQLite también se diseñaron en una época parecida, pero fueron sistemas creados por veteranos de la industria que ya desplegaban sistemas de alta carga antes de que existieran estas discusiones sobre hilos, y que además daban soporte a usuarios con hardware antiguo