Ajuste de rendimiento de Hiperescala (Citus)

APLICABLE A: Azure Database for PostgreSQL: Hiperescala (Citus)

La ejecución de una base de datos distribuida en todo su potencial ofrece alto rendimiento. Sin embargo, alcanzar ese rendimiento puede conllevar algunos ajustes en el código de aplicación y el modelado de datos. En este artículo se tratan algunas de las técnicas más comunes y eficaces para mejorar el rendimiento.

Agrupación de conexiones del lado cliente

Un grupo de conexiones contiene conexiones de base de datos abiertas que se pueden reutilizar. Una aplicación solicita una conexión desde el grupo cuando es necesario y el grupo devuelve una que ya se ha establecido si es posible o establece una nueva. Cuando ha terminado, la aplicación vuelve a liberar la conexión al grupo en lugar de cerrarla.

Agregar un grupo de conexiones del lado cliente es una manera fácil de aumentar el rendimiento de la aplicación con cambios mínimos en el código. Según nuestras medidas, la ejecución de instrucciones de inserción de una sola fila es aproximadamente 24 veces más rápido en un grupo de servidores de Hiperescala (Citus) con agrupación habilitada.

Para ver ejemplos específicos del lenguaje sobre cómo agregar agrupación en el código de la aplicación, consulte la guía de pilas de aplicaciones.

Nota

Hiperescala (Citus) también proporciona agrupación de conexiones del lado servidor mediante pgbouncer, pero principalmente sirve para aumentar el límite de conexión de cliente. El rendimiento de una aplicación individual se beneficia más de la agrupación del lado cliente que del lado servidor. (Aunque ambas formas de agrupación se pueden usar a la vez sin daños).

Determinación del ámbito de las consultas distribuidas

Actualizaciones

Al actualizar una tabla distribuida, intente filtrar las consultas por la columna de distribución, al menos cuando tenga sentido, si es que los nuevos filtros no cambian el significado de la consulta.

En algunas cargas de trabajo, es fácil. Las cargas de trabajo transaccionales u operativas, como aplicaciones SaaS multiinquilino o Internet de las cosas distribuyen las tablas por inquilino o dispositivo. Las consultas tienen como ámbito un inquilino o un identificador de dispositivo.

Por ejemplo, en nuestro tutorial multiinquilino, tenemos una tabla distribuida ads por company_id. La manera ingenua de actualizar un anuncio es destacarlo de la siguiente manera:

-- slow

UPDATE ads
   SET impressions_count = impressions_count+1
 WHERE id = 42; -- missing filter on distribution column

Aunque la consulta identifica de forma única una fila y la actualiza, Hiperescala (Citus) no sabe, en tiempo de planeamiento, que partición actualizará la consulta. Citus toma un elemento ShareUpdateExclusiveLock de todas las particiones para estar seguro, lo que bloquea otras consultas que intentan actualizar la tabla.

Aunque id era suficiente para identificar una fila, podemos incluir un filtro adicional para que la consulta sea más rápida:

-- fast

UPDATE ads
   SET impressions_count = impressions_count+1
 WHERE id = 42
   AND company_id = 1; -- the distribution column

El planeador de consultas de Hiperescala (Citus) ve un filtro directo por la columna de distribución y sabe exactamente qué partición se va a bloquear. En nuestras pruebas, al agregar filtros de la columna de distribución se ha aumentado el rendimiento de las actualizaciones paralelas en 100 veces.

Combinaciones y CTE

Hemos visto cómo las instrucciones UPDATE deben definir el ámbito de la columna de distribución para evitar bloqueos de particiones innecesarios. Otras consultas también se benefician de la definición del ámbito, normalmente para evitar la sobrecarga de red de los datos innecesariamente aleatorios entre los nodos de trabajo.

-- logically correct, but slow

WITH single_ad AS (
  SELECT *
    FROM ads
   WHERE id=1
)
SELECT *
  FROM single_ad s
  JOIN campaigns c ON (s.campaign_id=c.id);

Podemos acelerar la consulta filtrando por la columna de distribución, company_id, en el CTE y la instrucción principal SELECT.

-- faster, joining on distribution column

WITH single_ad AS (
  SELECT *
    FROM ads
   WHERE id=1 and company_id=1
)
SELECT *
  FROM single_ad s
  JOIN campaigns c ON (s.campaign_id=c.id)
 WHERE s.company_id=1 AND c.company_id = 1;

