Ahorre mucho dinero en grandes volúmenes en PostgreSQL

Continuando con el tema de la grabación de grandes flujos de datos, planteados por el artículo anterior sobre particionamiento , en esto consideramos las formas en que puede reducir el tamaño "físico" almacenado en PostgreSQL y su impacto en el rendimiento del servidor.

Se trata de la configuración de TOAST y la alineación de datos . "En promedio", estos métodos ahorrarán no demasiados recursos, pero sin ninguna modificación en el código de la aplicación.


Sin embargo, nuestra experiencia resultó ser muy productiva a este respecto, ya que el repositorio de casi cualquier monitoreo es, por su naturaleza, mayormente solo en términos de datos registrados. Y si está interesado en cómo puede enseñarle a una base de datos a escribir en un disco en lugar de 200 MB / s la mitad, le pido un corte.

Pequeños secretos de Big Data


Según el perfil de nuestro servicio , recibe regularmente paquetes de texto de los registros .

Y dado que el complejo VLSI , cuyas bases de datos estamos monitoreando, es un producto multicomponente con estructuras de datos complejas, las consultas para lograr el máximo rendimiento se obtienen mediante tales "volúmenes múltiples" con lógica algorítmica compleja . Por lo tanto, el volumen de cada instancia individual de la solicitud o el plan de ejecución resultante en el registro que llega a nosotros resulta ser "promedio" bastante grande.

Veamos la estructura de una de las tablas en la que escribimos los datos "en bruto", es decir, aquí está el texto original de la entrada del registro:

CREATE TABLE rawdata_orig(
  pack -- PK
    uuid NOT NULL
, recno -- PK
    smallint NOT NULL
, dt --  
    date
, data --  
    text
, PRIMARY KEY(pack, recno)
);

Una placa típica (ya dividida, por supuesto, es una plantilla de sección), donde lo más importante es el texto. A veces bastante voluminoso.

Recuerde que el tamaño "físico" de un registro en PG no puede ocupar más de una página de datos, pero el tamaño "lógico" es un asunto completamente diferente. Para escribir un valor de volumen (varchar / text / bytea) en el campo, se utiliza la tecnología TOAST :
PostgreSQL utiliza un tamaño de página fijo (generalmente 8 KB) y no permite que las tuplas abarquen varias páginas. Por lo tanto, es imposible almacenar directamente valores de campo muy grandes. Para superar esta limitación, los valores de campo grandes se comprimen y / o dividen en varias líneas físicas. Esto pasa desapercibido para el usuario y afecta ligeramente a la mayoría del código del servidor. Este método se conoce como TOAST ...

De hecho, para cada tabla con campos "potencialmente grandes" , se crea automáticamente una tabla emparejada con "corte" de cada registro "grande" en segmentos de 2 KB:

TOAST(
  chunk_id
    integer
, chunk_seq
    integer
, chunk_data
    bytea
, PRIMARY KEY(chunk_id, chunk_seq)
);

Es decir, si tenemos que escribir una fila con un valor "grande" data, el registro real se producirá no solo en la tabla principal y su PK, sino también en TOAST y su PK .

Reduce el efecto TOSTADA


Pero la mayoría de los registros aquí todavía no son tan grandes, deberían caber en 8 KB : ¿cómo ahorraría en esto? ...

Aquí el atributo STORAGEen la columna de la tabla nos ayuda :
  • EXTENDIDO permite tanto la compresión como el almacenamiento por separado. Esta es la opción estándar para la mayoría de los tipos de datos compatibles con TOAST. Primero, se intenta realizar la compresión, luego se guarda fuera de la tabla si la fila sigue siendo demasiado grande.
  • PRINCIPAL permite la compresión, pero no el almacenamiento por separado. (De hecho, se realizará un almacenamiento por separado para tales columnas, pero solo como último recurso , cuando no haya otra forma de reducir la fila para que se ajuste a la página).
