Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

Le rapport présente quelques approches qui permettent surveiller les performances des requêtes SQL lorsqu'il y en a des millions par jour, et il existe des centaines de serveurs PostgreSQL surveillés.

Quelles solutions techniques nous permettent de traiter efficacement un tel volume d'informations, et en quoi cela facilite-t-il la vie d'un développeur ordinaire ?


Qui est intéressé ? analyse de problèmes spécifiques et de diverses techniques d'optimisation Requêtes SQL et résolution des problèmes DBA typiques dans PostgreSQL - vous pouvez également lire une série d'articles sur ce sujet.

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)
Je m'appelle Kirill Borovikov, je représente Société Tenseur. Plus précisément, je me spécialise dans le travail avec des bases de données dans notre entreprise.

Aujourd'hui, je vais vous expliquer comment nous optimisons les requêtes, lorsque vous n'avez pas besoin de « séparer » les performances d'une seule requête, mais de résoudre le problème en masse. Quand il y a des millions de demandes et que vous devez en trouver approches de solution ce gros problème.

En général, Tensor pour un million de nos clients est VLSI est notre application: réseau social d'entreprise, solutions de communication vidéo, pour flux de documents internes et externes, systèmes comptables pour la comptabilité et les entrepôts,... C'est-à-dire une telle « méga-combinaison » pour la gestion intégrée d'entreprise, dans laquelle il existe plus de 100 différents projets internes.

Pour garantir qu'ils fonctionnent et se développent tous normalement, nous disposons de 10 centres de développement dans tout le pays, et d'autres en leur sein. 1000 développeurs.

Nous travaillons avec PostgreSQL depuis 2008 et avons accumulé une grande quantité de ce que nous traitons - données clients, statistiques, analytiques, données provenant de systèmes d'information externes - plus de 400 To. Il y a environ 250 serveurs en production seulement, et au total, nous surveillons environ 1000 XNUMX serveurs de bases de données.

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

SQL est un langage déclaratif. Vous décrivez non pas « comment » quelque chose devrait fonctionner, mais « ce » que vous voulez réaliser. Le SGBD sait mieux comment faire un JOIN - comment connecter vos tables, quelles conditions imposer, qu'est-ce qui passera par l'index, qu'est-ce qui ne passera pas par l'index...

Certains SGBD acceptent les indices : « Non, connectez ces deux tables dans telle ou telle file d'attente », mais PostgreSQL ne peut pas faire cela. C'est la position consciente des principaux développeurs : « Nous préférons terminer l'optimiseur de requêtes plutôt que de permettre aux développeurs d'utiliser des astuces. »

Mais, malgré le fait que PostgreSQL ne permet pas à « l'extérieur » de se contrôler, il permet parfaitement vois ce qui se passe en luiquand vous exécutez votre requête et où elle rencontre des problèmes.

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

En général, quels sont les problèmes classiques auxquels un développeur [pour un administrateur de base de données] est généralement confronté ? « Ici, nous avons répondu à la demande, et tout est lent chez nous, tout est suspendu, il se passe quelque chose... Une sorte de problème !

Les raisons sont presque toujours les mêmes :

  • algorithme de requête inefficace
    Développeur : "Maintenant, je lui donne 10 tables en SQL via JOIN..." - et s'attend à ce que ses conditions soient miraculeusement "déliées" et qu'il obtienne tout rapidement. Mais les miracles ne se produisent pas, et tout système avec une telle variabilité (10 tables dans un FROM) donne toujours une sorte d'erreur. [article]
  • statistiques non pertinentes
    Ce point est très pertinent spécifiquement pour PostgreSQL, lorsque vous « déversez » un grand ensemble de données sur le serveur, faites une requête, et cela « sexcanits » votre tablette. Parce qu'hier, il y avait 10 enregistrements, et aujourd'hui il y en a 10 millions, mais PostgreSQL n'en est pas encore conscient, et nous devons lui en parler. [article]
  • "brancher" sur les ressources
    Vous avez installé une base de données volumineuse et très chargée sur un serveur faible qui ne dispose pas de suffisamment de performances de disque, de mémoire ou de processeur. Et c’est tout… Il existe quelque part un plafond de performance au-dessus duquel on ne peut plus sauter.
  • blocage
    C'est un point difficile, mais ils sont plus pertinents pour diverses requêtes de modification (INSERT, UPDATE, DELETE) - il s'agit d'un grand sujet distinct.

