درس 4 — Construire

Réessais et lettres mortes

7 دقائق قراءة

هذا المحتوى غير مترجم بعد. تُعرض النسخة الأصلية.

Un mardi à 9 h, le quota Vertex AI partagé par trois équipes sature pendant quatre minutes. Avec la politique de réessai par défaut d'un abonnement push, Pub/Sub relivre immédiatement, le worker reçoit à nouveau un 429, et la boucle consomme les instances Cloud Run sans rien produire. Le même jour, un message mal formé publié par un script de test tourne en rond toute la journée. Il faut deux choses distinctes : des réessais qui laissent au système le temps de récupérer, et une sortie pour les messages qui ne passeront jamais.

Le mécanisme

Le projet empile trois niveaux, du plus rapide au plus lent :

NiveauOùRéglageAbsorbe
1. Dans la requêteclient google-genai (HttpRetryOptions)3 tentatives, 2 s puis 4 sun 429 ou un 503 de quelques secondes
2. Par Pub/Subabonnement pushdélai exponentiel de 10 s à 60 s, 10 livraisonsune indisponibilité de quelques minutes, un conflit BigQuery
3. Lettres mortessujet lab-pipeline-evt-dlq + abonnement de lecture (7 jours)après la 10e livraisonce qui ne passera jamais sans intervention
stateDiagram-v2
  [*] --> Livraison
  Livraison --> Acquitté: 200
  Livraison --> Attente: 400 / 500 / délai dépassé
  Attente --> Livraison: 10 s, 20 s, 40 s, 60 s, 60 s…
  Attente --> LettresMortes: 10e échec
  LettresMortes --> Rejeu: correction, puis republication
  Acquitté --> [*]

Le niveau 1 est court à dessein : chaque seconde d'attente dans la requête occupe une des 4 places de l'instance. Au-delà, mieux vaut rendre la main à Pub/Sub, qui attend sans rien consommer.

Pour router vers les lettres mortes, Pub/Sub agit sous son agent de service (service-<numéro>@gcp-sa-pubsub.iam.gserviceaccount.com). Il lui faut deux droits, faute de quoi le message n'est jamais déplacé et continue à être relivré, sans erreur visible :

  • roles/pubsub.publisher sur le sujet de lettres mortes ;
  • roles/pubsub.subscriber sur l'abonnement d'origine, pour acquitter le message qu'il déplace.

Un message déplacé reçoit un nouvel ID ; il garde ses attributs et en gagne quatre : abonnement et projet d'origine, nombre de livraisons, heure de publication d'origine.

La mise en œuvre

files/infra/cloud-up.sh (extrait)
PUSH_OPTS=(--push-endpoint="$URL/" --push-auth-service-account="$PUSH_SA" --ack-deadline=120
  --dead-letter-topic="$P-dlq" --max-delivery-attempts=10 --min-retry-delay=10s --max-retry-delay=60s)
if $G pubsub subscriptions describe "$P-push" >/dev/null 2>&1; then
  $G pubsub subscriptions update "$P-push" "${PUSH_OPTS[@]}"
else
  $G pubsub subscriptions create "$P-push" --topic="$P-docs" --labels="$LABEL" "${PUSH_OPTS[@]}"
fi
# L'agent de service Pub/Sub doit pouvoir republier vers les lettres mortes et acquitter l'original.
$G pubsub topics add-iam-policy-binding "$P-dlq" --member="serviceAccount:$PUBSUB_AGENT" \
  --role=roles/pubsub.publisher >/dev/null
$G pubsub subscriptions add-iam-policy-binding "$P-push" --member="serviceAccount:$PUBSUB_AGENT" \
  --role=roles/pubsub.subscriber >/dev/null

Le sujet de lettres mortes a son propre abonnement, lab-pipeline-evt-dlq-sub, avec 7 jours de rétention : un message publié sur un sujet sans abonnement est perdu.

Le niveau 1, dans gcp.py :

files/app/gcp.py (extrait)
RETRY = types.HttpOptions(retry_options=types.HttpRetryOptions(
    attempts=3, initial_delay=2.0, max_delay=8.0, http_status_codes=[429, 500, 503]))

Le test e2e publie un message empoisonné, sans attribut eventType, que le worker refuse en 400 à chaque livraison, puis attend qu'il apparaisse dans la file de lettres mortes. Il le reconnaît à son attribut run, puisque l'ID a changé.

Résultat obtenu (2026-10-16)

