Je crois que presque tous les projets utilisant Ruby on Rails et Postgres comme arme principale sur le backend sont dans une lutte constante entre la vitesse de dĂ©veloppement, la lisibilitĂ© / maintenabilitĂ© du code et la vitesse du projet en production. Je vais vous raconter mon expĂ©rience d'Ă©quilibrage entre ces trois baleines dans un cas oĂč la lisibilitĂ© et la rapiditĂ© de travail ont souffert Ă l'entrĂ©e, et Ă la fin, il s'est avĂ©rĂ© faire ce que plusieurs ingĂ©nieurs talentueux ont tentĂ© de faire avant moi sans succĂšs.
L'histoire entiĂšre prendra plusieurs parties. C'est le premier oĂč je vais parler de ce qu'est PMDSC pour optimiser les requĂȘtes SQL, partager des outils utiles pour mesurer les performances des requĂȘtes dans postgres et me rappeler une ancienne feuille de triche utile qui est toujours pertinente.
Maintenant, aprÚs un certain temps, «avec le recul» je comprends qu'à l'entrée de cette affaire je ne m'attendais pas du tout à réussir. Par conséquent, ce post sera plutÎt utile pour les développeurs audacieux et pas les plus expérimentés que pour les super-seniors qui ont vu des rails avec du SQL nu.
Des données d'entrée
Chez Appbooster, nous faisons la promotion des applications mobiles. Pour émettre et tester facilement des hypothÚses, nous développons plusieurs de nos applications. Le backend pour la plupart d'entre eux est l'API Rails et Postgresql.
Le héros de cette publication est en cours de développement depuis fin 2013 - alors rails 4.1.0.beta1 venait de sortir. Depuis lors, le projet est devenu une application Web entiÚrement chargée qui s'exécute sur plusieurs serveurs dans Amazon EC2 avec une instance de base de données distincte dans Amazon RDS (db.t3.xlarge avec 4 processeurs virtuels et 16 Go de RAM). Les charges de pointe atteignent 25 000 tr / min, la charge moyenne journaliÚre de 8 à 10 000 tr / min.
Cette histoire a commencé avec une instance de base de données, ou plutÎt avec son solde créditeur.
Fonctionnement d'une instance de type «t» Postgres dans Amazon RDS: si votre base de donnĂ©es s'exĂ©cute avec une consommation de processeur moyenne infĂ©rieure Ă une certaine valeur, vous accumulez des crĂ©dits sur votre compte, que l'instance peut dĂ©penser en consommation de processeur pendant les heures de charge Ă©levĂ©e - cela vous Ă©vite de payer trop cher pour la capacitĂ© du serveur et pour faire face Ă une charge Ă©levĂ©e. Plus de dĂ©tails sur ce qu'ils paient et combien ils paient avec AWS peuvent ĂȘtre trouvĂ©s dans l'article de notre CTO .
Le solde des prĂȘts Ă un moment donnĂ© Ă©tait Ă©puisĂ©. Pendant un certain temps, cela n'a pas eu beaucoup d'importance, car le solde des prĂȘts peut ĂȘtre reconstituĂ© avec de l'argent - cela nous a coĂ»tĂ© environ 20 dollars par mois, ce qui n'est pas trĂšs perceptible pour le coĂ»t total de location de la puissance de calcul. Dans le dĂ©veloppement de produits, il est habituel de prĂȘter principalement attention aux tĂąches formulĂ©es Ă partir des exigences de l'entreprise. L'augmentation de la consommation de CPU du serveur de base de donnĂ©es s'inscrit dans la dette technique et est compensĂ©e par le faible coĂ»t d'achat d'un solde crĂ©diteur.
Un beau jour, j'ai Ă©crit dans le summari quotidien que j'Ă©tais trĂšs fatiguĂ© d'Ă©teindre les «incendies» qui apparaissaient pĂ©riodiquement dans diffĂ©rentes parties du projet. Si cela continue, le dĂ©veloppeur Ă©puisĂ© consacrera du temps aux tĂąches commerciales. Le mĂȘme jour, je suis allĂ© voir le chef de projet principal, lui ai expliquĂ© l'alignement et demandĂ© du temps pour enquĂȘter sur les causes des incendies pĂ©riodiques et des rĂ©parations. AprĂšs avoir reçu le feu vert, j'ai commencĂ© Ă collecter des donnĂ©es Ă partir de divers systĂšmes de surveillance.
Nous utilisons Newrelic pour suivre le temps de réponse total par jour. L'image ressemblait à ceci:
Une partie du temps de rĂ©ponse pris par Postgres est surlignĂ©e en jaune dans le graphique. Comme vous pouvez le voir, parfois le temps de rĂ©ponse atteignait 1000 ms, et la plupart du temps c'Ă©tait la base de donnĂ©es qui rĂ©flĂ©chissait Ă la rĂ©ponse. Vous devez donc regarder ce qui se passe avec les requĂȘtes SQL.
PMDSC est une pratique simple et directe pour tout travail d' optimisation SQL ennuyeux
Joue-le!
Mesure le!
Dessine le!
Supposez-le!
Vérifie ça!
Joue-le!
Peut-ĂȘtre la partie la plus importante de toute la pratique. Quand quelqu'un dit l'expression "Optimiser les requĂȘtes SQL" - cela provoque plutĂŽt un accĂšs de bĂąillement et d'ennui chez la grande majoritĂ© des gens. Quand vous dites «enquĂȘte dĂ©tective et recherche de mĂ©chants dangereux», cela vous engage davantage et vous met de bonne humeur. Par consĂ©quent, il est important d'entrer dans le jeu. J'ai aimĂ© jouer au dĂ©tective. J'ai imaginĂ© que les problĂšmes avec la base de donnĂ©es Ă©taient soit des criminels dangereux, soit des maladies rares. Et il s'est imaginĂ© dans le rĂŽle de Sherlock Holmes, du lieutenant Columbo ou du docteur House. Choisissez un hĂ©ros Ă votre goĂ»t et c'est parti!
Mesure le!
Pour analyser les statistiques de demande, j'ai installĂ© PgHero . C'est un moyen trĂšs pratique pour lire les donnĂ©es de l'extension Postgres pg_stat_statements. AccĂ©dez Ă / queries et consultez les statistiques de toutes les requĂȘtes des derniĂšres 24 heures. Le tri des requĂȘtes par dĂ©faut en fonction de la colonne DurĂ©e totale - la proportion du temps total pendant lequel la base de donnĂ©es traite la requĂȘte - une source prĂ©cieuse pour trouver des suspects. Temps moyen - combien, en moyenne, la demande est exĂ©cutĂ©e. Appels - combien de demandes ont Ă©tĂ© faites pendant la pĂ©riode sĂ©lectionnĂ©e. PgHero considĂšre que les demandes sont lentes si elles ont Ă©tĂ© exĂ©cutĂ©es plus de 100 fois par jour et ont pris plus de 20 millisecondes en moyenne. Liste des requĂȘtes lentes sur la premiĂšre page, immĂ©diatement aprĂšs la liste des index en double.
Nous prenons le premier de la liste et examinons les dĂ©tails de la requĂȘte, vous pouvez immĂ©diatement le voir expliquer analyser. Si le temps de planification est bien infĂ©rieur au temps d'exĂ©cution, alors quelque chose ne va pas avec cette demande et nous concentrons notre attention sur ce suspect.
PgHero a sa propre méthode de visualisation, mais j'ai aimé utiliser davantage Expliquer.depesz.com , en copiant les données depuis l'expliquer.
L'une des requĂȘtes suspectes utilise l'analyse d'index. La visualisation montre que cet index n'est pas efficace et est un point faible - mis en Ă©vidence en rouge. Bien! Nous avons examinĂ© les traces du suspect et trouvĂ© des preuves importantes! La justice est inĂ©vitable!
Dessine le!
Tirons beaucoup de donnĂ©es utilisĂ©es dans la partie problĂ©matique de la requĂȘte. Il sera utile de comparer avec les donnĂ©es couvertes par l'index.
Un peu de contexte. Nous avons testé l'un des moyens de garder le public dans l'application - quelque chose comme une loterie dans laquelle vous pouvez gagner de la monnaie locale. Vous placez un pari, devinez un nombre de 0 à 100 et prenez tout le pot si votre nombre est le plus proche de celui que le générateur de nombres aléatoires a reçu. Nous l'avons appelé "Arena" et appelé les rallyes "Battles".
La base de donnĂ©es au moment de l'enquĂȘte contient environ cinq cent mille enregistrements de batailles. Dans la partie problĂ©matique de la demande, nous recherchons des batailles dans lesquelles le taux ne dĂ©passe pas le solde de l'utilisateur et le statut de la bataille attend les joueurs. On voit que l'intersection des ensembles (surlignĂ©e en orange) est un trĂšs petit nombre d'enregistrements.
L'index utilisĂ© dans la partie suspecte de la requĂȘte couvre toutes les batailles crĂ©Ă©es sur le champ created_at. La demande passe par 505330 enregistrements dans lesquels elle sĂ©lectionne 40 et 505290 Ă©limine. Cela semble trĂšs inutile.
Supposez-le!
Nous émettons une hypothÚse. Qu'est-ce qui aidera la base de données à trouver quarante des cinq cent mille enregistrements? Essayons de créer un index qui couvre le champ de taux, uniquement pour les batailles avec le statut «en attente de joueurs» - un index partiel.
add_index :arena_battles, :bet,
where: "status = 'waiting_for_players'",
name: "index_arena_battles_on_bet_partial_status"
Index partiel - n'existe que pour les enregistrements qui correspondent Ă la condition: le champ d'Ă©tat est Ă©gal à «en attente de joueurs» et indexe le champ de taux - exactement ce qui est dans la condition de requĂȘte. Il est trĂšs avantageux d'utiliser cet index particulier: il ne prend que 40 kilo-octets et ne couvre pas les batailles qui ont dĂ©jĂ Ă©tĂ© jouĂ©es et nous n'avons pas besoin d'en obtenir un Ă©chantillon. A titre de comparaison, l'index index_arena_battles_on_created_at, qui a Ă©tĂ© utilisĂ© par le suspect, prend environ 40 Mo, et la table des batailles est d'environ 70 Mo. Cet index peut ĂȘtre supprimĂ© en toute sĂ©curitĂ© si d'autres requĂȘtes ne l'utilisent pas.
Vérifie ça!
Nous déployons la migration avec le nouvel index en production et observons comment la réponse du point final aux batailles a changé.
Le graphique montre l'heure à laquelle nous avons déployé la migration. Le soir du 6 décembre, le temps de réponse a diminué d'environ 10 fois, passant d'environ 500 ms à ~ 50 ms. Le suspect au tribunal a reçu le statut de prisonnier et est maintenant en prison. Bien!
Ăvasion de la prison
Quelques jours plus tard, nous avons rĂ©alisĂ© que nous Ă©tions heureux tĂŽt. Il semble que le prisonnier ait trouvĂ© des complices, Ă©laborĂ© et mis en Ćuvre un plan d'Ă©vasion.
Le matin du 11 dĂ©cembre, le planificateur de requĂȘtes postgres a dĂ©cidĂ© que l'utilisation d'un nouvel index analysĂ© n'Ă©tait plus rentable pour lui et a recommencĂ© Ă utiliser l'ancien.
Nous sommes de retour au stade Suppose it! Préparer un diagnostic différentiel, dans l'esprit du Dr House:
- Il peut ĂȘtre nĂ©cessaire d'optimiser les paramĂštres postgres;
- peut-ĂȘtre mettre Ă jour postgres vers une version plus rĂ©cente en termes mineurs (9.6.11 -> 9.6.15);
- et peut-ĂȘtre, encore une fois, Ă©tudier attentivement quelles formes de requĂȘte SQL Rails?
Nous avons testé les trois hypothÚses. Ce dernier nous a conduit sur la piste d'un complice.
SELECT "arena_battles".*
FROM "arena_battles"
WHERE "arena_battles"."status" = 'waiting_for_players'
AND (arena_battles.bet <= 98.13)
AND (NOT EXISTS (
SELECT 1 FROM arena_participations
WHERE arena_battle_id = arena_battles.id
AND (arena_profile_id = 46809)
))
ORDER BY "arena_battles"."created_at" ASC
LIMIT 10 OFFSET 0
Passons en revue ce SQL ensemble. Nous sélectionnons tous les champs de bataille de la table de bataille dont le statut est égal à «en attente de joueurs» et le taux est inférieur ou égal à un certain nombre. Jusqu'à présent, tout est clair. Le prochain terme de la condition semble effrayant.
NOT EXISTS (
SELECT 1 FROM arena_participations
WHERE arena_battle_id = arena_battles.id
AND (arena_profile_id = 46809)
)
Nous recherchons un rĂ©sultat de sous-requĂȘte inexistant. Obtenez le premier champ de la table de participation au combat, oĂč l'ID de combat correspond et le profil du participant appartient Ă notre joueur. Je vais essayer de dessiner l'ensemble dĂ©crit dans la sous-requĂȘte.
C'est difficile Ă comprendre, mais Ă la fin, avec cette sous-requĂȘte, nous avons essayĂ© d'exclure les batailles auxquelles le joueur participe dĂ©jĂ . Nous regardons l'explication gĂ©nĂ©rale de la requĂȘte et voyons Temps de planification: 0,180 ms, Temps d'exĂ©cution: 12,119 ms. Nous avons trouvĂ© un complice!
Il est temps pour ma feuille de triche préférée, qui circule sur Internet depuis 2008. Le voici:
Oui! DĂšs qu'une requĂȘte rencontre quelque chose qui devrait exclure un certain nombre d'enregistrements en fonction des donnĂ©es d'une autre table, ce mĂšme avec barbe et boucles devrait apparaĂźtre en mĂ©moire.
En fait, c'est ce dont nous avons besoin:
Enregistrez cette image pour vous-mĂȘme ou, mieux encore, imprimez-la et accrochez-la Ă plusieurs endroits du bureau.
Nous rĂ©Ă©crivons la sous-requĂȘte en LEFT JOIN WHERE B.key EST NULL, nous obtenons:
SELECT "arena_battles".*
FROM "arena_battles"
LEFT JOIN arena_participations
ON arena_participations.arena_battle_id = arena_battles.id
AND (arena_participations.arena_profile_id = 46809)
WHERE "arena_battles"."status" = 'waiting_for_players'
AND (arena_battles.bet <= 98.13)
AND (arena_participations.id IS NULL)
ORDER BY "arena_battles"."created_at" ASC
LIMIT 10 OFFSET 0
La requĂȘte corrigĂ©e s'exĂ©cute sur deux tables Ă la fois. Nous avons ajoutĂ© un tableau avec les enregistrements de la participation de l'utilisateur aux batailles sur la «gauche» et ajoutĂ© la condition que l'identifiant de participation n'existe pas. Voyons l'expliquer analyser la requĂȘte reçue: Temps de planification: 0,185 ms, Temps d'exĂ©cution: 0,337 ms. Bien! DĂ©sormais, le planificateur de requĂȘtes n'hĂ©sitera pas Ă utiliser l'index partiel, mais utilisera l'option la plus rapide. Le prisonnier Ă©vadĂ© et son complice ont Ă©tĂ© condamnĂ©s Ă la rĂ©clusion Ă perpĂ©tuitĂ© dans une institution Ă rĂ©gime strict. Il leur sera plus difficile de s'Ă©chapper.
Le résumé est bref.
- Utilisez Newrelic ou un autre service similaire pour trouver des prospects. Nous nous sommes rendu compte que le problĂšme rĂ©sidait prĂ©cisĂ©ment dans les requĂȘtes de base de donnĂ©es.
- Utilisez la pratique PMDSC - cela fonctionne et dans tous les cas est trĂšs engageant.
- Utilisez PgHero pour trouver des suspects et enquĂȘter sur des indices dans les statistiques de requĂȘtes SQL.
- Utilisez expliquer.depesz.com - il est facile de lire expliquer analyser les requĂȘtes lĂ -bas.
- Essayez de dessiner beaucoup de données lorsque vous ne savez pas exactement ce que fait la demande.
- Pensez au dur Ă cuire avec des boucles partout sur la tĂȘte lorsque vous voyez une sous-requĂȘte Ă la recherche de quelque chose qui ne figure pas dans une autre table.
- Jouez au dĂ©tective, vous pourriez mĂȘme obtenir un badge.