Aller au contenu
Tous les articles

Filetable· Article 3 sur 3

Requêtes lentes : du symptôme à la cause, sans deviner

Mesurer chaque requête sans ralentir l'application, reconnaître trois formes, et quatre cas réels où le coupable n'était pas celui qu'on croyait.

5 min de lectureRead in English

Quand une page de Filetable est lente, tout pointe vers la base de données. Souvent à tort. Une page lente peut venir d'une requête mal écrite, d'une requête répétée trop souvent, ou d'une base qui n'y est pour rien. La différence ne se voit pas en lisant le code : elle se mesure.

Le contexte : Postgres, et ce qu'il ne dit pas

Filetable stocke tout dans PostgreSQL : les tables des clients, leurs relations, leurs filtres. C'est un choix assumé : une base relationnelle solide, des transactions, des index, et des colonnes JSON quand il faut de la souplesse.

Postgres sait dire quelles requêtes lui coûtent le plus. Mais il ne sait pas quelle page les a lancées, ni combien de temps l'utilisateur a vraiment attendu : entre l'application et la base, il y a un pool de connexions, un réseau, un processeur partagé. Il faut donc mesurer des deux côtés, et relier les deux : chaque requête lente garde la trace de la page qui l'a émise, et chaque page lente renvoie vers ses requêtes, grâce aux traces distribuées d'OpenTelemetry, le standard ouvert de l'observabilité.

Mesurer

La requêteporte le nom de la page qui l'a lancée
Le chronoautour de chaque appel, attente comprise
Un tamponécrit par lots, hors du chemin
Le journalla forme de la requête, jamais ses valeurs

Puis lire : du coût à la cause

  1. 1Le coût par pagetrié par temps total
  2. 2Ses requêtes lentescombien, médiane, maximum
  3. 3La tracechaque appel sur une ligne du temps
Chaque requête est chronométrée autour de l'appel à la base, attente comprise. Les plus lentes sont gardées avec la page qui les a lancées, puis écrites par lots, hors du chemin de l'utilisateur.

Le défi. Mesurer chaque requête sans en ralentir aucune. Trois règles : jamais sur le chemin de l'utilisateur, jamais la valeur des paramètres (ce sont des données clients), et jamais sa propre écriture, sinon la première mesure lente finit par se mesurer elle-même.

Trier par la facture, pas par le record

Le p95, la durée sous laquelle tombent 95 % des appels, désigne la page la plus lente, pas la plus coûteuse. Un export presque jamais appelé peut afficher trente secondes et ne gêner personne ; une lecture de deux secondes, appelée à chaque clic, coûte des heures d'attente par jour. On trie donc par temps total : nombre d'appels × durée moyenne.

Lire la forme, pas les valeurs

Une fois la page trouvée, ses requêtes lentes ont presque toujours l'une de ces trois formes, et chacune pointe vers une cause différente.

Lire la forme, pas les valeurs

beaucoup d'exécutions, toutes pareillesUne requête bon marché, lancée trop souvent : un N+1
peu d'exécutions, une très lenteUne seule requête, chère : regarder son plan
tout autour de la même duréeLa base n'y est pour rien : le pool ou le processeur
Chaque point est une exécution lente captée, sur un axe logarithmique. Schéma : ce qui compte, c'est la forme du nuage, pas la valeur exacte.

La trace confirme

La trace d'une requête HTTP montre chaque appel à la base sur une ligne du temps. On compte plutôt qu'on interprète :

Beaucoup d'appels côte à côte

→ N+1 : une seule requête groupée pour tous les éléments.

Un seul appel, long

→ Requête lourde : lire son plan d'exécution, l'index, les statistiques.

Schéma. À gauche, la même requête par élément, lancée en parallèle, qui fait la queue pour une connexion. À droite, une seule requête qui prend tout le temps.

Quatre cas réels

Aucun ne se voyait en lisant le code. Tous se voyaient en quelques minutes dans les mesures.

  • × 40la même petite requête dans une seule page
    N+1
    Cause
    Une boucle : une requête par élément, toutes lancées d'un coup.
    Correctif
    Une requête groupée. Le nombre d'appels ne dépend plus du nombre d'éléments.
  • > 10 spour supprimer une seule ligne
    cascade
    Cause
    Les suppressions en cascade, invisibles dans le plan, parcouraient des tables entières faute d'index.
    Correctif
    Un index sur chaque clé étrangère, et une requête qui liste celles qui en manquent.
  • × 400plus de temps à compiler la requête qu'à l'exécuter
    planificateur
    Cause
    Le compilateur JIT de Postgres se déclenche sur une estimation trop haute, et ne s'annule pas.
    Correctif
    JIT désactivé : à nos volumes, il ralentissait même les gros calculs.
  • × 4000entre la durée vue par l'app et celle vue par Postgres
    ressources
    Cause
    L'attente d'une connexion libre, et un processeur bridé par sa limite.
    Correctif
    Limiter les requêtes en parallèle, et surveiller le bridage du conteneur, pas le CPU de la machine.
Quatre cas mesurés en développement, valeurs arrondies. Aucun ne se voyait en lisant le code.

Ce que j'en retiens

  • La forme avant la valeur. Le nombre d'appels et leur dispersion disent déjà où chercher.
  • Demander à Postgres ce qu'il en pense. Quand l'application mesure 400 ms et la base 0,1 ms, le problème n'est pas dans la requête.
  • Un commentaire qui défend une boucle n'est pas une preuve. Une requête groupée, mesurée, si.