De hecho, esto es exactamente lo que necesitamos para el texto: exprimirlo tanto como sea posible, e incluso si no cabe en absoluto, ponerlo en TOAST . Puede hacer esto directamente "sobre la marcha", con un comando:

ALTER TABLE rawdata_orig ALTER COLUMN data SET STORAGE MAIN;

¿Cómo evaluar el efecto?


Dado que el flujo de datos cambia todos los días, no podemos comparar números absolutos, pero en términos relativos, cuanto menor sea la proporción que registramos en TOAST, mejor. Pero existe un peligro: cuanto mayor es el volumen "físico" de cada registro individual, más "ancho" se vuelve el índice, porque se deben cubrir más páginas de datos.

Sección antes de los cambios :
heap  = 37GB (39%)
TOAST = 54GB (57%)
PK    =  4GB ( 4%)

Sección después de los cambios :
heap  = 37GB (67%)
TOAST = 16GB (29%)
PK    =  2GB ( 4%)

De hecho, comenzamos a escribir en TOAST 2 veces con menos frecuencia , lo que descargó no solo el disco, sino también la CPU:



Observo que también comenzamos a "leer" menos el disco, no solo a "escribir", porque cuando inserta un registro en una tabla, también tiene que "restar" una parte del árbol de cada uno de los índices para determinar su posición futura en ellos.

Quién en PostgreSQL 11 vive bien


Después de actualizar a PG11, decidimos continuar "ajustando" TOAST y notamos que a partir de esta versión, el parámetro estuvo disponible para la configuración toast_tuple_target:
El código de procesamiento de TOAST se activa solo cuando el valor de fila que se almacenará en la tabla es mayor que TOAST_TUPLE_THRESHOLD bytes (generalmente 2 Kb). El código de TOAST comprimirá y / o moverá los valores de campo fuera de la tabla hasta que el valor de la fila sea menor que TOAST_TUPLE_TARGET bytes (variable, generalmente también 2 KB) o sea imposible reducir el tamaño.
Decidimos que los datos que usualmente tenemos son "muy cortos" o inmediatamente "muy largos", por lo que decidimos limitarnos al valor más bajo posible:

ALTER TABLE rawplan_orig SET (toast_tuple_target = 128);

Veamos cómo la nueva configuración afectó la carga del disco después de la migración:


¡No está mal! La cola promedio para un disco se redujo aproximadamente 1.5 veces, y la "ocupación" del disco - ¡en un 20 por ciento! Pero tal vez esto de alguna manera afectó a la CPU?


Al menos, definitivamente no empeoró. Sin embargo, es difícil juzgar si incluso tales volúmenes aún no pueden elevar la carga promedio de la CPU por encima del 5% .

Desde un cambio de posición, la suma ... ¡cambia!


Como sabe, un centavo ahorra un rublo, y con nuestros volúmenes de almacenamiento de aproximadamente 10 TB / mes, incluso una pequeña optimización puede dar una buena ganancia. Por lo tanto, llamamos la atención sobre la estructura física de nuestros datos: cómo se presentan exactamente los "campos" dentro del registro de cada tabla.

Debido a la alineación de datos, esto afecta directamente el volumen resultante :
Muchas arquitecturas proporcionan la alineación de datos a través de los límites de palabras de máquina. Por ejemplo, en un sistema x86 de 32 bits, los enteros (tipo entero, ocupa 4 bytes) se alinearán en el borde de las palabras de 4 bytes, así como los números de coma flotante de precisión doble (tipo de precisión doble, 8 bytes). Y en un sistema de 64 bits, los valores dobles se alinearán en el borde de las palabras de 8 bytes. Este es otro motivo de incompatibilidad.

Debido a la alineación, el tamaño de la fila de la tabla depende del orden de los campos. Por lo general, este efecto no es muy notable, pero en algunos casos puede conducir a un aumento significativo de tamaño. Por ejemplo, si coloca campos de tipos char (1) y enteros mezclados, entre ellos, por regla general, se perderán 3 bytes por nada.

