Ajuste de los puntos de control

En los sistemas que realizan una gran cantidad de escrituras, el ajuste de los puntos de control (checkpoints) es crucial para un buen rendimiento. Sin embargo, los puntos de control son uno de los ámbitos en los que a menudo nos topamos con problemas de configuración, tanto en las listas de correo de la comunidad como al revisar el rendimiento de los sistemas de nuestros clientes.

El objetivo de los puntos de control

PostgreSQL emplea el registro WAL (Write Ahead Log): todos los cambios se escriben primero en el WAL y sólo entonces se aplican a los archivos de datos. El propósito principal de WAL es la durabilidad - en caso de una caída, la base de datos puede leer los cambios desde el WAL y volver a aplicarlos a los archivos de datos, reconstruyéndolos a un estado consistente.

Supongamos que se produce una caída del sistema y la base de datos necesita realizar una recuperación. El enfoque más sencillo sería empezar desde cero y reproducir por completo el WAL desde el momento en que se inicializó la base de datos. El resultado debería ser una base de datos completa (y consistente). Sin embargo, una desventaja obvia es la necesidad de almacenar y reproducir todo el WAL. A menudo trabajamos con bases de datos cuyo tamaño no es excesivo (por ejemplo, unos pocos cientos de GB), pero que generan varios TB de WAL al día porque tienen un alto índice de actualizaciones y eliminaciones. Así que empezar desde cero no sería muy práctico, tanto por el espacio requerido en el disco para almacenar el WAL como por el tiempo necesario para reproducir todos los cambios.

Los puntos de control constituyen un método para solucionar este problema, puesto que limitan la cantidad de WAL que es necesario volver a aplicar durante la recuperación. La base de datos se asegura periódicamente de que todos los cambios, hasta un punto determinado del WAL, sean escritos de forma duradera en los archivos de datos, de modo que no sea necesario consultar los WAL más antiguos durante la recuperación. Como resultado, puesto que la base de datos no necesita conservar el WAL ni reproducirlo durante la recuperación, se solucionan los problemas de espacio en el disco y de duración.

La cuestión es entonces, con cuánta frecuencia deben crearse los puntos de control. Es evidente que hacerlo con muy poca frecuencia aumenta la cantidad de WAL que necesitamos conservar así como la duración de la recuperación. En cambio, hacerlo con mucha frecuencia (más o menos cada minuto) minimiza la cantidad de WAL y la duración de la recuperación, aunque afecta negativamente al rendimiento de la base de datos si no está en fase de recuperación (que debería ser la mayor parte del tiempo).

¿Cómo mejoran el rendimiento los puntos de control? Aunque en un caso extremo la escritura de datos en el WAL y en los archivos de datos de hecho pudiera duplicar la cantidad de escrituras, en la práctica raramente esto ocurre, ya que los bloques suelen modificarse de forma repetida y, posteriormente, el punto de control escribe la página de datos modificada sólo una vez en segundo plano, combinando de forma efectiva todas las escrituras. Esto es un detalle particularmente importante porque, mientras que las escrituras en el WAL son por naturaleza secuenciales, las escrituras en los archivos de datos (tablas de respaldo e índices) son esencialmente más aleatorias. El checkpointer realiza varias optimizaciones adicionales para lograr que las escrituras sean más secuenciales (ordenando los datos, etc.), aunque todos estos beneficios sólo funcionan con una cantidad suficiente de datos en escritura.

Puesto que la base de datos también necesita protegerse del riesgo que suponen las "páginas dañadas", al modificar por primera vez una página después de un punto de control, la escribirá en el WAL de forma completa (no sólo la parte modificada). En consecuencia, puntos de control frecuentes pueden causar un incremento de la escritura en el WAL (lo cual a su vez, podría activar puntos de control con mayor frecuencia).

Finalmente, los puntos de control deben comprobar que todos los datos estén realmente en el disco, y no en la caché del búfer del sistema operativo o en la caché de escritura volátil de un controlador del disco. Así que PostgreSQL debe pedir al sistema operativo que sincronice todas las escrituras con el almacenamiento permanente, normalmente con fsync(). Esto puede requerir mucho tiempo e interrumpir otras actividades simultáneas.

