Read in English · Leer en español · Auf Deutsch lesen
Section 9.3 sur 9.8 · Intermédiaire · 29 min de lecture
Asynchronisme, files d’attente et back pressure
Leçon vidéo
5 min · narré en anglais, avec sous-titresÉcouter seulement (audio)
Lire la transcription
Bienvenue dans la section neuf point trois. La charge n’arrive jamais de façon régulière. Cette leçon porte sur ce qu’un système doit faire quand le travail arrive plus vite qu’il ne peut être accompli.
Pensez à une boulangerie très fréquentée, à l’heure de l’ouverture. Elle distribue des tickets numérotés, si bien que les clients attendent dans une file ordonnée et que les boulangers travaillent à un rythme régulier. C’est une file d’attente. Mais une file ne peut pas s’allonger indéfiniment avant que le pain vienne à manquer, alors une bonne boulangerie cesse de distribuer des tickets quand la file est trop longue. Elle sert d’abord les précommandes, et limite chaque client à un nombre raisonnable de pains. Et elle dit honnêtement aux retardataires de revenir dans dix minutes, plutôt que de les laisser attendre pour rien.
Ici, l’API de commandes place du travail dans une file, et plusieurs workers identiques l’en retirent. Chaque message ne va qu’à un seul worker. On les appelle des consommateurs concurrents. Quand les arrivées dépassent brusquement ce que les consommateurs peuvent traiter, le travail excédentaire attend dans la file sous forme d’arriéré. Les workers continuent à leur rythme régulier, et résorbent l’arriéré après le pic. Les consommateurs peuvent donc être dimensionnés pour la moyenne, et non pour le pic. Le prix à payer, c’est le délai. Un arriéré n’est rien d’autre qu’une attente que subira chaque élément placé derrière lui. Acceptable pour un e-mail, inacceptable pour un paiement que le client surveille.
Si un worker plante avant d’avoir accusé réception d’un message, un autre worker le reçoit. La livraison se fait donc au moins une fois, et les consommateurs doivent être idempotents : traiter deux fois un message doit être sans danger. Une file absorbe un excès temporaire, jamais un excès permanent. Une file non bornée grossit jusqu’à épuiser la mémoire, ou conserve du travail pendant des heures alors que plus personne ne s’en soucie. Donnez donc une borne à chaque file. La loi de Little transforme l’attente que vous jugez acceptable en une longueur maximale. Et mesurez l’âge du plus ancien message en plus de la longueur. L’âge, c’est ce que vivent les utilisateurs.
Dimensionnons une file. Des billets de concert sont mis en vente à dix heures. Quarante workers mettent chacun deux cent cinquante millisecondes par commande, ils traitent donc cent soixante commandes par seconde. La prévision est de six cents par seconde pendant deux minutes. Cela laisse cinquante-deux mille huit cents commandes en attente, et la dernière attend cinq minutes et demie. La promesse était une réponse en moins de deux minutes. Pour tenir cette promesse, la loi de Little indique que la file peut contenir au plus cent soixante fois cent vingt : dix-neuf mille deux cents commandes. Plus de workers ? Le prestataire de paiement n’autorise que trois cents requêtes par seconde, donc soixante-quinze workers au plus. L’attente devient exactement deux minutes. Une promesse tenue de justesse, sans aucune marge. Mais il y a vingt-cinq mille billets, à deux et demi par commande : seules dix mille commandes peuvent aboutir. Mettre le reste en file ne fait que retarder l’inévitable « épuisé ». L’équipe utilise donc soixante-quinze workers, une salle d’attente et une file bornée à douze mille : le stock plus vingt pour cent pour les paiements échoués. Ensuite, les nouvelles commandes reçoivent immédiatement la réponse « épuisé ».
Le back pressure, c’est tout moyen par lequel une étape plus lente fait ralentir une étape plus rapide. Ici, le producteur envoie cinq cents éléments par seconde, le consommateur en traite trois cents, et la file bornée est pleine. Vous pouvez bloquer le producteur, pour qu’il attende de la place. Cela convient aux pipelines et aux traitements par lots. Vous pouvez rejeter le nouveau travail en échouant rapidement avec une erreur quatre cent vingt-neuf ou cinq cent trois, pour que le client puisse réessayer plus tard. Ou vous pouvez abandonner des éléments, comme les plus anciens ou les moins prioritaires. Chaque option est la bonne quelque part. L’erreur, c’est de ne pas choisir.
Un serveur qui accepte tout en situation de surcharge consacre toute sa capacité à des requêtes dont les clients ont déjà abandonné. Le goodput, c’est-à-dire les requêtes traitées à temps pour être utiles, s’effondre vers zéro. Le délestage de charge rejette délibérément certaines requêtes. Il doit être précoce et peu coûteux, avant tout travail coûteux. Il doit être priorisé. Dans la plateforme de livraison de repas, les validations de commande et les positions des livreurs passent avant les recommandations, qui passent avant les événements analytiques. Et il doit tenir compte du temps : une requête qui a attendu plus longtemps que le délai d’expiration de son appelant doit être abandonnée, pas traitée.
La limitation de débit partage équitablement la capacité entre les clients. L’algorithme habituel est le token bucket. Ce seau contient au plus vingt jetons, et se recharge de cinq jetons par seconde. Chaque requête, comptée par clé d’API, doit prendre un jeton. S’il reste un jeton, la requête est transmise. Sinon, la réponse est quatre cent vingt-neuf, Too Many Requests, idéalement avec un en-tête Retry-After. Un client resté calme peut donc envoyer une rafale : sur trente requêtes envoyées d’un coup, vingt sont transmises et dix rejetées. Mais sur soixante secondes quelconques, il peut envoyer au plus vingt plus cinq fois soixante : trois cent vingt requêtes.
La section complète compare les algorithmes de limitation de débit et traite des files de lettres mortes. Ensuite, dans la section neuf point quatre, nous verrons les patrons qui empêchent une défaillance de se propager. À très vite.
Ce qu’il faut savoir
- Une file d’attente entre producteur et consommateur transforme un pic en un arriéré que les consommateurs résorbent à leur propre rythme, ce qui permet de les dimensionner pour la moyenne ; le prix à payer est un délai pour chaque élément placé derrière l’arriéré.
- Les consommateurs concurrents font passer le travail d’arrière-plan à l’échelle par l’ajout de workers ; la livraison se fait au moins une fois, donc les consommateurs doivent être idempotents, et les délais de visibilité et le préchargement doivent correspondre aux temps de traitement.
- Une file ne peut pas absorber un excès durable ; bornez chaque file à l’aide de la loi de Little (longueur maximale = débit de traitement × attente acceptable) et surveillez l’âge du plus ancien message en plus de la longueur.
- Le back pressure fait ralentir les étapes plus rapides ; quand une file bornée est pleine, le système doit bloquer le producteur, rejeter le nouveau travail ou abandonner une partie du travail, et il devrait choisir délibérément.
- Le délestage de charge rejette une partie du travail tôt et à moindre coût, par priorité et par ancienneté, afin que le goodput (débit utile) reste élevé en cas de surcharge.
- La limitation de débit répartit la capacité par client : le token bucket autorise des rafales jusqu’à sa capacité, avec un rythme de recharge régulier, le leaky bucket lisse la sortie, et les fenêtres fixes permettent des doubles rafales aux frontières.
- Répondez aux dépassements de limite par un 429 (ou un 503 quand c’est l’ensemble du serveur qui est surchargé) accompagné de Retry-After, et envoyez les messages qui échouent de façon répétée dans une file de lettres mortes surveillée et dotée d’un responsable.
Les files d’attente transforment les pics en arriérés
La charge n’arrive jamais de façon régulière. Les commandes du déjeuner se concentrent à 12:30, les acheteurs de billets appuient tous sur le bouton à 10:00:00, et un traitement par lots déverse un million d’événements à minuit. Un service synchrone doit soit disposer de la capacité nécessaire pour le pic le plus élevé, soit défaillir pendant ce pic. Placer une file d’attente entre le producteur du travail et le service qui l’effectue change le calcul : pendant un pic, le travail excédentaire attend dans la file, et les consommateurs le traitent à leur propre rythme régulier, en rattrapant leur retard une fois le pic passé. Le catalogue de patrons cloud de Microsoft appelle cela le nivellement de charge par file d’attente (queue-based load levelling). La section 2.4 a montré pourquoi des files se forment chaque fois que les arrivées se concentrent ; cette section porte sur leur utilisation délibérée.
L’avantage est réel : les consommateurs peuvent être dimensionnés pour la moyenne plutôt que pour le pic, un consommateur lent ou en cours de redémarrage ne perd pas de travail, et le producteur n’est plus couplé temporellement au consommateur (section 6.5). Le prix à payer est que le travail est désormais effectué plus tard. Une file transforme un pic en arriéré, et un arriéré n’est rien d’autre qu’un délai que subira chaque élément placé derrière lui. C’est acceptable pour envoyer des e-mails de confirmation ou mettre à jour un index de recherche, discutable pour affecter un livreur à une commande, et inacceptable pour un paiement dont le client attend la confirmation. La première question de conception pour toute file est donc : combien de temps un élément peut-il attendre, et qu’advient-il des éléments qui attendraient plus longtemps ?
Une file d’attente qui absorbe un pic
Files de travail et consommateurs concurrents
L’usage le plus courant est une file de travail avec des consommateurs concurrents (competing consumers) : plusieurs workers identiques lisent une même file, chaque message ne va qu’à l’un d’eux, et le débit augmente quand on ajoute des workers. C’est l’axe X du cube de scalabilité appliqué au travail d’arrière-plan, et il passe naturellement à l’échelle sur le bon signal : l’arriéré par worker, ou l’âge du plus ancien message.
Les mécanismes comptent pour la justesse. Un worker prend un message, le traite, puis en accuse réception ; si le worker plante avant, le courtier rend le message de nouveau visible après un délai (SQS l’appelle le délai de visibilité, ou visibility timeout) et un autre worker le traite. La livraison se fait donc au moins une fois, et les consommateurs doivent être idempotents (section 6.4). Un délai de visibilité plus court que le temps de traitement réel fait traiter le même message deux fois en parallèle. L’ordre est perdu dès qu’il y a plus d’un consommateur, sauf si les messages sont partitionnés par clé, auquel cas le nombre de partitions plafonne le nombre de consommateurs utiles. Enfin, le préchargement (prefetch : un worker qui prend plusieurs messages à la fois pour économiser des allers-retours) doit rester faible pour les tâches lentes, sinon un worker accapare des messages que des workers inactifs auraient pu traiter.
Une file d’attente ne crée pas de capacité
Une file absorbe un excès temporaire ; elle ne peut pas absorber un excès permanent. Si le travail arrive plus vite qu’il n’est accompli pendant assez longtemps, l’arriéré croît sans limite, et la loi de Little (L = λ × W) indique que le temps d’attente croît avec lui. Une file en mémoire non bornée finit par épuiser la mémoire et faire planter le processus, en perdant tout ce qu’elle contient ; une file durable non bornée remplit son disque ou conserve du travail pendant des heures alors que plus personne ne s’en soucie. Pire, la file masque le problème : les producteurs reçoivent des accusés de réception rapides, les taux d’erreur restent à zéro, et le seul symptôme est que les résultats arrivent de plus en plus tard. Des ingénieurs d’Amazon décrivent de tels arriérés comme potentiellement insurmontables, car un service qui prend assez de retard peut consacrer toute sa capacité à traiter du travail que ses clients ont abandonné depuis longtemps.
Deux pratiques en découlent. D’abord, mesurez l’âge du plus ancien message en plus de la longueur de la file : l’âge est ce que vivent les utilisateurs, et il réagit immédiatement quand les consommateurs se bloquent. Ensuite, donnez à chaque file une borne déduite du temps d’attente que vous jugez acceptable, à l’aide de la loi de Little, et décidez à l’avance de ce qui se passe quand la borne est atteinte. Cette décision fait l’objet des trois parties suivantes de cette section.
Exemple commenté
Exemple commenté : dimensionner la file des commandes pour une ouverture de billetterie
Les billets d’un concert dans un stade sont mis en vente à 10:00. Les commandes soumises sont placées dans une file ; des workers autorisent le paiement et attribuent les places, en 250 ms chacun. La prévision est de 600 commandes soumises par seconde pendant les deux premières minutes. La promesse faite aux clients est une réponse en moins de deux minutes. Il y a 25 000 billets et la commande moyenne porte sur 2,5 billets.
| Capacité des workers avec 40 workers | 40 ÷ 0,25 s = 160 commandes par seconde |
|---|---|
| Arriéré après le pic de deux minutes | (600 − 160) × 120 s = 52 800 commandes en attente |
| Attente de la dernière commande de la file (loi de Little, W = L ÷ λ) | 52 800 ÷ 160 = 330 s, soit cinq minutes et demie : la promesse n’est pas tenue |
| File la plus longue qui permet de tenir la promesse | L = λ × W = 160 × 120 = 19 200 commandes |
| Plus de workers ? | Le contrat du prestataire de paiement autorise 300 requêtes par seconde, donc 75 workers au plus (300 commandes par seconde). L’arriéré devient (600 − 300) × 120 = 36 000 et l’attente 36 000 ÷ 300 = 120 s : la promesse est tenue de justesse, sans aucune marge |
| Ce que permet le stock | 25 000 ÷ 2,5 = 10 000 commandes peuvent aboutir. À 600 par seconde, ce nombre de commandes arrive en moins de 17 secondes ; mettre en file les quelque 62 000 autres commandes ne fait que retarder l’inévitable « épuisé » |
| Décision | 75 workers ; une salle d’attente placée devant la page de commande admet 300 acheteurs par seconde ; la file des commandes est bornée à 12 000 (le stock plus 20 % pour les paiements échoués) ; dès que les réservations couvrent le stock, les nouvelles soumissions reçoivent immédiatement la réponse « épuisé, inscrivez-vous sur la liste d’attente » au lieu de rejoindre la file |
La loi de Little transforme une promesse sur le temps d’attente en une longueur maximale de file. Toute file autorisée à croître au-delà est une promesse non tenue qui n’a pas encore été annoncée.
C’est la limite de débit en aval, et non le nombre de workers que la plateforme pouvait démarrer, qui a fixé la capacité réelle : l’ajout de consommateurs se heurtait donc à un plafond strict.
La meilleure borne pour une file est souvent une limite métier : il ne sert à rien de conserver du travail qui ne peut en aucun cas aboutir.
Back pressure : faire ralentir les producteurs
Le back pressure (contre-pression) désigne tout mécanisme par lequel une étape aval plus lente fait ralentir une étape amont plus rapide, au lieu de laisser le travail s’accumuler entre elles. Le terme vient des fluides dans les canalisations, et l’idée a déjà été rencontrée avec les tubes et filtres (section 5.4). Il est intégré à de nombreuses couches de la pile que vous utilisez déjà. Le contrôle de flux de TCP arrête un émetteur quand le tampon du récepteur est plein. Une file bloquante bornée fait attendre le put du producteur jusqu’à ce qu’il y ait de la place. Les consommateurs en mode pull, comme dans Kafka, ne prennent du travail que lorsqu’ils sont prêts, si bien que c’est le journal du courtier, et non la mémoire du consommateur, qui contient l’arriéré, et un producteur Kafka se bloque ou échoue quand son propre tampon d’envoi est plein. La spécification Reactive Streams permet à un abonné de demander un nombre précis d’éléments, de sorte qu’un flux ne livre jamais plus que ce que l’abonné a demandé.
Le back pressure fonctionne bien à l’intérieur d’un processus ou le long d’un pipeline. À travers une frontière requête-réponse avec des utilisateurs humains, il se traduit généralement par un rejet : un service HTTP surchargé ne peut pas faire patienter poliment un téléphone, il répond donc 429 Too Many Requests ou 503 Service Unavailable, idéalement avec un en-tête Retry-After, et compte sur le client pour ralentir. Quand une file bornée est pleine, il n’y a que trois choix : bloquer le producteur, rejeter le nouveau travail, ou abandonner quelque chose de déjà accepté. Chacun est le bon quelque part ; l’erreur est de ne pas choisir.
Trois réponses à une file pleine
Choisir ce qui se passe quand une file est pleine
| Stratégie | Comportement | Convient à | Risque |
|---|---|---|---|
| Bloquer le producteur | Le producteur attend qu’il y ait de la place | Pipelines et traitements par lots où le producteur peut faire une pause | Les producteurs bloqués monopolisent threads et connexions ; entre services, le ralentissement se propage vers l’amont |
| Rejeter le nouveau travail | Échouer rapidement avec une erreur comme 429 ou 503 | Requêtes interactives ; le client peut réessayer, attendre ou afficher un message | Les clients qui réessaient immédiatement transforment le rejet en charge supplémentaire (section 9.4) |
| Abandonner le plus ancien | Écarter l’élément qui attend depuis le plus longtemps | Télémétrie, mises à jour de position, prix en direct : les données les plus récentes sont les plus précieuses | Perte silencieuse ; elle doit être comptée et visible |
| Abandonner par priorité | Écarter d’abord les éléments les moins importants | Charges mixtes : garder les validations de commande, abandonner les événements analytiques | Les basses priorités risquent la famine ; les priorités doivent être attribuées honnêtement |
| Déverser vers un stockage durable | Débordement sur disque ou dans une file durable | Travail qui doit finir par être fait mais peut attendre des heures | Ce n’est qu’une file plus grande : il lui faut toujours une borne et une estimation du temps de résorption |
Délestage de charge et priorisation
Un serveur soumis à plus de charge qu’il ne peut en traiter fait pire que servir sa capacité et refuser le reste. Il accepte tout, chaque requête attend dans ses files internes, les temps de réponse dépassent les délais d’expiration des clients, et il finit par consacrer toute sa capacité à des requêtes dont les clients ont déjà abandonné. Le débit reste élevé tandis que le goodput (débit utile), le rythme des requêtes traitées à temps pour être utiles, s’effondre vers zéro. Le délestage de charge (load shedding) l’empêche en rejetant délibérément certaines requêtes pour que les autres réussissent.
Un bon délestage a trois propriétés. Il est précoce et peu coûteux : rejeter une requête doit coûter bien moins cher que la servir, il a donc lieu à l’entrée, avant tout travail lourd d’authentification ou de base de données, à partir d’un signal peu coûteux comme le nombre de requêtes en cours ou l’âge de la file. Il est priorisé : le livre SRE de Google décrit l’étiquetage de chaque requête avec une criticité (de CRITICAL_PLUS à SHEDDABLE), afin que le trafic de traitement par lots et de préchargement soit abandonné avant le trafic destiné aux utilisateurs. Dans la plateforme de livraison de repas, les validations de commande et les mises à jour de position des livreurs passent avant les recommandations, qui passent elles-mêmes avant les événements analytiques. Enfin, il est conscient du temps : une requête qui a déjà attendu plus longtemps que le délai d’expiration de son appelant doit être abandonnée plutôt que traitée. Les ingénieurs de Facebook ont décrit deux raffinements : une file à délai contrôlé qui n’autorise qu’une courte attente quand une file ne s’est pas vidée récemment, et le passage au mode dernier entré, premier sorti sous charge, afin que les requêtes les plus récentes, dont les utilisateurs attendent encore, soient servies en premier.
Une étape supplémentaire consiste à adapter la limite elle-même. Des limites de concurrence fixes sont soit trop basses un bon jour, soit trop hautes un mauvais jour ; les limiteurs de concurrence adaptatifs augmentent le nombre autorisé de requêtes en cours tant que la latence reste basse et le réduisent quand elle augmente, en empruntant l’idée au contrôle de congestion de TCP. Tout à l’entrée d’un système, le contrôle d’admission peut prendre la forme d’une salle d’attente virtuelle, comme pour l’ouverture de billetterie ci-dessus : les utilisateurs sont retenus dans une file à la périphérie et admis au rythme que le cœur du système peut servir, ce qui revient à du délestage de charge présenté comme de l’équité.
Algorithmes de limitation de débit
La limitation de débit (rate limiting) restreint la quantité de travail qu’un client, une clé d’API, un locataire (tenant) ou une adresse IP donnés peuvent soumettre sur une période. Son objectif diffère de celui du délestage de charge : le délestage protège le serveur contre une surcharge totale, quelle qu’en soit la cause, tandis que la limitation de débit partage équitablement la capacité et protège contre tout client isolé, qu’il soit bogué ou malveillant. Les deux répondent de la même façon, généralement par un HTTP 429. Quatre algorithmes dominent.
Comparaison des algorithmes de limitation de débit
| Algorithme | Comment ça marche | Rafales | État par clé | Remarques |
|---|---|---|---|---|
| Token bucket (seau à jetons) | Des jetons sont ajoutés au rythme r jusqu’à la capacité b ; chaque requête en dépense un | Autorise des rafales jusqu’à b, puis r en moyenne | Deux nombres : les jetons et l’heure de la dernière recharge | Le plus utilisé ; la rafale et le débit soutenu se règlent séparément |
| Leaky bucket (seau percé) | Les requêtes entrent dans un seau qui se vide à un rythme constant ; quand il est plein, les nouvelles requêtes sont rejetées | La sortie est parfaitement lisse ; les rafales attendent ou sont refusées | Une file, ou un compteur et un horodatage | Utilisé pour la mise en forme du trafic (traffic shaping) ; la limitation des requêtes de NGINX repose sur lui |
| Compteur à fenêtre fixe | Compter les requêtes par fenêtre calendaire (par minute) ; remise à zéro à la frontière | Jusqu’à deux fois la limite de part et d’autre d’une frontière | Un compteur | Le plus simple ; 100 requêtes à 12:00:59 et 100 à 12:01:00 passent toutes |
| Journal à fenêtre glissante | Conserver l’horodatage de chaque requête de la dernière fenêtre ; les compter | Exact | Une entrée par requête | Précis mais gourmand en mémoire à haut débit |
| Compteur à fenêtre glissante | Pondérer le décompte de la fenêtre précédente par la part de celle-ci qui chevauche encore la fenêtre courante, et ajouter le décompte courant | Approximation fidèle | Deux compteurs | À 30 s d’une minute, avec 80 la minute précédente et 40 maintenant : 80 × 0,5 + 40 = 80 |
Le token bucket (seau à jetons) mérite qu’on s’y attarde, car c’est le choix par défaut habituel. Le seau contient au plus b jetons et en gagne r par seconde. Chaque requête prend un jeton ; s’il n’en reste aucun, elle est rejetée (ou, dans la variante de mise en forme, retardée). Un client resté silencieux peut donc envoyer d’un coup une rafale de b requêtes, ce qui convient aux clients réels, comme une application mobile qui ouvre un écran, alors que sur toute période plus longue il ne peut pas dépasser r en moyenne. Le leaky bucket (seau percé) en est l’image inversée : il lisse la sortie à un rythme constant, ce qui est utile pour protéger un système aval qui ne peut accepter aucune rafale.
Le token bucket
Exemple commenté
Exemple commenté : un token bucket pour une API partenaire
La clé d’API d’un restaurant partenaire est limitée par un token bucket de capacité b = 20 et de rythme de recharge r = 5 jetons par seconde (300 par minute en moyenne). Le seau est plein à t = 0.
| t = 0 : une rafale de 30 requêtes | Les 20 premières prennent les 20 jetons et sont transmises ; 10 sont rejetées avec 429 |
|---|---|
| t = 0 à 1 s | 5 nouveaux jetons arrivent (5 par seconde) |
| t = 1 à 10 s : 8 requêtes par seconde en continu | Environ 5 par seconde sont acceptées et 3 rejetées ; le seau reste presque vide |
| t = 10 à 14 s : aucune requête | Le seau se recharge à 5 par seconde et est de nouveau plein (20) à t = 14 |
| Borne supérieure sur toute période T | Au plus b + r × T requêtes : sur 60 secondes quelconques, 20 + 5 × 60 = 320, donc la moyenne à long terme ne peut jamais dépasser 5 par seconde |
| Retry-After pour une requête rejetée | Le jeton suivant arrive dans 1 ÷ r = 0,2 s, mais l’en-tête compte en secondes entières, donc la réponse indique Retry-After: 1 |
La capacité fixe la rafale qu’un client respectueux des règles peut envoyer après une période calme ; le rythme de recharge fixe le débit soutenu. Choisissez-les séparément.
Un client qui respecte Retry-After, avec un peu de jitter, se stabilise au rythme de recharge ; un client qui réessaie immédiatement ne fait que collectionner davantage de 429.
import math, time
class TokenBucket:
def __init__(self, rate, capacity):
self.rate = rate # tokens added per second
self.capacity = capacity # largest burst allowed
self.tokens = capacity # start full
self.updated = time.monotonic()
def allow(self, cost=1):
now = time.monotonic()
elapsed = now - self.updated
self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
self.updated = now
if self.tokens >= cost:
self.tokens -= cost
return True, 0
wait = (cost - self.tokens) / self.rate # seconds until enough tokens
return False, max(1, math.ceil(wait)) # for the Retry-After header
bucket = TokenBucket(rate=5, capacity=20) # one bucket per API key
allowed, retry_after = bucket.allow()HTTP 429, Retry-After et où appliquer les limites
HTTP dispose de signaux standard pour les deux situations. 429 Too Many Requests, défini dans la RFC 6585 (2012), indique à un client qu’il a envoyé trop de requêtes en un temps donné. 503 Service Unavailable indique que le serveur dans son ensemble est temporairement surchargé ou en maintenance. L’un comme l’autre peut porter un en-tête Retry-After (RFC 9110), qui donne un nombre de secondes ou une date après laquelle il est judicieux de réessayer. De nombreuses API renvoient aussi des en-têtes qui annoncent la limite, le quota restant et l’heure de remise à zéro, traditionnellement X-RateLimit-* ; un projet de l’IETF est en train de normaliser les champs RateLimit et RateLimit-Policy. Les clients bien conçus respectent Retry-After, ajoutent du jitter pour que les clients rejetés ne reviennent pas tous au même instant, et voient dans des 429 répétés une raison de ralentir plutôt que de réessayer plus fort (section 9.4).
Les limites peuvent être appliquées à plusieurs couches, et les systèmes matures en utilisent plus d’une : à la périphérie ou dans l’API gateway, par adresse IP et par clé d’API, pour arrêter les abus et les clients incontrôlés avant qu’ils ne coûtent quoi que ce soit ; par locataire, pour qu’un gros client ne puisse pas affamer les autres ; et à l’intérieur de chaque service, pour protéger sa propre capacité et ses dépendances. Une limite globale sur de nombreuses instances de gateway nécessite un état partagé, généralement des compteurs atomiques dans Redis, ce qui ajoute un appel réseau par requête ; un compromis courant consiste à donner à chacune des N instances une limite locale d’environ 1 ÷ N du total et à accepter une certaine imprécision. Les limites doivent être fixées à partir de la capacité mesurée et publiées, pour que les partenaires puissent en tenir compte dans leur conception, et le nombre de rejets doit être surveillé, car une hausse soudaine signale soit une attaque, soit un bug chez un client, soit une limite qui ne correspond plus à la réalité.
Files de lettres mortes
Certains messages échouent à chaque fois : un contenu mal formé, une commande pour un restaurant qui n’existe plus, un bug déclenché par une entrée particulière. Réessayé indéfiniment, un tel message empoisonné bloque une partition ou gaspille un worker pour toujours. Une file de lettres mortes (dead-letter queue, présentée à la section 6.4) reçoit les messages qui ont échoué un nombre défini de fois (SQS appelle cela le nombre maximal de réceptions d’une politique de redrive), pour que le reste du travail puisse continuer.
Une file de lettres mortes est un outil pour les défauts, pas pour la surcharge : si des messages y atterrissent parce que les consommateurs sont trop lents, la solution est la capacité ou le back pressure, pas une seconde file. Chaque message dans une DLQ signifie que quelque chose que le métier attendait ne s’est pas produit ; la DLQ a donc besoin d’un responsable, d’une alerte sur sa profondeur ou son âge, d’outils pour inspecter les messages avec leur erreur et leur nombre de tentatives, et d’un moyen de les rejouer une fois la cause corrigée. Décidez à l’avance de ce que signifie un rejeu tardif pour l’ordre des messages et pour les messages dont le moment métier est passé : un remboursement peut être rejoué avec un jour de retard, une affectation de livreur ne le peut pas.
Vérifiez votre compréhension
Des workers traitent 200 messages par seconde et les messages peuvent attendre au plus 30 secondes. Quelle est la plus grande longueur de file acceptable ?
D’après la loi de Little, 200 × 30 = 6000 messages ; une file plus longue signifie que certains messages attendront plus de 30 secondes.
Pourquoi l’âge du plus ancien message est-il une meilleure alarme que la longueur de la file ?
L’âge mesure le délai que subissent les utilisateurs et augmente immédiatement si les consommateurs se bloquent, alors qu’une longueur qui paraît normale peut masquer une file bloquée ou une résorption lente.
Un token bucket a une capacité de 50 et se recharge de 10 jetons par seconde. Combien de requêtes un client peut-il faire dans la première seconde après une longue période calme ?
Environ 60 : les 50 jetons déjà présents dans le seau plus les 10 ajoutés pendant cette seconde.
Quelle est la différence entre le délestage de charge et la limitation de débit ?
Le délestage de charge protège le serveur contre une surcharge totale, quelle qu’en soit la cause ; la limitation de débit plafonne ce que chaque client peut envoyer, pour que la capacité soit partagée équitablement.
Sources et lectures complémentaires
Une question sur cette section ? ArchitectBot, l’assistant d’étude IA du cours, répond d’abord à partir du cours.
Demander : « Notre file d’ouverture de billetterie a atteint 200 000 commandes et les clients ont attendu 40 minutes pour obtenir un résultat. Comment aurions-nous dû borner la file, et que devrait-il advenir des commandes excédentaires ? »