Ajustar PostgreSQL gestionado, donde no controlas la máquina
Casi todos los incidentes de rendimiento que investigamos son un índice que falta, una reserva de conexiones que no existe, o un autovacío dimensionado para una base de datos diez veces menor. Agrandar la instancia es lo primero que prueba la gente y casi nunca es la respuesta.
La llamada llega en plena campaña. La base de datos va lenta, el panel muestra CPU alta, y alguien ya ha propuesto pasar al tamaño de instancia siguiente. Funcionará, brevemente, porque duplicar el hardware enmascara una consulta que hace un escaneo secuencial sobre una tabla de cuatro millones de filas. Dos meses después ocurre la misma conversación al doble de coste.
En un servicio gestionado no puedes tocar el núcleo, el sistema de ficheros ni casi nada de la configuración, y está bien, porque casi ninguna de las victorias está ahí. Están en las consultas, los índices, las conexiones y el vacío.
Empieza por la extensión de estadísticas, siempre
La extensión de estadísticas de sentencias es lo primero que hay que activar y el primer sitio donde mirar. Agrega estadísticas de ejecución por consulta normalizada, y convierte "la base de datos va lenta" en una lista ordenada.
Ordena por tiempo total de ejecución y no por media. La consulta que tarda doscientos milisegundos y se ejecuta cuarenta mil veces por hora cuesta mucho más que la que tarda cuatro segundos dos veces al día, y los equipos optimizan sistemáticamente la segunda porque se siente más lenta.
Mira las diez primeras por tiempo total y hazle tres preguntas a cada una: con qué frecuencia se ejecuta, necesita ejecutarse tan a menudo, y qué pinta tiene su plan. Cachear o agrupar la frecuente es con frecuencia una victoria mayor que optimizarla.
Después activa el registro de consultas lentas con un umbral, y la extensión que muestrea planes de ejecución si tu proveedor la ofrece, porque el plan de producción es el que importa y a menudo difiere del que sale en local con un conjunto de datos pequeño.
Índices, en las dos direcciones
Los índices que faltan son la causa más común de un incidente de rendimiento de base de datos. Encuéntralos buscando escaneos secuenciales sobre tablas grandes en las vistas de estadísticas, y leyendo los planes de las consultas principales.
Tres cosas que se escapan. El orden de las columnas de un índice compuesto importa, y tiene que coincidir con el patrón de filtrado y ordenación de la consulta, así que un índice sobre dos columnas sirve a las consultas que filtran por la primera pero no a las que filtran solo por la segunda. Un índice parcial con una condición es mucho más pequeño y rápido cuando la mayoría de las filas son irrelevantes, por ejemplo indexando solo las filas que no están borradas lógicamente. Y un índice que incluya las columnas seleccionadas permite a la base de datos responder sin tocar la tabla.
Los índices innecesarios son la otra mitad y se ignoran. Cada índice se escribe en cada inserción, actualización y borrado. Una tabla con once índices hace once veces el trabajo de escritura más el de la tabla, y su hinchazón y su coste de vacío suben en consecuencia. Las vistas de estadísticas te dicen qué índices no se han usado nunca; bórralos, con cuidado y de uno en uno, en una ventana que puedas revertir.
Construye siempre los índices en modo concurrente sobre una tabla viva. La versión no concurrente toma un bloqueo que impide escrituras, que es la forma en que un índice pensado para arreglar un problema de rendimiento provoca una caída.
Conexiones y la reserva que probablemente te falta
PostgreSQL usa un proceso por conexión, lo que es caro. Cada conexión consume memoria esté haciendo algo o no, y a partir de unos cientos la sobrecarga de planificación degrada todo.
Los marcos de aplicación modernos traen por defecto una reserva por instancia. Diez instancias con una reserva de veinte cada una son doscientas conexiones, y un grupo con autoescalado o una función sin servidor que se despliega hace ese número ilimitado, que es la forma más común en que una base de datos se cae durante un pico de tráfico.
La respuesta es un agrupador de conexiones en modo transacción entre la aplicación y la base de datos, o el proxy gestionado que ofrezca tu proveedor o PgBouncer. Multiplexa muchas conexiones de cliente sobre pocas de servidor, y es la diferencia entre una base de datos que sobrevive a un pico y una que no.
La trampa del modo transacción es que las funciones de sesión dejan de comportarse como se espera: sentencias preparadas, bloqueos consultivos, variables de sesión y la mensajería por notificaciones. Comprueba qué hace tu mapeador de objetos antes de cambiar, porque el fallo es sutil en vez de ruidoso.
El autovacío casi siempre está infradimensionado
PostgreSQL no sobrescribe filas en el sitio. Una actualización escribe una versión nueva y deja la antigua muerta, y el autovacío recupera ese espacio. Cuando no puede seguir el ritmo, las tablas se hinchan, los índices se hinchan, los planes se degradan, y con el tiempo el sistema empieza a avisar del reinicio del contador de transacciones.
Los valores por defecto son conservadores y se eligieron para una base de datos mucho más pequeña. En una tabla con mucho tráfico de actualización o borrado, el factor de escala significa que el vacío espera hasta que una proporción fija de la tabla está muerta, que en una tabla grande es un número enorme de filas.
Ajusta por tabla en vez de globalmente. Para el puñado de tablas con mucha rotación, baja el factor de escala para que el vacío corra más a menudo sobre cantidades menores, y sube el límite de coste para que trabaje más rápido cuando corra. Deja el resto en paz.
Vigila el número de tuplas muertas y la edad de la transacción más antigua. Los dos están en las vistas de estadísticas y los dos son indicadores adelantados de un problema que se presenta mucho después como lentitud inexplicable.
Réplicas de lectura y el retraso que te pilla
Una réplica de lectura saca el tráfico de lectura del primario y es el movimiento de escalado obvio. También introduce retraso de replicación, y el retraso es donde están los errores.
El patrón que se rompe: una escritura seguida inmediatamente de una lectura del mismo dato, encaminada a una réplica que todavía no lo ha recibido. El usuario crea algo y no está. Esto no es raro, es el comportamiento por defecto bajo carga, y hay que tratarlo explícitamente encaminando la lectura posterior a escritura al primario o esperando a la posición de la réplica.
Vigila el retraso como métrica de primer nivel con una alerta, y decide qué hace la aplicación cuando el retraso es alto. Servir datos rancios en silencio es una decisión; tomarla a propósito es mejor que descubrirla.
Las réplicas no ayudan nada a las cargas intensivas en escritura, y los equipos recurren a ellas cuando el problema real es la amplificación de escritura por tener demasiados índices.
Dimensionar la instancia, al final
Una vez atendidos consultas, índices, conexiones y vacío, el tamaño es una palanca real y menor de lo esperado.
Dos puntos prácticos en servicios gestionados. El rendimiento del almacenamiento se aprovisiona con frecuencia aparte de la capacidad, y una base de datos que ha agotado su crédito de ráfaga en un volumen de propósito general produce un incidente de latencia que parece un problema de CPU; comprueba el caudal aprovisionado del volumen antes de añadir núcleos. Y la memoria importa más que los núcleos en casi todas las cargas, porque determina cuánto del conjunto de trabajo se queda en caché, que es por lo que las formas flexibles de las que habla controlar el coste de Oracle Cloud resultan útiles cuando una carga es intensiva en memoria.
Toma la decisión de tamaño con la consecuencia de coste a la vista, ya que una clase de instancia de base de datos es una decisión con coste de salida y no un alquiler mensual, como se señala en el coste se decide en el pull request.
Lo que se olvida
- Las estadísticas se quedan rancias tras una carga masiva. Ejecuta un análisis después de una importación grande, o el planificador elegirá mal usando estimaciones de filas desactualizadas.
- Las transacciones largas bloquean el vacío. Una sesión inactiva dentro de una transacción mantiene abierto el horizonte e impide la limpieza en toda la base de datos.
- Las migraciones de esquema toman bloqueos. Añadir una columna con valor por defecto, cambiar un tipo o añadir una restricción puede bloquear una tabla mientras dure. Pon un tiempo de espera de bloqueo y usa las formas que no bloquean.
- Las actualizaciones de versión mayor vacían la caché de planes y pueden cambiarlos. Prueba sobre una copia de los datos de producción, no sobre un esquema vacío.
- Las extensiones están limitadas en los servicios gestionados. Comprueba la disponibilidad antes de diseñar sobre una.
- El servicio gestionado hace copias; la prueba de restauración sigue siendo tuya, que es la disciplina de objetivos de recuperación y la prueba de restauración.
Qué hacer esta semana
Activa la extensión de estadísticas de sentencias si no está, espera un día, y saca las diez consultas con más tiempo total de ejecución. Coge la primera y lee su plan. En casi todos los proyectos que hemos hecho, esa única consulta es responsable de buena parte de la carga, y el arreglo es un índice y no una instancia. Arrancamos la línea de base de datos de un proyecto cloud con exactamente esa lista.