Obtenir un plan

...Et pour tout le reste, nous besoin d'un plan! Nous devons voir ce qui se passe à l'intérieur du serveur.

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

Un plan d'exécution de requête pour PostgreSQL est un arbre de l'algorithme d'exécution de requête en représentation textuelle. C’est précisément l’algorithme qui, suite à l’analyse du planificateur, s’est révélé le plus efficace.

Chaque nœud d'arbre est une opération : récupérer des données à partir d'une table ou d'un index, créer un bitmap, joindre deux tables, joindre, croiser ou exclure des sélections. Exécuter une requête implique de parcourir les nœuds de cet arbre.

Pour obtenir le plan de requête, le moyen le plus simple est d'exécuter l'instruction EXPLAIN. Pour obtenir tous les attributs réels, c'est-à-dire exécuter réellement une requête sur la base - EXPLAIN (ANALYZE, BUFFERS) SELECT ....

Le mauvais côté : lorsque vous l'exécutez, cela se produit "ici et maintenant", il ne convient donc qu'au débogage local. Si vous prenez un serveur très chargé qui subit un fort flux de modifications de données, et que vous voyez : « Oh ! Ici nous avons une exécution lenteXia demande." Il y a une demi-heure, une heure - pendant que vous exécutiez et récupériez cette requête à partir des journaux, en la ramenant au serveur, l'ensemble de votre ensemble de données et vos statistiques ont changé. Vous l'exécutez pour déboguer - et il s'exécute rapidement ! Et tu ne peux pas comprendre pourquoi, pourquoi было lentement.

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

Afin de comprendre ce qui s'est passé exactement au moment où la requête a été exécutée sur le serveur, des gens intelligents ont écrit module auto_explain. Il est présent dans presque toutes les distributions PostgreSQL les plus courantes et peut simplement être activé dans le fichier de configuration.

S'il se rend compte qu'une requête dure plus longtemps que la limite que vous lui avez indiquée, il le fait. « instantané » du plan de cette demande et les écrit ensemble dans le journal.

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

Tout semble aller bien maintenant, nous allons au journal et voyons là... [texte chaussette]. Mais nous ne pouvons rien en dire, à part que c’est un excellent plan car il a fallu 11 ms pour l’exécuter.

Tout semble aller bien, mais on ne sait pas exactement ce qui s'est réellement passé. A part l’heure générale, on ne voit vraiment rien. Parce que regarder un tel « agneau » de texte brut n’est généralement pas visuel.

Mais même si ce n’est pas évident, même si cela est gênant, il existe des problèmes plus fondamentaux :

  • Le nœud indique somme des ressources de tout le sous-arbre sous lui. Autrement dit, vous ne pouvez pas simplement savoir combien de temps a été consacré à cette analyse d'index particulière s'il contient une condition imbriquée. Nous devons regarder dynamiquement s'il y a des « enfants » et des variables conditionnelles, des CTE à l'intérieur - et soustraire tout cela « dans nos esprits ».
  • Deuxième point : l'heure qui est indiquée sur le nœud est temps d'exécution d'un seul nœud. Si ce nœud a été exécuté à la suite, par exemple, d'une boucle à travers les enregistrements de la table plusieurs fois, alors le nombre de boucles (cycles de ce nœud) augmente dans le plan. Mais le temps d’exécution atomique lui-même reste le même en termes de plan. Autrement dit, pour comprendre combien de temps ce nœud a été exécuté au total, vous devez multiplier une chose par une autre - encore une fois, "dans votre tête".

Dans de telles situations, comprenez « Qui est le maillon le plus faible ? » presque impossible. Par conséquent, même les développeurs eux-mêmes écrivent dans le « manuel » que « Comprendre un plan est un art qui s’apprend, s’expérimente… ».

Mais nous avons 1000 développeurs, et on ne peut pas transmettre cette expérience à chacun d’eux. Moi, toi, il sait, mais quelqu'un là-bas ne sait plus. Peut-être qu'il apprendra, ou peut-être pas, mais il doit travailler maintenant - et où obtiendrait-il cette expérience ?