En general, al combinar tablas distribuidas, intente incluir la columna de distribución en las condiciones de combinación. Sin embargo, al realizar la combinación entre una tabla distribuida y una tabla de referencia no es necesario, ya que el contenido de la tabla de referencia se replica a todos los nodos de trabajo.

Si parece inconveniente agregar los filtros adicionales a todas las consultas, tenga en cuenta que hay bibliotecas auxiliares para varios marcos de aplicaciones populares que facilitan la tarea. Consulte las instrucciones siguientes:

Registro eficaz de bases de datos

Al registrar todas las instrucciones SQL, todo el tiempo se agrega sobrecarga. En nuestras mediciones, el uso de más un nivel de registro prudente ha mejorado las transacciones por segundo en 10 veces frente al registro completo.

Para una operación diaria eficaz, puede deshabilitar el registro, excepto los errores y las consultas de ejecución anómalas de larga duración:

establecer value reason
log_statement_stats Apagado Evitar la sobrecarga de generación de perfiles
log_duration Apagado No es necesario conocer la duración de las consultas normales.
log_statement Ninguno No registrar consultas sin un motivo más específico.
log_min_duration_statement Un valor más largo de lo que cree que deben tardar las consultas normales. Muestra las consultas anormalmente largas.

Nota

La configuración relacionada con el registro en nuestro servicio administrado tiene en cuenta las recomendaciones anteriores. Puede dejarlos como están. Sin embargo, a veces hemos visto que los clientes cambian la configuración para que el registro sea agresivo, lo que ha provocado problemas de rendimiento.

Contención de bloqueo

La base de datos usa bloqueos para mantener los datos coherentes en el acceso simultáneo. Sin embargo, algunos patrones de consulta requieren una cantidad excesiva de bloqueo y existen alternativas más rápidas.

Estado y bloqueos del sistema

Antes de profundizar en las ineficiencias comunes de bloqueo, veamos cómo ver los bloqueos y la actividad en todo el clúster de bases de datos. La vista citus_stat_activity proporciona una vista detallada.

La vista muestra, entre otras cosas, cómo los "eventos de espera" bloquean las consultas, incluidos los bloqueos. La agrupación por wait_event_type pinta una imagen del estado del sistema:

-- general system health

SELECT wait_event_type, count(*)
  FROM citus_stat_activity
 WHERE state != 'idle'
 GROUP BY 1
 ORDER BY 2 DESC;

Un valor wait_event_type NULL significa que la consulta no espera nada.

Si observa bloqueos en la salida de la actividad de estadísticas, puede ver las consultas bloqueadas específicas mediante citus_lock_waits:

SELECT * FROM citus_lock_waits;

Por ejemplo, si una consulta está bloqueada en otra que intenta actualizar la misma fila, verá que aparecen las instrucciones bloqueadas y bloqueadoras:

-[ RECORD 1 ]-------------------------+--------------------------------------
waiting_gpid                          | 10000011981
blocking_gpid                         | 10000011979
blocked_statement                     | UPDATE numbers SET j = 3 WHERE i = 1;
current_statement_in_blocking_process | UPDATE numbers SET j = 2 WHERE i = 1;
waiting_nodeid                        | 1
blocking_nodeid                       | 1

Para ver no solo los bloqueos que se producen en este momento, sino los patrones históricos, puede capturar bloqueos en los registros de PostgreSQL. Para más información, consulte la configuración del servidor log_lock_waits en la documentación de PostgreSQL. Otro gran recurso es Siete sugerencias para tratar con bloqueos en el blog de datos de Citus.

Problemas y soluciones habituales

Comandos DDL

Los comandos DDL, como truncate, drop y create index, aceptan todos bloqueos de escritura y bloquean las escrituras en toda la tabla. Minimizar tales operaciones reduce los problemas de bloqueo.

Sugerencias:

  • Intente consolidar DDL en ventanas de mantenimiento o úselos con menos frecuencia.

  • PostgreSQL admite la creación de índices simultáneamente para evitar aceptar un bloqueo de escritura en la tabla.

  • Considere la posibilidad de establecer lock_timeout en una sesión SQL antes de ejecutar un comando DDL pesado. Con lock_timeout, PostgreSQL anulará el comando DDL si el comando espera demasiado tiempo para un bloqueo de escritura. Un comando DDL que espera un bloqueo puede hacer que las consultas posteriores se pongan en cola detrás de él.

