Modélisez une file d'attente
Le module de paiement de la billetterie traite une commande en 20 ms en moyenne. En régime normal, il en reçoit 10 par seconde ; au pic, 45. À 45 commandes par seconde, il n’est occupé qu’à 90 % : il lui reste de la marge, non ? Pourtant, les clients se plaignent d’attendre.
Les lois opérationnelles ne suffisent plus ici : elles disent combien de travail passe, pas combien de temps on attend. Pour cela, il faut modéliser l’attente elle-même, avec la théorie des files d’attente. Vous allez découvrir le modèle le plus simple et le plus utile, la file M/M/1, et une conclusion qui surprend toujours la première fois.
Décrire une file : la notation de Kendall
Section intitulée « Décrire une file : la notation de Kendall »Une file d’attente se compose d’un ou plusieurs serveurs et d’une zone d’attente. Des clients (requêtes, paquets, tâches) arrivent, attendent si tous les serveurs sont occupés, sont servis, puis repartent. Pour décrire une file sans ambiguïté, on utilise la notation de Kendall :
| Lettre | Signification | Valeurs courantes |
|---|---|---|
| loi des temps entre deux arrivées | M, D, E, H, G | |
| loi des temps de service | M, D, E, H, G | |
| nombre de serveurs | 1, 2, … | |
| capacité du système (attente comprise) | par défaut | |
| taille de la population de clients | par défaut | |
| discipline de service | FCFS par défaut |
Les lettres pour les lois sont : M pour Markov (loi exponentielle, sans mémoire : les arrivées forment un processus de Poisson), D pour déterministe (durée constante), E pour Erlang, H pour hyper-exponentielle et G pour une loi générale quelconque. Les disciplines les plus courantes sont FCFS (premier arrivé, premier servi), LCFS (dernier arrivé, premier servi) et RR (tourniquet, round-robin, où chaque client reçoit à tour de rôle une petite tranche de service).
Quand , et prennent leur valeur par défaut, on les omet : M/M/1 désigne une file à arrivées de Poisson, services exponentiels, un seul serveur, capacité et population infinies, discipline FCFS.
Les variables d’une file
Section intitulée « Les variables d’une file »- : temps entre deux arrivées ; est le taux d’arrivée ;
- : temps de service d’un client ; est le taux de service d’un serveur (et pour serveurs) ;
- et : nombre de clients en attente et en service, ;
- : temps d’attente, : temps de réponse.
L’utilisation d’une file à un serveur est (loi d’utilisation). La file n’est stable que si : sinon, il arrive plus de travail que le serveur ne peut en traiter, et la file grandit sans limite.
La file M/M/1
Section intitulée « La file M/M/1 »Pourquoi l’exponentielle ?
Section intitulée « Pourquoi l’exponentielle ? »La loi exponentielle a une propriété unique : elle est sans mémoire. Si un service dure en moyenne 20 ms et qu’il a déjà duré 50 ms, la durée restante vaut encore 20 ms en moyenne. Cela semble artificiel, mais c’est une bonne approximation quand beaucoup d’utilisateurs indépendants envoient des requêtes (les arrivées forment alors presque un processus de Poisson), et surtout cela rend les calculs possibles. Le nombre de clients dans une file M/M/1 est une chaîne de Markov à temps continu, de type naissance et mort, dont on sait calculer la loi stationnaire. Vous trouverez cette démarche dans le cours de Modèles stochastiques.
Ici, retenons le résultat : en régime stationnaire, la probabilité d’avoir clients dans le système vaut , une loi géométrique.
Les formules, par le raisonnement
Section intitulée « Les formules, par le raisonnement »On peut retrouver les formules principales sans chaîne de Markov, avec un raisonnement que l’on réutilisera au chapitre 5. Un client qui arrive trouve en moyenne clients devant lui (avec des arrivées de Poisson, un arrivant voit l’état moyen de la file). Il doit attendre leur service à tous, puis recevoir le sien. Grâce à l’absence de mémoire, le client en cours de service a encore à faire en moyenne. Donc :
Par la loi de Little appliquée à la file, . En remplaçant : , d’où :
- est le nombre moyen de clients dans la file (en attente et en service) ;
- est le temps de réponse moyen ;
- est le temps de service moyen, et l’utilisation.
On en déduit le temps d’attente .
Pourquoi la latence explose près de la saturation
Section intitulée « Pourquoi la latence explose près de la saturation »La formule est sans doute la plus importante du cours pour un ingénieur. Le facteur vaut 2 à 50 % d’utilisation, 5 à 80 %, 10 à 90 %, 100 à 99 %. La courbe a la forme d’une crosse de hockey :
Mais pourquoi ? Bonne question ! Parce que les arrivées sont aléatoires. Même quand le serveur a en moyenne 10 % de temps libre, les requêtes arrivent parfois en rafale ; la file se remplit, et il faut du temps libre pour la vider. Plus ce temps libre est rare, plus les rafales s’accumulent. Avec des arrivées parfaitement régulières et des services constants (une file D/D/1), il n’y aurait aucune attente jusqu’à 100 %. C’est la variabilité qui crée l’attente.
Et les percentiles ?
Section intitulée « Et les percentiles ? »Dans une file M/M/1 FCFS, le temps de réponse suit lui-même une loi exponentielle de moyenne . Son percentile vaut donc : le p95 vaut environ et le p99 environ . À 40 commandes/s, le paiement a un temps moyen de 100 ms, mais un p95 de 300 ms et un p99 de 460 ms. Si votre objectif porte sur le p99, la saturation arrive encore plus vite.
Au-delà de M/M/1
Section intitulée « Au-delà de M/M/1 »Le centre de délai
Section intitulée « Le centre de délai »Certains « serveurs » n’ont pas de file : chaque client est servi immédiatement, comme s’il y avait une infinité de serveurs. C’est le cas du temps de réflexion des utilisateurs (chacun réfléchit de son côté) ou d’un réseau à très grande capacité. Pour un tel centre de délai, et il n’y a jamais d’attente.
Plusieurs serveurs : M/M/c
Section intitulée « Plusieurs serveurs : M/M/c »Avec serveurs identiques, la file est stable si . Les formules générales sont plus lourdes (formule d’Erlang C), mais le cas reste simple : en notant l’utilisation de chaque serveur, .
Voici une question classique : vaut-il mieux un serveur deux fois plus rapide ou deux serveurs ? À 40 commandes/s, un serveur à 10 ms () donne ms. Deux serveurs à 20 ms () donnent ms. Le serveur rapide gagne : quand la charge est faible, chaque requête profite de toute sa vitesse, alors qu’avec deux serveurs lents, une requête isolée prend toujours 20 ms. En contrepartie, deux serveurs offrent la tolérance aux pannes.
Un réseau ouvert de files
Section intitulée « Un réseau ouvert de files »Revenons à la billetterie entière, vue comme un réseau ouvert (les commandes arrivent de l’extérieur à un taux ). Si l’on approche chaque ressource par une file M/M/1, son utilisation vaut et le temps passé sur la ressource par requête, toutes visites comprises, vaut :
def reseau_ouvert(D, lam): """Temps de réponse d'un réseau ouvert de files M/M/1 (approximation).""" R = 0.0 for nom, d in D.items(): U = lam * d # loi d'utilisation if U >= 1: raise ValueError(f"{nom} saturé (U = {U:.2f})") R += d / (1 - U) # temps de résidence sur la ressource print(f" {nom:<7} U = {U:.2f} R_i = {d / (1 - U) * 1000:6.1f} ms") return R
D = {"web": 0.020, "cpu": 0.030, "disque": 0.060} # demandes du chapitre 2 (s)for lam in (10, 14): print(f"lambda = {lam} req/s") print(f" R total = {reseau_ouvert(D, lam) * 1000:.0f} ms")À 10 commandes/s, on obtient environ 218 ms, dont 150 ms sur le disque. À 14 commandes/s (+40 %), le disque passe à 84 % d’utilisation et le temps de réponse total à environ 455 ms : il fait plus que doubler, presque entièrement à cause du goulot.
En résumé
Section intitulée « En résumé »- La notation de Kendall A/B/c/K/N/D décrit une file : lois d’arrivée et de service (M, D, E, H, G), nombre de serveurs, capacité, population, discipline (FCFS, LCFS, RR).
- Une file à un serveur est stable si .
- M/M/1 : et , obtenus par et la loi de Little.
- La latence explose près de la saturation : ×2 à 50 %, ×5 à 80 %, ×10 à 90 %. La cause est la variabilité des arrivées et des services.
- Dans une M/M/1, le temps de réponse est exponentiel : p95 ≈ , p99 ≈ .
- Un centre de délai n’a pas d’attente () ; un réseau ouvert s’approche par .
Exercices
Section intitulée « Exercices »Exercice · Une file M/M/1
facileUn service de redimensionnement d’images reçoit 30 images par seconde selon un processus de Poisson. Chaque image demande en moyenne 25 ms de traitement (loi exponentielle), sur un seul serveur.
Calculez l’utilisation, le nombre moyen d’images dans le système, le temps de réponse moyen et le temps d’attente moyen.
Voir la solution
- .
- images.
- ms. On vérifie Little : .
- ms : les trois quarts du temps de réponse sont de l’attente.
En code
lam, S = 30, 0.025 # arrivées (images/s), temps de service moyen (s)
U = lam * S # utilisationQ = U / (1 - U) # nombre moyen d'images dans le systèmeR = S / (1 - U) # temps de réponse moyenW = R - S # temps d'attente moyenprint(U, Q, round(R * 1000, 1), round(W * 1000, 1)) # 0.75 3.0 100.0 75.0 (ms)print(lam * R) # 3.0 : Little redonne bien Qlam <- 30; S <- 0.025 # arrivées (images/s), temps de service moyen (s)
U <- lam * S # utilisationQ <- U / (1 - U) # nombre moyen d'images dans le systèmeR <- S / (1 - U) # temps de réponse moyenW <- R - S # temps d'attente moyenc(U = U, Q = Q, R_ms = R * 1000, W_ms = W * 1000) # 0.75 3 100 75lam * R # 3 : Little redonne bien QExercice · Respecter un objectif de latence
moyenUn service d’inférence a un temps de service moyen de 8 ms. On l’approche par une file M/M/1.
- Quel taux d’arrivée maximal permet de garder un temps de réponse moyen sous 40 ms ?
- Même question pour un p99 sous 40 ms.
Voir la solution
- ms donne , soit , donc req/s.
- Le p99 vaut . Il faut ms, soit , donc et req/s. Un objectif sur le p99 aussi serré que le temps de service impose de laisser le serveur presque vide : il faudrait plutôt accélérer le service ou ajouter des serveurs.
En code
import numpy as np
S, cible = 0.008, 0.040 # temps de service et objectif (s)
# 1. Moyenne : S / (1 - U) <= cible => U <= 1 - S / cibleU_moy = 1 - S / cibleprint(U_moy, U_moy / S) # 0.8 100 req/s
# 2. p99 : le temps de réponse est exponentiel, p99 = R * ln(100)R_max = cible / np.log(100) # R à ne pas dépasserU_p99 = 1 - S / R_maxprint(R_max * 1000, U_p99, U_p99 / S) # 8.69 ms 0.079 9.87 req/sS <- 0.008; cible <- 0.040 # temps de service et objectif (s)
# 1. Moyenne : S / (1 - U) <= cible => U <= 1 - S / cibleU_moy <- 1 - S / ciblec(U_moy, U_moy / S) # 0.8 100 req/s
# 2. p99 : le temps de réponse est exponentiel, p99 = R * ln(100)R_max <- cible / log(100) # R à ne pas dépasserU_p99 <- 1 - S / R_maxc(R_max * 1000, U_p99, U_p99 / S) # 8.69 ms 0.079 9.87 req/sExercice · Un goulot dans un réseau ouvert
difficileUn pipeline de traitement de journaux reçoit des lots au taux . Chaque lot a une demande de 50 ms sur le processeur, 80 ms sur le disque et 20 ms sur le réseau.
- Quel est le taux maximal supportable ?
- Calculez le temps de réponse à lots/s puis à lots/s, en approchant chaque ressource par une file M/M/1.
- On remplace le disque par un modèle deux fois plus rapide. Que devient le temps de réponse à 10 lots/s ?
Voir la solution
- Le goulot est le disque ( ms), donc lots/s.
- À : utilisations 0,25, 0,4 et 0,1. ms. À : utilisations 0,5, 0,8 et 0,2. ms.
- Avec ms, à : le disque est à 0,4 et ms. Le temps de réponse est presque divisé par 3, et le processeur (50 %) est désormais le goulot.
En code
def reseau_ouvert(D, lam): """Temps de réponse (s) d'un réseau ouvert de files M/M/1 : somme des D_i / (1 - U_i).""" return sum(d / (1 - lam * d) for d in D)
D = [0.050, 0.080, 0.020] # processeur, disque, réseau (s)print(1 / max(D)) # 12.5 lots/s au maximumfor lam in (5, 10): print(lam, round(reseau_ouvert(D, lam) * 1000, 1)) # 5 -> 222.2 ms, 10 -> 525.0 ms
D_ssd = [0.050, 0.040, 0.020] # disque deux fois plus rapideprint(round(reseau_ouvert(D_ssd, 10) * 1000, 1)) # 191.7 ms# Temps de réponse (s) d'un réseau ouvert de files M/M/1 : somme des D_i / (1 - U_i)reseau_ouvert <- function(D, lam) sum(D / (1 - lam * D))
D <- c(0.050, 0.080, 0.020) # processeur, disque, réseau (s)1 / max(D) # 12.5 lots/s au maximumsapply(c(5, 10), function(lam) reseau_ouvert(D, lam) * 1000) # 222.2 525.0 ms
D_ssd <- c(0.050, 0.040, 0.020) # disque deux fois plus rapidereseau_ouvert(D_ssd, 10) * 1000 # 191.7 ms