Skip to main content

Dimensionnez un pool de threads applicatif selon le temps que les requêtes passent à travailler plutôt qu'à attendre.

8
80%
20 ms
80 ms
300
Taille de pool recommandée
32
8 × 80% × (1 + 80/20)
Concurrence exigée par votre objectif
30
300 × 100 ms
Débit que ce pool soutient
320/s
32 ÷ 100 ms
Latence de requête supposée
100 ms
20 + 80

Chaque thread qui touche la base retient une connexion pendant qu'il attend : ce pool fixe donc le plancher de votre pool de connexions.

Pourquoi le bon pool de threads est plus petit qu'on ne croit — jusqu'à ce qu'il ne le soit plus

Un pool de threads n'a qu'une mission : garder le CPU occupé sans laisser le travail s'accumuler dans le processus. La taille qui y parvient dépend presque entièrement d'un rapport — le temps qu'une requête passe à attendre autre chose face au temps qu'elle passe réellement à calculer. Cet outil applique la formule standard de ce rapport, la vérifie avec la loi de Little, et vous dit quand le résultat signifie qu'il faut un autre modèle de concurrence plutôt qu'un nombre plus grand.

Comment ça marche

  • Applique la formule classique : cœurs × utilisation visée × (1 + attente ÷ service).
  • Utilise la loi de Little pour déterminer la concurrence qu'implique réellement votre débit cible.
  • Indique le débit que le pool recommandé peut soutenir, pour voir s'il atteint votre objectif.
  • Avertit quand le calcul dépasse ce qu'un modèle un-thread-par-requête peut raisonnablement faire.
threads = coeurs x utilisation x (1 + temps_attente / temps_service)

loi de Little
  concurrence = taux_arrivee x latence

plafond de debit
  max_rps = threads / (temps_service + temps_attente)

Exemple chiffré

Un service sur 8 cœurs, visant 80% de CPU, où une requête calcule 20 ms et attend 80 ms la base, à un pic de 300 requêtes par seconde.

  1. attente ÷ service = 80 ÷ 20 = 4
  2. threads = 8 × 0,8 × (1 + 4) = 32
  3. latence = 20 + 80 = 100 ms
  4. loi de Little : 300 requêtes/s × 0,1 s = 30 requêtes simultanées
  5. 32 threads ≥ 30, le pool atteint donc l'objectif
  6. plafond : 32 ÷ 100 ms = 320 requêtes par seconde

Trente-deux threads sur huit cœurs. Le pool vaut quatre fois le nombre de cœurs non parce que les threads seraient gratuits, mais parce que chacun est inactif 80% du temps — il occupe une place dans la file d'attente de la base, pas un CPU. Mettez l'attente à zéro et la même formule renvoie 6, la bonne réponse pour du travail qui ne bloque jamais.

Lire le résultat

  • La formule ne vaut que par les deux durées qu'on lui donne. Le temps de service est du temps CPU, pas du temps horloge, et on les confond aisément — l'horloge inclut l'attente, c'est-à-dire l'autre variable. Un profileur les sépare, un chronomètre non.
  • Chaque thread qui interroge la base retient une connexion pendant qu'il attend. Le pool de threads devient ainsi le plancher du pool de connexions, et c'est la raison habituelle pour laquelle une limite de connexions est atteinte : personne n'a dimensionné les deux ensemble.
  • Au-delà de quelques centaines de threads, c'est le modèle lui-même qui pose problème. Chaque thread plateforme coûte environ un mégaoctet de pile et une place dans l'ordonnanceur : un pool de 1 300 passe plus de temps à changer de contexte qu'à travailler. À ce rapport, la réponse est l'E/S non bloquante ou les threads virtuels — d'où cet avertissement plutôt qu'un simple chiffre.
  • Une file devant le pool n'est pas de la capacité en plus. Si les arrivées dépassent ce que le pool peut évacuer, la file croît sans borne et la latence avec elle : les requêtes échouent quand même, plus tard et après avoir consommé de la mémoire. Bornez la file et délestez plutôt que de prendre la profondeur pour du débit.
  • Les objectifs d'utilisation au-delà d'environ 80% cessent d'être sûrs. La théorie des files est impitoyable près de la saturation : quand l'utilisation approche 100%, le temps d'attente croît sans limite, et les derniers 20% d'un cœur coûtent bien plus en latence qu'ils ne rapportent en débit.

Questions fréquentes

D'où vient la formule (1 + attente/service) ?
C'est le résultat standard pour garder N cœurs occupés quand une tâche n'occupe un cœur qu'une partie de sa vie, popularisé par Brian Goetz dans Java Concurrency in Practice. Sans attente, elle se réduit à cœurs × utilisation, ce qui est correct pour du travail lié au CPU ; le rapport ne fait que comptabiliser les threads parqués sur des E/S à chaque instant.
Le pool de threads doit-il correspondre au pool de connexions ?
Le pool de connexions doit être au moins aussi grand que le nombre de threads pouvant être simultanément dans un appel base, sans dépasser ce que la limite de la base supporte pour l'ensemble de votre flotte. En cas de conflit, la solution est moins de threads ou plus d'instances — pas un pool de connexions plus grand, qui ne fait que déplacer la contention dans la base.
Les threads virtuels rendent-ils cela obsolète ?
Ils déplacent le chiffre qui compte. Les threads virtuels sont assez peu coûteux pour en avoir un par requête sans dimensionner de pool : le rapport d'attente cesse alors de vous contraindre. Reste la limite en aval — le pool de connexions, la limite de débit d'une API tierce, le CPU lui-même. Il faut toujours les dimensionner, et ce calcul indique toujours où elles mordent.
Comment mesurer le temps de service et le temps d'attente ?
Le temps de service par profilage CPU, ou par la différence entre le temps horloge et la somme de vos appels externes instrumentés. Le temps d'attente par les spans que votre traçage émet déjà pour les appels base et HTTP. À défaut des deux, partez d'une estimation raisonnée et vérifiez la prédiction : si le pool soutient bien moins de débit que la formule ne l'annonce, votre estimation d'attente était trop basse.