Nous écrivons en PostgreSQL sur sublight : 1 hôte, 1 jour, 1 To

Récemment je vous ai expliqué comment, en utilisant des recettes standards augmenter les performances des requêtes de lecture SQL à partir de la base de données PostgreSQL. Aujourd'hui, nous allons parler de la façon dont l'enregistrement peut être effectué plus efficacement dans la base de données sans utiliser de « rebondissements » dans la configuration - simplement en organisant correctement les flux de données.

Nous écrivons en PostgreSQL sur sublight : 1 hôte, 1 jour, 1 To

#1. Sectionnement

Un article sur comment et pourquoi cela vaut la peine d'être organisé partitionnement appliqué « en théorie » cela a déjà été le cas, nous parlerons ici de la pratique consistant à appliquer certaines approches au sein de notre service de surveillance pour des centaines de serveurs PostgreSQL.

"Les choses d'autrefois..."

Au départ, comme tout MVP, notre projet a démarré avec une charge assez légère - la surveillance n'était effectuée que pour les dix serveurs les plus critiques, toutes les tables étaient relativement compactes... Mais au fil du temps, le nombre d'hôtes surveillés est devenu de plus en plus , et encore une fois nous avons essayé de faire quelque chose avec l'un des tables d'une taille de 1.5 To, nous avons réalisé que même s’il était possible de continuer à vivre ainsi, c’était très gênant.

Les temps étaient presque épiques, différentes versions de PostgreSQL 9.x étaient pertinentes, donc tout le partitionnement devait être effectué « manuellement » - via héritage de table et déclencheurs routage avec dynamique EXECUTE.

Nous écrivons en PostgreSQL sur sublight : 1 hôte, 1 jour, 1 To
La solution résultante s’est avérée suffisamment universelle pour pouvoir être appliquée à toutes les tables :

  • Une table parent « en-tête » vide a été déclarée, qui décrivait tous les index et déclencheurs nécessaires.
  • L'enregistrement du point de vue du client a été réalisé dans la table « racine », et en interne à l'aide déclencheur de routage BEFORE INSERT le dossier a été « physiquement » inséré dans la section requise. Si cela n'existait pas encore, nous avons détecté une exception et...
  • … en utilisant CREATE TABLE ... (LIKE ... INCLUDING ...) a été créé sur la base du modèle de la table parent section avec une restriction sur la date souhaitéede sorte que lorsque les données sont récupérées, la lecture y est effectuée uniquement.

PG10 : première tentative

Mais le partitionnement par héritage n'a historiquement pas été bien adapté au traitement d'un flux d'écriture actif ou d'un grand nombre de partitions enfants. Par exemple, vous pouvez rappeler que l'algorithme de sélection de la section souhaitée avait complexité quadratique, qu'il fonctionne avec plus de 100 sections, vous comprenez vous-même comment...

Dans PG10, cette situation a été grandement optimisée par la mise en œuvre du support partitionnement natif. Par conséquent, nous avons immédiatement essayé de l'appliquer immédiatement après la migration du stockage, mais...

Comme il s'est avéré après avoir parcouru le manuel, la table partitionnée nativement dans cette version est :

  • ne prend pas en charge les descriptions d'index
  • ne prend pas en charge les déclencheurs
  • ne peut être le « descendant » de personne
  • ne prend pas en charge INSERT ... ON CONFLICT
  • impossible de générer une section automatiquement

Après avoir reçu un coup de râteau douloureux au front, nous avons réalisé qu'il serait impossible de se passer de modifier l'application et avons reporté de six mois la poursuite des recherches.

PG10 : deuxième chance

Nous avons donc commencé à résoudre les problèmes qui se posaient un par un :

  1. Parce que les déclencheurs et ON CONFLICT Nous avons constaté que nous en avions encore besoin ici et là, nous avons donc fait une étape intermédiaire pour les élaborer. table proxy.
  2. Je me suis débarrassé du "routage" dans les déclencheurs - c'est-à-dire à partir de EXECUTE.
  3. Ils l'ont sorti séparément table modèle avec tous les indexde sorte qu'ils ne sont même pas présents dans la table proxy.

Nous écrivons en PostgreSQL sur sublight : 1 hôte, 1 jour, 1 To
Finalement, après tout cela, nous avons partitionné la table principale de manière native. La création d'une nouvelle section est encore laissée à la conscience de l'application.

Dictionnaires « Sciage »

Comme dans tout système analytique, nous avions également "faits" et "coupures" (dictionnaires). Dans notre cas, à ce titre, ils ont agi, par exemple, corps du modèle requêtes lentes similaires ou le texte de la requête elle-même.

Les « faits » étaient segmentés par jour depuis longtemps déjà, nous avons donc calmement supprimé les sections obsolètes, et elles ne nous ont pas dérangés (logs !). Mais il y a eu un problème avec les dictionnaires...

Cela ne veut pas dire qu'il y en avait beaucoup, mais environ 100 To de « faits » ont donné lieu à un dictionnaire de 2.5 To. Vous ne pouvez pas facilement supprimer quoi que ce soit d’une telle table, vous ne pouvez pas la compresser dans un délai suffisant et l’écriture y est progressivement devenue plus lente.

