Comment multiplier par 10 le nombre de requĂȘtes Ă une base de donnĂ©es sans changer pour un serveur plus performant tout en maintenant le bon fonctionnement du systĂšme ? Je vais vous raconter comment nous avons luttĂ© contre la baisse de performance de notre base de donnĂ©es, comment nous avons optimisĂ© les requĂȘtes SQL pour servir le plus grand nombre d'utilisateurs possible sans augmenter les coĂ»ts des ressources de calcul.
Je dĂ©veloppe un service de gestion des processus d'affaires pour les entreprises de construction. Environ 3 000 entreprises travaillent avec nous. Plus de 10 000 personnes utilisent notre systĂšme chaque jour pendant 4 Ă 10 heures. Il rĂ©sout divers problĂšmes de planification, d'alerte, de notification, de validation⊠Nous utilisons PostgreSQL 9.6. Nous avons environ 300 tables dans notre base de donnĂ©es et jusqu'Ă 200 millions de requĂȘtes y parviennent chaque jour (10 000 diffĂ©rentes). En moyenne, nous traitons 3 000 Ă 4 000 requĂȘtes par seconde, et dans les moments les plus actifs, plus de 10 000 requĂȘtes par seconde. La plupart des requĂȘtes sont des OLAP. Les ajouts, modifications et suppressions sont beaucoup moins frĂ©quents, ce qui signifie que la charge OLTP est relativement faible. J'ai prĂ©sentĂ© tous ces chiffres pour que vous puissiez Ă©valuer l'ampleur de notre projet et comprendre Ă quel point notre expĂ©rience peut vous ĂȘtre utile.
PremiĂšre image. Lyrique
Lorsque nous avons commencĂ© le dĂ©veloppement, nous ne pensions pas vraiment Ă la charge qui pĂšserait sur la base de donnĂ©es et ce que nous ferions si le serveur cessait de fonctionner. Lors de la conception de la base de donnĂ©es, nous avons suivi des recommandations gĂ©nĂ©rales et avons essayĂ© de ne pas nous tirer une balle dans le pied, mais au-delĂ des conseils gĂ©nĂ©raux tels que « ne pas utiliser le modĂšle nous n'avons pas approfondi. Nous avons conçu en nous basant sur les principes de normalisation, Ă©vitant la redondance des donnĂ©es, sans nous soucier d'accĂ©lĂ©rer certaines requĂȘtes. DĂšs que les premiers utilisateurs sont arrivĂ©s, nous avons Ă©tĂ© confrontĂ©s Ă des problĂšmes de performance. Comme d'habitude, nous Ă©tions totalement mal prĂ©parĂ©s. Les premiers problĂšmes Ă©taient simples. En rĂšgle gĂ©nĂ©rale, il suffisait d'ajouter un nouvel index. Mais il est arrivĂ© un moment oĂč ces solutions simples ont cessĂ© de fonctionner. RĂ©alisant que nous manquions d'expĂ©rience et que nous avions de plus en plus de mal Ă comprendre la cause des problĂšmes, nous avons engagĂ© des spĂ©cialistes qui nous ont aidĂ©s Ă configurer correctement le serveur, Ă connecter le monitoring et Ă nous montrer oĂč regarder pour obtenir .
DeuxiĂšme image. Statistique
Nous avons environ 10 000 requĂȘtes diffĂ©rentes qui s'exĂ©cutent sur notre base de donnĂ©es chaque jour. Parmi ces 10 000, il y a des monstres qui s'exĂ©cutent 2 Ă 3 millions de fois avec un temps d'exĂ©cution moyen de 0,1 Ă 0,3 ms, ainsi que des requĂȘtes avec un temps d'exĂ©cution moyen de 30 secondes, invoquĂ©es 100 fois par jour.
Il n'Ă©tait pas possible d'optimiser les 10 000 requĂȘtes, alors nous avons dĂ©cidĂ© de dĂ©terminer oĂč concentrer nos efforts pour amĂ©liorer correctement les performances de la base de donnĂ©es. AprĂšs plusieurs itĂ©rations, nous avons commencĂ© Ă classer les requĂȘtes par types.
REQUĂTES TOP
Ce sont les requĂȘtes les plus lourdes qui prennent le plus de temps (temps total). Ce sont des requĂȘtes qui sont soit trĂšs souvent appelĂ©es, soit des requĂȘtes qui prennent beaucoup de temps Ă s'exĂ©cuter (les requĂȘtes longues et frĂ©quentes avaient dĂ©jĂ Ă©tĂ© optimisĂ©es lors des premiĂšres itĂ©rations pour gagner en vitesse). Au final, le serveur passe le plus de temps Ă les exĂ©cuter. Il est Ă©galement important de sĂ©parer les requĂȘtes top par le temps d'exĂ©cution total et par le temps IO. Les mĂ©thodes d'optimisation de ces requĂȘtes sont lĂ©gĂšrement diffĂ©rentes.
La pratique courante de toutes les entreprises est de travailler sur les REQUĂTES TOP. Elles sont peu nombreuses, et l'optimisation d'une seule requĂȘte peut libĂ©rer 5 Ă 10 % des ressources. Cependant, Ă mesure que le projet « grandit », l'optimisation des REQUĂTES TOP devient une tĂąche de plus en plus complexe. Tous les moyens simples ont dĂ©jĂ Ă©tĂ© Ă©prouvĂ©s, et la requĂȘte la plus « lourde » n'accapare « que » 3 Ă 5 % des ressources. Si les REQUĂTES TOP occupent moins de 30 Ă 40 % du temps total, vous avez probablement dĂ©jĂ travaillĂ© pour qu'elles fonctionnent rapidement et il est temps de passer Ă l'optimisation des requĂȘtes du groupe suivant.
Il reste Ă rĂ©pondre Ă la question de combien de requĂȘtes supĂ©rieures inclure dans ce groupe. Je prends gĂ©nĂ©ralement pas moins de 10, mais pas plus de 20. J'essaie de m'assurer que le temps d'exĂ©cution de la premiĂšre et de la derniĂšre requĂȘte dans le groupe TOP ne diffĂšre pas de plus de 10 fois. C'est-Ă -dire que si le temps d'exĂ©cution des requĂȘtes chute brusquement de la premiĂšre Ă la dixiĂšme place, je prends le TOP-10, si la chute est plus progressive, j'augmente la taille du groupe Ă 15 ou 20.