Visualisation des plans

Nous avons donc réalisé que pour résoudre ces problèmes, nous avions besoin bonne visualisation du plan. [article]

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

Nous avons d'abord parcouru le marché - regardons sur Internet pour voir ce qui existe.

Mais il s'est avéré qu'il existe très peu de solutions relativement « vivantes » qui se développent plus ou moins - littéralement, une seule : expliquer.depesz.com par Hubert Lubaczewski. Lorsque vous entrez dans le champ « flux » une représentation textuelle du plan, il vous montre un tableau avec les données analysées :

  • le temps de traitement du nœud
  • temps total pour tout le sous-arbre
  • nombre d'enregistrements récupérés qui étaient statistiquement attendus
  • le corps du nœud lui-même

Ce service a également la possibilité de partager une archive de liens. Vous avez lancé votre plan là-dedans et avez dit : "Hé, Vasya, voici un lien, il y a quelque chose qui ne va pas là-bas."

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

Mais il y a aussi de petits problèmes.

Premièrement, une énorme quantité de « copier-coller ». Vous prenez un morceau de bûche, vous le collez là, et encore et encore.

Deuxièmement, le pas d'analyse de la quantité de données lues - les mêmes tampons qui génèrent EXPLAIN (ANALYZE, BUFFERS), on ne le voit pas ici. Il ne sait tout simplement pas comment les démonter, les comprendre et travailler avec eux. Lorsque vous lisez beaucoup de données et réalisez que vous risquez une mauvaise allocation du disque et du cache mémoire, ces informations sont très importantes.

Le troisième point négatif est le très faible développement de ce projet. Les commits sont très petits, c'est bien si une fois tous les six mois, et le code est en Perl.

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

Mais ce ne sont que des « paroles », nous pourrions en quelque sorte vivre avec cela, mais il y a une chose qui nous a fortement détournés de ce service. Il s'agit d'erreurs dans l'analyse de Common Table Expression (CTE) et de divers nœuds dynamiques comme InitPlan/SubPlan.

Si vous croyez cette image, alors le temps d'exécution total de chaque nœud individuel est supérieur au temps d'exécution total de l'ensemble de la requête. C'est simple - le temps de génération de ce CTE n'a pas été soustrait du nœud CTE Scan. Par conséquent, nous ne connaissons plus la bonne réponse quant à la durée du scan CTE lui-même.

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

Ensuite, nous avons réalisé qu'il était temps d'écrire le nôtre - hourra ! Chaque développeur dit : « Maintenant, nous allons écrire le nôtre, ce sera super simple ! »

Nous avons pris une stack typique des services web : un noyau basé sur Node.js + Express, utilisé Bootstrap et D3.js pour de beaux diagrammes. Et nos attentes ont été pleinement justifiées : nous avons reçu le premier prototype en 2 semaines :

  • analyseur de plan personnalisé
    Autrement dit, nous pouvons désormais analyser n'importe quel plan parmi ceux générés par PostgreSQL.
  • analyse correcte des nœuds dynamiques - Scan CTE, InitPlan, Sous-Plan
  • analyse de la distribution des tampons - où les pages de données sont lues depuis la mémoire, où depuis le cache local, où depuis le disque
  • j'ai de la clarté
    Afin de ne pas « creuser » tout cela dans le journal, mais de voir tout de suite le « maillon le plus faible » dans l'image.

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

Nous avons obtenu quelque chose comme ceci, avec la coloration syntaxique incluse. Mais généralement, nos développeurs ne travaillent plus avec une représentation complète du plan, mais avec une représentation plus courte. Après tout, nous avons déjà analysé tous les nombres et les avons jetés à gauche et à droite, et au milieu nous n'avons laissé que la première ligne, de quel type de nœud il s'agit : CTE Scan, génération CTE ou Seq Scan selon un signe.

C'est la représentation abrégée que nous appelons modèle de plan.

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

Quoi d'autre serait pratique ? Il serait pratique de voir quelle part de notre temps total est allouée à quel nœud - et de simplement le « mettre » de côté. camembert.

