La flotte de workers traite des travaux depuis SQS. Le CPU sur les workers fait 40 % en moyenne — confortable. La profondeur de file est à 50 000 travaux et monte. Les SLA commencent à glisser. L’équipe a une politique de suivi de cible sur CPU à 60 %, et elle ne fait rien parce que le CPU n’est pas stressé. Le goulot n’est pas le CPU du tout — c’est l’attente I/O sur l’API en aval que les workers appellent. Le CPU est inactif parce que les workers sont bloqués sur le réseau. Scaler sur CPU ici, c’est scaler sur une approximation qui ne reflète pas la vraie charge. Le bon truc à scaler dessus, c’est la file elle-même : combien de travaux attendent par worker. Voici comment publier cette métrique, alarmer dessus, et choisir une cible qui fait la bonne math.
Ce qui se passe
Le suivi de cible et le scaling par paliers acceptent tous deux n’importe quelle métrique CloudWatch en entrée. Les métriques par défaut — CPUUtilization, NetworkIn, NetworkOut, ASGAverageCPUUtilization, ALBRequestCountPerTarget — sont commodes parce qu’elles ne demandent pas de configuration. Mais elles ne sont la bonne métrique que quand le CPU ou le nombre de requêtes est vraiment le goulot. Pour les workers pilotés par file, les services contraints par la latence p95, les charges limitées par requêtes en vol, ou tout cas où la ressource limitante est autre chose que le CPU, la bonne métrique est une métrique CloudWatch custom.
Le motif standard de scaling sur métrique custom :
- Publier la métrique. Soit en push depuis l’agent CloudWatch dans la charge elle-même, soit via
aws cloudwatch put-metric-dataà intervalle régulier. La métrique doit être une valeur normalisée « par instance » ou « par cible », pas un agrégat global — le suivi de cible fait la math correctement quand la métrique est déjà dans la bonne forme. - Configurer la politique de scaling pour cibler la métrique publiée. Le suivi de cible crée les alarmes ; le scaling par paliers exige que vous créiez les alarmes d’abord.
- Définir ce que veut dire « la bonne ». Pour la profondeur de file, c’est les travaux-par-instance. Pour la latence, c’est le p95 sur lequel vous vous engagez. Pour les requêtes en vol, c’est la limite de concurrence par instance.
Le motif pour les workers pilotés par file, spécifiquement :
- La métrique est
BacklogPerInstance, calculée commeprofondeur_file / nombre_instances_actuel. Publier ça plutôt que la profondeur brute fait que le suivi de cible ne double-corrige pas quand le scale-out atterrit. - La cible est le nombre de travaux que chaque worker peut finir dans une fenêtre de latence acceptable. Si les workers traitent un travail en 5 secondes et que vous voulez que le plus vieux travail dans la file n’ait jamais plus de 2 minutes, la cible de backlog est 24 travaux par instance (120 secondes / 5 secondes par travail).
- L’intervalle de publication est plus court que la période d’alarme. Si vous publiez chaque minute et que l’alarme évalue sur trois périodes de 60 secondes, c’est bien. Publier toutes les 5 minutes contre une alarme d’1 minute crée des trous « métrique manquante » et du scaling imprévisible.
La math qui piège les équipes. Une politique de suivi à cible=24 backlog-par-instance, file à 240, flotte actuelle à 5 :
- Backlog-par-instance actuel = 240 / 5 = 48. Au-dessus de la cible. Scaler.
- AWS estime : pour atteindre cible=24, besoin de 240 / 24 = 10 instances. Ajouter 5.
- Les nouvelles instances démarrent et commencent à consommer la file.
- Cinq minutes plus tard : file à 100 (les workers drainent plus vite), flotte à 10, backlog-par-instance = 10. Sous la cible.
- Le cooldown protège contre un scale-in immédiat, mais éventuellement le backlog-par-instance se stabilise à 24 une fois la file stable — si le taux de publication de travaux dans la file est régulier. Si le taux baisse, la flotte scale à la baisse.
Le piège, c’est quand la publication est en rafale. La profondeur de file pique à 5 000 pendant un envoi marketing, puis retombe à la base en 10 minutes. Le backlog-par-instance atteint 5000/5 = 1000, l’alarme se déclenche, le scale-out ajoute beaucoup d’instances. Le temps qu’elles soient chaudes, la rafale est finie, le backlog tombe, le scale-in se déclenche, les instances terminent. C’est le même motif d’oscillation que dans l’article suivi-de-cible-vs-paliers, et la réponse là-bas s’applique : le suivi de cible est le mauvais outil pour les métriques en rafale ; le scaling par paliers sur une alarme à plusieurs périodes d’évaluation est ce que vous voulez pour les surges courts.
Comment vérifier
Vérifier quelle métrique la politique actuelle utilise :
aws autoscaling describe-policies \
--auto-scaling-group-name my-worker-asg \
--query 'ScalingPolicies[].[PolicyName,PolicyType,TargetTrackingConfiguration.CustomizedMetricSpecification.MetricName]' \
--output table
Si la politique est TargetTrackingScaling et que CustomizedMetricSpecification est null, la politique utilise une métrique prédéfinie (CPU ou nombre de requêtes ALB). Si CustomizedMetricSpecification a un nom de métrique, c’est la métrique custom qui pilote le scaling — lire ses détails :
aws autoscaling describe-policies \
--auto-scaling-group-name my-worker-asg \
--query 'ScalingPolicies[0].TargetTrackingConfiguration' \
--output json
Vérifier que la métrique est publiée avec les dimensions attendues et au rythme attendu :
aws cloudwatch list-metrics \
--namespace MyApp/Workers \
--metric-name BacklogPerInstance \
--output table
aws cloudwatch get-metric-statistics \
--namespace MyApp/Workers \
--metric-name BacklogPerInstance \
--dimensions Name=AutoScalingGroupName,Value=my-worker-asg \
--start-time $(date -u -v-1h +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 60 \
--statistics Average \
--output text
Des trous dans la série temporelle veulent dire que le publieur est non fiable. Le suivi de cible traite les données manquantes comme « considérer comme dépassant le seuil » par défaut, ce qui cause un scale-out sur une panne de métrique — pas ce que vous voulez. Si vous voyez des trous, réparer le publieur avant d’ajuster la politique.
Pour le cas de profondeur de file spécifiquement, regarder la métrique SQS et le BacklogPerInstance dérivé côte à côte :
aws cloudwatch get-metric-statistics \
--namespace AWS/SQS \
--metric-name ApproximateNumberOfMessagesVisible \
--dimensions Name=QueueName,Value=my-app-jobs \
--start-time $(date -u -v-1h +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 60 \
--statistics Average
Les deux devraient suivre — BacklogPerInstance est la métrique SQS divisée par la taille de flotte. Une divergence veut dire que le publieur utilise un compte de flotte périmé, ce qui pointe habituellement vers du cache dans le processus de publication.
La correction
Publier la métrique, le motif Lambda qui est le moins cher et le plus fiable :
- Lambda se déclenche chaque minute sur un planning EventBridge.
- Lambda lit
ApproximateNumberOfMessagesVisibledepuis la file SQS. - Lambda lit
DesiredCapacitydepuis l’ASG (pas InService — DesiredCapacity est la cible, qui est ce contre quoi le suivi de cible devrait normaliser). - Lambda calcule
backlog_per_instance = max(profondeur_file, 1) / max(desired, 1)— le plancher empêche la division par zéro et les valeurs bizarres en scalant depuis zéro. - Lambda publie la métrique avec
aws cloudwatch put-metric-data, dimensionnée parAutoScalingGroupName.
Les permissions IAM pour le Lambda sont minimales : lire SQS, lire ASG, écrire CloudWatch. Le code du Lambda fait vingt lignes. Le coût est une invocation par minute, bien en dessous du palier gratuit.
# Exemple put-metric-data, la ligne que le Lambda exécute
aws cloudwatch put-metric-data \
--namespace MyApp/Workers \
--metric-name BacklogPerInstance \
--value "${BACKLOG_PER_INSTANCE}" \
--dimensions AutoScalingGroupName=my-worker-asg \
--unit Count
Configurer le suivi de cible sur la métrique custom :
aws autoscaling put-scaling-policy \
--auto-scaling-group-name my-worker-asg \
--policy-name backlog-target-tracking \
--policy-type TargetTrackingScaling \
--target-tracking-configuration '{
"TargetValue": 24.0,
"CustomizedMetricSpecification": {
"MetricName": "BacklogPerInstance",
"Namespace": "MyApp/Workers",
"Dimensions": [{"Name": "AutoScalingGroupName", "Value": "my-worker-asg"}],
"Statistic": "Average"
}
}'
La math, explicite. Choisir la valeur cible est la partie que les équipes pensent trop :
- Choisir le SLA sur le plus vieux travail dans la file. « Aucun travail ne reste dans la file plus de N secondes avant qu’un worker le prenne. »
- Mesurer le temps de traitement par travail du worker. Tirer
ApproximateAgeOfOldestMessagedepuis CloudWatch et la métrique de durée du worker. - Calculer cible = SLA / temps_par_travail. SLA=120 s, par_travail=5 s → cible=24 travaux par instance.
- Ajouter une marge pour la variance. Si par-travail est bimodal (5 s pour la plupart, 30 s pour certains), la moyenne trompe. Utiliser le p95 du temps par travail pour la math : cible=120/p95. Sinon la file violera régulièrement le SLA sur les travaux à longue queue.
- Arrondir la valeur cible à quelque chose qu’un humain reconnaîtra dans un an. 24 va bien. 23,7 a l’air d’une dérivation que personne ne se rappelle. La précision est fausse face à la variance réelle de la charge.
Pour les charges où la bonne métrique est la latence plutôt que la profondeur de file :
ALBTargetResponseTimep95 est une métrique intégrée sur les target groups ALB. Le suivi de cible l’accepte nativement comme spécification de métrique prédéfinie, pas de publication custom requise.- La math : choisir un SLA de latence p95, multiplier par un facteur de confiance (1,2 est commun — le suivi de cible tend à dépasser un peu, et vous voulez de la marge), régler ça comme cible.
- Le piège : une flotte qui atteint à peine le p95 sous charge actuelle et juste sous la cible scalera à la baisse si la latence baisse un peu. Vous finissez par osciller autour de la cible.
Pour les charges limitées par les requêtes en vol (contraintes par la concurrence, communes pour l’inférence IA et les services contraints par API externes) :
- Publier
InFlightRequestsPerInstancedepuis la charge — incrémenté au début de requête, décrémenté à la réponse. - Cible = limite_concurrence_par_instance × 0,7. Le 0,7 est la marge ; suivi de cible à 100 % sur une limite dure fait que les requêtes se mettent en file derrière la limite, augmentant la latence.
Trois règles à chaque politique de scaling sur métrique custom :
Publier une métrique par instance, pas un agrégat global. Le suivi de cible fait la bonne math quand la métrique est dans la forme par-instance. Si vous publiez la profondeur de file globale et ciblez « 1000 », la politique doit faire sa propre division par taille-de-flotte, ce qu’elle fait — mais les effets de second ordre (ce qui compte comme « capacité actuelle » au moment où l’alarme se déclenche) sont subtils et sujets au dépassement.
Régler la cible depuis le SLA et la performance réelle du worker. « 60 % CPU » est une tradition. « 24 travaux par instance parce que le SLA est de 120 secondes et que le traitement p95 est de 5 secondes » est une dérivation. La tradition ne survit pas aux changements de charge.
Surveiller les trous de publication de la métrique. Le suivi de cible scalera sur des données manquantes sauf configuration explicite. Un publieur fragile cause plus d’incidents qu’une mauvaise valeur cible.
La boucle publication-et-scaling fait quarante lignes de code Lambda et un seul appel put-scaling-policy. Bien faite, la flotte se dimensionne pour atteindre le SLA sur lequel vous vous êtes engagé, pas le SLA pour lequel la métrique se trouve être une approximation.
Stack Harbor opère les paliers applicatifs sur ASG et les politiques d’auto-scaling à coût plafonné dans le cadre de Gestion AWS et de la couche Auto-scaling. Si vos événements de scaling sont encore des surprises, commencez par une consultation d’infrastructure.