RequĂȘtes intermĂ©diaires (medium)
Ce sont toutes les requĂȘtes qui viennent juste aprĂšs les REQUĂTES TOP, Ă l'exception des 5 Ă 10 % derniĂšres. L'optimisation de ces requĂȘtes recĂšle souvent la possibilitĂ© d'augmenter considĂ©rablement les performances du serveur. Ces requĂȘtes peuvent reprĂ©senter jusqu'Ă 80 %. Mais mĂȘme si leur part dĂ©passe 50 %, il est temps de les examiner de plus prĂšs.
Queue (tail)
Comme mentionnĂ©, ces requĂȘtes arrivent Ă la fin et prennent 5 Ă 10 % de notre temps. On peut les oublier, sauf si vous utilisez des outils d'analyse de requĂȘtes automatiques, alors leur optimisation peut aussi coĂ»ter peu.
Comment évaluer chaque groupe ?
J'utilise une requĂȘte SQL qui permet de faire cette Ă©valuation pour PostgreSQL (je suis sĂ»r qu'il est possible d'Ă©crire une requĂȘte similaire pour de nombreux autres SGBD).
RequĂȘte SQL pour Ă©valuer la taille des groupes TOP-MEDIUM-TAIL
SELECT sum(time_top) AS sum_top, sum(time_medium) AS sum_medium, sum(time_tail) AS sum_tail
FROM
(
SELECT CASE WHEN rn 20 AND rn 800 THEN tt_percent ELSE 0 END AS time_tail
FROM (
SELECT total_time / (SELECT sum(total_time) FROM pg_stat_statements) * 100 AS tt_percent, query,
ROW_NUMBER () OVER (ORDER BY total_time DESC) AS rn
FROM pg_stat_statements
ORDER BY total_time DESC
) AS t
)
AS ts
Le rĂ©sultat de la requĂȘte comporte trois colonnes, chacune contenant le pourcentage de temps consacrĂ© au traitement des requĂȘtes de ce groupe. Ă l'intĂ©rieur de la requĂȘte, il y a deux nombres (dans mon cas, 20 et 800) qui sĂ©parent les requĂȘtes d'un groupe Ă l'autre.
Voici comment se rapportaient les parts des requĂȘtes au dĂ©but de l'optimisation et maintenant.