Nous pointons le nœud et voyons - il s'avère que Seq Scan a pris moins d'un quart du temps total et les 3/4 restants ont été pris par CTE Scan. Horreur! Ceci est une petite note sur la « cadence de tir » de CTE Scan si vous les utilisez activement dans vos requêtes. Ils ne sont pas très rapides - ils sont même inférieurs à l'analyse de table classique. [article] [article]

Mais généralement, de tels diagrammes sont plus intéressants, plus complexes, lorsque nous pointons immédiatement un segment et constatons, par exemple, que plus de la moitié du temps, un Seq Scan « mange ». De plus, il y avait une sorte de filtre à l'intérieur, de nombreux enregistrements ont été supprimés en conséquence... Vous pouvez directement lancer cette image au développeur et dire : « Vasya, tout va mal ici pour toi ! Comprenez-le, regardez – quelque chose ne va pas !

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

Naturellement, il y avait quelques « râteaux » impliqués.

La première chose que nous avons rencontrée était le problème d’arrondi. L'heure de chaque nœud individuel du plan est indiquée avec une précision de 1 μs. Et lorsque le nombre de cycles de nœuds dépasse, par exemple, 1000 0.95 - après exécution, PostgreSQL a divisé « avec précision », alors lors du calcul, nous obtenons le temps total « quelque part entre 1.05 ms et XNUMX ms ». Lorsque le décompte atteint les microsecondes, ce n'est pas grave, mais lorsqu'il est déjà de [milli]secondes, vous devez prendre en compte cette information lors du « déliement » des ressources aux nœuds du plan « qui a consommé combien ».

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

Le deuxième point, plus complexe, est la répartition des ressources (ces buffers) entre les nœuds dynamiques. Cela nous a coûté les 2 premières semaines du prototype plus 4 semaines supplémentaires.

Il est assez facile d'avoir ce genre de problème - nous faisons un CTE et sommes censés y lire quelque chose. En fait, PostgreSQL est « intelligent » et n’y lira rien directement. Ensuite, nous en retirons le premier enregistrement, et le cent unième du même CTE.

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

Nous regardons le plan et comprenons - c'est étrange, nous avons 3 tampons (pages de données) "consommés" dans Seq Scan, 1 de plus dans le CTE Scan et 2 de plus dans le deuxième CTE Scan. Autrement dit, si nous résumons simplement tout, nous en obtiendrons 6, mais sur la tablette, nous n'en lisons que 3 ! CTE Scan ne lit rien de n'importe où, mais fonctionne directement avec la mémoire du processus. Autrement dit, quelque chose ne va clairement pas ici !

En fait, il s'avère que voici toutes ces 3 pages de données qui ont été demandées à Seq Scan, la première a demandé le 1er CTE Scan, puis la 1ème, et 2 autres lui ont été lues. 2 pages ont été lues, pas 3.

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

Et cette image nous a fait comprendre que l'exécution d'un plan n'est plus un arbre, mais simplement une sorte de graphe acyclique. Et nous avons obtenu un diagramme comme celui-ci, afin que nous comprenions « ce qui est venu d'où en premier lieu ». Autrement dit, ici, nous avons créé un CTE à partir de pg_class et l'avons demandé deux fois, et presque tout notre temps a été passé sur la branche lorsque nous l'avons demandé pour la deuxième fois. Il est clair que la lecture de la 2e entrée coûte bien plus cher que la simple lecture de la 101ère entrée depuis la tablette.

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

Nous avons expiré pendant un moment. Ils ont dit : « Maintenant, Neo, tu connais le kung-fu ! Notre expérience est désormais directement sur votre écran. Maintenant, vous pouvez l'utiliser. [article]

Consolidation des journaux

Nos 1000 développeurs ont poussé un soupir de soulagement. Mais nous avons compris que nous n'avons que des centaines de serveurs de « combat », et tout ce « copier-coller » de la part des développeurs n'est pas du tout pratique. Nous avons réalisé que nous devions le récupérer nous-mêmes.

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

En général, il existe un module standard qui peut collecter des statistiques, cependant, il doit également être activé dans la configuration - ceci module pg_stat_statements. Mais il ne nous convenait pas.

