Économisez beaucoup d'argent sur de gros volumes dans PostgreSQL

Poursuivant le sujet de l'enregistrement de grands flux de données, soulevé par l' article précédent sur le partitionnement , nous examinons ici les façons dont vous pouvez réduire la taille "physique" stockée dans PostgreSQL, et leur impact sur les performances du serveur.

Il s'agit des paramètres TOAST et de l'alignement des données . «En moyenne», ces méthodes permettront d'économiser pas trop de ressources, mais sans aucune modification du code de l'application.


Cependant, notre expérience s'est avérée très productive à cet égard, car le référentiel de presque toutes les surveillances est de par sa nature principalement composé uniquement en termes de données enregistrées. Et si vous êtes intéressé par la façon dont vous pouvez apprendre à une base de données à écrire sur un disque au lieu de la moitié de 200 Mo / s - je demande une coupe.

Petits secrets du Big Data


Selon le profil de notre service , il reçoit régulièrement des paquets de texte des journaux .

Et comme le complexe VLSI , dont nous surveillons les bases de données, est un produit multicomposant avec des structures de données complexes, les requêtes pour obtenir des performances maximales sont obtenues par de tels «multi-volumes» avec une logique algorithmique complexe . Ainsi, le volume de chaque instance individuelle de la demande ou du plan d'exécution résultant dans le journal qui nous parvient s'avère assez «moyen».

Examinons la structure de l'une des tables dans lesquelles nous écrivons les données "brutes" - c'est-à-dire, voici le texte original de l'entrée de journal:

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

