1 puntos por GN⁺ 2 시간 전 | Aún no hay comentarios. | Compartir por WhatsApp
  • En Linux 7.0 se eliminó el modo de expulsión PREEMPT_NONE, que era el valor predeterminado tradicional en servidores, y eso provocó una regresión de rendimiento grave: en el mismo hardware, el rendimiento de PostgreSQL cayó a la mitad
  • Un ingeniero de AWS ejecutó pgbench en una máquina Graviton4 de 96 vCPU, y el resultado fue que en Linux 7.0, frente a Linux 6.x, las transacciones por segundo cayeron de 98,565 a 50,751, mientras que el 55% de la CPU se consumía en una sola función de spinlock
  • Un spinlock que protege el acceso al shared buffer pool de PostgreSQL, combinado con fallos de página menores sobre páginas de memoria de 4 KB, hace que si el scheduler expulsa un proceso mientras mantiene el lock, todos los backends en espera desperdicien CPU girando
  • Al activar Huge Pages (2 MB o 1 GB), la cantidad de fallos de página potenciales baja de 31 millones a decenas o cientos de miles, resolviendo la regresión
  • Desde el lado del kernel se propuso adoptar Restartable Sequences (rseq), pero la comunidad de PostgreSQL sostiene que una degradación de rendimiento causada por una actualización del kernel viola el principio de que no se debe “romper el espacio de usuario”

El problema

  • El ingeniero de AWS Salvatore Dipietro ejecutó pgbench en un procesador Graviton4 de 96 vCPU, con una prueba de alta concurrencia configurada con scale factor 8,470 (una tabla de alrededor de 847 millones de filas), 1,024 clientes y 96 hilos
  • El rendimiento bajó casi a la mitad: 98,565 TPS en Linux 6.x frente a 50,751 TPS en Linux 7.0
  • Según el perfilado con perf, el 55.60% del tiempo de CPU se consumía dentro de la función s_lock
    • Ruta de llamadas: StartReadBufferGetVictimBufferStrategyGetBuffers_lock

Qué es la expulsión (preemption)

  • Expulsión es la decisión del scheduler del sistema operativo de interrumpir un hilo en ejecución y ceder la CPU a otro hilo
  • Antes de Linux 7.0 existían tres opciones
    • PREEMPT_NONE: casi no interrumpe un hilo hasta que este cede voluntariamente la CPU (syscall, bloqueo de I/O, sleep). Era el valor predeterminado tradicional en servidores, con menos cambios de contexto y mayor rendimiento
    • PREEMPT_FULL: puede interrumpir un hilo en casi cualquier punto seguro. Reduce la latencia, pero aumenta el overhead de cambios de contexto. Era el valor predeterminado tradicional en escritorios
    • PREEMPT_LAZY: una opción intermedia introducida en Linux 6.12, que espera límites naturales pero permite expulsión cuando hace falta. Fue diseñada para aproximarse a las características de rendimiento de PREEMPT_NONE
  • En Linux 7.0, PREEMPT_NONE fue eliminado en arquitecturas de CPU modernas, y solo quedaron PREEMPT_FULL y PREEMPT_LAZY
    • Aunque PREEMPT_LAZY funciona como reemplazo para la mayoría del software de servidor, en PostgreSQL aparece una diferencia crítica

Gestión de memoria en PostgreSQL

  • PostgreSQL usa páginas de datos de tamaño fijo (8 KB por defecto) como unidad básica de almacenamiento, y ahí guarda filas de tablas, nodos de índices B-tree, metadatos, etc.
  • Para reducir lecturas de disco, almacena en caché las páginas leídas recientemente en una gran región de memoria compartida llamada shared buffer pool
  • Cuando se conecta un cliente, se crea un proceso backend dedicado; si la página no está en el buffer pool, hay que leerla desde disco y luego buscar un buffer vacío o uno que se pueda expulsar
    • La función encargada de esa selección de buffer es StrategyGetBuffer

Los spinlocks de PostgreSQL

  • Un spinlock es un mecanismo de bloqueo que no duerme mientras espera, sino que se queda en bucle revisando continuamente si el lock ya se liberó
    • En secciones críticas muy cortas, girar puede ser más eficiente que dormir y despertar hilos
  • La suposición clave es: el hilo que tiene el lock lo liberará muy rápido
  • StrategyGetBuffer usa un único spinlock global para proteger la selección del buffer
    • En un entorno con 96 vCPU y 1,024 clientes, todos los backends compiten por el mismo lock