Premièrement, il attribue aux mêmes requêtes en utilisant différents schémas au sein de la même base de données différents ID de requête. Autrement dit, si vous le faites d'abord SET search_path = '01'; SELECT * FROM user LIMIT 1;et alors SET search_path = '02'; et la même requête, alors les statistiques de ce module auront des enregistrements différents, et je ne pourrai pas collecter de statistiques générales spécifiquement dans le cadre de ce profil de requête, sans prendre en compte les schémas.

Le deuxième point qui nous a empêché de l'utiliser est manque de projets. Autrement dit, il n’y a pas de plan, il n’y a que la demande elle-même. Nous voyons ce qui ralentissait, mais nous ne comprenons pas pourquoi. Et nous revenons ici au problème d’un ensemble de données en évolution rapide.

Et le dernier moment - manque de "faits". Autrement dit, vous ne pouvez pas traiter une instance spécifique d'exécution de requête - il n'y en a pas, il n'y a que des statistiques agrégées. Bien qu’il soit possible de travailler avec cela, c’est tout simplement très difficile.

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

Nous avons donc décidé de lutter contre le copier-coller et avons commencé à écrire collectionneur.

Le collecteur se connecte via SSH, établit une connexion sécurisée au serveur avec la base de données à l'aide d'un certificat et tail -F « s'y accroche » dans le fichier journal. Donc dans cette séance nous obtenons un « miroir » complet de l’intégralité du fichier journal, que le serveur génère. La charge sur le serveur lui-même est minime, car nous n'y analysons rien, nous reflétons simplement le trafic.

Puisque nous avions déjà commencé à écrire l'interface dans Node.js, nous avons continué à y écrire le collecteur. Et cette technologie s'est justifiée, car il est très pratique d'utiliser JavaScript pour travailler avec des données texte faiblement formatées, qui sont le journal. Et l'infrastructure Node.js elle-même, en tant que plate-forme backend, vous permet de travailler facilement et commodément avec des connexions réseau, et même avec n'importe quel flux de données.

En conséquence, nous « étirons » deux connexions : la première pour « écouter » le journal lui-même et le prendre pour nous, et la seconde pour interroger périodiquement la base. "Mais le journal montre que le signe avec l'oid 123 est bloqué", mais cela ne veut rien dire pour le développeur, et ce serait bien de demander à la base de données : "Qu'est-ce que l'OID = 123 de toute façon ?" Nous demandons donc périodiquement à la base ce que nous ne savons pas encore sur nous-mêmes.

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

« Il n’y a qu’une chose que vous n’avez pas prise en compte, il y a une espèce d’abeilles ressemblant à des éléphants !.. » Nous avons commencé à développer ce système alors que nous voulions surveiller 10 serveurs. Le plus critique à notre avis, où sont survenus certains problèmes difficiles à résoudre. Mais au cours du premier trimestre, nous en avons reçu une centaine pour le suivi - parce que le système fonctionnait, tout le monde le voulait, tout le monde était à l'aise.

Tout cela doit être additionné, le flux de données est important et actif. En fait, ce que nous surveillons, ce que nous pouvons gérer, c'est ce que nous utilisons. Nous utilisons également PostgreSQL comme stockage de données. Et rien n'est plus rapide pour y « verser » des données que l'opérateur COPY Pas encore.

Mais simplement « verser » des données n’est pas vraiment notre technologie. Parce que si vous avez environ 50 100 requêtes par seconde sur une centaine de serveurs, cela générera 150 à XNUMX Go de journaux par jour. Par conséquent, nous avons dû soigneusement « couper » la base.

Premièrement, nous avons fait partitionnement par jour, car, dans l’ensemble, personne ne s’intéresse à la corrélation entre les jours. Quelle différence cela fait-il avec ce que vous aviez hier, si ce soir vous déployiez une nouvelle version de l'application - et déjà de nouvelles statistiques.

Deuxièmement, nous avons appris (nous avons été forcés) très, très rapide à écrire en utilisant COPY. Autrement dit, pas seulement COPYparce qu'il est plus rapide que INSERT, et encore plus vite.

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