Une telle plaque typique (déjà partitionnée, bien sûr, c'est donc un modèle de section), où le plus important est le texte. Parfois assez volumineux.

Rappelons que la taille «physique» d'un enregistrement dans PG ne peut pas occuper plus d'une page de données, mais la taille «logique» est une question complètement différente. Pour écrire une valeur de volume (varchar / text / bytea) dans le champ, la technologie TOAST est utilisée :
PostgreSQL utilise une taille de page fixe (généralement 8 Ko) et ne permet pas aux tuples de s'étendre sur plusieurs pages. Par conséquent, il est impossible de stocker directement de très grandes valeurs de champ. Pour surmonter cette limitation, les grandes valeurs de champ sont compressées et / ou divisées en plusieurs lignes physiques. Cela se passe inaperçu par l'utilisateur et affecte légèrement la plupart du code du serveur. Cette méthode est connue sous le nom de TOAST ...

En fait, pour chaque table avec des champs "potentiellement volumineux" , une table couplée est automatiquement créée avec "découpage" de chaque enregistrement "volumineux" en segments de 2 Ko:

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

Autrement dit, si nous devons écrire une ligne avec une valeur "grande" data, alors le véritable enregistrement se produira non seulement dans la table principale et son PK, mais aussi dans TOAST et son PK .

Réduit l'effet TOAST


Mais la plupart des enregistrements ici ne sont toujours pas si gros, ils devraient tenir dans 8 Ko - comment économiseriez-vous là-dessus? ..

Ici l'attribut STORAGEde la colonne du tableau vient à notre aide :
  • EXTENDED permet à la fois la compression et le stockage séparé. Il s'agit de l' option standard pour la plupart des types de données compatibles TOAST. Tout d'abord, une tentative est effectuée pour effectuer la compression, puis elle est enregistrée en dehors du tableau si la ligne est toujours trop grande.
  • MAIN permet la compression, mais pas un stockage séparé. (En fait, un stockage séparé sera cependant effectué pour ces colonnes, mais uniquement en dernier recours , lorsqu'il n'y a pas d'autre moyen de réduire la ligne pour qu'elle tienne sur la page.)
En fait, c'est exactement ce dont nous avons besoin pour le texte - serrez-le autant que possible, et même s'il ne correspond pas du tout - mettez-le dans TOAST . Vous pouvez le faire directement "à la volée", avec une seule commande:

ALTER TABLE rawdata_orig ALTER COLUMN data SET STORAGE MAIN;

Comment évaluer l'effet


Étant donné que le flux de données change chaque jour, nous ne pouvons pas comparer les nombres absolus, mais en termes relatifs, plus la proportion que nous avons enregistrée dans TOAST est petite , mieux c'est. Mais il y a un danger - plus le volume «physique» de chaque enregistrement est important, plus l'indice est «large», car plus de pages de données doivent être couvertes.

Section avant modifications :
heap  = 37GB (39%)
TOAST = 54GB (57%)
PK    =  4GB ( 4%)

Section après modifications :
heap  = 37GB (67%)
TOAST = 16GB (29%)
PK    =  2GB ( 4%)

En fait, nous avons commencé à écrire dans TOAST 2 fois moins souvent , ce qui a déchargé non seulement le disque, mais aussi le CPU:



Je note que nous avons également commencé à «lire» moins le disque, pas seulement à «écrire» - parce que lorsque vous insérez un enregistrement dans une table, vous devez également «soustraire» une partie de l'arborescence de chacun des indices pour déterminer sa position future.

Qui sur PostgreSQL 11 vit bien


Après la mise à niveau vers PG11, nous avons décidé de continuer à "régler" TOAST et avons remarqué qu'à partir de cette version, le paramètre devenait disponible pour la configuration toast_tuple_target:
Le code de traitement TOAST n'est déclenché que lorsque la valeur de la ligne à stocker dans la table est supérieure à TOAST_TUPLE_THRESHOLD octets (généralement 2 Ko). Le code TOAST compressera et / ou déplacera les valeurs de champ hors de la table jusqu'à ce que la valeur de la ligne soit inférieure à TOAST_TUPLE_TARGET octets (variable, généralement 2 Ko également) ou qu'il devienne impossible de réduire la taille.
Nous avons décidé que les données dont nous disposons habituellement sont soit «très courtes», soit immédiatement «très longues», nous avons donc décidé de nous limiter à la valeur la plus basse possible:

ALTER TABLE rawplan_orig SET (toast_tuple_target = 128);

Voyons comment les nouveaux paramètres ont affecté le chargement du disque après la migration:


Pas mal! La file d'attente moyenne d'un disque a été réduite d' environ 1,5 fois, et l'occupation du disque - de 20%! Mais peut-être que cela a affecté le processeur?


Au moins, cela n'a certainement pas empiré. Cependant, il est difficile de juger si même de tels volumes ne peuvent toujours pas augmenter la charge CPU moyenne au-dessus de 5% .

D'un changement de position, la somme ... change!


Comme vous le savez, un sou économise un rouble, et avec nos volumes de stockage d'environ 10 To / mois, même une petite optimisation peut donner un bon profit. Par conséquent, nous avons attiré l'attention sur la structure physique de nos données - comment exactement les «champs» sont disposés à l'intérieur de l'enregistrement de chaque table.

Parce qu'en raison de l'alignement des données, cela affecte directement le volume résultant :
De nombreuses architectures assurent l'alignement des données au-delà des limites des mots machine. Par exemple, sur un système x86 32 bits, les entiers (type entier, occupent 4 octets) seront alignés sur la bordure des mots de 4 octets, ainsi que les nombres à virgule flottante double précision (type double précision, 8 octets). Et sur un système 64 bits, les valeurs doubles seront alignées sur la bordure des mots de 8 octets. C'est une autre raison de l'incompatibilité.

En raison de l'alignement, la taille de la ligne du tableau dépend de l'ordre des champs. Habituellement, cet effet n'est pas très visible, mais dans certains cas, il peut entraîner une augmentation significative de la taille. Par exemple, si vous placez des champs de types char (1) et entier mélangés, entre eux, en règle générale, 3 octets seront gaspillés pour rien.

Commençons par les modèles synthétiques:

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 

D'où venait la paire d'octets supplémentaire dans le premier cas? Tout est simple - un petit entier de 2 octets est aligné sur une limite de 4 octets avant le champ suivant, et quand c'est le dernier, il n'y a rien et pas besoin de l'aligner.

En théorie, tout va bien et vous pouvez réorganiser les champs à votre guise. Vérifions les données réelles sur l'exemple de l'une des tables, dont la section quotidienne prend 10-15 Go.

Structure source:

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 section après avoir changé l'ordre des colonnes est exactement le même champ, seul l'ordre est différent :

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)

Le volume total de la section est déterminé par le nombre de «faits» et dépend uniquement des processus externes, donc nous divisons la taille de heap ( pg_relation_size) par le nombre d'enregistrements qu'il contient - c'est-à-dire que nous obtenons la taille moyenne de l'enregistrement stocké réel :


Moins 6% du volume , excellent!

Mais tout, bien sûr, n'est pas si rose - car dans les indices on ne peut pas changer l'ordre des champs , et donc "en général" ( pg_total_relation_size) ...


... après tout, ils ont économisé 1,5% ici , sans changer une seule ligne de code. Oui oui!



Je note que l'agencement des champs ci-dessus n'est pas le fait que le plus optimal. Parce que certains blocs de champ ne veulent pas déjà être «déchirés» pour des raisons esthétiques - par exemple, une paire (pack, recno), qui est PK pour cette table.

En général, la définition de l'arrangement de champ "minimum" est une tâche "exhaustive" assez simple. Par conséquent, vous pouvez obtenir des résultats sur vos données encore meilleurs que les nôtres - essayez-le!

All Articles