15:17:34 message empoisonné publié : 22188242576114171
15:18:18 lettres mortes : pas encore arrivé (10 tentatives, délai de 10 à 60 s)
...
15:23:19 lettres mortes : message 22188242576114171 reçu, attributs {'nouvel_id': '22035090386617531',
  'CloudPubSubDeadLetterSourceSubscription': 'lab-pipeline-evt-push', 'origine': 'e2e',
  'CloudPubSubDeadLetterSourceTopicPublishTime': '2026-10-16T13:17:34.17+00:00',
  'CloudPubSubDeadLetterSourceSubscriptionProject': '<projet>', 'run': '20261016-151732-1e59',
  'CloudPubSubDeadLetterSourceDeliveryCount': '10'}
15:23:20 OK   message empoisonné en lettres mortes

Dix livraisons en 5 min 45 s, puis déplacement. Côté worker, les journaux comptent exactement dix lignes message_empoisonne pour ce passage : aucune livraison de plus, aucune de moins.

Ce qui casse en production

  • Droits de l'agent Pub/Sub manquants. Symptôme : CloudPubSubDeadLetterSourceDeliveryCount n'apparaît jamais, le message est relivré bien au-delà de 10 fois, la console signale un problème de permission sur la lettre morte. Parade : les deux liaisons ci-dessus, dans le script, pas à la main.
  • File de lettres mortes que personne ne lit. Elle se remplit en silence pendant des semaines, puis la rétention efface les messages. Parade : une alerte Cloud Monitoring sur pubsub.googleapis.com/subscription/num_undelivered_messages de lab-pipeline-evt-dlq-sub supérieur à 0, et une procédure de rejeu écrite.
  • Rejeu sans correction. Republier les messages de la file vers le sujet principal avant d'avoir corrigé la cause les renvoie dix fois en échec, puis en lettres mortes à nouveau. Parade : rejouer un message, vérifier sa ligne BigQuery, puis le reste.
  • Réessai qui amplifie la panne. Pendant une saturation du quota, 300 documents en attente reviennent tous en même temps à chaque palier. Parade : --max-instances=3 borne la concurrence vers Gemini à 12 requêtes ; le délai exponentiel de Pub/Sub étale les retours.
  • Lettres mortes et redéploiement. Un worker déployé avec un bogue qui répond 500 à tout envoie toute la production du jour en lettres mortes en moins de six minutes. Parade : déploiement progressif (--no-traffic puis bascule), et une alerte sur le taux de 5xx.

La règle de décision

Réessai court dans la requête pour les erreurs de quelques secondes, réessai Pub/Sub à délai exponentiel pour les minutes, lettres mortes au-delà. Réglez max-delivery-attempts pour que la durée totale de réessai couvre la plus longue panne passagère acceptable (ici environ 6 minutes) ; une panne plus longue relève d'une intervention, pas d'un réessai. Toute file de lettres mortes a un abonnement, une alerte et une procédure de rejeu.

Pratique

Une panne Vertex AI dure 25 minutes. Avec la configuration du projet, que deviennent les documents déposés au début de la panne ?

La file de lettres mortes contient 40 messages dont l'attribut CloudPubSubDeadLetterSourceDeliveryCount vaut 10. Tous datent de 14 h 02 à 14 h 07, juste après un déploiement. Par quoi commencez-vous ?

Exercice. Écrivez le script de rejeu : lire les messages de lab-pipeline-evt-dlq-sub, les republier sur lab-pipeline-evt-docs avec leurs attributs d'origine, et n'acquitter dans la file de lettres mortes qu'après la republication réussie.

Voir la solution
from google.cloud import pubsub_v1

PROJECT = "mon-projet"
sub, pub = pubsub_v1.SubscriberClient(), pubsub_v1.PublisherClient()
src = sub.subscription_path(PROJECT, "lab-pipeline-evt-dlq-sub")
dst = pub.topic_path(PROJECT, "lab-pipeline-evt-docs")

while True:
    resp = sub.pull(request={"subscription": src, "max_messages": 10}, timeout=30)
    if not resp.received_messages:
        break
    for m in resp.received_messages:
        attrs = {k: v for k, v in m.message.attributes.items() if not k.startswith("CloudPubSubDeadLetter")}
        pub.publish(dst, m.message.data, **attrs).result()  # attend la confirmation
        sub.acknowledge(request={"subscription": src, "ack_ids": [m.ack_id]})

Le message republié reçoit un nouvel ID : si le document avait été traité entre-temps, la déduplication par messageId ne le verra pas. C'est acceptable ici parce qu'un message n'arrive en lettres mortes qu'après dix échecs, donc sans ligne ok. Avec une clé object_uri (exercice de la leçon 2), le rejeu deviendrait sûr dans tous les cas.

  • Je règle les trois niveaux de réessai et je sais ce que chacun absorbe.
  • J'accorde à l'agent de service Pub/Sub les deux droits de la file de lettres mortes.
  • Je calcule la durée couverte par une politique de réessai.
  • J'ai une alerte et une procédure de rejeu pour la file de lettres mortes.
التالي