Así que crear puntos de control muy frecuentes (digamos, cada par de segundos o cada minuto más o menos) reduciría al mínimo la cantidad de WAL necesaria para la recuperación y la duración de la misma. Por otro lado, también convertiría las escrituras asíncronas en segundo plano en síncronas, lo cual perjudicaría seriamente la actividad del usuario (por ejemplo, aumentando la latencia de COMMIT, reduciendo el rendimiento, etc.). De hecho, este es uno de los problemas que se observan con los valores de configuración por defecto en bases de datos de gran tamaño y/o con mucha actividad.

Así que en la práctica, queremos que los puntos de control se produzcan con una frecuencia mínima para no afectar a los usuarios, pero lo suficientemente frecuente como para limitar razonablemente el tiempo de recuperación y los requisitos de espacio en el disco. El objetivo del ajuste es encontrar un compromiso razonable.

Activación de los puntos de control

Existen unas tres o cuatro razones por las que se puede activar un punto de control:

  • ejecutando directamente el comando `CHECKPOINT
  • ejecutando un comando que requiere un punto de control (por ejemplo, pg_start_backupCREATE DATABASE, o pg_ctl stop|restart y un par de otros más)
  • alcanzando una cantidad de tiempo establecida desde el último punto de control
  • generando una cantidad establecida de WAL desde el punto de control anterior (también conocido como “running out of WAL” o "filling WAL")

Los dos primeros casos son bastante irrelevantes en este contexto, ya que se trata de eventos poco comunes y activados de forma manual. Este artículo habla de la configuración de puntos de control periódicos regulares, activados ya sea por el tiempo y/o la cantidad de WAL generado desde el último punto de control.

Estos límites de tiempo y tamaño se establecen mediante dos opciones de configuración:

checkpoint_timeout = 5min
max_wal_size = 1GB (before PostgreSQL 9.5 this was checkpoint_segments)

Con estos valores (por defecto), PostgreSQL activará un CHECKPOINT cada 5 minutos, o después de que el WAL alcance aproximadamente 1GB en el disco.

Nota: La opción max_wal_size (que representa un límite blando para la capacidad total del WAL) tiene dos consecuencias. En primer lugar, la base de datos procurará no excederlo (aunque se le permite hacerlo), por lo que deberá mantenerse suficiente espacio libre en la partición y monitorearlo. En segundo lugar, no se trata de un límite "por punto de control" - debido a la distribución de los puntos de control (que se explica más adelante) la cuota del WAL se divide entre 2 o 3 puntos de control. Así que con max_wal_size la base de datos comenzará un CHECKPOINT después de escribir 300 - 500 MB de WAL, dependiendo de checkpoint_completion_target.

Como la mayoría de los valores por defecto en el archivo de configuración de ejemplo, los valores por defecto son bastante bajos, diseñados para funcionar incluso en sistemas de tamaño reducido como la Raspberry Pi.

Pero, ¿cómo determinar los valores adecuados para cada sistema? El objetivo es no crear puntos de control con demasiada o con poca frecuencia. Nuestra "práctica recomendada" de ajuste consiste en dos pasos:

  1. elegir un valor "razonable" de checkpoint_timeout.
  2. establecer un valor de max_wal_size lo suficientemente alto como para que rara vez se alcance.

La definición de valor "razonable" para checkpoint_timeout depende sobre todo del objetivo de tiempo de recuperación (RTO), es decir, de la duración máxima aceptable para la recuperación.

Hay que tomar en cuenta que checkpoint_timeout es un límite relacionado con el tiempo requerido para generar el WAL, no directamente con el tiempo de recuperación. Existen dos razones por las que no se puede determinar con exactitud el tiempo de recuperación. En primer lugar, el WAL suele ser generado por múltiples procesos concurrentes (consultas, etc.), mientras que la recuperación la realiza un único proceso (y es poco probable que esta limitación desaparezca pronto). En segundo lugar, la recuperación suele producirse justo después de un reinicio, cuando las cachés del sistema de archivos están frías.

Sin embargo, en general, el valor por defecto (5 minutos) es demasiado bajo y los valores entre 30 minutos y 1 hora son muy comunes. PostgreSQL 9.6 incluso aumenta el valor máximo de 1 hora a 1 día. De todos modos, aunque 30 minutos puede no ser perfecto, es probablemente más razonable que el valor por defecto

checkpoint_timeout = 30min

El siguiente paso consiste en estimar la cantidad de WAL que produce la base de datos en 30 minutos, de modo que podamos utilizarla para max_wal_size. Existen varias formas para determinar la cantidad de WAL que se genera:

  • Observar la posición real del WAL (esencialmente el offset en un archivo) usando pg_current_xlog_insert_location cada 30 minutos, y calculando la diferencia entre las posiciones.
  • Activar log_checkpoints=on y luego extraer la información del registro del servidor (habrá estadísticas detalladas para cada punto de control completado, incluyendo la cantidad de WAL).
  • Utilizando los datos de pg_stat_bgwriter, que también incluye información sobre el número de puntos de control (la cual puede combinarse con el conocimiento del valor actual de max_wal_size).

Utilicemos el primer método, por ejemplo. En una máquina de pruebas que ejecute pgbench, podría observarse lo siguiente:

postgres=# SELECT pg_current_xlog_insert_location();
 pg_current_xlog_insert_location 
---------------------------------
 3D/B4020A58
(1 row)

... después de 5 minutos ...

postgres=# SELECT pg_current_xlog_insert_location();
 pg_current_xlog_insert_location 
---------------------------------
 3E/2203E0F8
(1 row)

postgres=# SELECT pg_xlog_location_diff('3E/2203E0F8', '3D/B4020A58');
 pg_xlog_location_diff 
-----------------------
            1845614240
(1 row)

Esto significa que cada 5 minutos, la base de datos generaba aproximadamente 1,8 GB de WAL, es decir, unos unos 10 GB de WAL cada 30 minutos. Sin embargo, como se mencionó antes, max_wal_size es una cuota para 2 - 3 puntos de control combinados, por lo que max_wal_size = 30GB (3 x 10 GB) resulta correcto.

Los demás enfoques utilizan diferentes fuentes de datos, aunque la idea es la misma.

Distribución de los puntos de control

El ajuste de checkpoint_timeout y max_wal_size no es lo único. Existe otro parámetro, llamado checkpoint_completion_target, aunque para ajustarlo es necesario entender el concepto de "distribución de los puntos de control".

Durante un CHECKPOINT, la base de datos necesita realizar estos tres pasos básicos:

  • identificar todos los bloques sucios (modificados) en los buffers compartidos
  • escribir todos esos buffers en el disco (o más bien en la caché del sistema de archivos)
  • realizar un fsync de todos los archivos modificados al disco

Sólo al finalizar todos esos pasos, el punto de control puede considerarse completo. Se podrían realizar estos pasos "lo más rápido posible", es decir, escribir todos los buffers sucios de una sola vez y luego invocar fsync en los archivos (eso es lo que hacía PostgreSQL hasta la versión 8.2). Sin embargo, este procedimiento provoca suspensiones de E/S debido a la saturación de las cachés del sistema de archivos y de los dispositivos, lo cual afecta negativamente a las sesiones de los usuarios.

Para solucionar este problema, PostgreSQL 8.3 introdujo el concepto de "distribución de los puntos de control": en lugar de escribir todos los datos simultáneamente, las escrituras se distribuyen a lo largo de un período de tiempo más amplio. Esto le da tiempo al sistema operativo para eliminar los datos sucios en segundo plano, logrando que el fsync final sea mucho más sencillo. Sin embargo, se reduce la capacidad de PostgreSQL para combinar las escrituras y eliminar los duplicados de las que se repiten en la misma página, por lo que puede aumentar la carga total de E/S, además de uniformizarla. Las mejoras de latencia y la uniformidad del rendimiento ofrecidas por los puntos de control casi siempre valen este coste.

Las escrituras se aceleran en función del progreso hacia el siguiente punto de control: la base de datos sabe cuánto tiempo / WAL queda hasta un nuevo punto de control, y puede calcular cuántos búferes deberían ya estar escritos. La base de datos, sin embargo, no debe generar escrituras hasta el final - eso implicaría que el último lote de escrituras estuviera todavía en la caché del sistema de archivos, por lo que las llamadas finales a fsync (emitidas justo antes de comenzar el siguiente punto de control) resultarían de nuevo costosas.

Por lo tanto, necesitamos distribuir las escrituras a lo largo de la duración del punto de control (esencialmente checkpoint_timeout), aunque dejando al kernel suficiente tiempo para enviar los datos al disco en segundo plano. Por lo general, la expulsión de la caché de páginas (caché del sistema de archivos de Linux) se realiza en función del tiempo, especialmente mediante este parámetro del kernel:

vm.dirty_expire_centisecs = 3000

lo cual indica que los datos expiran después de 30 segundos (por defecto).

Nota: Cuando se trata de parámetros del kernel, es importante calibrar vm.dirty_background_bytes. En sistemas con mucha memoria el valor por defecto es demasiado alto, lo cual hace que el kernel acumule una gran cantidad de datos sucios en la caché del sistema de archivos. El kernel a menudo decide sincronizarlos todos a la vez, reduciendo el beneficio de los puntos de control distribuidos.

Los puntos de control distribuidos se configuran mediante este parámetro de PostgreSQL:

checkpoint_completion_target = 0.5

que dice cuánto falta para el siguiente punto de control para que se completen todas las escrituras. Por ejemplo, asumiendo que los puntos de control se activan sólo con checkpoint_timeout = 5min, la base de datos acelerará las escrituras para que la última escritura se realice después de 2,5 minutos desde el inicio del punto de control. El sistema operativo tiene entonces otros 2,5 minutos para sincronizar los datos en el disco, de modo que las llamadas fsync emitidas al final de un punto de control son sencillas y rápidas.

Dejar al sistema 2,5 minutos es algo excesivo, teniendo en cuenta que el tiempo de expiración es de sólo 30 segundos, y se podría incrementar checkpoint_completion_target por ejemplo a 0,85 lo que dejaría al sistema unos 45 segundos, un poco más de los 30 segundos que necesita. No obstante, esto no es recomendable, porque en caso de escrituras intensivas, el punto de control puede activarse por max_wal_size mucho antes de los 5 minutos, dejando al sistema operativo menos de 30 segundos.

Sin embargo, los sistemas que manejan cargas de trabajo de escritura intensiva suelen funcionar con valores de checkpoint_timeouts mucho más altos, lo que hace que el valor por defecto de completion_target sea definitivamente demasiado bajo. Por ejemplo, si el tiempo de espera se establece en 30 minutos, se obliga a la base de datos a realizar todas las escrituras en los primeros 15 minutos (al doble de la velocidad de escritura), y luego a quedarse inactiva durante los 15 minutos restantes. En cambio, se podría intentar establecer checkpoint_completion_target de forma aproximada utilizando esta fórmula

(checkpoint_timeout - 2min) / checkpoint_timeout

que para 30 minutos equivale a unos 0,93. En ocasiones se recomienda no exceder el 0.9 - lo cual probablemente esté bien, y es poco probable que se observe una diferencia significativa entre esos dos valores. (La situación puede cambiar cuando se utilizan valores muy elevados de checkpoint_timeout, que ahora pueden ser de hasta 1 día desde PostgreSQL 9.6).

En resumen

Así que ahora deberían saber cuál es el propósito de los puntos de control, y también los conceptos básicos de su configuración:

  • La mayoría de los puntos de control deberían basarse en el factor tiempo, es decir, ser activados por checkpoint_timeout.
  • busquen el compromiso entre rendimiento (puntos de control poco frecuentes) y tiempo necesario para la recuperación (puntos de control más frecuentes)
  • los valores entre 15-30 minutos son los más comunes, aunque llegar a 1 hora puede ser razonable
  • tras decidir el tiempo de espera, elijan max_wal_size estimando la cantidad de WAL
  • configuren checkpoint_completion_target para que el kernel tenga suficiente (aunque no innecesariamente mucho) tiempo para sincronizar los datos en el disco
  • ajusten también vm.dirty_background_bytes para evitar que el kernel acumule muchos datos sucios en la caché de página
  • al determinar la dimension de los puntos de control, consideren cuánto tiempo pueden tolerar que se ejecute la recuperación antes de que el sistema vuelva a estar disponible después de una caída