Comencemos con los modelos sintéticos:

SELECT pg_column_size(ROW(
  '0000-0000-0000-0000-0000-0000-0000-0000'::uuid
, 0::smallint
, '2019-01-01'::date
));
-- 48 

SELECT pg_column_size(ROW(
  '2019-01-01'::date
, '0000-0000-0000-0000-0000-0000-0000-0000'::uuid
, 0::smallint
));
-- 46 

¿De dónde vino el par extra de bytes en el primer caso? Todo es simple: una letra pequeña de 2 bytes se alinea en un límite de 4 bytes antes del siguiente campo, y cuando es el último, no hay nada y no es necesario alinearlo.

En teoría, todo está bien y puede reorganizar los campos a su gusto. Veamos datos reales en el ejemplo de una de las tablas, cuya sección diaria toma 10-15GB.

Estructura fuente:

CREATE TABLE public.plan_20190220
(
--  from table plan:  pack uuid NOT NULL,
--  from table plan:  recno smallint NOT NULL,
--  from table plan:  host uuid,
--  from table plan:  ts timestamp with time zone,
--  from table plan:  exectime numeric(32,3),
--  from table plan:  duration numeric(32,3),
--  from table plan:  bufint bigint,
--  from table plan:  bufmem bigint,
--  from table plan:  bufdsk bigint,
--  from table plan:  apn uuid,
--  from table plan:  ptr uuid,
--  from table plan:  dt date,
  CONSTRAINT plan_20190220_pkey PRIMARY KEY (pack, recno),
  CONSTRAINT chck_ptr CHECK (ptr IS NOT NULL),
  CONSTRAINT plan_20190220_dt_check CHECK (dt = '2019-02-20'::date)
)
INHERITS (public.plan)

La sección después de cambiar el orden de las columnas es exactamente el mismo campo, solo el orden es diferente :

CREATE TABLE public.plan_20190221
(
--  from table plan:  dt date NOT NULL,
--  from table plan:  ts timestamp with time zone,
--  from table plan:  pack uuid NOT NULL,
--  from table plan:  recno smallint NOT NULL,
--  from table plan:  host uuid,
--  from table plan:  apn uuid,
--  from table plan:  ptr uuid,
--  from table plan:  bufint bigint,
--  from table plan:  bufmem bigint,
--  from table plan:  bufdsk bigint,
--  from table plan:  exectime numeric(32,3),
--  from table plan:  duration numeric(32,3),
  CONSTRAINT plan_20190221_pkey PRIMARY KEY (pack, recno),
  CONSTRAINT chck_ptr CHECK (ptr IS NOT NULL),
  CONSTRAINT plan_20190221_dt_check CHECK (dt = '2019-02-21'::date)
)
INHERITS (public.plan)

El volumen total de la sección está determinado por el número de "hechos" y depende solo de procesos externos, por lo que dividimos el tamaño del montón ( pg_relation_size) por el número de registros que contiene; es decir, obtenemos el tamaño promedio del registro almacenado real :


Menos del 6% del volumen , ¡excelente!

Pero todo, por supuesto, no es tan optimista, porque en los índices no podemos cambiar el orden de los campos y, por lo tanto, "en general" ( pg_total_relation_size) ...


... después de todo, ahorraron 1.5% aquí , sin cambiar una sola línea de código. ¡Sí Sí!



Observo que la disposición anterior de los campos no es el hecho más óptimo. Debido a que algunos bloques de campo ya no quieren ser "desgarrados" por razones estéticas, por ejemplo, un par (pack, recno), que es PK para esta tabla.

En general, la definición de la disposición de campo "mínimo" es una tarea "exhaustiva" bastante simple. Por lo tanto, puede obtener resultados en sus datos incluso mejores que los nuestros: ¡pruébelo!

All Articles