Le troisième point - je devais abandonner les déclencheurs, respectivement, et les clés étrangères. Autrement dit, nous n’avons aucune intégrité référentielle. Parce que si vous avez une table qui contient une paire de FK et que vous dites dans la structure de la base de données que « voici un enregistrement de journal référencé par FK, par exemple, à un groupe d'enregistrements », alors lorsque vous l'insérez, PostgreSQL il ne reste plus qu'à savoir comment le prendre et le faire honnêtement SELECT 1 FROM master_fk1_table WHERE ... avec l'identifiant que vous essayez d'insérer - juste pour vérifier que cet enregistrement y est présent, que vous ne « rompez » pas cette clé étrangère avec votre insertion.

Au lieu d'un seul enregistrement dans la table cible et ses index, nous bénéficions de l'avantage supplémentaire de lire toutes les tables auxquelles elle fait référence. Mais nous n'en avons pas du tout besoin - notre tâche est d'enregistrer autant que possible et le plus rapidement possible avec le moins de charge possible. Alors FK - à terre !

Le point suivant est l'agrégation et le hachage. Au départ, nous les avons implémentés dans la base de données - après tout, il est pratique de le faire immédiatement, lorsqu'un enregistrement arrive, dans une sorte de tablette "plus un" directement dans la gâchette. Eh bien, c'est pratique, mais c'est la même mauvaise chose - vous insérez un enregistrement, mais êtes obligé de lire et d'écrire autre chose à partir d'une autre table. De plus, non seulement vous lisez et écrivez, mais vous le faites également à chaque fois.

Imaginez maintenant que vous ayez un tableau dans lequel vous comptez simplement le nombre de requêtes qui ont transité par un hôte spécifique : +1, +1, +1, ..., +1. Et en principe, vous n'en avez pas besoin - tout est possible somme en mémoire sur le collecteur et envoyer à la base de données en une seule fois +10.

Oui, en cas de problèmes, votre intégrité logique peut « s'effondrer », mais c'est un cas presque irréaliste - parce que vous avez un serveur normal, il a une batterie dans le contrôleur, vous avez un journal des transactions, un journal sur le système de fichiers... En général, ça n'en vaut pas la peine. La perte de productivité que vous obtenez en exécutant des déclencheurs/FK ne vaut pas la dépense que vous engagez.

C'est la même chose avec le hachage. Une certaine demande vous parvient, vous en calculez un certain identifiant dans la base de données, vous l'écrivez dans la base de données puis vous le dites à tout le monde. Tout va bien jusqu'à ce qu'au moment de l'enregistrement, une deuxième personne vienne vers vous et veuille enregistrer la même chose - et vous êtes bloqué, et c'est déjà mauvais. Par conséquent, si vous pouvez transférer la génération de certains identifiants au client (par rapport à la base de données), il est préférable de le faire.

C'était tout simplement parfait pour nous d'utiliser MD5 à partir du texte - demande, plan, modèle,... Nous le calculons côté collecteur et « versons » l'ID prêt à l'emploi dans la base de données. La longueur du MD5 et le partitionnement quotidien nous permettent de ne pas nous soucier d'éventuelles collisions.

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

Mais pour enregistrer tout cela rapidement, nous avons dû modifier la procédure d'enregistrement elle-même.

Comment écrivez-vous habituellement les données ? Nous avons une sorte d'ensemble de données, nous le divisons en plusieurs tables, puis le COPIONS - d'abord dans la première, puis dans la seconde, dans la troisième... Ce n'est pas pratique, car nous semblons écrire un flux de données en trois étapes séquentiellement. Désagréable. Est-ce que cela peut être fait plus rapidement ? Peut!

Pour ce faire, il suffit simplement de décomposer ces flux en parallèle les uns avec les autres. Il s'avère que nous avons des erreurs, des requêtes, des modèles, des blocages, ... qui volent dans des threads séparés - et nous écrivons tout en parallèle. Assez pour ça garder un canal COPY constamment ouvert pour chaque table cible individuelle.

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

C'est-à-dire chez le collectionneur il y a toujours un flux, dans lequel je peux écrire les données dont j'ai besoin. Mais pour que la base de données voie ces données et que quelqu'un ne reste pas coincé à attendre que ces données soient écrites, La COPIE doit être interrompue à certains intervalles. Pour nous, la période la plus efficace était d'environ 100 ms - nous la fermons et l'ouvrons immédiatement à nouveau sur la même table. Et si nous n’avons pas assez d’un flux lors de certains pics, alors nous faisons du pooling jusqu’à une certaine limite.