Comme dans un dictionnaire... chaque entrée doit y être présentée exactement une fois... et c'est exact, mais !.. Personne ne nous empêche d'avoir un dictionnaire séparé pour chaque jour! Oui, cela apporte une certaine redondance, mais cela permet :

  • écrire/lire plus vite en raison de la taille plus petite de la section
  • consomme moins de mémoire en travaillant avec des index plus compacts
  • stocker moins de données en raison de la possibilité de supprimer rapidement les éléments obsolètes

Grâce à l'ensemble des mesures La charge du processeur a diminué d'environ 30 %, la charge du disque d'environ 50 %:

Nous écrivons en PostgreSQL sur sublight : 1 hôte, 1 jour, 1 To
En même temps, nous avons continué à écrire exactement la même chose dans la base de données, mais avec moins de charge.

#2. Evolution et refactorisation de la base de données

Nous avons donc décidé de ce que nous avons chaque jour a sa propre section avec des données. En fait, CHECK (dt = '2018-10-12'::date) - et il existe une clé de partitionnement et la condition pour qu'un enregistrement tombe dans une section spécifique.

Étant donné que tous les rapports de notre service sont construits dans le contexte d'une date spécifique, les index correspondants depuis les « heures non partitionnées » sont de tous types. (Serveur, date, modèle de plan), (Serveur, date, nœud Planifier), (date, Classe d'erreur, Serveur), ...

Mais maintenant, ils vivent dans chaque section vos copies chacun de ces index... Et dans chaque section la date est une constante... Il s'avère que nous sommes maintenant dans chacun de ces index entrez simplement une constante comme l'un des champs, ce qui augmente à la fois son volume et le temps de recherche, mais n'apporte aucun résultat. Ils ont laissé le râteau pour eux, oups...

Nous écrivons en PostgreSQL sur sublight : 1 hôte, 1 jour, 1 To
La direction de l'optimisation est évidente - simple supprimer le champ de date de tous les index sur des tables partitionnées. Compte tenu de nos volumes, le gain est d'environ 1 To/semaine!

Notons maintenant que ce téraoctet devait encore être enregistré d'une manière ou d'une autre. Autrement dit, nous aussi le disque devrait maintenant moins charger! Cette photo montre clairement l'effet obtenu du nettoyage, auquel nous avons consacré une semaine :

Nous écrivons en PostgreSQL sur sublight : 1 hôte, 1 jour, 1 To

#3. « Répartir » la charge de pointe

L'un des gros problèmes des systèmes chargés est synchronisation redondante certaines opérations qui ne le nécessitent pas. Parfois « parce qu’ils n’avaient pas remarqué », parfois « c’était plus facile ainsi », mais tôt ou tard il faudra s’en débarrasser.

Zoomons sur l'image précédente et voyons que nous avons un disque « pompes » sous la charge avec double amplitude entre des échantillons adjacents, ce qui clairement « statistiquement » ne devrait pas se produire avec un tel nombre d'opérations :

Nous écrivons en PostgreSQL sur sublight : 1 hôte, 1 jour, 1 To

C'est assez facile à réaliser. Nous avons déjà commencé à surveiller près de 1000 serveurs, chacun est traité par un thread logique distinct, et chaque thread réinitialise les informations accumulées à envoyer à la base de données à une certaine fréquence, quelque chose comme ceci :

setInterval(sendToDB, interval)

Le problème ici réside précisément dans le fait que tous les threads démarrent à peu près en même temps, donc leurs heures d’envoi coïncident presque toujours « au point ». Oups #2...

Heureusement, c'est assez simple à résoudre, ajout d'un élan "aléatoire" par le temps:

setInterval(sendToDB, interval * (1 + 0.1 * (Math.random() - 0.5)))

#4. Nous mettons en cache ce dont nous avons besoin

Le troisième problème traditionnel de surcharge est pas de cache où il est pourrait être.

Par exemple, nous avons permis d'analyser en termes de nœuds du plan (tous ces Seq Scan on users), mais je pense immédiatement qu'ils sont, pour la plupart, les mêmes - ils ont oublié.

Non, bien sûr, rien n'est à nouveau écrit dans la base de données, cela coupe le déclencheur avec INSERT ... ON CONFLICT DO NOTHING. Mais ces données parviennent toujours à la base de données, et ce n’est pas nécessaire lire pour vérifier s'il y a un conflit devoir faire. Oups #3...

La différence dans le nombre d'enregistrements envoyés à la base de données avant/après l'activation de la mise en cache est évidente :

Nous écrivons en PostgreSQL sur sublight : 1 hôte, 1 jour, 1 To

Et voici la baisse de charge de stockage qui l’accompagne :

Nous écrivons en PostgreSQL sur sublight : 1 hôte, 1 jour, 1 To

En tout

« Téraoctet par jour » semble effrayant. Si vous faites tout correctement, alors c'est juste 2 ^ 40 octets / 86400 12.5 secondes = ~ XNUMX Mo/sque même les vis IDE de bureau tenaient. 🙂

Mais sérieusement, même avec une charge décuplée pendant la journée, vous pouvez facilement atteindre les capacités des SSD modernes.

Nous écrivons en PostgreSQL sur sublight : 1 hôte, 1 jour, 1 To

Source: habr.com

Ajouter un commentaire