Inactividad en las conexiones de transacción

Las transacciones inactivas (sin confirmar) a veces bloquean otras consultas innecesariamente. Por ejemplo:

BEGIN;

UPDATE ... ;

-- Suppose the client waits now and doesn't COMMIT right away.
--
-- Other queries that want to update the same rows will be blocked.

COMMIT; -- finally!

Para limpiar manualmente las consultas de inactividad prolongada en el nodo de coordinación, puede ejecutar un comando como este:

SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname = 'citus'
 AND pid <> pg_backend_pid()
 AND state in ('idle in transaction')
 AND state_change < current_timestamp - INTERVAL '15' MINUTE;

PostgreSQL también ofrece un valor idle_in_transaction_session_timeout para automatizar la terminación de la sesión inactiva.

Interbloqueos

Citus detecta interbloqueos distribuidos y cancela sus consultas, pero la situación es menos eficaz que evitar interbloqueos en primer lugar. Un origen común de interbloqueos procede de actualizar el mismo conjunto de filas en un orden diferente desde varias transacciones a la vez.

Por ejemplo, al ejecutar estas transacciones en paralelo:

Sesión A:

BEGIN;
UPDATE ads SET updated_at = now() WHERE id = 1 AND company_id = 1;
UPDATE ads SET updated_at = now() WHERE id = 2 AND company_id = 1;

Sesión B:

BEGIN;
UPDATE ads SET updated_at = now() WHERE id = 2 AND company_id = 1;
UPDATE ads SET updated_at = now() WHERE id = 1 AND company_id = 1;

-- ERROR:  canceling the transaction since it was involved in a distributed deadlock

La sesión A actualizó el identificador 1, mientras que la sesión B actualizó el identificador 2 y luego el 1. Escriba código SQL para las transacciones con cuidado para actualizar las filas en el mismo orden. (A veces, el orden de actualización se denomina "jerarquía de bloqueo").

Según nuestros cálculos, la actualización masiva de un conjunto de filas con muchas transacciones era 3 veces más rápida al evitar el interbloqueo.

E/S durante la ingesta

Los cuellos de botella de E/S suelen ser menos problemáticos en Hiperescala (Citus) que en PostgreSQL de un solo nodo debido al particionamiento. Las particiones son tablas individualmente más pequeñas, con mejores tasas de aciertos de índice y caché, lo que produce un mejor rendimiento.

Sin embargo, incluso con Hiperescala (Citus), a medida que las tablas y los índices se hacen más grandes, la E/S de disco puede convertirse en un problema para la ingesta de datos. Lo que hay que tener en cuenta es el número creciente de entradas wait_event_type de "E/S" que aparecen en citus_stat_activity:

SELECT wait_event_type, wait_event count(*)
  FROM citus_stat_activity
 WHERE state='active'
 GROUP BY 1,2;

Ejecute la consulta anterior repetidamente para capturar información relacionada con los eventos de espera. Observe cómo cambian los recuentos de diferentes tipos de eventos de espera.

Examine también las métricas de Azure Portal, especialmente la métrica de IOPS que llegan al límite.

Sugerencias:

  • Si los datos están ordenados de forma natural, como en una serie temporal, use la creación de particiones de tablas de PostgreSQL. Consulte esta guía para información sobre cómo crear particiones de tablas distribuidas en Hiperescala (Citus).

  • Quite los índices sin usar. El mantenimiento del índice provoca la amplificación de E/S durante la ingesta. Para encontrar qué índices no se usan, utilice esta consulta.

  • Si es posible, evite indexar datos aleatorios. Por ejemplo, algunos algoritmos de generación UUID no siguen ningún orden. La indexación de este tipo de valor provoca mucha sobrecarga. Pruebe en su lugar una secuencia bigint o aumente de forma monotónica los UUID.

Resumen de los resultados

En las pruebas comparativas de ingesta sencilla con INSERT, UPDATE y bloques de transacciones, observamos las siguientes aceleraciones de consulta en las técnicas de este artículo.

Técnica Aceleración de consultas
Ámbito de las consultas 100x
Agrupación de conexiones 24x
Registro eficaz 10x
Evitar interbloqueo 3x

Pasos siguientes