Concurrence PostgreSQL: pas sphérique, pas cheval, pas dans le vide



La mise Ă  l'Ă©chelle d'un SGBD est un avenir en constante Ă©volution. Les SGBD s'amĂ©liorent et Ă©voluent mieux sur les plates-formes matĂ©rielles, tandis que les plates-formes matĂ©rielles elles-mĂȘmes augmentent la productivitĂ©, le nombre de cƓurs et la mĂ©moire - Achilles rattrape la tortue, mais ne l'a toujours pas fait. Le problĂšme de la mise Ă  l'Ă©chelle du SGBD bat son plein.

Postgres Professional avait un problÚme avec la mise à l'échelle non seulement théoriquement, mais aussi pratiquement: avec ses clients. Et plus d'une fois. Un de ces cas sera discuté dans cet article.

PostgreSQL Ă©volue bien sur les systĂšmes NUMA s'il s'agit d'une seule carte mĂšre avec plusieurs processeurs et plusieurs bus de donnĂ©es. Quelques optimisations peuvent ĂȘtre lues ici et ici . Cependant, il existe une autre classe de systĂšmes, ils ont plusieurs cartes mĂšres, dont l'Ă©change de donnĂ©es s'effectue via une interconnexion, tandis qu'une instance du systĂšme d'exploitation fonctionne sur eux et pour l'utilisateur, cette conception ressemble Ă  une seule machine. Et bien que formellement, ces systĂšmes puissent Ă©galement ĂȘtre attribuĂ©s Ă  NUMA, mais ils sont essentiellement plus proches des superordinateurs, comme l'accĂšs Ă  la mĂ©moire locale du nƓud et l'accĂšs Ă  la mĂ©moire du nƓud voisin diffĂšrent radicalement. La communautĂ© PostgreSQL estime que la seule instance Postgres exĂ©cutĂ©e sur de telles architectures est une source de problĂšmes, et il n'y a pas encore d'approche systĂ©matique pour les rĂ©soudre.

En effet, l'architecture logicielle utilisant la mĂ©moire partagĂ©e est fondamentalement conçue pour le fait que le temps d'accĂšs des diffĂ©rents processus Ă  leur mĂ©moire propre et distante est plus ou moins comparable. Dans le cas oĂč nous travaillons avec de nombreux nƓuds, le pari sur la mĂ©moire partagĂ©e en tant que canal de communication rapide cesse de se justifier, car en raison de la latence, il est beaucoup "moins cher" d'envoyer une demande pour effectuer une certaine action sur le nƓud (nƓud) oĂč des donnĂ©es intĂ©ressantes que d'envoyer ces donnĂ©es sur le bus. Par consĂ©quent, pour les supercalculateurs et, en gĂ©nĂ©ral, les systĂšmes Ă  nombreux nƓuds, les solutions de cluster sont pertinentes.

Cela ne signifie pas qu'il faut mettre un terme Ă  la combinaison de systĂšmes multi-nƓuds et d'une architecture de mĂ©moire partagĂ©e Postgres classique. AprĂšs tout, si les processus postgres passent la plupart de leur temps Ă  effectuer des calculs complexes localement, cette architecture sera mĂȘme trĂšs efficace. Dans notre situation, le client avait dĂ©jĂ  achetĂ© un puissant serveur multi-nƓuds, et nous devions y rĂ©soudre les problĂšmes de PostgreSQL.

Mais les problĂšmes Ă©taient sĂ©rieux: les demandes d'Ă©criture les plus simples (changer plusieurs valeurs de champ dans un enregistrement) ont Ă©tĂ© exĂ©cutĂ©es en quelques minutes Ă  une heure. Comme il a Ă©tĂ© confirmĂ© par la suite, ces problĂšmes se sont manifestĂ©s dans toute leur splendeur prĂ©cisĂ©ment en raison du grand nombre de cƓurs et, par consĂ©quent, du parallĂ©lisme radical dans l'exĂ©cution des requĂȘtes avec un Ă©change relativement lent entre les nƓuds.

