El mòdul 6 va acabar amb un diagnòstic incòmode. La capa de dades de MercadoFresco està ben repartida —Aurora per a comandes i estoc, DynamoDB per a cistelles i sessions, Redshift per als informes de la Sara, ElastiCache per al catàleg—, però la petició que confirma una comanda continua fent vuit coses seguides abans de respondre. La Marta ho va mesurar al pic del divendres: 2.900 ms de mediana, 9.100 ms al percentil 95, i un 0,4 % de comandes que fallen després d'haver cobrat perquè l'ERP del magatzem va retornar un 503.
Amazon SQS (Simple Queue Service) trenca aquesta cadena. És una cua de missatges gestionada: un component escriu un missatge, un altre el llegeix quan pot, i cap dels dos no necessita que l'altre estigui viu en aquell instant. És el servei més antic d'AWS i també el més avorrit en el millor sentit: sense servidors, sense versions, sense capacitat que calgui dimensionar, i escala de zero a milions de missatges per segon sense que ningú toqui res. Aquí el Luis converteix les vuit crides encadenades en una confirmació de 400 ms més set tasques que passen després, i aprèn pel camí el concepte que més incidents causa en producció: el temps d'espera de visibilitat.
Avís de cost. SQS cobra per petició a l'API: 1 milió gratis al mes de manera permanent i 0,40 USD per milió addicional en cues estàndard (0,50 en FIFO). Una cua buida no costa res, però un consumidor amb sondeig curt pot generar centenars de milions de peticions buides al mes. Al final tens les ordres de neteja. Totes les dades són fictícies.
Contingut
- Síncron davant d'asíncron: què hi guanya MercadoFresco
- Anatomia d'una cua i el model de sondeig
- Cues estàndard i cues FIFO
- Quina cua fa servir cada tasca de MercadoFresco
- Cicle de vida d'un missatge
- El temps d'espera de visibilitat, en detall
- Sondeig curt i sondeig llarg
- Retenció, retard, temporitzadors i càrregues grans
- Cues de missatges fallits i redrive
- Crear les cues per CLI, política de cua i xifratge
- Productor i consumidor en Python
- Consumir des de Lambda: mapatge d'origen d'esdeveniments
- Mètriques, alarmes i autoescalat per profunditat de cua
- Cost real i neteja
- Errors habituals i consells
- Exercicis
- Conclusió
Síncron davant d'asíncron: què hi guanya MercadoFresco
Una crida síncrona és aquella en què qui truca espera la resposta abans de continuar. No té res de dolent mentre es faci servir per al que toca: obtenir una dada que es necessita ara per poder respondre. El problema apareix en encadenar crides síncrones a coses que no es necessiten ara. Aquestes són les vuit tasques de MercadoFresco, mesurades al pic del divendres:
| # | Tasca | Destinació | p50 | p95 | El client necessita el resultat? |
|---|---|---|---|---|---|
| 1 | Cobrar la targeta | Passarel·la externa | 240 ms | 3.100 ms | Sí |
| 2 | Escriure la comanda | Aurora | 38 ms | 60 ms | Sí |
| 3 | Buidar la cistella | DynamoDB | 11 ms | 22 ms | No |
| 4 | Invalidar la memòria cau | ElastiCache | 4 ms | 9 ms | No |
| 5 | Avisar l'ERP del magatzem | HTTP del soci | 1.180 ms | 6.500 ms | No |
| 6 | Correu de confirmació | Proveïdor SMTP | 820 ms | 4.200 ms | No |
| 7 | Notificar el repartidor | API del soci | 410 ms | 2.900 ms | No |
| 8 | Publicar esdeveniment d'analítica | Firehose | 190 ms | 700 ms | No |
Només les dues primeres són imprescindibles per dir «la teva comanda està confirmada». Les sis restants són conseqüències de la comanda, no requisits, i tanmateix el client les paga en temps d'espera i en risc. La suma seqüencial dona 2.893 ms de mediana, però el greu és la fragilitat multiplicativa: si cada tasca té un 99,9 % de disponibilitat, la cadena de vuit té un 99,2 %. Amb 900 comandes per hora al pic, són set comandes trencades per hora, totes ja cobrades.
flowchart TD
C[Client prem Confirmar comanda] --> A[Servidor de la botiga]
A --> P1[1. Passarella de pagament 240 ms]
P1 --> P2[2. Aurora INSERT 38 ms]
P2 --> P3[3. DynamoDB buidar cistella 11 ms]
P3 --> P4[4. ElastiCache invalidar 4 ms]
P4 --> P5[5. ERP del magatzem 1.180 ms]
P5 --> P6[6. Correu de confirmacio 820 ms]
P6 --> P7[7. API del repartidor 410 ms]
P7 --> P8[8. Esdeveniment analitic 190 ms]
P8 --> R[Resposta al client 2.893 ms p50]
P5 -. ERP retorna 503 .-> X[Comanda cobrada i perduda]
style X fill:#f8d7da,stroke:#c00
style R fill:#fff3cd
Una crida asíncrona trenca l'encadenament: la botiga deixa constància que hi ha feina pendent i respon. La feina es fa després, per un altre procés, amb els seus propis reintents.
flowchart TD
C[Client prem Confirmar comanda] --> A[Servidor de la botiga]
A --> P1[1. Passarella de pagament 240 ms]
P1 --> P2[2. Aurora INSERT 38 ms]
P2 --> Q[(cola-mercadofresco-pedidos<br/>SendMessage 12 ms)]
Q --> R[Resposta al client ~400 ms p95]
Q --> W1[Consumidor magatzem]
Q --> W2[Consumidor correu]
Q --> W3[Consumidor analitica]
W1 --> ERP[ERP del magatzem]
W2 --> SMTP[Proveidor de correu]
W3 --> FH[Firehose i S3]
ERP -. 503 .-> RT[Reintent automatic:<br/>el missatge segueix a la cua]
style R fill:#d4edda
style RT fill:#fff3cd
El que s'hi guanya no és només velocitat: la fallada de l'ERP deixa de ser la fallada de la comanda. El missatge segueix a la cua, el consumidor el reintenta, i si l'ERP triga dues hores a tornar, les comandes d'aquestes dues hores es processen quan torni. El client no se n'assabenta mai.
Anatomia d'una cua i el model de sondeig
Quatre conceptes i prou. El productor envia missatges; a MercadoFresco, la botiga a
asg-mercadofresco-tienda, que només necessita sqs:SendMessage i l'URL de la cua. La cua és el
magatzem durador: SQS replica cada missatge en diversos servidors de diverses AZ d'eu-west-1 abans de
confirmar l'enviament, i no té mida màxima ni capacitat que calgui aprovisionar. El missatge té un
cos (Body) de fins a 256 KiB de text, fins a 10 atributs de missatge tipats que viatgen fora
del cos, i atributs del sistema (MessageId, SentTimestamp, ApproximateReceiveCount). El
consumidor llegeix, treballa i esborra; n'hi pot haver un o mil sense que es trepitgin.
La distinció entre cos i atributs importa: els atributs s'inspeccionen sense deserialitzar el cos i —clau a 07-02— SNS filtra per atributs. Regla de MercadoFresco: als atributs hi va el que serveix per decidir què fer amb el missatge; al cos, la dada.
{
"MessageAttributes": {
"tipo_evento": { "DataType": "String", "StringValue": "PedidoConfirmado" },
"franja_entrega": { "DataType": "String", "StringValue": "24h" },
"version": { "DataType": "Number", "StringValue": "1" }
},
"MessageBody": "{\"pedido_id\":\"PED-2026-084417\",\"cliente_id\":\"CLI-30912\",\"importe_eur\":48.20,\"franja_entrega\":\"24h\",\"lineas\":[{\"sku\":\"FRUT-FRES-011\",\"unidades\":2}],\"confirmado_en\":\"2026-08-02T18:41:07Z\"}"
}El cos és una cadena, no un objecte: SQS no sap què hi ha a dins. La serialització i el versionatge del contracte són cosa teva, i hi tornarem a 07-03.
Sondeig (pull) davant d'inserció (push). Aquesta és la característica que defineix SQS: mai no truca a ningú. No hi ha manera de dir-li «quan arribi un missatge, envia'l a aquesta URL». És el consumidor qui pregunta.
| Sondeig (SQS) | Inserció (SNS, webhooks) | |
|---|---|---|
| Qui inicia | El consumidor pregunta | El servei lliura |
| El ritme el marca | El consumidor | El productor |
| Si el consumidor està caigut | Els missatges esperen | Es perden o es reintenten a cegues |
| Contrapressió natural | Sí: es llegeix el que es pot | No: arriba el que arriba |
| Necessita punt final públic | No | Sí (llevat de destinacions internes d'AWS) |
| Reintent | Implícit: si no esborres, torna | Configurat per l'emissor |
La conseqüència arquitectònica és enorme: la cua actua d'esmorteïdor. Si entren 900 comandes/hora i els consumidors en processen 600, no es perd res: la cua creix i a les 22:00 es buida. Aquesta propietat —la contrapressió— és la raó que les cues continuïn sent la peça bàsica d'integració, i la reprendrem a 07-05.
L'aparent excepció és Lambda: quan connectes una cua a una funció sembla que SQS «empenyi». No és així;
el servei Lambda manté sondejadors propis que criden ReceiveMessage per tu. El model continua sent de
sondeig, només que el sondejador el posa AWS.
Cues estàndard i cues FIFO
| Cua estàndard | Cua FIFO | |
|---|---|---|
| Nom | Lliure | Ha d'acabar en .fifo |
| Ordre | Best-effort: gairebé sempre, no garantit | Estricte dins de cada grup |
| Lliurament | Almenys un cop (pot duplicar) | Exactament un cop dins de la finestra de deduplicació |
| Rendiment | Pràcticament il·limitat | 300 msg/s (3.000 amb lots); alt rendiment: desenes de milers |
| Grups | No existeixen | MessageGroupId obligatori |
| Deduplicació | No | Per MessageDeduplicationId o hash del cos, finestra de 5 min |
| Paral·lelisme | Total | Un missatge en vol per grup |
| Preu | 0,40 USD/milió | 0,50 USD/milió |
Tres idees que convé interioritzar. «Almenys un cop» significa que hi haurà duplicats: no és teòric, SQS emmagatzema cada missatge en diversos servidors i ocasionalment un no s'assabenta que ja va ser esborrat i el torna a lliurar. Si el teu consumidor cobra una targeta, un duplicat és cobrar dues vegades, i la solució no és FIFO sinó la idempotència del consumidor (07-05).
L'«exactament un cop» de FIFO té lletra petita: només actua dins d'una finestra de 5 minuts i només cobreix l'enviament duplicat del mateix missatge; si el consumidor processa i mor abans d'esborrar, el missatge torna. FIFO redueix molt la probabilitat de duplicat; no l'elimina.
Els grups són la unitat d'ordre i de paral·lelisme alhora. Per garantir l'ordre, SQS no lliura un
segon missatge del grup fins que el primer s'ha esborrat. Un únic grup per a tota la cua dona ordre
global i un sol consumidor efectiu; el sku com a grup dona ordre per producte i tant paral·lelisme com
productes hi hagi. És la mateixa decisió que triar clau de partició a DynamoDB (06-02).
Quina cua fa servir cada tasca de MercadoFresco
| Tasca | Cua | Tipus | Per què |
|---|---|---|---|
| Preparar la caixa | cola-mercadofresco-almacen |
Estàndard | El magatzem reconcilia per pedido_id; l'ordre entre comandes és igual |
| Correu de confirmació | cola-mercadofresco-correo |
Estàndard | Duplicar un correu és molest, no catastròfic; es deduplica al consumidor |
| Notificar el repartidor | cola-mercadofresco-reparto |
Estàndard | Ídem |
| Esdeveniment d'analítica | cola-mercadofresco-analitica |
Estàndard | Redshift agrega; els duplicats es filtren a la càrrega |
| Moviments d'estoc | cola-mercadofresco-stock.fifo |
FIFO, grup = sku |
«Reservar 2» i «alliberar 2» del mateix SKU han d'anar en ordre |
| Miniatures de fotos | cola-mercadofresco-miniaturas |
Estàndard | Regenerar una miniatura és idempotent per naturalesa |
Només una de les sis necessita FIFO, i és justament la que toca comptadors. És l'habitual: la majoria de les càrregues no necessiten ordre global, necessiten un consumidor idempotent. Pagar el preu de FIFO quan no cal és un error de disseny car.
En aquesta lliçó el Luis comença pel mínim viable: cola-mercadofresco-pedidos recull tota la feina
posterior a la confirmació, i cola-mercadofresco-correo separa l'enviament de correus, el consumidor
més lent i el que més falla. A 07-02 veurem per què aquesta cua única acaba sent un problema.
Cicle de vida d'un missatge
sequenceDiagram
participant P as Productor (botiga)
participant Q as cola-mercadofresco-pedidos
participant C as Consumidor
P->>Q: SendMessage(Body, MessageAttributes)
Q-->>P: MessageId (replicat i durador)
Note over Q: Estat: visible
C->>Q: ReceiveMessage(Max=10, WaitTimeSeconds=20)
Q-->>C: Missatges + ReceiptHandle
Note over Q: Estat: en vol (invisible)<br/>durant VisibilityTimeout
alt Feina correcta
C->>Q: DeleteMessage(ReceiptHandle)
Note over Q: El missatge desapareix
else El consumidor falla o cau
Note over Q: Expira el temps de visibilitat
Note over Q: Visible un altre cop<br/>ApproximateReceiveCount += 1
Q-->>C: Es torna a lliurar
end
Tres estats i només tres: visible, en vol i esborrat. Amb això s'explica tot el comportament de SQS, inclosos els que semblen fallades del servei. La part que sorprèn sempre: rebre un missatge no l'elimina. SQS el lliura i l'amaga de la resta de consumidors, però continua allà; si el teu procés mor a mitges, si el contenidor es reinicia, si l'ASG apaga la instància, el missatge reapareix i un altre consumidor l'agafa. Aquesta és la garantia de durabilitat.
El corol·lari: no esborrar equival a reprocessar. Si el consumidor llança una excepció i no arriba
al DeleteMessage, el missatge tornarà. I si triga més que el temps de visibilitat, tornarà encara
que acabi bé, amb la qual cosa la feina es farà dues vegades: la causa número u de duplicats en producció.
El temps d'espera de visibilitat, en detall
El VisibilityTimeout és el nombre de segons que un missatge roman invisible després de ser rebut. Per
defecte 30 s; màxim 12 hores. La regla és una sola frase:
El temps de visibilitat ha de ser més gran que el temps que triga el consumidor més lent a processar el missatge i esborrar-lo.
Vegem què passa si no es compleix. El consumidor de correu crida un SMTP que al p99 triga 42 s, i el temps de visibilitat està als 30 per defecte:
| t | Consumidor A | Consumidor B | Estat del missatge |
|---|---|---|---|
| 0 s | Rep PED-084417 | — | En vol |
| 5 s | Crida l'SMTP | — | En vol |
| 30 s | Continua esperant | — | Torna a ser visible |
| 31 s | Continua esperant | Rep el mateix missatge | En vol (per a B) |
| 42 s | Respon l'SMTP, DeleteMessage |
Crida l'SMTP | Esborrat |
| 73 s | — | DeleteMessage → ReceiptHandleIsInvalid |
— |
El client ha rebut dos correus. I la fallada és silenciosa, difícil de reproduir en proves i només apareix sota càrrega. Hi ha tres maneres de configurar-ho:
1. A la cua, com a valor per defecte:
aws sqs set-queue-attributes \
--queue-url https://sqs.eu-west-1.amazonaws.com/111122223333/cola-mercadofresco-correo \
--attributes VisibilityTimeout=90 --profile mercadofresco-dev --region eu-west-12. A la recepció, passant VisibilityTimeout a ReceiveMessage, útil quan el mateix consumidor
atén feines de cost dispar.
3. Ampliant-lo sobre la marxa (heartbeat). Si la feina pot durar molt i no saps quant, no posis 12
hores: posa un valor curt i allarga'l mentre treballes amb ChangeMessageVisibility. Si el procés mor,
el missatge torna en segons en lloc d'en hores.
def batec(receipt_handle, parar):
"""Amplia la visibilitat a 60 s cada 30 s mentre la feina segueixi en curs."""
while not parar.wait(30):
try:
sqs.change_message_visibility(QueueUrl=CUA, ReceiptHandle=receipt_handle,
VisibilityTimeout=60) # 60 s DES D'ARA
except sqs.exceptions.ReceiptHandleIsInvalid:
break # ja s'ha esborrat o ha expirat: res a ampliar
def processar_amb_batec(missatge):
parar = threading.Event()
fil = threading.Thread(target=batec, args=(missatge["ReceiptHandle"], parar), daemon=True)
fil.start()
try:
preparar_caixa_al_magatzem(missatge["Body"]) # de 2 s a 8 minuts
sqs.delete_message(QueueUrl=CUA, ReceiptHandle=missatge["ReceiptHandle"])
finally:
parar.set() # atura el batec passi el que passi
fil.join(timeout=2)Dos detalls: VisibilityTimeout=60 significa «invisible 60 segons des d'aquest moment», no «afegeix
60 al que quedava»; i el total acumulat des de la primera recepció no pot superar les 12 hores. La
mateixa ordre serveix per retornar un missatge immediatament amb VisibilityTimeout=0 quan detectes
que no el pots processar ara, o ajornar-lo cinc minuts amb 300.
Sondeig curt i sondeig llarg
Amb sondeig curt (WaitTimeSeconds=0, valor per defecte en una cua acabada de crear) SQS consulta un
subconjunt de servidors i respon immediatament, encara que sigui amb una llista buida. Pots rebre una
resposta buida amb missatges a la cua, i el teu bucle genera peticions a màxima velocitat. Amb
sondeig llarg (1 a 20 s) SQS consulta tots els servidors i manté la connexió oberta fins que hi ha
un missatge o s'esgota l'espera.
| Sondeig curt | Sondeig llarg (20 s) | |
|---|---|---|
| Respostes buides amb cua no buida | Possible | No |
| Peticions/hora d'un consumidor ociós | Fins a ~360.000 | 180 |
| Latència en arribar un missatge | Fins al sondeig següent | Pràcticament immediata |
| Cost mensual de 4 consumidors ociosos | ~415 USD | ~0,21 USD |
| Quan fer-lo servir | Gairebé mai | Sempre |
Els 415 USD no són retòrica: quatre processos sondejant en bucle generen de l'ordre de mil milions de peticions al mes. És l'error de novell més car de SQS i apareix a Cost Explorer (11-03) com una línia desproporcionada. Configura'l a la cua i a cada crida:
aws sqs set-queue-attributes \
--queue-url https://sqs.eu-west-1.amazonaws.com/111122223333/cola-mercadofresco-pedidos \
--attributes ReceiveMessageWaitTimeSeconds=20 --profile mercadofresco-dev --region eu-west-1Si fas servir sondeig llarg, apuja el temps d'espera de lectura del client HTTP per sobre dels 20 segons o l'SDK tallarà la connexió abans d'hora.
Retenció, retard, temporitzadors i càrregues grans
| Atribut | Rang | Per defecte | Ús a MercadoFresco |
|---|---|---|---|
MessageRetentionPeriod |
60 s – 14 dies | 4 dies | 14 dies a les DLQ, 4 a les normals |
DelaySeconds de cua |
0 – 15 min | 0 | 10 s a cola-mercadofresco-correo |
DelaySeconds per missatge |
0 – 15 min | — | 900 s a «la teva comanda surt del magatzem» |
MaximumMessageSize |
1 KiB – 256 KiB | 256 KiB | Per defecte |
Retenció. Un missatge que ningú no esborra s'elimina en complir el període. És una xarxa de seguretat, no una funció: si els teus missatges caduquen, tens un consumidor aturat i una alarma que no va saltar. A les DLQ s'hi posa el màxim perquè allà el missatge espera que un humà se'l miri.
Cua de retard. Amb DelaySeconds a escala de cua, tots els missatges neixen invisibles. El Luis
ho fa servir a la cua de correu amb 10 segons: dona marge que l'escriptura a Aurora es repliqui a
aurora-mf-lector-1/-2 abans que el consumidor llegeixi la comanda. Sense aquest marge, un 0,3 % dels
correus sortien amb «comanda no trobada».
Temporitzador per missatge. El mateix efecte però per missatge, amb DelaySeconds a SendMessage.
No està disponible en cues FIFO, on només existeix el retard a escala de cua.
Càrregues grans. 256 KiB és molt per a una comanda i poc per a una factura PDF. La solució no és
comprimir: és la biblioteca de client estesa (Extended Client Library), que desa la càrrega a S3 i
envia per la cua només un punter {"s3_bucket": ..., "s3_key": ...}. A mà són tres línies: un
put_object a mercadofresco-informes-analitica i un send_message amb la referència. Compte amb el
cicle de vida: si el consumidor esborra el missatge i ningú no esborra l'objecte, pagues emmagatzematge
indefinidament; una regla de cicle de vida d'S3 (02-03) amb caducitat a 7 dies ho resol.
Cues de missatges fallits i redrive
Un missatge que el consumidor no pot processar torna a la cua. Si la causa és transitòria —l'ERP caigut—
és justament el que vols. Si és permanent —JSON mal format, sku inexistent, un None on hi havia
d'haver un número— el missatge tornarà per sempre, consumint peticions, bloquejant l'ordre en cues
FIFO i embrutant els registres. És un missatge enverinat (poison pill).
La cua de missatges fallits (DLQ) és una cua normal on SQS mou els missatges rebuts massa vegades. Es configura amb una política de redrive a la cua d'origen:
{
"deadLetterTargetArn": "arn:aws:sqs:eu-west-1:111122223333:mercadofresco-miniaturas-fallidas",
"maxReceiveCount": 3
}maxReceiveCount compta recepcions, no fallades: amb 3, el missatge s'intenta tres vegades i a la
quarta recepció es mou.
maxReceiveCount |
Efecte |
|---|---|
| 1 | Sense reintents: qualsevol fallada transitòria envia el missatge a la DLQ. Gairebé mai correcte |
| 3 – 5 | Recomanat: absorbeix caigudes curtes i aïlla ràpid els missatges enverinats |
| 50+ | L'enverinat es reintenta durant hores; els registres s'omplen de soroll |
Aquí formalitzem mercadofresco-miniaturas-fallidas, que va aparèixer al mòdul 2 al costat de
mercadofresco-generar-miniaturas i mai no es va configurar del tot. L'arquitectura queda així: en
pujar una foto a mercadofresco-catalogo-fotos, S3 publica a cola-mercadofresco-miniaturas; la Lambda
consumeix; si falla tres vegades, el missatge acaba a mercadofresco-miniaturas-fallidas.
aws sqs set-queue-attributes \
--queue-url https://sqs.eu-west-1.amazonaws.com/111122223333/cola-mercadofresco-miniaturas \
--attributes "{\"RedrivePolicy\":\"{\\\"deadLetterTargetArn\\\":\\\"$DLQ_ARN\\\",\\\"maxReceiveCount\\\":\\\"3\\\"}\"}" \
--profile mercadofresco-dev --region eu-west-1Aquest escapament triple és tan lleig com sembla: RedrivePolicy és un JSON dins d'una cadena dins d'un
altre JSON. A 09-01 l'escriurem en CloudFormation i el problema desapareix.
Què mirar quan alguna cosa cau a la DLQ, en aquest ordre: (1) l'ApproximateReceiveCount —si és
exactament maxReceiveCount + 1, va esgotar els reintents—; (2) el cos: JSON vàlid?, camps esperats?,
versió del contracte coneguda?; (3) els registres de CloudWatch del consumidor filtrats pel
MessageId. Aquí es paga haver posat el MessageId a cada línia de registre.
Redrive. Corregida la fallada, no cal reenviar a mà:
aws sqs start-message-move-task \
--source-arn arn:aws:sqs:eu-west-1:111122223333:mercadofresco-miniaturas-fallidas \
--max-number-of-messages-per-second 20 --profile mercadofresco-dev --region eu-west-1Sense --destination-arn els missatges tornen a la seva cua d'origen. El límit de velocitat no és
opcional a la pràctica: reprocessar 40.000 missatges de cop pot tombar el sistema que ja era fràgil. A
07-05 escriurem el runbook complet de la Marta.
Regla dura de MercadoFresco: tota cua té DLQ, i tota DLQ té alarma. Una DLQ sense alarma és una galleda d'escombraries on es perden vendes en silenci.
Crear les cues per CLI, política de cua i xifratge
Comencem per la DLQ, perquè la cua principal necessita el seu ARN.
export PERFIL="--profile mercadofresco-dev --region eu-west-1"
export ETIQUETES='Proyecto=mercadofresco,Entorno=produccion,Componente=integracion,Propietario=marta,CentroCoste=tecnologia'
aws sqs create-queue --queue-name mercadofresco-pedidos-fallidos \
--attributes MessageRetentionPeriod=1209600 --tags "$ETIQUETES" $PERFIL
DLQ=$(aws sqs get-queue-attributes \
--queue-url https://sqs.eu-west-1.amazonaws.com/111122223333/mercadofresco-pedidos-fallidos \
--attribute-names QueueArn --query 'Attributes.QueueArn' --output text $PERFIL)
aws sqs create-queue --queue-name cola-mercadofresco-pedidos --tags "$ETIQUETES" $PERFIL \
--attributes "{\"VisibilityTimeout\":\"120\",\"MessageRetentionPeriod\":\"345600\",
\"ReceiveMessageWaitTimeSeconds\":\"20\",\"KmsMasterKeyId\":\"alias/mercadofresco-datos\",
\"KmsDataKeyReusePeriodSeconds\":\"300\",
\"RedrivePolicy\":\"{\\\"deadLetterTargetArn\\\":\\\"$DLQ\\\",\\\"maxReceiveCount\\\":\\\"4\\\"}\"}"
aws sqs create-queue --queue-name cola-mercadofresco-correo --tags "$ETIQUETES" $PERFIL \
--attributes '{"VisibilityTimeout":"90","DelaySeconds":"10",
"ReceiveMessageWaitTimeSeconds":"20",
"KmsMasterKeyId":"alias/mercadofresco-datos"}'Cada valor respon a un mesurament: VisibilityTimeout=120 a comandes perquè el consumidor de l'ERP
triga 6,5 s al p95 i 38 s al pitjor cas observat; 90 a correu perquè l'SMTP arriba a 42 s al p99;
maxReceiveCount=4 per a tres reintents reals abans d'aïllar; i
KmsDataKeyReusePeriodSeconds=300, que fa que SQS reutilitzi la clau de dades 5 minuts i redueix
dràsticament les crides a KMS sense comprometre la seguretat de manera apreciable. La cua FIFO d'estoc
es crea diferent:
aws sqs create-queue --queue-name cola-mercadofresco-stock.fifo \
--attributes '{"FifoQueue":"true","ContentBasedDeduplication":"false",
"DeduplicationScope":"messageGroup","FifoThroughputLimit":"perMessageGroupId",
"VisibilityTimeout":"60","ReceiveMessageWaitTimeSeconds":"20"}' \
--tags "$ETIQUETES" $PERFILDeduplicationScope=messageGroup amb FifoThroughputLimit=perMessageGroupId activen el mode d'alt
rendiment: el límit passa a aplicar-se per grup en lloc de per cua. Amb el sku com a grup i 3.400
SKU, el paral·lelisme és més que suficient.
Les dues capes d'autorització. La política d'identitat (IAM, 04-01) s'adjunta al rol que crida. La política de cua (basada en recurs) s'adjunta a la cua i és imprescindible quan qui envia no és un principal IAM del teu compte: un altre servei d'AWS (SNS, S3, EventBridge) o un altre compte.
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "PermitirAvisosDeS3Catalogo",
"Effect": "Allow",
"Principal": { "Service": "s3.amazonaws.com" },
"Action": "sqs:SendMessage",
"Resource": "arn:aws:sqs:eu-west-1:111122223333:cola-mercadofresco-miniaturas",
"Condition": {
"ArnLike": { "aws:SourceArn": "arn:aws:s3:::mercadofresco-catalogo-fotos" },
"StringEquals": { "aws:SourceAccount": "111122223333" }
}
}]
}aws:SourceArn limita el permís a aquell bucket; sense ell, qualsevol bucket d'S3 del món podria
escriure a la teva cua. aws:SourceAccount tanca el problema del diputat confós, en què un servei
d'AWS actua legítimament en nom d'un tercer (04-01).
Xifratge. SQS ofereix SSE-SQS (claus d'AWS, gratuït, actiu per defecte en cues noves) i SSE-KMS amb
la teva clau. MercadoFresco fa servir alias/mercadofresco-datos a les cues amb dades personals —les
comandes porten adreça de lliurament— per coherència amb el mòdul 4 i perquè les crides a KMS queden a
trail-mercadofresco. Compte amb el detall que trenca desplegaments: si la cua està xifrada amb KMS, el
productor necessita kms:GenerateDataKey i el consumidor kms:Decrypt sobre la clau, no només
sobre la cua; i si el productor és un servei d'AWS, la política de la clau l'hi ha de permetre.
Productor i consumidor en Python
import json, os
from datetime import datetime, timezone
import boto3
from botocore.config import Config
# Un sol client reutilitzat: crear-lo per peticio resol credencials i obre
# connexions noves cada cop, un error de rendiment classic.
sqs = boto3.client("sqs", config=Config(
region_name="eu-west-1",
retries={"max_attempts": 5, "mode": "standard"}, # reintents davant de 5xx i estrangulament
connect_timeout=2, read_timeout=5, # acoten el pitjor cas de l'enviament
))
CUA_COMANDES = os.environ["URL_COLA_PEDIDOS"]
def confirmar_comanda(cistella, client, targeta):
"""Confirma una comanda: cobra, persisteix i encua. Res mes."""
cobrament = passarella.cobrar(targeta, cistella.import_eur) # imprescindible
comanda_id = aurora.inserir_comanda(cistella, client, cobrament.referencia) # imprescindible
cos = {
"version": 1, "pedido_id": comanda_id, "cliente_id": client.id,
"importe_eur": float(cistella.import_eur),
"franja_entrega": cistella.franja, # "24h" o "estandar"
"referencia_cobro": cobrament.referencia,
"lineas": [{"sku": l.sku, "unidades": l.unitats} for l in cistella.linies],
"confirmado_en": datetime.now(timezone.utc).isoformat(),
}
r = sqs.send_message(
QueueUrl=CUA_COMANDES,
MessageBody=json.dumps(cos, ensure_ascii=False),
MessageAttributes={
"tipo_evento": {"DataType": "String", "StringValue": "PedidoConfirmado"},
"franja_entrega": {"DataType": "String", "StringValue": cistella.franja},
"version": {"DataType": "Number", "StringValue": "1"},
},
)
log.info("comanda encuada", extra={"pedido_id": comanda_id, "message_id": r["MessageId"]})
return comanda_idTres decisions que mereixen comentari. El send_message va després de l'INSERT: si s'encués
primer i l'INSERT fallés, hi hauria un missatge anunciant una comanda inexistent. En aquest ordre la
fallada possible és la contrària —comanda escrita i missatge no enviat—, menys greu però real, i té
nom: el problema de la doble escriptura, que resoldrem amb el patró outbox a 07-05. Els reintents
de l'SDK estan configurats amb retrocés exponencial davant de 5xx. I els temps d'espera són
curts: sense ells botocore fa servir 60 segons, de sobres per esgotar el pool de connexions de la
botiga durant un incident de SQS.
Per a volums alts —la càrrega nocturna encua 40.000 moviments d'estoc— fes servir send_message_batch,
que accepta 10 missatges per petició (Entries amb Id i MessageBody) i divideix la factura per deu.
El parany: retorna HTTP 200 encara que hi hagi entrades fallides, així que cal recórrer
r.get("Failed", []) i registrar cada fallada. Si no ho fas, perds missatges sense assabentar-te'n.
El consumidor que corre en una instància o contenidor té una forma canònica que convé copiar tal com és:
_continuar = True
def _aturar(signum, frame):
"""Aturada ordenada: amb SIGTERM de l'ASG o d'ECS, acaba el lot en curs."""
global _continuar
_continuar = False
signal.signal(signal.SIGTERM, _aturar)
def bucle_de_consum():
while _continuar:
resposta = sqs.receive_message(
QueueUrl=CUA,
MaxNumberOfMessages=10, # el maxim: menys peticions, menys cost
WaitTimeSeconds=20, # sondeig llarg: imprescindible
MessageAttributeNames=["All"], # sense aixo, MessageAttributes arriba buit
AttributeNames=["ApproximateReceiveCount", "SentTimestamp"],
)
missatges = resposta.get("Messages", []) # la clau NO existeix si no hi ha missatges!
if not missatges:
continue
esborrables = []
for m in missatges:
intents = int(m["Attributes"]["ApproximateReceiveCount"])
try:
processar_comanda(json.loads(m["Body"]), intents)
esborrables.append({"Id": m["MessageId"], "ReceiptHandle": m["ReceiptHandle"]})
except ErrorPermanent as e:
# Dades invalides: reintentar no arregla res. S'arxiva i s'esborra perque
# no consumeixi quatre recepcions abans d'acabar a la DLQ.
log.error("missatge invalid", extra={"message_id": m["MessageId"], "error": str(e)})
arxivar_per_revisio(m)
esborrables.append({"Id": m["MessageId"], "ReceiptHandle": m["ReceiptHandle"]})
except Exception as e:
# Transitori: NO s'esborra. Tornara despres del temps de visibilitat.
log.warning("fallada transitoria", extra={"message_id": m["MessageId"],
"intents": intents, "error": str(e)})
if esborrables:
r = sqs.delete_message_batch(QueueUrl=CUA, Entries=esborrables)
for fallada in r.get("Failed", []):
log.error("no s'ha pogut esborrar", extra={"id": fallada["Id"], "code": fallada["Code"]})El que separa un consumidor correcte d'un que dona problemes al cap de tres setmanes: fer servir
resposta.get("Messages", []) (la resposta no inclou la clau quan està buida, i
resposta["Messages"] llança KeyError al primer sondeig); demanar MessageAttributeNames=["All"] o
els atributs arribaran buits; distingir error permanent de transitori; esborrar per lots; gestionar
SIGTERM perquè l'ASG no mati el procés a mig missatge; i fer servir ApproximateReceiveCount com a
senyal per registrar més detall o passar a un camí degradat al tercer intent.
Consumir des de Lambda: mapatge d'origen d'esdeveniments
Per al consumidor de correu, muntar instàncies és desproporcionat: feina curta, esporàdica i sense estat. La connexió entre cua i funció s'anomena mapatge d'origen d'esdeveniments.
aws lambda create-event-source-mapping \
--function-name mercadofresco-enviar-correo-pedido \
--event-source-arn arn:aws:sqs:eu-west-1:111122223333:cola-mercadofresco-correo \
--batch-size 10 --maximum-batching-window-in-seconds 5 \
--scaling-config MaximumConcurrency=20 \
--function-response-types ReportBatchItemFailures $PERFIL| Paràmetre | Què fa | Valor a MercadoFresco |
|---|---|---|
--batch-size |
Missatges per invocació (1–10.000) | 10: l'SMTP no es beneficia de més |
--maximum-batching-window-in-seconds |
Espera a omplir el lot (0–300) | 5: menys invocacions en vall |
MaximumConcurrency |
Topall d'instàncies concurrents (2–1.000) | 20: protegeix el límit de l'SMTP |
--function-response-types |
Activa les fallades parcials de lot | ReportBatchItemFailures |
El topall de concurrència és la peça defensiva clau. Lambda escala agressivament amb la profunditat de la cua: pot passar de 5 a 60 sondejadors en un minut. Si al darrere hi ha Aurora amb 200 connexions o un SMTP amb límit de 50 enviaments per segon, aquesta elasticitat es converteix en una denegació de servei que et fas a tu mateix. És la mateixa idea de contrapressió que generalitzarem a 07-05.
El problema del lot sencer. Per defecte, si la funció llança una excepció, Lambda considera fallit
tot el lot i no esborra cap dels deu missatges: si nou anaven bé i un malament, els nou es
reprocessen. ReportBatchItemFailures ho arregla retornant només els que han fallat.
import json
def handler(event, context):
"""Consumidor de cola-mercadofresco-correo amb fallades parcials de lot."""
fallits = []
for registre in event["Records"]:
try:
comanda = json.loads(registre["body"])
franja = registre.get("messageAttributes", {}) \
.get("franja_entrega", {}).get("stringValue", "estandar")
enviar_correu_confirmacio(comanda["cliente_id"], comanda["pedido_id"],
comanda["importe_eur"], franja)
except Exception as e:
print(json.dumps({"nivell": "ERROR", "message_id": registre["messageId"],
"error": str(e)}))
# Nomes aquest missatge torna a la cua; la resta del lot s'esborra.
fallits.append({"itemIdentifier": registre["messageId"]})
return {"batchItemFailures": fallits}Dues condicions perquè funcioni: declarar ReportBatchItemFailures al mapatge i retornar
l'estructura exacta {"batchItemFailures": [{"itemIdentifier": "..."}]}. Si el nom de la clau no és
aquest, Lambda l'ignora en silenci.
A més: no cal cridar delete_message —Lambda esborra els missatges en acabar bé— i el temps de
visibilitat de la cua ha de ser com a mínim 6 vegades el timeout de la funció, que és la
recomanació d'AWS i evita que Lambda rebi el mateix missatge mentre encara el processa.
Mètriques, alarmes i autoescalat per profunditat de cua
SQS publica mètriques a CloudWatch cada minut i sense cost addicional.
| Mètrica | Què significa | Senyal |
|---|---|---|
ApproximateNumberOfMessagesVisible |
Missatges esperant consumidor | Profunditat de la cua |
ApproximateNumberOfMessagesNotVisible |
Missatges en vol | Feina en curs |
ApproximateAgeOfOldestMessage |
Segons del missatge més antic | La mètrica de salut real |
NumberOfMessagesSent / Deleted |
Flux | Sent > Deleted sostingut = s'acumula |
NumberOfEmptyReceives |
Sondeigs buits | Alt = sondeig curt = factura |
SentMessageSize |
Mida mitjana | A prop de 256 KiB = passar a S3 |
Si només en pots vigilar una, vigila ApproximateAgeOfOldestMessage. La profunditat enganya: 5.000
missatges amb consumidors ràpids és normal un divendres a les 19:00, i 40 missatges aturats una hora és
un incident. L'antiguitat respon a la pregunta que importa: quant triga avui una comanda a arribar al
magatzem?
aws cloudwatch put-metric-alarm --alarm-name mercadofresco-pedidos-cola-retrasada \
--namespace AWS/SQS --metric-name ApproximateAgeOfOldestMessage \
--dimensions Name=QueueName,Value=cola-mercadofresco-pedidos \
--statistic Maximum --period 60 --evaluation-periods 3 --threshold 300 \
--comparison-operator GreaterThanThreshold --treat-missing-data notBreaching \
--alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco $PERFILLa segona alarma és la mateixa ordre canviant la mètrica a
ApproximateNumberOfMessagesVisible, la dimensió a mercadofresco-pedidos-fallidos, --threshold 0 i
--period 300: qualsevol missatge a la DLQ és un incident. --treat-missing-data notBreaching és
important a totes dues, perquè SQS deixa de publicar mètriques d'una cua que fa hores que està buida i
sense aquest ajust l'alarma passaria a INSUFFICIENT_DATA generant soroll nocturn. Totes dues s'afegeixen
al tauler mercadofresco-produccion.
Autoescalat per profunditat. L'ASG de treballadors asg-mercadofresco-trabajadores no ha d'escalar
per CPU: un consumidor que espera l'ERP té la CPU al 4 % i està saturat. La mètrica correcta és el
retard per instància.
def publicar_retard_per_instancia():
"""S'executa cada minut (EventBridge Scheduler, que veurem a 07-03)."""
a = sqs.get_queue_attributes(QueueUrl=CUA, AttributeNames=[
"ApproximateNumberOfMessages", "ApproximateNumberOfMessagesNotVisible"])["Attributes"]
pendents = int(a["ApproximateNumberOfMessages"]) + \
int(a["ApproximateNumberOfMessagesNotVisible"])
grup = asg.describe_auto_scaling_groups(
AutoScalingGroupNames=["asg-mercadofresco-trabajadores"])["AutoScalingGroups"][0]
en_servei = max(1, sum(1 for i in grup["Instances"]
if i["LifecycleState"] == "InService"))
cw.put_metric_data(Namespace="MercadoFresco/Tienda", MetricData=[{
"MetricName": "MensajesPendientesPorInstancia",
"Dimensions": [{"Name": "Cola", "Value": "cola-mercadofresco-pedidos"}],
"Value": pendents / en_servei, "Unit": "Count"}])Sobre aquesta mètrica es defineix una política de seguiment d'objectiu amb valor 35. El número no és arbitrari: un treballador processa uns 7 missatges per minut contra l'ERP i la Marta vol buidar la cua en 5 minuts com a màxim, així que 7 × 5 = 35.
Cost real i neteja
SQS cobra exclusivament per petició a l'API, amb el primer milió mensual gratis de manera permanent. Un enviament, una recepció (porti 0 o 10 missatges) i un esborrat són una petició cadascun; les operacions per lots compten com una de sola, que és la raó econòmica de fer-les servir sempre.
| Concepte | Peticions/mes |
|---|---|
| Enviaments a la cua de comandes (240.000 comandes/mes) | 240.000 |
| Recepcions amb sondeig llarg i lots de 10 | 190.000 |
| Esborrats per lots | 24.000 |
| Cua de correu (enviament + recepció + esborrat) | 300.000 |
| Miniatures i estoc | 160.000 |
| Total | ~914.000 → dins del milió gratuït |
KMS (GenerateDataKey, reutilització 300 s) |
~8.600 → 0,03 USD |
Cost total: pràcticament zero. Tot el desacoblament de MercadoFresco cap al nivell gratuït
permanent, i l'única despesa apreciable són les crides a KMS. A canvi, 2.500 ms menys per petició
alliberen fils a asg-mercadofresco-tienda molt abans i el grup escala a 3 en lloc de a 4 els
divendres. Dues maneres de convertir aquest zero en una factura desagradable: sondeig curt en bucle
(uns 100 USD al mes per procés ociós) i un KmsDataKeyReusePeriodSeconds baix amb molts consumidors.
for C in cola-mercadofresco-pedidos cola-mercadofresco-correo \
cola-mercadofresco-stock.fifo mercadofresco-pedidos-fallidos; do
aws sqs delete-queue --queue-url https://sqs.eu-west-1.amazonaws.com/111122223333/$C $PERFIL
done
aws lambda delete-event-source-mapping --uuid <UUID_DEL_MAPATGE> $PERFIL
aws cloudwatch delete-alarms --alarm-names mercadofresco-pedidos-cola-retrasada \
mercadofresco-pedidos-dlq-con-mensajes $PERFILEsborrar una cua triga fins a 60 segons i no se'n pot crear una altra amb el mateix nom durant aquest minut, un detall molest en scripts que esborren i recreen.
Errors Habituals i Consells
Temps de visibilitat més curt que el processament. L'error número u. Símptoma: duplicats aleatoris
que creixen amb la càrrega i ReceiptHandleIsInvalid als registres. Mesura el p99 del teu consumidor,
multiplica'l per dos, i si no el pots acotar fes servir batec amb ChangeMessageVisibility.
Deixar el sondeig curt per defecte. Una cua creada per consola té
ReceiveMessageWaitTimeSeconds=0. Posa'l a 20 a la cua i a cada crida.
Suposar que un missatge arriba un sol cop. Les cues estàndard dupliquen per disseny. Tot consumidor amb efectes externs —cobrar, enviar correu, cridar un soci— ha de ser idempotent (07-05).
Crear una cua FIFO «per si de cas». Limita el rendiment, complica el consumidor i indueix a creure
que ja no calen reintents idempotents. I un sol MessageGroupId per a tota la cua FIFO converteix
una cua distribuïda en un procés en sèrie: un missatge en vol com a màxim, tinguis els consumidors que
tinguis.
Cua sense DLQ. Un missatge enverinat es reintenta fins a esgotar la retenció i —en FIFO— bloqueja tot el seu grup durant dies. I DLQ sense alarma és pitjor que no tenir-ne: dona falsa sensació de control mentre les comandes s'acumulen en silenci.
Llegir resposta["Messages"] sense .get() dona KeyError al primer sondeig buit; oblidar
MessageAttributeNames=["All"] fa que els atributs arribin buits i el consumidor decideixi amb
valors per defecte; i ignorar Failed a send_message_batch i delete_message_batch amaga
fallades parcials sota un HTTP 200.
Xifrar amb KMS i oblidar els permisos de clau. El símptoma és un AccessDenied confús que esmenta
KMS i no SQS.
Consell: posa el MessageId a totes les línies de registre. Quan investiguis un missatge de la DLQ
tres dies després, serà l'única cosa que et permetrà reconstruir la història.
Consell: una cua per tipus de feina. Una cua compartida fa que el consumidor lent de l'ERP endarrereixi els correus. Cues separades permeten visibilitat, concurrència i prioritats diferents. I no facis servir SQS per a prioritats: no existeix la prioritat de missatge; si necessites urgent i normal, crea dues cues i dona més consumidors a la urgent.
Exercicis
Exercici 1: dimensionar la cua del magatzem
El consumidor de cola-mercadofresco-almacen crida l'ERP del soci. La Marta mesura: p50 1.180 ms, p95
6.500 ms, p99 31 s, pitjor cas en un mes 74 s. L'ERP accepta com a màxim 12 peticions concurrents. El
divendres entren 900 comandes/hora i cap comanda no pot trigar més de 10 minuts a arribar al magatzem.
Determina: (a) el VisibilityTimeout i la seva justificació; (b) si convé Lambda o instàncies; (c) els
paràmetres del mapatge d'origen d'esdeveniments si tries Lambda; (d) maxReceiveCount i retenció de la
DLQ; (e) dues alarmes amb llindars calculats a partir de les dades, no inventats.
Exercici 2: el diagnòstic del correu duplicat
Tres divendres seguits arriben queixes de correus de confirmació duplicats —dos, de vegades tres—
sempre entre les 18:00 i les 21:00. Les dades: ApproximateNumberOfMessagesVisible oscil·la entre 0 i
40; ApproximateAgeOfOldestMessage màxim 55 s; la Lambda té timeout 60 s, durada p50 900 ms i p99
47 s; la cua té VisibilityTimeout 60 s, batch-size 10 i sense ReportBatchItemFailures; la DLQ
està buida; apareixen Task timed out after 60.00 seconds ocasionals.
Explica: (a) les dues causes independents de duplicat; (b) per què només passa al pic; (c) per què la DLQ buida no és tranquil·litzadora; (d) les correccions amb valors concrets; (e) quina elimina el problema d'arrel i quina només en redueix la freqüència.
Exercici 3: FIFO per a l'estoc
Dissenya cola-mercadofresco-stock.fifo. Els missatges són {"sku": "...", "delta": -2, "pedido_id": "...", "motivo": "reserva|liberacion|reposicion"}. Hi ha 3.400 SKU i al pic es generen 2.800 moviments
per hora, concentrats en els 200 SKU més venuts.
Respon: (a) què faries servir com a MessageGroupId i quines tres alternatives descartes; (b) què
faries servir com a MessageDeduplicationId i si activaries ContentBasedDeduplication; (c) què passa
si un missatge del SKU FRUT-FRES-011 és enverinat i com ho mitigues; (d) si el mode d'alt rendiment és
adequat; (e) per què aquesta cua no pot fer servir DelaySeconds per missatge i què faries si et calgués.
Solucions
Solució 1
(a) El pitjor cas és 74 s, així que 180 segons és defensable: 2,4 vegades el pitjor cas registrat. No convé pujar-lo a 900, perquè si el treballador mor el missatge quedaria bloquejat 15 minuts i l'objectiu és de 10. Alternativa superior: 120 s amb batec, que dona recuperació ràpida davant d'una caiguda del treballador sense límit superior real de durada.
(b) Lambda, amb reserves. A favor: feina esporàdica, sense estat i de durada variable; amb gairebé
gens de trànsit de matinada, pagar instàncies enceses és absurd. En contra: el p99 de 31 s obliga a un
timeout alt i una funció que espera es paga sencera. El factor decisiu és el límit de 12 concurrents
de l'ERP: MaximumConcurrency l'imposa de manera declarativa, mentre que amb instàncies caldria un
semàfor distribuït.
(c) --batch-size 1 (amb lots de 10 i 74 s per missatge, una invocació podria necessitar 740 s);
--maximum-batching-window-in-seconds 0; MaximumConcurrency=12; timeout de la funció 120 s; i
VisibilityTimeout de la cua 720 s, aplicant la regla de 6 × timeout, que aquí guanya a
l'estimació de (a). Comprovació de capacitat: 12 ÷ 1,18 s = 10 comandes/s = 36.000/hora, i fins i tot al
p95 (6,5 s) són 6.600/hora. Marge de sobres sobre 900.
(d) maxReceiveCount=4: amb 720 s de visibilitat, tres reintents cobreixen uns 36 minuts
d'indisponibilitat abans d'aïllar. Retenció de la DLQ 14 dies, el màxim, perquè el que hi cau són
vendes cobrades que la Marta ha de poder recuperar encara que l'incident sigui un divendres de pont.
(e) ApproximateAgeOfOldestMessage > 480 durant 2 períodes de 60 s: el requisit són 600 s, així que
s'avisa als 8 minuts per deixar marge de reacció. I ApproximateNumberOfMessagesVisible > 0 a la DLQ.
Una tercera recomanable: Errors de la funció > 10 en 5 minuts, que detecta l'ERP caigut abans.
Solució 2
(a) Causa 1: el temps de visibilitat és igual al timeout de la funció. Tots dos són 60 s: quan una
invocació triga 47 s o esgota el timeout, el missatge es fa visible abans o just quan la funció acaba,
i Lambda el lliura un altre cop. Falta el factor 6 recomanat. Causa 2: no està activat
ReportBatchItemFailures. Amb batch-size 10, si el missatge número 7 falla o el lot esgota el
timeout, cap dels deu no s'esborra i els altres nou es reprocessen; això explica els casos de tres
duplicats.
(b) Totes dues causes depenen de la latència de l'SMTP, que es degrada quan s'envien molts correus alhora. En vall la funció triga 900 ms i no s'acosta a cap límit; a partir de les 18:00 la concurrència creix amb la profunditat de la cua, el proveïdor estrangula, la durada es dispara al p99 i les dues causes s'activen alhora. És una fallada que no es reprodueix en proves perquè només apareix amb concurrència real.
(c) Que la DLQ estigui buida significa que cap missatge no va esgotar les seves recepcions, no que tot vagi bé. Aquí la fallada és de doble lliurament amb èxit: el missatge es processa correctament dues vegades i s'esborra. Un duplicat reeixit mai no arriba a la DLQ. La DLQ detecta feina que no es va fer, mai feina que es va fer de més.
(d) VisibilityTimeout a 360 s (6 × 60); activar ReportBatchItemFailures i retornar
batchItemFailures; MaximumConcurrency=20 per no estrangular l'SMTP; batch-size a 5 per acotar la
durada per invocació; idempotència al consumidor escrivint PEDIDO#<id>#correo-confirmacion a
DynamoDB amb escriptura condicional i TTL de 24 h; i alarmes sobre Duration p99 i Throttles.
(e) Les quatre primeres redueixen la freqüència: fan els duplicats molt més rars però no impossibles, perquè les cues estàndard dupliquen per disseny. L'única que elimina el problema d'arrel és la idempotència: rebre el missatge dues vegades passa a ser inofensiu. Aquesta és la tesi de 07-05, i el motiu que la configuració correcta sigui necessària però mai suficient.
Solució 3
(a) MessageGroupId = sku. L'ordre només importa entre moviments del mateix producte, i amb
3.400 SKU hi ha paral·lelisme de sobres. Descartades: un grup fix, que imposa ordre global
innecessari i un sol missatge en vol; pedido_id, que garanteix l'ordre dins d'una comanda però
no entre comandes diferents del mateix SKU, que és justament on hi ha la condició de cursa; i
categoria, que deixa una dotzena de grups i crea grups calents a les categories més venudes —el
mateix error que una clau de partició calenta a DynamoDB (06-02)—.
(b) Un identificador determinista i únic per moviment lògic: f"{pedido_id}:{sku}:{motivo}". Si el
productor reintenta després d'un temps d'espera esgotat, el mateix moviment produeix el mateix
identificador i SQS el descarta dins de la finestra de 5 minuts. ContentBasedDeduplication es deixa
desactivat: el cos inclou una marca de temps, amb la qual cosa el hash canviaria entre reintents; i
pitjor encara, dos moviments legítimament idèntics —dues reposicions de 10 unitats del mateix SKU a la
mateixa finestra— es deduplicarien per error, perdent estoc real.
(c) És l'escenari més greu de FIFO: com que SQS no lliura el missatge següent del grup fins que
l'actual s'esborra, tot el grup FRUT-FRES-011 queda bloquejat, i amb retenció de 4 dies aquell SKU
passaria dies sense actualitzar estoc mentre els altres funcionen —una fallada parcial silenciosa—.
Mitigació: DLQ amb maxReceiveCount=3; alarma sobre la DLQ; validació d'esquema al productor;
distingir permanent de transitori al consumidor; i vigilar ApproximateAgeOfOldestMessage, que en FIFO
delata un grup bloquejat encara que la profunditat total sigui baixa.
(d) Sí, i és gratuït. DeduplicationScope=messageGroup amb
FifoThroughputLimit=perMessageGroupId aixeca el límit de 300 msg/s per cua. Encara que 2.800
moviments/hora són només 0,8 msg/s, activar-ho protegeix davant de campanyes o reposicions massives;
l'única conseqüència és que la deduplicació passa a ser per grup, irrellevant perquè l'identificador ja
inclou el sku.
(e) Les cues FIFO no admeten temporitzador per missatge, només el retard a escala de cua. Per
ajornar un moviment concret —alliberar l'estoc d'una comanda no pagada als 15 minuts— hi ha tres
opcions: DelaySeconds de cua, descartada perquè retardaria també les reserves urgents; retornar el
missatge amb ChangeMessageVisibility, descartada perquè en FIFO bloqueja el grup sencer; i la
correcta, treure l'ajornament de la cua i fer servir un estat Wait de Step Functions (07-04) o una
regla programada d'EventBridge (07-03), de manera que el missatge només entri a la cua quan calgui
aplicar-lo.
Conclusió
Confirmar una comanda a MercadoFresco ha passat de 2.893 ms de mediana a uns 400 ms al percentil 95, i
de vuit punts de fallada encadenats a dos: cobrar la targeta i escriure a Aurora. La resta viu ara a
cola-mercadofresco-pedidos i cola-mercadofresco-correo, amb cola-mercadofresco-stock.fifo per a
l'única cosa que necessitava ordre real i mercadofresco-pedidos-fallidos recollint el que no es va
poder processar. L'ERP del magatzem pot caure dues hores sense que es perdi ni una sola venda.
Pel camí has vist els conceptes que es repetiran tot el mòdul: el model de sondeig, que converteix
la cua en esmorteïdor i dona contrapressió de franc; el temps d'espera de visibilitat, font número u
de duplicats quan es queda curt; el fet que no esborrar és reprocessar, que és alhora la garantia de
durabilitat i la raó que els consumidors hagin de ser idempotents; el sondeig llarg, que separa una
factura de zero d'una de centenars d'euros; i les cues de missatges fallits amb el seu
maxReceiveCount, la seva alarma obligatòria i el seu redrive.
Però ha quedat una costura a la vista. cola-mercadofresco-pedidos és un canal punt a punt: cada
missatge el processa un consumidor i desapareix. Amb magatzem, correu, repartiment i analítica
interessats en el mateix fet —«s'ha confirmat una comanda»—, o la botiga envia quatre missatges a quatre
cues diferents —i aleshores torna a saber qui són els seus consumidors, just el que volíem evitar— o un
consumidor únic reparteix la feina i es converteix en el nou punt de fallada. Quan el mes que ve
màrqueting demani assabentar-se també de les comandes per al seu programa de fidelització, caldrà tornar
a tocar el codi de la botiga. El desacoblament està a mitges.
A 07-02, «Amazon SNS», resoldrem aquesta meitat que falta: un tema al qual la botiga publica un
cop i diverses cues subscrites que reben cadascuna la seva còpia. Veurem el patró fan-out SNS→SQS, per
què convé posar una cua entre el tema i cada consumidor en lloc de subscriure funcions Lambda
directament, com filtrar per atributs perquè la cua del repartiment en 24 hores només rebi el que li
pertoca, i per què el tema alertas-mercadofresco que fem servir des del mòdul 5 és exactament el
mateix mecanisme aplicat a les alarmes.
Curs d'AWS
Mòdul 1: Introducció a AWS
- Què és AWS?
- Configuració del teu compte d'AWS
- Infraestructura global d'AWS
- Consola d'administració d'AWS
- AWS CLI i SDK
Mòdul 2: Serveis principals d'AWS
Mòdul 3: Xarxes i lliurament de contingut
- Amazon VPC
- Grups de seguretat i llistes de control d'accés
- Elastic Load Balancing
- Amazon CloudFront
- Route 53
Mòdul 4: Seguretat i identitat
- AWS Identity and Access Management (IAM)
- AWS Key Management Service (KMS)
- Secrets Manager i Parameter Store
- AWS Shield
- AWS WAF
Mòdul 5: Monitoratge i gestió
- Amazon CloudWatch
- AWS X-Ray i traçabilitat distribuïda
- AWS CloudTrail
- AWS Config
- AWS Trusted Advisor
Mòdul 6: Bases de dades
- Com triar la base de dades adequada
- Amazon DynamoDB
- Amazon Aurora
- Amazon Redshift
- Amazon ElastiCache
Mòdul 7: Integració d'aplicacions
- Amazon SQS
- Amazon SNS
- Amazon EventBridge
- AWS Step Functions
- Patrons d'integració: idempotència, reintents i cues de missatges fallits