Le diagramme montre que la part des requĂȘtes TOP a fortement diminuĂ©, tandis que celle des "moyennes" a augmentĂ©.
Au dĂ©part, les erreurs Ă©videntes figuraient dans les requĂȘtes TOP. Avec le temps, les maladies de jeunesse ont disparu, la part des requĂȘtes TOP a diminuĂ© et il a fallu fournir de plus en plus d'efforts pour accĂ©lĂ©rer les requĂȘtes lourdes.
Pour obtenir le texte des requĂȘtes, nous utilisons cette requĂȘte.
SELECT * FROM (
SELECT ROW_NUMBER () OVER (ORDER BY total_time DESC) AS rn, total_time / (SELECT sum(total_time) FROM pg_stat_statements) * 100 AS tt_percent, query
FROM pg_stat_statements
ORDER BY total_time DESC
) AS T
WHERE
rn 20 AND rn 800 -- TAIL
Voici une liste des techniques les plus souvent utilisĂ©es qui nous ont aidĂ©s Ă accĂ©lĂ©rer les requĂȘtes TOP :
- Redesign du systĂšme, par exemple, la refonte de la logique des notifications sur un message broker au lieu de requĂȘtes pĂ©riodiques Ă la base de donnĂ©es.
- Ajout ou modification d'index.
- Réécriture des requĂȘtes ORM en SQL pur.
- Réécriture de la logique de chargement paresseux des données.
- Mise en cache par dĂ©normalisation des donnĂ©es. Par exemple, nous avons une relation entre les tables Livraison -> Facture -> RequĂȘte -> Demande. Cela signifie que chaque livraison est liĂ©e Ă une demande via d'autres tables. Pour ne pas lier toutes les tables dans chaque requĂȘte, nous avons dupliquĂ© le lien vers la demande dans la table Livraison.
- Mise en cache des tables statiques avec des référentiels et des tables rarement modifiées en mémoire du programme.
Parfois, les modifications entraßnaient un redesign important, mais offraient une réduction de 5 à 10% de la charge systÚme et étaient justifiées. Au fil du temps, les résultats devenaient de moins en moins significatifs, tandis qu'un redesign plus sérieux était de plus en plus nécessaire.
Nous avons alors prĂȘtĂ© attention Ă un deuxiĂšme groupe de requĂȘtes, le groupe intermĂ©diaire. Ce groupe comportait beaucoup plus de requĂȘtes et semblait nĂ©cessiter beaucoup de temps pour l'analyse. Cependant, la plupart des requĂȘtes se sont avĂ©rĂ©es trĂšs simples Ă optimiser, et de nombreux problĂšmes se rĂ©pĂ©taient des dizaines de fois sous diffĂ©rentes variations. Voici quelques exemples d'optimisations typiques que nous avons appliquĂ©es Ă des dizaines de requĂȘtes similaires, chacune de ces groupes optimisĂ©s allĂ©geant la base de donnĂ©es de 3 Ă 5%.
- Au lieu de vérifier la présence d'enregistrements avec COUNT et de faire un scan complet de la table, nous avons commencé à utiliser EXISTS.
- Nous nous sommes dĂ©barrassĂ©s de DISTINCT (il nây a pas de recette universelle, mais parfois on peut facilement sâen passer en accĂ©lĂ©rant la requĂȘte de 10 Ă 100 fois).
Par exemple, au lieu de la requĂȘte pour extraire tous les chauffeurs d'une grande table de livraisons (DELIVERY),
SELECT DISTINCT P.ID, P.FIRST_NAME, P.LAST_NAME FROM DELIVERY D JOIN PERSON P ON D.DRIVER_ID = P.IDnous avons effectuĂ© une requĂȘte sur une table PERSON relativement petite.
SELECT P.ID, P.FIRST_NAME, P.LAST_NAME FROM PERSON WHERE EXISTS(SELECT D.ID FROM DELIVERY WHERE D.DRIVER_ID = P.ID)Il semblerait que nous ayons utilisĂ© une sous-requĂȘte corrĂ©lĂ©e, mais elle offre une acceleration de plus de 10 fois.
- Dans de nombreux cas, nous avons mĂȘme renoncĂ© Ă COUNT et
- au lieu de
UPPER(s) LIKE JOHN%nous utilisons
s ILIKE âJohn%â
Chaque requĂȘte spĂ©cifique a parfois pu ĂȘtre accĂ©lĂ©rĂ©e de 3 Ă 1000 fois. MalgrĂ© ces performances impressionnantes, au dĂ©but, nous pensions qu'il n'y avait pas de sens Ă optimiser une requĂȘte qui s'exĂ©cutait en 10 ms, qui figurait dans le 300e rang des requĂȘtes les plus lourdes et qui, en termes de charge totale sur la base de donnĂ©es, ne reprĂ©sentait qu'un infime pourcentage. Mais en appliquant la mĂȘme recette Ă un groupe de requĂȘtes similaires, nous regagnions plusieurs pourcentages. Pour ne pas perdre de temps Ă examiner manuellement des centaines de requĂȘtes, nous avons Ă©crit quelques scripts simples qui, grĂące Ă des expressions rĂ©guliĂšres, identifiaient des requĂȘtes similaires. Au final, la recherche automatique de groupes de requĂȘtes nous a permis d'amĂ©liorer encore notre performance avec des efforts modestes.
Nous travaillons depuis trois ans sur le mĂȘme matĂ©riel. La charge moyenne quotidienne est d'environ 30 %, atteignant jusqu'Ă 70 % lors des pics. Le nombre de requĂȘtes ainsi que le nombre d'utilisateurs ont augmentĂ© d'environ dix fois. Tout cela grĂące Ă un suivi constant de ces groupes de requĂȘtes TOP-MEDIUM. DĂšs qu'une nouvelle requĂȘte apparaĂźt dans le groupe TOP, nous l'analysions immĂ©diatement et essayons d'accĂ©lĂ©rer. Nous examinons le groupe MEDIUM une fois par semaine Ă l'aide de scripts d'analyse des requĂȘtes. Si de nouvelles requĂȘtes apparaissent que nous savons dĂ©jĂ comment optimiser, nous les modifions rapidement. Parfois, nous trouvons de nouvelles mĂ©thodes d'optimisation applicables instantanĂ©ment Ă plusieurs requĂȘtes.
Selon nos prĂ©visions, le serveur actuel pourra supporter une augmentation du nombre d'utilisateurs encore de 3 Ă 5 fois. Cependant, nous avons un autre atout dans notre manche : nous n'avons toujours pas transfĂ©rĂ© les requĂȘtes SELECT sur le miroir, comme cela est recommandĂ©. Mais nous ne le faisons pas consciemment, car nous souhaitons d'abord exploiter pleinement les possibilitĂ©s de "l'optimisation intelligente" avant de recourir Ă "l'artillerie lourde".
Un regard critique sur le travail accompli peut suggĂ©rer l'utilisation du dimensionnement vertical. Acheter un serveur plus puissant au lieu de faire perdre du temps aux spĂ©cialistes. Un serveur peut ne pas coĂ»ter si cher, surtout que nos limites de dimensionnement vertical ne sont pas encore atteintes. Cependant, le nombre de requĂȘtes n'a augmentĂ© que de dix fois. Au cours des derniĂšres annĂ©es, la fonctionnalitĂ© du systĂšme s'est Ă©largie et nous avons maintenant plus de variĂ©tĂ©s de requĂȘtes. La fonctionnalitĂ© existante, grĂące Ă la mise en cache, est exĂ©cutĂ©e avec moins de requĂȘtes et, de plus, des requĂȘtes plus efficaces. Cela signifie que nous pouvons multiplier par cinq de maniĂšre optimiste pour obtenir le vĂ©ritable coefficient d'accĂ©lĂ©ration. Ainsi, selon des calculs trĂšs conservateurs, on peut dire que l'accĂ©lĂ©ration a Ă©tĂ© de 50 fois et plus. Dimensionner verticalement le serveur 50 fois coĂ»terait plus cher. Surtout en tenant compte que l'optimisation rĂ©alisĂ©e une fois fonctionne en permanence, tandis que la facture pour le serveur louĂ© arrive chaque mois.
Source : habr.com