De plus, nous avons découvert que pour un tel profil de charge, toute agrégation, lorsque les enregistrements sont collectés par lots, est mauvaise. Le mal classique est INSERT ... VALUES et 1000 autres enregistrements. Parce qu'à ce stade, vous avez un pic d'écriture sur le support, et tous ceux qui essaient d'écrire quelque chose sur le disque attendront.

Pour se débarrasser de telles anomalies, il suffit de ne rien agréger, ne tamponne pas du tout. Et si une mise en mémoire tampon sur le disque se produit (heureusement, l'API Stream de Node.js vous permet de le savoir), reportez cette connexion. Lorsque vous recevez un événement indiquant qu'il est à nouveau gratuit, écrivez-y à partir de la file d'attente accumulée. Et pendant qu’il est occupé, prenez le prochain gratuit de la piscine et écrivez-lui.

Avant d'introduire cette approche de l'enregistrement des données, nous avions environ 4K opérations d'écriture, ce qui nous a permis de réduire la charge de 4 fois. Aujourd'hui, ils ont encore augmenté 6 fois grâce aux nouvelles bases de données surveillées - jusqu'à 100 Mo/s. Et maintenant, nous stockons les journaux des 3 derniers mois dans un volume d'environ 10 à 15 To, en espérant qu'en seulement trois mois, tout développeur sera en mesure de résoudre n'importe quel problème.

Nous comprenons les problèmes

Mais le simple fait de collecter toutes ces données est une bonne chose, utile, pertinente, mais cela ne suffit pas : il faut les comprendre. Parce que ce sont des millions de projets différents par jour.

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

Mais des millions sont ingérables, il faut d’abord faire « plus petit ». Et, tout d’abord, vous devez décider comment vous allez organiser cette « plus petite » chose.

Nous avons identifié trois points clés :

  • qui envoyé cette demande
    Autrement dit, de quelle application est-il « arrivé » : interface Web, backend, système de paiement ou autre chose.
  • c'est arrivé
    Sur quel serveur spécifique ? Parce que si vous avez plusieurs serveurs sous une même application, et que soudainement l'un d'eux « devient stupide » (parce que « le disque est pourri », « la mémoire a fui », un autre problème), alors vous devez vous adresser spécifiquement au serveur.
  • comme le problème s'est manifesté d'une manière ou d'une autre

Pour comprendre « qui » ​​nous a envoyé une demande, nous utilisons un outil standard : définir une variable de session : SET application_name = '{bl-host}:{bl-method}'; — nous envoyons le nom de l'hôte de logique métier d'où provient la requête, ainsi que le nom de la méthode ou de l'application qui l'a initiée.

Après avoir transmis le "propriétaire" de la requête, celle-ci doit être consignée dans le journal - pour cela, nous configurons la variable log_line_prefix = ' %m [%p:%v] [%d] %r %a'. Pour ceux que ça intéresse, peut-être regarde dans le manuelQu'est-ce que tout cela veut dire. Il s'avère que nous voyons dans le journal :

  • temps
  • identifiants de processus et de transaction
  • nom de la base de données
  • IP de la personne qui a envoyé cette demande
  • et le nom de la méthode

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

Ensuite, nous avons réalisé qu'il n'était pas très intéressant de regarder la corrélation d'une requête entre différents serveurs. Il n’est pas fréquent de se retrouver dans une situation où une application échoue également ici et là. Mais même si c’est la même chose, regardez n’importe lequel de ces serveurs.

Alors voici la coupe "un serveur - un jour" cela s'est avéré suffisant pour nous pour toute analyse.

La première section analytique est la même "échantillon" - une forme abrégée de présentation du plan, débarrassée de tout indicateur numérique. La deuxième coupe est l'application ou la méthode, et la troisième coupe est le nœud de plan spécifique qui nous a posé des problèmes.

Lorsque nous sommes passés d'instances spécifiques aux modèles, nous avons obtenu deux avantages à la fois :

  • réduction multiple du nombre d'objets à analyser
    Nous devons analyser le problème non plus par milliers de requêtes ou de plans, mais par des dizaines de modèles.
  • chronologie
    Autrement dit, en résumant les « faits » dans une certaine section, vous pouvez afficher leur apparition au cours de la journée. Et ici, vous pouvez comprendre que si vous avez une sorte de schéma qui se produit, par exemple, une fois par heure, mais cela devrait se produire une fois par jour, vous devriez réfléchir à ce qui n'a pas fonctionné - qui l'a causé et pourquoi, peut-être que cela devrait être ici je ne devrais pas. Il s’agit d’une autre méthode d’analyse non numérique, purement visuelle.

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

Les méthodes restantes sont basées sur les indicateurs que nous extrayons du plan : combien de fois un tel modèle s'est produit, la durée totale et moyenne, la quantité de données lues sur le disque et la quantité de la mémoire...

Parce que, par exemple, vous arrivez sur la page d'analyse de l'hôte, regardez - quelque chose commence à trop lire sur le disque. Le disque du serveur ne peut pas le gérer – qui le lit ?

Et vous pouvez trier selon n'importe quelle colonne et décider de ce que vous allez traiter maintenant - la charge sur le processeur ou le disque, ou le nombre total de requêtes... Nous l'avons trié, regardé les « premiers », l'avons corrigé et a déployé une nouvelle version de l'application.
[conférence vidéo]

Et immédiatement, vous pouvez voir différentes applications fournies avec le même modèle à partir d'une requête telle que SELECT * FROM users WHERE login = 'Vasya'. Frontend, backend, traitement... Et vous vous demandez pourquoi le traitement lirait l'utilisateur s'il n'interagit pas avec lui.

L’inverse est de voir immédiatement depuis l’application ce qu’elle fait. Par exemple, le frontend est ceci, ceci, ceci et cela une fois par heure (la chronologie aide). Et la question se pose immédiatement : il semble que ce n'est pas le rôle du frontend de faire quelque chose une fois par heure...

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

Après un certain temps, nous avons réalisé qu'il nous manquait une statistiques par nœuds de plan. Nous avons isolé des plans uniquement les nœuds qui font quelque chose avec les données des tables elles-mêmes (les lire/écrire par index ou non). En fait, un seul aspect est ajouté par rapport à l'image précédente - combien d'enregistrements ce nœud nous a-t-il apporté ?, et combien ont été supprimés (lignes supprimées par filtre).

Vous n'avez pas d'index adapté sur la plaque, vous lui faites une requête, il dépasse l'index, tombe dans Seq Scan... vous avez filtré tous les enregistrements sauf un. Pourquoi avez-vous besoin de 100 millions d'enregistrements filtrés par jour ? N'est-il pas préférable de regrouper l'index ?

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

Après avoir analysé tous les plans nœud par nœud, nous avons réalisé qu'il existe certaines structures typiques dans les plans qui sont très susceptibles de paraître suspectes. Et ce serait bien de dire au développeur : « Ami, ici vous lisez d'abord par index, puis triez, puis coupez » - en règle générale, il y a un enregistrement.

Tous ceux qui ont écrit des requêtes ont probablement rencontré ce schéma : « Donnez-moi la dernière commande de Vasya, sa date. » Et si vous n'avez pas d'index par date, ou s'il n'y a pas de date dans l'index que vous avez utilisé, alors vous marcher exactement sur le même « râteau ».

Mais nous savons qu'il s'agit d'un « râteau » - alors pourquoi ne pas dire immédiatement au développeur ce qu'il doit faire. En conséquence, lorsqu'il ouvre un plan maintenant, notre développeur voit immédiatement une belle image avec des conseils, où il lui dit immédiatement : « Vous avez des problèmes ici et là, mais ils sont résolus de cette façon et de cette façon.

En conséquence, la quantité d’expérience nécessaire pour résoudre les problèmes au début et aujourd’hui a considérablement diminué. C'est le genre d'outil dont nous disposons.

Optimisation groupée des requêtes PostgreSQL. Kirill Borovikov (Tenseur)

Source: habr.com

Ajouter un commentaire