Par conséquent, l'article se révélera, pour ainsi dire, à deux fins:

  • Partager l'expĂ©rience: que faire si dans un systĂšme Ă  plusieurs nƓuds la base de donnĂ©es ralentit sĂ©rieusement. Par oĂč commencer, comment diagnostiquer oĂč aller.
  • DĂ©crivez comment les problĂšmes du SGBD PostgreSQL lui-mĂȘme peuvent ĂȘtre rĂ©solus avec un niveau de concurrence Ă©levĂ©. Notamment comment la modification de l'algorithme de prise de verrous affecte les performances de PostgreSQL.

Serveur et DB


Le systĂšme Ă©tait composĂ© de 8 lames avec 2 prises chacune. Au total, plus de 300 cƓurs (hors hypertreading). Un pneu rapide (technologie propriĂ©taire du fabricant) relie les pales. Ce n'est pas un supercalculateur, mais pour une instance du SGBD, la configuration est impressionnante.
La charge est également assez importante. Plus d'un téraoctet de données. Environ 3000 transactions par seconde. Plus de 1000 connexions aux postgres.

Ayant commencé à faire face aux attentes d'enregistrement horaire, la premiÚre chose que nous avons faite a été d'écrire sur le disque comme cause de retards. DÚs que des retards incompréhensibles ont commencé, les tests ont commencé à se faire exclusivement sur tmpfs . L'image n'a pas changé. Le disque n'y est pour rien.

Prise en main des diagnostics: vues


Les problĂšmes Ă©tant probablement dus Ă  la forte concurrence des processus qui «frappent» les mĂȘmes objets, la premiĂšre chose Ă  vĂ©rifier est les verrous. Dans PostgreSQL, il existe une vue pg.catalog.pg_locks et pg_stat_activity pour une telle vĂ©rification. Le second, dĂ©jĂ  dans la version 9.6, a ajoutĂ© des informations sur ce que le processus attend ( Amit Kapila, Ildus Kurbangaliev ) - wait_event_type . Les valeurs possibles pour ce champ sont dĂ©crites ici .

Mais d'abord, comptez:

 postgres=# SELECT COUNT(*) FROM pg_locks; count —---— 88453 (1 row) postgres=# SELECT COUNT(*) FROM pg_stat_activity; count —---— 1826 (1 row) postgres=# SELECT COUNT(*) FROM pg_stat_activity WHERE state ='active'; count —---— 1005 

Ce sont de vrais chiffres. Atteint jusqu'Ă  200 000 verrous.
Dans le mĂȘme temps, de tels verrous Ă©taient suspendus Ă  la demande malheureuse:

 SELECT COUNT(mode), mode FROM pg_locks WHERE pid =580707 GROUP BY mode; count | mode —-----+---------------— 93 | AccessShareLock 1 | ExclusiveLock 

Lors de la lecture du tampon, le SGBD utilise le verrouillage de share , tout en écrivant - exclusive . Autrement dit, les verrous en écriture représentaient moins de 1% de toutes les demandes.
Dans la vue pg_locks , les types de verrous ne ressemblent pas toujours à ceux décrits dans la documentation utilisateur.

Voici la plaque d'allumettes:

 AccessShareLock = LockTupleKeyShare RowShareLock = LockTupleShare ExclusiveLock = LockTupleNoKeyExclusive AccessExclusiveLock = LockTupleExclusive 

La requĂȘte SELECT mode FROM pg_locks a montrĂ© que CREATE INDEX (sans CONCURRENTLY) attendrait 234 INSERTs et 390 INSERTs pour le buffer content lock . Une solution possible consiste Ă  «apprendre» aux INSERT de diffĂ©rentes sessions Ă  se croiser moins dans les tampons.

Il est temps d'utiliser la perf