Memoria virtual y TLB

  • Todos los procesos usan direcciones de memoria virtual, y el hardware las traduce a direcciones físicas mediante tablas de páginas con estructura de árbol multinivel
  • Recorrer esas tablas cada vez sería lento, así que la CPU mantiene una TLB (Translation Lookaside Buffer) que cachea traducciones recientes
    • Si hay acierto en la TLB, el acceso es rápido; si hay TLB miss, hace falta un page-table walk y eso cuesta tiempo
  • Linux usa el principio de asignación diferida (lazy allocation): al reservar memoria virtual, la página física real se asigna y mapea solo en el primer acceso
    • En ese primer acceso ocurre un minor page fault: el kernel asigna la página física, guarda el mapeo y eso tarda microsegundos, bastante más que una lectura/escritura normal

El problema de las páginas de 4 KB

  • En el benchmark, shared_buffers estaba configurado en 120 GB; con páginas de memoria de 4 KB eso equivale a unos 31 millones de páginas de memoria, es decir, 31 millones de fallos de página potenciales en el primer acceso
  • En un benchmark largo que usa un shared buffer pool de 120 GB, nuevas regiones de memoria siguen entrando al working set, así que los page faults no ocurren solo al inicio, sino de forma continua
  • Si dentro de StrategyGetBuffer, mientras mantiene el spinlock, se accede a una zona de memoria compartida que todavía no está mapeada, ocurre un minor page fault
  • PREEMPT_NONE (antes de Linux 7.0): aunque el backend A entre al manejador de page faults, evita puntos voluntarios de reprogramación, así que es poco probable que sea desprogramado antes de resolver el fallo. La espera se alarga más de lo esperado, pero el daño es limitado
  • PREEMPT_LAZY (desde Linux 7.0): el scheduler puede expulsar al backend A dentro del manejador de page faults y programar otro proceso. Incluso cuando el fallo ya fue resuelto, aparece un tiempo de espera adicional t hasta que el scheduler le devuelva el control
    • Ese tiempo extra no cuesta solo t, sino que se amplifica como cantidad de backends girando en ese momento × t en CPU desperdiciada
    • En un entorno con 96 vCPU y cientos de backends, ese efecto multiplicador es devastador, y por eso el 56% de la CPU termina consumiéndose en s_lock

La solución con Huge Pages

  • Con shared_buffers de 120 GB, cambiar el tamaño de página de memoria reduce drásticamente la cantidad de fallos de página potenciales
    • Páginas de 4 KB: ~31,000,000 fallos de página potenciales
    • Huge Pages de 2 MB: ~61,440
    • Huge Pages de 1 GB: ~120
  • Aumentar el tamaño de página no solo reduce los page faults, también alivia la presión sobre la TLB: la misma memoria se cubre con muchas menos entradas de TLB, reduciendo los TLB misses y los page-table walks
  • Así, StrategyGetBuffer deja de provocar fallos mientras mantiene el lock, el dueño del lock termina rápido y los demás backends esperan microsegundos en vez de milisegundos. La regresión desaparece
  • En PostgreSQL, la configuración de huge pages se controla con el parámetro huge_pages
    • Soporta tres valores: off, on, try (predeterminado)
    • try usa huge pages si están disponibles, y si no, hace fallback silencioso a 4 KB, lo que puede ocultar una configuración incorrecta
    • Si se configura en on, PostgreSQL falla al arrancar cuando no puede usar huge pages, permitiendo detectar el problema de inmediato
  • Trade-off: las huge pages usan preasignación y reserva, así que aunque PostgreSQL no use toda esa memoria, el resto del sistema no podrá aprovecharla. Si solo se usa parte de una página, el resto se desperdicia. Aun así, en entornos de producción con shared_buffers grandes, normalmente vale la pena aceptar ese costo

Qué sigue

  • Peter Zijlstra, ingeniero del kernel de Intel que diseñó el cambio de expulsión, propuso que PostgreSQL adopte Restartable Sequences (rseq)
    • rseq es una función del kernel de Linux que permite que el código en espacio de usuario detecte si hubo expulsión o migración durante una sección crítica y reinicie esa sección
    • Si se aplicara rseq en la ruta de spinlocks de PostgreSQL, se podría evitar el escenario en el que un dueño de lock expulsado retrasa a todos los backends en espera
  • La reacción de la comunidad de PostgreSQL ha sido negativa
    • Consideran difícil aceptar que haya que adoptar una función adicional del kernel para recuperar un rendimiento que antes de Linux 7.0 se obtenía sin costo
    • También sostienen que esto va contra el viejo principio del kernel de “no romper el espacio de usuario” (si el software funcionaba bien antes de una actualización del kernel, debería seguir funcionando bien después)

Aún no hay comentarios.

Aún no hay comentarios.