L'utilitaire perf collecte de nombreuses informations de diagnostic. En mode record ... il écrit les statistiques des événements systÚme dans des fichiers (par défaut, ils sont en ./perf_data ), et en mode report , il analyse les données collectées, par exemple, vous pouvez filtrer les événements qui ne concernent que les postgres ou un pid donné:

 $ perf record -u postgres  $ perf record -p 76876  ,  $ perf report > ./my_results 

En conséquence, nous verrons quelque chose comme



Comment utiliser perf pour diagnostiquer PostgreSQL est décrit, par exemple, ici , ainsi que dans le wiki pg .

Dans notre cas, mĂȘme le mode le plus simple a donnĂ© des informations importantes Ă  perf top - perf top , qui fonctionne, bien sĂ»r, dans l'esprit du systĂšme d'exploitation le top performant. Avec perf top nous avons vu que la plupart du temps, le processeur passe dans les PinBuffer() base, ainsi que dans les fonctions PinBuffer() et LWLockAttemptLock(). .

PinBuffer() est une fonction qui augmente le compteur de rĂ©fĂ©rences au tampon (mappage d'une page de donnĂ©es Ă  la RAM), grĂące Ă  laquelle les processus postgres savent quels tampons peuvent ĂȘtre forcĂ©s et lesquels ne le peuvent pas.

LWLockAttemptLock() - LWLock de capture de LWLock . LWLock est une sorte de verrou avec deux niveaux de shared et d' exclusive , sans définir d' deadlock , les verrous sont pré-alloués à shared memory , les processus en attente attendent dans une file d'attente.

Ces fonctions ont déjà été sérieusement optimisées dans PostgreSQL 9.5 et 9.6. Les verrous à l'intérieur d'eux ont été remplacés par l'utilisation directe des opérations atomiques.

Graphes de flamme


C’est impossible sans eux: mĂȘme s’ils Ă©taient inutiles, il vaudrait quand mĂȘme la peine d’en parler - ils sont d’une beautĂ© exceptionnelle. Mais ils sont utiles. Voici une illustration de github , pas de notre cas (ni nous ni le client ne sommes encore prĂȘts Ă  divulguer les dĂ©tails).



Ces belles images montrent trĂšs clairement ce que prennent les cycles du processeur. Le mĂȘme perf peut collecter des donnĂ©es, mais le flame graph visualise intelligemment les donnĂ©es et construit des arbres basĂ©s sur les piles d'appels collectĂ©es. Vous pouvez en savoir plus sur le profilage avec des graphiques de flamme, par exemple, ici , et tĂ©lĂ©charger tout ce dont vous avez besoin ici .

Dans notre cas, une énorme quantité de nestloop était visible sur les graphiques de flamme. Apparemment, les jointures d'un grand nombre de tables dans de nombreuses demandes de lecture simultanées ont provoqué un grand nombre de verrous de access share .

Les statistiques collectĂ©es par perf montrent oĂč vont les cycles du processeur. Et bien que nous ayons vu que la plupart du temps du processeur passe sur les verrous, nous n'avons pas vu ce qui conduit exactement Ă  de si longues attentes en matiĂšre de verrous, car nous ne voyons pas exactement oĂč les attentes de verrouillage se produisent, car Le temps CPU n'est pas perdu Ă  attendre.

Afin de voir les attentes elles-mĂȘmes, vous pouvez crĂ©er une demande Ă  la vue systĂšme pg_stat_activity .

 SELECT wait_event_type, wait_event, COUNT(*) FROM pg_stat_activity GROUP BY wait_event_type, wait_event; 

a révélé que:

 LWLockTranche | buffer_content | UPDATE ************* LWLockTranche | buffer_content | INSERT INTO ******** LWLockTranche | buffer_content | \r | | insert into B4_MUTEX | | values (nextval('hib | | returning ID Lock | relation | INSERT INTO B4_***** LWLockTranche | buffer_content | UPDATE ************* Lock | relation | INSERT INTO ******** LWLockTranche | buffer_mapping | INSERT INTO ******** LWLockTranche | buffer_content | \r 

(les astérisques ici remplacent simplement les détails de la demande que nous ne divulguons pas).

Vous pouvez voir les valeurs buffer_content (blocage du contenu des tampons) et buffer_mapping (blocage des composants de la plaque de hachage shared_buffers ).

Pour obtenir de l'aide sur gdb


Mais pourquoi tant d'attentes pour ces types de serrures? Pour des informations plus détaillées sur les attentes, j'ai dû utiliser le débogueur GDB . Avec GDB nous pouvons obtenir une pile d'appels de processus spécifiques. En appliquant l'échantillonnage, c'est-à-dire AprÚs avoir collecté un certain nombre de piles d'appels aléatoires, vous pouvez vous faire une idée des piles qui ont les attentes les plus longues.

Considérez le processus de compilation des statistiques. Nous considérerons la collecte «manuelle» de statistiques, bien que dans la vie réelle des scripts spéciaux soient utilisés pour le faire automatiquement.

Tout d'abord, gdb doit ĂȘtre attachĂ© au processus PostgreSQL. Pour ce faire, recherchez le pid processus serveur, par exemple

 $ ps aux | grep postgres 

Disons que nous avons trouvé:

 postgres 2025 0.0 0.1 172428 1240 pts/17  S   23  0:00 /usr/local/pgsql/bin/postgres -D /usr/local/pgsql/data 

et maintenant insérez le pid dans le débogueur:

 igor_le:~$gdb -p 2025 

Une fois à l'intérieur du débogueur, nous écrivons bt [c'est-à-dire backtrace ] ou where . Et nous obtenons beaucoup d'informations sur ce type:

 (gdb) bt #0 0x00007fbb65d01cd0 in __write_nocancel () from /lib64/libc.so.6 #1 0x00000000007c92f4 in write_pipe_chunks ( data=0x110e6e8 "2018‐06‐01 15:35:38 MSK [524647]: [392‐1] db=bp,user=bp,app=[unknown],client=192.168.70.163 (http://192.168.70.163) LOG: relation 23554 new block 493: 248.389503\n2018‐06‐01 15:35:38 MSK [524647]: [393‐1] db=bp,user=bp,app=["..., len=409, dest=dest@entry=1) at elog.c:3123 #2 0x00000000007cc07b in send_message_to_server_log (edata=0xc6ee60 <errordata>) at elog.c:3024 #3 EmitErrorReport () at elog.c:1479 

AprÚs avoir collecté des statistiques, y compris des piles d'appels de tous les processus postgres, collectées à plusieurs reprises à différents moments, nous avons vu que le buffer partition lock à l'intérieur du relation extension lock duré 3706 secondes (environ une heure), c'est-à-dire qu'il se verrouille sur un morceau de la table de hachage du tampon , qui était nécessaire pour remplacer l'ancien tampon, afin de le remplacer par la suite par un nouveau correspondant à la partie étendue de la table. Un certain nombre de verrous de buffer content lock de buffer content lock étaient également perceptibles, ce qui correspondait à l'attente de verrouillage des pages de l'index de l' B-tree pour l'insertion.



Au début, deux explications sont venues pour un temps d'attente aussi monstrueux:

  • Quelqu'un d'autre a pris ce LWLock et est restĂ©. Mais c'est peu probable. Parce que rien de compliquĂ© ne se passe Ă  l'intĂ©rieur du verrou de partition tampon.
  • Nous avons rencontrĂ© un comportement pathologique de LWLock . Autrement dit, malgrĂ© le fait que personne n'a pris le verrou trop longtemps, son attente a durĂ© trop longtemps.

Patchs de diagnostic et traitement des arbres


En réduisant le nombre de connexions simultanées, nous déchargerions probablement le flux de demandes de verrous. Mais ce serait comme se rendre. Au lieu de cela, Alexander Korotkov , l'architecte en chef de Postgres Professional (bien sûr, il a aidé à préparer cet article), a proposé une série de correctifs.

Tout d'abord, il était nécessaire d'avoir une image plus détaillée de la catastrophe. Peu importe la qualité des outils finis, les correctifs de diagnostic de leur propre fabrication seront également utiles.

Un correctif a été écrit qui ajoute une journalisation détaillée du temps passé dans l' relation extension , ce qui se passe à l'intérieur de la fonction RelationAddExtraBlocks() . Nous découvrons donc ce que le temps est passé à l'intérieur de RelationAddExtraBlocks().

Et à l'appui de lui, un autre correctif a été écrit dans pg_stat_activity sur ce que nous faisons maintenant en ce qui relation extension . Cela a été fait de cette façon: lorsque la relation développe, application_name devient RelationAddExtraBlocks . Ce processus est désormais facilement analysé avec un maximum de détails à l'aide de gdb bt et perf .

En fait, les correctifs mĂ©dicaux (et non diagnostiques) ont Ă©tĂ© Ă©crits deux fois. Le premier correctif a changĂ© le comportement des verrous de feuille de B‐tree : auparavant, lors de la demande d'insertion, la feuille Ă©tait bloquĂ©e en tant que share , puis elle devenait exclusive . Maintenant, il devient immĂ©diatement exclusive . Maintenant, ce correctif a dĂ©jĂ  Ă©tĂ© validĂ© pour PostgreSQL 12 . Heureusement, cette annĂ©e, Alexander Korotkov a reçu le statut de committer - le deuxiĂšme committer PostgreSQL en Russie et le second dans l'entreprise.

La valeur NUM_BUFFER_PARTITIONS a également été augmentée de 128 à 512 pour réduire la charge sur les verrous de mappage: la table de hachage du gestionnaire de tampons a été divisée en morceaux plus petits, dans l'espoir que la charge sur chaque piÚce spécifique soit réduite.

AprÚs avoir appliqué ce correctif, les verrous sur le contenu des tampons ont disparu, mais malgré l'augmentation de NUM_BUFFER_PARTITIONS , buffer_mapping est resté, c'est-à-dire que nous vous rappelons de bloquer des morceaux de la table de hachage du gestionnaire de tampons:

 locks_count | active_session | buffer_content | buffer_mapping ----‐‐‐--‐‐‐+‐------‐‐‐‐‐‐‐‐‐+‐‐‐------‐‐‐‐‐‐‐+‐‐------‐‐‐ 12549 | 1218 | 0 | 15 

Et mĂȘme ce n'est pas grand-chose. B - l'arbre n'est plus un goulot d'Ă©tranglement. L'extension du heap- est apparue.

Traitement de la conscience


Ensuite, Alexander a avancé l'hypothÚse et la solution suivantes:

Nous attendons beaucoup de temps sur le buffer parittion lock du buffer parittion lock lors de l' buffer parittion lock tampon. Peut-ĂȘtre sur le mĂȘme buffer parittion lock il y a une page trĂšs demandĂ©e, par exemple, la racine d'un B‐tree À ce stade, il existe un flux continu de demandes de shared lock partir des demandes de lecture.

La file d'attente Ă  LWLock «pas juste». Étant donnĂ© que shared lock peuvent ĂȘtre pris autant de fois que nĂ©cessaire, alors si le shared lock dĂ©jĂ  pris, les shared lock suivants passent sans file d'attente. Ainsi, si le flux de verrous partagĂ©s est d'une intensitĂ© suffisante pour qu'il n'y ait pas de «fenĂȘtres» entre eux, alors l'attente d'un exclusive lock va presque Ă  l'infini.

Pour rĂ©soudre ce problĂšme, vous pouvez essayer d'offrir - un patch de comportement "gentlemanly" des verrous. Cela Ă©veille la conscience des shared locker et ils font honnĂȘtement la queue lorsqu'ils ont dĂ©jĂ  une exclusive lock (il est intĂ©ressant que les serrures lourdes - hwlock - n'aient pas de problĂšmes de conscience: elles font toujours la queue honnĂȘtement)

 locks_count | active_session | buffer_content | buffer_mapping | reladdextra | inserts>30sec ‐‐‐‐‐‐-‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐+‐‐‐‐‐‐‐‐‐‐‐--‐-‐+‐‐‐‐‐‐-‐‐‐‐‐‐+‐‐‐‐------ 173985 | 1802 | 0 | 569 | 0 | 0 

Tout va bien! Il n'y a pas de longues insert . Bien que les verrous sur les morceaux des plaques de hachage soient restés. Mais que faire, ce sont les propriétés des pneus de notre petit supercalculateur.

Ce patch a Ă©galement Ă©tĂ© offert Ă  la communautĂ© . Mais quelle que soit l'Ă©volution du sort de ces correctifs dans la communautĂ©, rien ne les empĂȘche d'accĂ©der aux prochaines versions de Postgres Pro Enterprise , conçues spĂ©cifiquement pour les clients avec des systĂšmes lourdement chargĂ©s.

Moral


Des verrous de share lĂ©gers Ă  haute moralitĂ© - exclusive blocs exclusive sautent la file d'attente - ont rĂ©solu le problĂšme des retards horaires dans un systĂšme Ă  plusieurs nƓuds. La balise de hachage du buffer manager n'a pas fonctionnĂ© en raison d'un flux de share lock trop important, ce qui n'a laissĂ© aucune chance aux verrous nĂ©cessaires pour remplacer les anciens tampons et en charger de nouveaux. Les problĂšmes avec l'extension du tampon pour les tables de base de donnĂ©es n'Ă©taient qu'une consĂ©quence de cela. Avant cela, il Ă©tait possible d'Ă©largir le goulot d'Ă©tranglement avec l'accĂšs Ă  la racine de l' B-tree

PostgreSQL n'a pas Ă©tĂ© conçu pour les architectures NUMA et les superordinateurs. L'adaptation Ă  de telles architectures Postgres est un travail Ă©norme qui nĂ©cessiterait (et Ă©ventuellement nĂ©cessiterait) les efforts coordonnĂ©s de nombreuses personnes et mĂȘme d'entreprises. Mais les consĂ©quences dĂ©sagrĂ©ables de ces problĂšmes architecturaux peuvent ĂȘtre attĂ©nuĂ©es. Et nous devons le faire: les types de charges qui ont entraĂźnĂ© des retards similaires Ă  ceux dĂ©crits sont assez typiques, des signaux de dĂ©tresse similaires provenant d'autres endroits continuent de nous parvenir. Des problĂšmes similaires sont apparus plus tĂŽt - sur les systĂšmes avec moins de cƓurs, seules les consĂ©quences n'Ă©taient pas si monstrueuses et les symptĂŽmes ont Ă©tĂ© traitĂ©s avec d'autres mĂ©thodes et d'autres correctifs. Maintenant, un autre mĂ©dicament est apparu - pas universel, mais clairement utile.

Ainsi, lorsque PostgreSQL travaille avec la mĂ©moire de l'ensemble du systĂšme en tant que local, aucun bus Ă  grande vitesse entre les nƓuds ne peut se comparer au temps d'accĂšs Ă  la mĂ©moire locale. Les tĂąches surgissent Ă  cause de cela difficile, souvent urgent, mais intĂ©ressant. Et l'expĂ©rience de les rĂ©soudre est utile non seulement pour les dĂ©cisifs, mais aussi pour toute la communautĂ©.

Source: https://habr.com/ru/post/fr423685/


All Articles