El módulo 6 terminó con un diagnóstico incómodo. La capa de datos de MercadoFresco está bien repartida —Aurora para pedidos y stock, DynamoDB para carritos y sesiones, Redshift para los informes de Sara, ElastiCache para el catálogo—, pero la petición que confirma un pedido sigue haciendo ocho cosas seguidas antes de responder. Marta lo midió en el pico del viernes: 2.900 ms de mediana, 9.100 ms en el percentil 95, y un 0,4 % de pedidos que fallan después de haber cobrado porque el ERP del almacén devolvió un 503.
Amazon SQS (Simple Queue Service) rompe esa cadena. Es una cola de mensajes gestionada: un componente escribe un mensaje, otro lo lee cuando puede, y ninguno necesita que el otro esté vivo en ese instante. Es el servicio más antiguo de AWS y también el más aburrido en el mejor sentido: sin servidores, sin versiones, sin capacidad que dimensionar, y escala de cero a millones de mensajes por segundo sin que nadie toque nada. Aquí Luis convierte las ocho llamadas encadenadas en una confirmación de 400 ms más siete tareas que ocurren después, y aprende por el camino el concepto que más incidentes causa en producción: el tiempo de espera de visibilidad.
Aviso de coste. SQS cobra por petición a la API: 1 millón gratis al mes de forma permanente y 0,40 USD por millón adicional en colas estándar (0,50 en FIFO). Una cola vacía no cuesta nada, pero un consumidor con sondeo corto puede generar cientos de millones de peticiones vacías al mes. Al final tienes los comandos de limpieza. Todos los datos son ficticios.
Contenido
- Síncrono frente a asíncrono: qué gana MercadoFresco
- Anatomía de una cola y el modelo de sondeo
- Colas estándar y colas FIFO
- Qué cola usa cada tarea de MercadoFresco
- Ciclo de vida de un mensaje
- El tiempo de espera de visibilidad, en detalle
- Sondeo corto y sondeo largo
- Retención, retardo, temporizadores y cargas grandes
- Colas de mensajes fallidos y redrive
- Crear las colas por CLI, política de cola y cifrado
- Productor y consumidor en Python
- Consumir desde Lambda: mapeo de origen de eventos
- Métricas, alarmas y autoescalado por profundidad de cola
- Coste real y limpieza
- Errores comunes y consejos
- Ejercicios
- Conclusión
Síncrono frente a asíncrono: qué gana MercadoFresco
Una llamada síncrona es aquella en la que quien llama espera la respuesta antes de continuar. No tiene nada de malo mientras se use para lo que corresponde: obtener un dato que se necesita ahora para poder responder. El problema aparece al encadenar llamadas síncronas a cosas que no se necesitan ahora. Estas son las ocho tareas de MercadoFresco, medidas en el pico del viernes:
| # | Tarea | Destino | p50 | p95 | ¿El cliente necesita el resultado? |
|---|---|---|---|---|---|
| 1 | Cobrar la tarjeta | Pasarela externa | 240 ms | 3.100 ms | Sí |
| 2 | Escribir el pedido | Aurora | 38 ms | 60 ms | Sí |
| 3 | Vaciar el carrito | DynamoDB | 11 ms | 22 ms | No |
| 4 | Invalidar la caché | ElastiCache | 4 ms | 9 ms | No |
| 5 | Avisar al ERP del almacén | HTTP del socio | 1.180 ms | 6.500 ms | No |
| 6 | Correo de confirmación | Proveedor SMTP | 820 ms | 4.200 ms | No |
| 7 | Notificar al repartidor | API del socio | 410 ms | 2.900 ms | No |
| 8 | Publicar evento de analítica | Firehose | 190 ms | 700 ms | No |
Solo las dos primeras son imprescindibles para decir «tu pedido está confirmado». Las seis restantes son consecuencias del pedido, no requisitos, y sin embargo el cliente las paga en tiempo de espera y en riesgo. La suma secuencial da 2.893 ms de mediana, pero lo grave es la fragilidad multiplicativa: si cada tarea tiene un 99,9 % de disponibilidad, la cadena de ocho tiene 99,2 %. Con 900 pedidos por hora en el pico, son siete pedidos rotos por hora, todos ya cobrados.
flowchart TD
C[Cliente pulsa Confirmar pedido] --> A[Servidor de la tienda]
A --> P1[1. Pasarela de pago 240 ms]
P1 --> P2[2. Aurora INSERT 38 ms]
P2 --> P3[3. DynamoDB vaciar carrito 11 ms]
P3 --> P4[4. ElastiCache invalidar 4 ms]
P4 --> P5[5. ERP del almacen 1.180 ms]
P5 --> P6[6. Correo de confirmacion 820 ms]
P6 --> P7[7. API del repartidor 410 ms]
P7 --> P8[8. Evento de analitica 190 ms]
P8 --> R[Respuesta al cliente 2.893 ms p50]
P5 -. 503 del ERP .-> X[Pedido cobrado y perdido]
style X fill:#f8d7da,stroke:#c00
style R fill:#fff3cd
Una llamada asíncrona rompe el encadenamiento: la tienda deja constancia de que hay trabajo pendiente y responde. El trabajo se hace después, por otro proceso, con sus propios reintentos.
flowchart TD
C[Cliente pulsa Confirmar pedido] --> A[Servidor de la tienda]
A --> P1[1. Pasarela de pago 240 ms]
P1 --> P2[2. Aurora INSERT 38 ms]
P2 --> Q[(cola-mercadofresco-pedidos<br/>SendMessage 12 ms)]
Q --> R[Respuesta al cliente ~400 ms p95]
Q --> W1[Consumidor almacen]
Q --> W2[Consumidor correo]
Q --> W3[Consumidor analitica]
W1 --> ERP[ERP del almacen]
W2 --> SMTP[Proveedor de correo]
W3 --> FH[Firehose y S3]
ERP -. 503 .-> RT[Reintento automatico:<br/>el mensaje sigue en la cola]
style R fill:#d4edda
style RT fill:#fff3cd
Lo que se gana no es solo velocidad: el fallo del ERP deja de ser el fallo del pedido. El mensaje sigue en la cola, el consumidor lo reintenta, y si el ERP tarda dos horas en volver, los pedidos de esas dos horas se procesan cuando vuelva. El cliente nunca se entera.
Anatomía de una cola y el modelo de sondeo
Cuatro conceptos y nada más. El productor envía mensajes; en MercadoFresco, la tienda en
asg-mercadofresco-tienda, que solo necesita sqs:SendMessage y la URL de la cola. La cola es el
almacén duradero: SQS replica cada mensaje en varios servidores de varias AZ de eu-west-1 antes de
confirmar el envío, y no tiene tamaño máximo ni capacidad que aprovisionar. El mensaje tiene un
cuerpo (Body) de hasta 256 KiB de texto, hasta 10 atributos de mensaje tipados que viajan
fuera del cuerpo, y atributos del sistema (MessageId, SentTimestamp, ApproximateReceiveCount). El
consumidor lee, trabaja y borra; puede haber uno o mil sin que se pisen.
La distinción entre cuerpo y atributos importa: los atributos se inspeccionan sin deserializar el cuerpo y —clave en 07-02— SNS filtra por atributos. Regla de MercadoFresco: en atributos va lo que sirve para decidir qué hacer con el mensaje; en el cuerpo, el dato.
{
"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 cuerpo es una cadena, no un objeto: SQS no sabe qué hay dentro. La serialización y el versionado del contrato son cosa tuya, y volveremos a ello en 07-03.
Sondeo (pull) frente a inserción (push). Esta es la característica que define a SQS: nunca llama a nadie. No hay forma de decirle «cuando llegue un mensaje, envíalo a esta URL». Es el consumidor quien pregunta.
| Sondeo (SQS) | Inserción (SNS, webhooks) | |
|---|---|---|
| Quién inicia | El consumidor pregunta | El servicio entrega |
| Ritmo lo marca | El consumidor | El productor |
| Si el consumidor está caído | Los mensajes esperan | Se pierden o se reintentan a ciegas |
| Contrapresión natural | Sí: se lee lo que se puede | No: llega lo que llega |
| Necesita endpoint público | No | Sí (salvo destinos internos de AWS) |
| Reintento | Implícito: si no borras, vuelve | Configurado por el emisor |
La consecuencia arquitectónica es enorme: la cola actúa de amortiguador. Si entran 900 pedidos/hora y los consumidores procesan 600, no se pierde nada: la cola crece y a las 22:00 se vacía. Esa propiedad —la contrapresión— es la razón de que las colas sigan siendo la pieza básica de integración, y la retomaremos en 07-05.
La aparente excepción es Lambda: cuando conectas una cola a una función parece que SQS «empuja». No es
así; el servicio Lambda mantiene sondeadores propios que llaman a ReceiveMessage por ti. El modelo
sigue siendo de sondeo, solo que el sondeador lo pone AWS.
Colas estándar y colas FIFO
| Cola estándar | Cola FIFO | |
|---|---|---|
| Nombre | Libre | Debe acabar en .fifo |
| Orden | Best-effort: casi siempre, no garantizado | Estricto dentro de cada grupo |
| Entrega | Al menos una vez (puede duplicar) | Exactamente una vez dentro de la ventana de deduplicación |
| Rendimiento | Prácticamente ilimitado | 300 msg/s (3.000 con lotes); alto rendimiento: decenas de miles |
| Grupos | No existen | MessageGroupId obligatorio |
| Deduplicación | No | Por MessageDeduplicationId o hash del cuerpo, ventana de 5 min |
| Paralelismo | Total | Un mensaje en vuelo por grupo |
| Precio | 0,40 USD/millón | 0,50 USD/millón |
Tres ideas que conviene interiorizar. «Al menos una vez» significa que habrá duplicados: no es teórico, SQS almacena cada mensaje en varios servidores y ocasionalmente uno no se entera de que ya fue borrado y lo reentrega. Si tu consumidor cobra una tarjeta, un duplicado es cobrar dos veces, y la solución no es FIFO sino la idempotencia del consumidor (07-05).
El «exactamente una vez» de FIFO tiene letra pequeña: solo actúa dentro de una ventana de 5 minutos y solo cubre el envío duplicado del mismo mensaje; si el consumidor procesa y muere antes de borrar, el mensaje vuelve. FIFO reduce mucho la probabilidad de duplicado; no la elimina.
Los grupos son la unidad de orden y de paralelismo a la vez. Para garantizar el orden, SQS no
entrega un segundo mensaje del grupo hasta que el primero se ha borrado. Un único grupo para toda la
cola da orden global y un solo consumidor efectivo; el sku como grupo da orden por producto y tanto
paralelismo como productos haya. Es la misma decisión que elegir clave de partición en DynamoDB (06-02).
Qué cola usa cada tarea de MercadoFresco
| Tarea | Cola | Tipo | Por qué |
|---|---|---|---|
| Preparar la caja | cola-mercadofresco-almacen |
Estándar | El almacén reconcilia por pedido_id; el orden entre pedidos da igual |
| Correo de confirmación | cola-mercadofresco-correo |
Estándar | Duplicar un correo es molesto, no catastrófico; se deduplica en el consumidor |
| Notificar al repartidor | cola-mercadofresco-reparto |
Estándar | Ídem |
| Evento de analítica | cola-mercadofresco-analitica |
Estándar | Redshift agrega; los duplicados se filtran en la carga |
| Movimientos de stock | cola-mercadofresco-stock.fifo |
FIFO, grupo = sku |
«Reservar 2» y «liberar 2» del mismo SKU deben ir en orden |
| Miniaturas de fotos | cola-mercadofresco-miniaturas |
Estándar | Regenerar una miniatura es idempotente por naturaleza |
Solo una de las seis necesita FIFO, y es justo la que toca contadores. Es lo habitual: la mayoría de las cargas no necesitan orden global, necesitan un consumidor idempotente. Pagar el precio de FIFO cuando no hace falta es un error de diseño caro.
En esta lección Luis empieza por lo mínimo viable: cola-mercadofresco-pedidos recoge todo el trabajo
posterior a la confirmación, y cola-mercadofresco-correo separa el envío de correos, el consumidor más
lento y el que más falla. En 07-02 veremos por qué esa cola única acaba siendo un problema.
Ciclo de vida de un mensaje
sequenceDiagram
participant P as Productor (tienda)
participant Q as cola-mercadofresco-pedidos
participant C as Consumidor
P->>Q: SendMessage(Body, MessageAttributes)
Q-->>P: MessageId (replicado y duradero)
Note over Q: Estado: visible
C->>Q: ReceiveMessage(Max=10, WaitTimeSeconds=20)
Q-->>C: Mensajes + ReceiptHandle
Note over Q: Estado: en vuelo (invisible)<br/>durante VisibilityTimeout
alt Trabajo correcto
C->>Q: DeleteMessage(ReceiptHandle)
Note over Q: El mensaje desaparece
else El consumidor falla o se cae
Note over Q: Expira el tiempo de visibilidad
Note over Q: Visible otra vez<br/>ApproximateReceiveCount += 1
Q-->>C: Se vuelve a entregar
end
Tres estados y solo tres: visible, en vuelo y borrado. Con eso se explica todo el comportamiento de SQS, incluidos los que parecen fallos del servicio. La parte que sorprende siempre: recibir un mensaje no lo elimina. SQS lo entrega y lo esconde del resto de consumidores, pero sigue ahí; si tu proceso muere a mitad, si el contenedor se reinicia, si el ASG apaga la instancia, el mensaje reaparece y otro consumidor lo coge. Esa es la garantía de durabilidad.
El corolario: no borrar equivale a reprocesar. Si el consumidor lanza una excepción y no llega al
DeleteMessage, el mensaje volverá. Y si tarda más que el tiempo de visibilidad, volverá aunque
acabe bien, con lo que el trabajo se hará dos veces: la causa número uno de duplicados en producción.
El tiempo de espera de visibilidad, en detalle
El VisibilityTimeout es el número de segundos que un mensaje permanece invisible tras ser recibido.
Por defecto 30 s; máximo 12 horas. La regla es una sola frase:
El tiempo de visibilidad debe ser mayor que el tiempo que tarda el consumidor más lento en procesar el mensaje y borrarlo.
Veamos qué pasa si no se cumple. El consumidor de correo llama a un SMTP que en el p99 tarda 42 s, y el tiempo de visibilidad está en los 30 por defecto:
| t | Consumidor A | Consumidor B | Estado del mensaje |
|---|---|---|---|
| 0 s | Recibe PED-084417 | — | En vuelo |
| 5 s | Llama al SMTP | — | En vuelo |
| 30 s | Sigue esperando | — | Vuelve a ser visible |
| 31 s | Sigue esperando | Recibe el mismo mensaje | En vuelo (para B) |
| 42 s | Responde el SMTP, DeleteMessage |
Llama al SMTP | Borrado |
| 73 s | — | DeleteMessage → ReceiptHandleIsInvalid |
— |
El cliente ha recibido dos correos. Y el fallo es silencioso, difícil de reproducir en pruebas y solo aparece bajo carga. Hay tres formas de configurarlo:
1. En la cola, como valor por defecto:
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. En la recepción, pasando VisibilityTimeout a ReceiveMessage, útil cuando el mismo consumidor
atiende trabajos de coste dispar.
3. Extendiéndolo sobre la marcha (heartbeat). Si el trabajo puede durar mucho y no sabes cuánto, no
pongas 12 horas: pon un valor corto y aláragalo mientras trabajas con ChangeMessageVisibility. Si el
proceso muere, el mensaje vuelve en segundos en lugar de en horas.
def latido(receipt_handle, parar):
"""Extiende la visibilidad a 60 s cada 30 s mientras el trabajo siga en curso."""
while not parar.wait(30):
try:
sqs.change_message_visibility(QueueUrl=COLA, ReceiptHandle=receipt_handle,
VisibilityTimeout=60) # 60 s DESDE AHORA
except sqs.exceptions.ReceiptHandleIsInvalid:
break # ya se borró o expiró: nada que extender
def procesar_con_latido(mensaje):
parar = threading.Event()
hilo = threading.Thread(target=latido, args=(mensaje["ReceiptHandle"], parar), daemon=True)
hilo.start()
try:
preparar_caja_en_almacen(mensaje["Body"]) # de 2 s a 8 minutos
sqs.delete_message(QueueUrl=COLA, ReceiptHandle=mensaje["ReceiptHandle"])
finally:
parar.set() # detiene el latido pase lo que pase
hilo.join(timeout=2)Dos detalles: VisibilityTimeout=60 significa «invisible 60 segundos desde este momento», no «añade
60 a lo que quedaba»; y el total acumulado desde la primera recepción no puede superar 12 horas. El
mismo comando sirve para devolver un mensaje de inmediato con VisibilityTimeout=0 cuando detectas
que no puedes procesarlo ahora, o aplazarlo cinco minutos con 300.
Sondeo corto y sondeo largo
Con sondeo corto (WaitTimeSeconds=0, valor por defecto en una cola recién creada) SQS consulta un
subconjunto de servidores y responde de inmediato, aunque sea con una lista vacía. Puedes recibir una
respuesta vacía con mensajes en la cola, y tu bucle genera peticiones a máxima velocidad. Con
sondeo largo (1 a 20 s) SQS consulta todos los servidores y mantiene la conexión abierta hasta que
hay un mensaje o se agota la espera.
| Sondeo corto | Sondeo largo (20 s) | |
|---|---|---|
| Respuestas vacías con cola no vacía | Posible | No |
| Peticiones/hora de un consumidor ocioso | Hasta ~360.000 | 180 |
| Latencia al llegar un mensaje | Hasta el siguiente sondeo | Prácticamente inmediata |
| Coste mensual de 4 consumidores ociosos | ~415 USD | ~0,21 USD |
| Cuándo usarlo | Casi nunca | Siempre |
Los 415 USD no son retórica: cuatro procesos sondeando en bucle generan del orden de mil millones de peticiones al mes. Es el error de novato más caro de SQS y aparece en Cost Explorer (11-03) como una línea desproporcionada. Configúralo en la cola y en cada llamada:
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 usas sondeo largo, sube el tiempo de espera de lectura del cliente HTTP por encima de 20 segundos o el SDK cortará la conexión antes de tiempo.
Retención, retardo, temporizadores y cargas grandes
| Atributo | Rango | Por defecto | Uso en MercadoFresco |
|---|---|---|---|
MessageRetentionPeriod |
60 s – 14 días | 4 días | 14 días en las DLQ, 4 en las normales |
DelaySeconds de cola |
0 – 15 min | 0 | 10 s en cola-mercadofresco-correo |
DelaySeconds por mensaje |
0 – 15 min | — | 900 s en «tu pedido sale del almacén» |
MaximumMessageSize |
1 KiB – 256 KiB | 256 KiB | Por defecto |
Retención. Un mensaje que nadie borra se elimina al cumplir el periodo. Es una red de seguridad, no una función: si tus mensajes caducan, tienes un consumidor parado y una alarma que no saltó. En las DLQ se pone el máximo porque ahí el mensaje espera a que un humano lo mire.
Cola de retardo. Con DelaySeconds a nivel de cola, todos los mensajes nacen invisibles. Luis lo
usa en la cola de correo con 10 segundos: da margen a que la escritura en Aurora se replique a
aurora-mf-lector-1/-2 antes de que el consumidor lea el pedido. Sin ese margen, un 0,3 % de los correos
salían con «pedido no encontrado».
Temporizador por mensaje. El mismo efecto pero por mensaje, con DelaySeconds en SendMessage.
No está disponible en colas FIFO, donde solo existe el retardo a nivel de cola.
Cargas grandes. 256 KiB es mucho para un pedido y poco para una factura PDF. La solución no es
comprimir: es la biblioteca de cliente extendida (Extended Client Library), que guarda la carga en
S3 y envía por la cola solo un puntero {"s3_bucket": ..., "s3_key": ...}. A mano son tres líneas: un
put_object en mercadofresco-informes-analitica y un send_message con la referencia. Cuidado con el
ciclo de vida: si el consumidor borra el mensaje y nadie borra el objeto, pagas almacenamiento
indefinidamente; una regla de ciclo de vida de S3 (02-03) con caducidad a 7 días lo resuelve.
Colas de mensajes fallidos y redrive
Un mensaje que el consumidor no puede procesar vuelve a la cola. Si la causa es transitoria —el ERP
caído— es justo lo que quieres. Si es permanente —JSON mal formado, sku inexistente, un None donde
debía haber un número— el mensaje volverá para siempre, consumiendo peticiones, bloqueando el orden
en colas FIFO y ensuciando los registros. Es un mensaje envenenado (poison pill).
La cola de mensajes fallidos (DLQ) es una cola normal a la que SQS mueve los mensajes recibidos demasiadas veces. Se configura con una política de redrive en la cola de origen:
{
"deadLetterTargetArn": "arn:aws:sqs:eu-west-1:111122223333:mercadofresco-miniaturas-fallidas",
"maxReceiveCount": 3
}maxReceiveCount cuenta recepciones, no fallos: con 3, el mensaje se intenta tres veces y a la
cuarta recepción se mueve.
maxReceiveCount |
Efecto |
|---|---|
| 1 | Sin reintentos: cualquier fallo transitorio manda el mensaje a la DLQ. Casi nunca correcto |
| 3 – 5 | Recomendado: absorbe caídas cortas y aísla rápido los mensajes envenenados |
| 50+ | El envenenado se reintenta durante horas; los registros se llenan de ruido |
Aquí formalizamos mercadofresco-miniaturas-fallidas, que apareció en el módulo 2 junto a
mercadofresco-generar-miniaturas y nunca se configuró del todo. La arquitectura queda así: al subir
una foto a mercadofresco-catalogo-fotos, S3 publica en cola-mercadofresco-miniaturas; la Lambda
consume; si falla tres veces, el mensaje acaba en 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-1Ese escapado triple es tan feo como parece: RedrivePolicy es un JSON dentro de una cadena dentro de
otro JSON. En 09-01 lo escribiremos en CloudFormation y el problema desaparece.
Qué mirar cuando algo cae en la DLQ, en este orden: (1) el ApproximateReceiveCount —si es
exactamente maxReceiveCount + 1, agotó los reintentos—; (2) el cuerpo: ¿JSON válido?, ¿campos
esperados?, ¿versión del contrato conocida?; (3) los registros de CloudWatch del consumidor filtrados
por el MessageId. Aquí se paga el haber puesto el MessageId en cada línea de registro.
Redrive. Corregido el fallo, no hay que reenviar a mano:
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-1Sin --destination-arn los mensajes vuelven a su cola de origen. El límite de velocidad no es opcional
en la práctica: reprocesar 40.000 mensajes de golpe puede tumbar el sistema que ya estaba frágil. En
07-05 escribiremos el runbook completo de Marta.
Regla dura de MercadoFresco: toda cola tiene DLQ, y toda DLQ tiene alarma. Una DLQ sin alarma es un cubo de basura donde se pierden ventas en silencio.
Crear las colas por CLI, política de cola y cifrado
Empezamos por la DLQ, porque la cola principal necesita su ARN.
export PERFIL="--profile mercadofresco-dev --region eu-west-1"
export ETIQUETAS='Proyecto=mercadofresco,Entorno=produccion,Componente=integracion,Propietario=marta,CentroCoste=tecnologia'
aws sqs create-queue --queue-name mercadofresco-pedidos-fallidos \
--attributes MessageRetentionPeriod=1209600 --tags "$ETIQUETAS" $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 "$ETIQUETAS" $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 "$ETIQUETAS" $PERFIL \
--attributes '{"VisibilityTimeout":"90","DelaySeconds":"10",
"ReceiveMessageWaitTimeSeconds":"20",
"KmsMasterKeyId":"alias/mercadofresco-datos"}'Cada valor responde a una medición: VisibilityTimeout=120 en pedidos porque el consumidor del ERP
tarda 6,5 s en el p95 y 38 s en el peor caso observado; 90 en correo porque el SMTP llega a 42 s en el
p99; maxReceiveCount=4 para tres reintentos reales antes de aislar; y
KmsDataKeyReusePeriodSeconds=300, que hace que SQS reutilice la clave de datos 5 minutos y reduce
drásticamente las llamadas a KMS sin comprometer la seguridad de forma apreciable. La cola FIFO de stock
se crea distinto:
aws sqs create-queue --queue-name cola-mercadofresco-stock.fifo \
--attributes '{"FifoQueue":"true","ContentBasedDeduplication":"false",
"DeduplicationScope":"messageGroup","FifoThroughputLimit":"perMessageGroupId",
"VisibilityTimeout":"60","ReceiveMessageWaitTimeSeconds":"20"}' \
--tags "$ETIQUETAS" $PERFILDeduplicationScope=messageGroup con FifoThroughputLimit=perMessageGroupId activan el modo de alto
rendimiento: el límite pasa a aplicarse por grupo en lugar de por cola. Con el sku como grupo y
3.400 SKU, el paralelismo es más que suficiente.
Las dos capas de autorización. La política de identidad (IAM, 04-01) se adjunta al rol que llama. La política de cola (basada en recurso) se adjunta a la cola y es imprescindible cuando quien envía no es un principal IAM de tu cuenta: otro servicio de AWS (SNS, S3, EventBridge) u otra cuenta.
{
"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 permiso a ese bucket; sin él, cualquier bucket de S3 del mundo podría
escribir en tu cola. aws:SourceAccount cierra el problema del diputado confundido, en el que un
servicio de AWS actúa legítimamente en nombre de un tercero (04-01).
Cifrado. SQS ofrece SSE-SQS (claves de AWS, gratis, activo por defecto en colas nuevas) y SSE-KMS con
tu clave. MercadoFresco usa alias/mercadofresco-datos en las colas con datos personales —los pedidos
llevan dirección de entrega— por coherencia con el módulo 4 y porque las llamadas a KMS quedan en
trail-mercadofresco. Ojo con el detalle que rompe despliegues: si la cola está cifrada con KMS, el
productor necesita kms:GenerateDataKey y el consumidor kms:Decrypt sobre la clave, no solo sobre
la cola; y si el productor es un servicio de AWS, la política de la clave debe permitírselo.
Productor y consumidor en Python
import json, os
from datetime import datetime, timezone
import boto3
from botocore.config import Config
# Un solo cliente reutilizado: crearlo por petición resuelve credenciales y abre
# conexiones nuevas cada vez, un error de rendimiento clásico.
sqs = boto3.client("sqs", config=Config(
region_name="eu-west-1",
retries={"max_attempts": 5, "mode": "standard"}, # reintentos ante 5xx y throttling
connect_timeout=2, read_timeout=5, # acotan el peor caso del envío
))
COLA_PEDIDOS = os.environ["URL_COLA_PEDIDOS"]
def confirmar_pedido(carrito, cliente, tarjeta):
"""Confirma un pedido: cobra, persiste y encola. Nada más."""
cobro = pasarela.cobrar(tarjeta, carrito.importe_eur) # imprescindible
pedido_id = aurora.insertar_pedido(carrito, cliente, cobro.referencia) # imprescindible
cuerpo = {
"version": 1, "pedido_id": pedido_id, "cliente_id": cliente.id,
"importe_eur": float(carrito.importe_eur),
"franja_entrega": carrito.franja, # "24h" o "estandar"
"referencia_cobro": cobro.referencia,
"lineas": [{"sku": l.sku, "unidades": l.unidades} for l in carrito.lineas],
"confirmado_en": datetime.now(timezone.utc).isoformat(),
}
r = sqs.send_message(
QueueUrl=COLA_PEDIDOS,
MessageBody=json.dumps(cuerpo, ensure_ascii=False),
MessageAttributes={
"tipo_evento": {"DataType": "String", "StringValue": "PedidoConfirmado"},
"franja_entrega": {"DataType": "String", "StringValue": carrito.franja},
"version": {"DataType": "Number", "StringValue": "1"},
},
)
log.info("pedido encolado", extra={"pedido_id": pedido_id, "message_id": r["MessageId"]})
return pedido_idTres decisiones que merecen comentario. El send_message va después del INSERT: si se encolara
primero y el INSERT fallara, habría un mensaje anunciando un pedido inexistente. En este orden el
fallo posible es el contrario —pedido escrito y mensaje no enviado—, menos grave pero real, y tiene
nombre: el problema de la doble escritura, que resolveremos con el patrón outbox en 07-05. Los
reintentos del SDK están configurados con retroceso exponencial ante 5xx. Y los tiempos de espera
son cortos: sin ellos botocore usa 60 segundos, de sobra para agotar el pool de conexiones de la
tienda durante un incidente de SQS.
Para volúmenes altos —la carga nocturna encola 40.000 movimientos de stock— usa send_message_batch,
que acepta 10 mensajes por petición (Entries con Id y MessageBody) y divide la factura por diez.
La trampa: devuelve HTTP 200 aunque haya entradas fallidas, así que hay que recorrer
r.get("Failed", []) y registrar cada fallo. Si no lo haces, pierdes mensajes sin enterarte.
El consumidor que corre en una instancia o contenedor tiene una forma canónica que conviene copiar tal cual:
_seguir = True
def _parar(signum, frame):
"""Apagado ordenado: con SIGTERM del ASG o de ECS, termina el lote en curso."""
global _seguir
_seguir = False
signal.signal(signal.SIGTERM, _parar)
def bucle_de_consumo():
while _seguir:
respuesta = sqs.receive_message(
QueueUrl=COLA,
MaxNumberOfMessages=10, # el máximo: menos peticiones, menos coste
WaitTimeSeconds=20, # sondeo largo: imprescindible
MessageAttributeNames=["All"], # sin esto, MessageAttributes llega vacío
AttributeNames=["ApproximateReceiveCount", "SentTimestamp"],
)
mensajes = respuesta.get("Messages", []) # ¡la clave NO existe si no hay mensajes!
if not mensajes:
continue
borrables = []
for m in mensajes:
intentos = int(m["Attributes"]["ApproximateReceiveCount"])
try:
procesar_pedido(json.loads(m["Body"]), intentos)
borrables.append({"Id": m["MessageId"], "ReceiptHandle": m["ReceiptHandle"]})
except ErrorPermanente as e:
# Datos inválidos: reintentar no arregla nada. Se archiva y se borra para
# que no consuma cuatro recepciones antes de acabar en la DLQ.
log.error("mensaje invalido", extra={"message_id": m["MessageId"], "error": str(e)})
archivar_para_revision(m)
borrables.append({"Id": m["MessageId"], "ReceiptHandle": m["ReceiptHandle"]})
except Exception as e:
# Transitorio: NO se borra. Volverá tras el tiempo de visibilidad.
log.warning("fallo transitorio", extra={"message_id": m["MessageId"],
"intentos": intentos, "error": str(e)})
if borrables:
r = sqs.delete_message_batch(QueueUrl=COLA, Entries=borrables)
for fallo in r.get("Failed", []):
log.error("no se pudo borrar", extra={"id": fallo["Id"], "code": fallo["Code"]})Lo que separa un consumidor correcto de uno que da problemas a las tres semanas: usar
respuesta.get("Messages", []) (la respuesta no incluye la clave cuando está vacía, y
respuesta["Messages"] lanza KeyError en el primer sondeo); pedir MessageAttributeNames=["All"] o
los atributos llegarán vacíos; distinguir error permanente de transitorio; borrar por lotes; manejar
SIGTERM para que el ASG no mate al proceso a mitad de mensaje; y usar ApproximateReceiveCount como
señal para registrar más detalle o pasar a un camino degradado en el tercer intento.
Consumir desde Lambda: mapeo de origen de eventos
Para el consumidor de correo, montar instancias es desproporcionado: trabajo corto, esporádico y sin estado. La conexión entre cola y función se llama mapeo de origen de eventos.
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ámetro | Qué hace | Valor en MercadoFresco |
|---|---|---|
--batch-size |
Mensajes por invocación (1–10.000) | 10: el SMTP no se beneficia de más |
--maximum-batching-window-in-seconds |
Espera a llenar el lote (0–300) | 5: menos invocaciones en valle |
MaximumConcurrency |
Tope de instancias concurrentes (2–1.000) | 20: protege el límite del SMTP |
--function-response-types |
Activa los fallos parciales de lote | ReportBatchItemFailures |
El tope de concurrencia es la pieza defensiva clave. Lambda escala agresivamente con la profundidad de la cola: puede pasar de 5 a 60 sondeadores en un minuto. Si detrás hay Aurora con 200 conexiones o un SMTP con límite de 50 envíos por segundo, esa elasticidad se convierte en una denegación de servicio que te haces a ti mismo. Es la misma idea de contrapresión que generalizaremos en 07-05.
El problema del lote entero. Por defecto, si la función lanza una excepción, Lambda considera fallido
todo el lote y no borra ninguno de los diez mensajes: si nueve iban bien y uno mal, los nueve se
reprocesan. ReportBatchItemFailures lo arregla devolviendo solo los que fallaron.
import json
def handler(event, context):
"""Consumidor de cola-mercadofresco-correo con fallos parciales de lote."""
fallidos = []
for registro in event["Records"]:
try:
pedido = json.loads(registro["body"])
franja = registro.get("messageAttributes", {}) \
.get("franja_entrega", {}).get("stringValue", "estandar")
enviar_correo_confirmacion(pedido["cliente_id"], pedido["pedido_id"],
pedido["importe_eur"], franja)
except Exception as e:
print(json.dumps({"nivel": "ERROR", "message_id": registro["messageId"],
"error": str(e)}))
# Solo este mensaje vuelve a la cola; el resto del lote se borra.
fallidos.append({"itemIdentifier": registro["messageId"]})
return {"batchItemFailures": fallidos}Dos condiciones para que funcione: declarar ReportBatchItemFailures en el mapeo y devolver la
estructura exacta {"batchItemFailures": [{"itemIdentifier": "..."}]}. Si el nombre de la clave no es
ese, Lambda lo ignora en silencio.
Además: no hay que llamar a delete_message —Lambda borra los mensajes al terminar bien— y el
tiempo de visibilidad de la cola debe ser al menos 6 veces el timeout de la función, que es la
recomendación de AWS y evita que Lambda reciba el mismo mensaje mientras aún lo procesa.
Métricas, alarmas y autoescalado por profundidad de cola
SQS publica métricas en CloudWatch cada minuto y sin coste adicional.
| Métrica | Qué significa | Señal |
|---|---|---|
ApproximateNumberOfMessagesVisible |
Mensajes esperando consumidor | Profundidad de la cola |
ApproximateNumberOfMessagesNotVisible |
Mensajes en vuelo | Trabajo en curso |
ApproximateAgeOfOldestMessage |
Segundos del mensaje más antiguo | La métrica de salud real |
NumberOfMessagesSent / Deleted |
Flujo | Sent > Deleted sostenido = se acumula |
NumberOfEmptyReceives |
Sondeos vacíos | Alto = sondeo corto = factura |
SentMessageSize |
Tamaño medio | Cerca de 256 KiB = pasar a S3 |
Si solo puedes vigilar una, vigila ApproximateAgeOfOldestMessage. La profundidad engaña: 5.000
mensajes con consumidores rápidos es normal un viernes a las 19:00, y 40 mensajes parados una hora es un
incidente. La antigüedad responde a la pregunta que importa: ¿cuánto tarda hoy un pedido en llegar al
almacén?
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 segunda alarma es la misma orden cambiando la métrica a
ApproximateNumberOfMessagesVisible, la dimensión a mercadofresco-pedidos-fallidos, --threshold 0 y
--period 300: cualquier mensaje en la DLQ es un incidente. --treat-missing-data notBreaching es
importante en ambas, porque SQS deja de publicar métricas de una cola que lleva horas vacía y sin ese
ajuste la alarma pasaría a INSUFFICIENT_DATA generando ruido nocturno. Las dos se añaden al panel
mercadofresco-produccion.
Autoescalado por profundidad. El ASG de trabajadores asg-mercadofresco-trabajadores no debe escalar
por CPU: un consumidor que espera al ERP tiene la CPU al 4 % y está saturado. La métrica correcta es el
retraso por instancia.
def publicar_retraso_por_instancia():
"""Se ejecuta cada minuto (EventBridge Scheduler, que veremos en 07-03)."""
a = sqs.get_queue_attributes(QueueUrl=COLA, AttributeNames=[
"ApproximateNumberOfMessages", "ApproximateNumberOfMessagesNotVisible"])["Attributes"]
pendientes = int(a["ApproximateNumberOfMessages"]) + \
int(a["ApproximateNumberOfMessagesNotVisible"])
grupo = asg.describe_auto_scaling_groups(
AutoScalingGroupNames=["asg-mercadofresco-trabajadores"])["AutoScalingGroups"][0]
en_servicio = max(1, sum(1 for i in grupo["Instances"]
if i["LifecycleState"] == "InService"))
cw.put_metric_data(Namespace="MercadoFresco/Tienda", MetricData=[{
"MetricName": "MensajesPendientesPorInstancia",
"Dimensions": [{"Name": "Cola", "Value": "cola-mercadofresco-pedidos"}],
"Value": pendientes / en_servicio, "Unit": "Count"}])Sobre esa métrica se define una política de seguimiento de objetivo con valor 35. El número no es arbitrario: un trabajador procesa unos 7 mensajes por minuto contra el ERP y Marta quiere vaciar la cola en 5 minutos como máximo, así que 7 × 5 = 35.
Coste real y limpieza
SQS cobra exclusivamente por petición a la API, con el primer millón mensual gratis de forma permanente. Un envío, una recepción (traiga 0 o 10 mensajes) y un borrado son una petición cada uno; las operaciones por lotes cuentan como una sola, que es la razón económica de usarlas siempre.
| Concepto | Peticiones/mes |
|---|---|
| Envíos a la cola de pedidos (240.000 pedidos/mes) | 240.000 |
| Recepciones con sondeo largo y lotes de 10 | 190.000 |
| Borrados por lotes | 24.000 |
| Cola de correo (envío + recepción + borrado) | 300.000 |
| Miniaturas y stock | 160.000 |
| Total | ~914.000 → dentro del millón gratuito |
KMS (GenerateDataKey, reutilización 300 s) |
~8.600 → 0,03 USD |
Coste total: prácticamente cero. Todo el desacoplamiento de MercadoFresco cabe en el nivel gratuito
permanente, y el único gasto apreciable son las llamadas a KMS. A cambio, 2.500 ms menos por petición
liberan hilos en asg-mercadofresco-tienda mucho antes y el grupo escala a 3 en lugar de a 4 los
viernes. Dos formas de convertir ese cero en una factura desagradable: sondeo corto en bucle (unos
100 USD al mes por proceso ocioso) y un KmsDataKeyReusePeriodSeconds bajo con muchos consumidores.
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_MAPEO> $PERFIL
aws cloudwatch delete-alarms --alarm-names mercadofresco-pedidos-cola-retrasada \
mercadofresco-pedidos-dlq-con-mensajes $PERFILBorrar una cola tarda hasta 60 segundos y no se puede crear otra con el mismo nombre durante ese minuto, un detalle molesto en scripts que borran y recrean.
Errores Comunes y Consejos
Tiempo de visibilidad más corto que el procesamiento. El error número uno. Síntoma: duplicados
aleatorios que crecen con la carga y ReceiptHandleIsInvalid en los registros. Mide el p99 de tu
consumidor, multiplícalo por dos, y si no puedes acotarlo usa latido con ChangeMessageVisibility.
Dejar el sondeo corto por defecto. Una cola creada por consola tiene
ReceiveMessageWaitTimeSeconds=0. Ponlo a 20 en la cola y en cada llamada.
Suponer que un mensaje llega una sola vez. Las colas estándar duplican por diseño. Todo consumidor con efectos externos —cobrar, enviar correo, llamar a un socio— debe ser idempotente (07-05).
Crear una cola FIFO «por si acaso». Limita el rendimiento, complica el consumidor e induce a creer
que ya no hacen falta reintentos idempotentes. Y un solo MessageGroupId para toda la cola FIFO
convierte una cola distribuida en un proceso serie: un mensaje en vuelo como máximo, tengas los
consumidores que tengas.
Cola sin DLQ. Un mensaje envenenado se reintenta hasta agotar la retención y —en FIFO— bloquea todo su grupo durante días. Y DLQ sin alarma es peor que no tenerla: da falsa sensación de control mientras los pedidos se acumulan en silencio.
Leer respuesta["Messages"] sin .get() da KeyError en el primer sondeo vacío; olvidar
MessageAttributeNames=["All"] hace que los atributos lleguen vacíos y el consumidor decida con
valores por defecto; e ignorar Failed en send_message_batch y delete_message_batch esconde
fallos parciales bajo un HTTP 200.
Cifrar con KMS y olvidar los permisos de clave. El síntoma es un AccessDenied confuso que menciona
KMS y no SQS.
Consejo: mete el MessageId en todas las líneas de registro. Cuando investigues un mensaje de la DLQ
tres días después, será lo único que te permita reconstruir la historia.
Consejo: una cola por tipo de trabajo. Una cola compartida hace que el consumidor lento del ERP retrase los correos. Colas separadas permiten visibilidad, concurrencia y prioridades distintas. Y no uses SQS para prioridades: no existe la prioridad de mensaje; si necesitas urgente y normal, crea dos colas y da más consumidores a la urgente.
Ejercicios
Ejercicio 1: dimensionar la cola del almacén
El consumidor de cola-mercadofresco-almacen llama al ERP del socio. Marta mide: p50 1.180 ms, p95
6.500 ms, p99 31 s, peor caso en un mes 74 s. El ERP acepta como máximo 12 peticiones concurrentes. El
viernes entran 900 pedidos/hora y ningún pedido puede tardar más de 10 minutos en llegar al almacén.
Determina: (a) el VisibilityTimeout y su justificación; (b) si conviene Lambda o instancias; (c) los
parámetros del mapeo de origen de eventos si eliges Lambda; (d) maxReceiveCount y retención de la DLQ;
(e) dos alarmas con umbrales calculados a partir de los datos, no inventados.
Ejercicio 2: el diagnóstico del correo duplicado
Tres viernes seguidos llegan quejas de correos de confirmación duplicados —dos, a veces tres— siempre
entre las 18:00 y las 21:00. Los datos: ApproximateNumberOfMessagesVisible oscila entre 0 y 40;
ApproximateAgeOfOldestMessage máximo 55 s; la Lambda tiene timeout 60 s, duración p50 900 ms y p99
47 s; la cola tiene VisibilityTimeout 60 s, batch-size 10 y sin ReportBatchItemFailures; la DLQ
está vacía; aparecen Task timed out after 60.00 seconds ocasionales.
Explica: (a) las dos causas independientes de duplicado; (b) por qué solo ocurre en el pico; (c) por qué la DLQ vacía no es tranquilizadora; (d) las correcciones con valores concretos; (e) cuál elimina el problema de raíz y cuál solo reduce su frecuencia.
Ejercicio 3: FIFO para el stock
Diseña cola-mercadofresco-stock.fifo. Los mensajes son {"sku": "...", "delta": -2, "pedido_id": "...", "motivo": "reserva|liberacion|reposicion"}. Hay 3.400 SKU y en el pico se generan 2.800
movimientos por hora, concentrados en los 200 SKU más vendidos.
Responde: (a) qué usarías como MessageGroupId y qué tres alternativas descartas; (b) qué usarías como
MessageDeduplicationId y si activarías ContentBasedDeduplication; (c) qué pasa si un mensaje del SKU
FRUT-FRES-011 es envenenado y cómo lo mitigas; (d) si el modo de alto rendimiento es adecuado; (e) por
qué esta cola no puede usar DelaySeconds por mensaje y qué harías si lo necesitaras.
Soluciones
Solución 1
(a) El peor caso es 74 s, así que 180 segundos es defendible: 2,4 veces el peor caso registrado. No conviene subirlo a 900, porque si el trabajador muere el mensaje quedaría bloqueado 15 minutos y el objetivo es de 10. Alternativa superior: 120 s con latido, que da recuperación rápida ante caída del trabajador sin límite superior real de duración.
(b) Lambda, con reservas. A favor: trabajo esporádico, sin estado y de duración variable; con casi
nada de tráfico de madrugada, pagar instancias encendidas es absurdo. En contra: el p99 de 31 s obliga a
un timeout alto y una función que espera se paga íntegra. El factor decisivo es el límite de 12
concurrentes del ERP: MaximumConcurrency lo impone de forma declarativa, mientras que con instancias
haría falta un semáforo distribuido.
(c) --batch-size 1 (con lotes de 10 y 74 s por mensaje, una invocación podría necesitar 740 s);
--maximum-batching-window-in-seconds 0; MaximumConcurrency=12; timeout de la función 120 s; y
VisibilityTimeout de la cola 720 s, aplicando la regla de 6 × timeout, que aquí gana a la
estimación de (a). Comprobación de capacidad: 12 ÷ 1,18 s = 10 pedidos/s = 36.000/hora, y aun en el p95
(6,5 s) son 6.600/hora. Margen de sobra sobre 900.
(d) maxReceiveCount=4: con 720 s de visibilidad, tres reintentos cubren unos 36 minutos de
indisponibilidad antes de aislar. Retención de la DLQ 14 días, el máximo, porque lo que cae ahí son
ventas cobradas que Marta debe poder recuperar aunque el incidente sea un viernes de puente.
(e) ApproximateAgeOfOldestMessage > 480 durante 2 periodos de 60 s: el requisito son 600 s, así que
se avisa a los 8 minutos para dejar margen de reacción. Y ApproximateNumberOfMessagesVisible > 0 en la
DLQ. Una tercera recomendable: Errors de la función > 10 en 5 minutos, que detecta el ERP caído antes.
Solución 2
(a) Causa 1: el tiempo de visibilidad es igual al timeout de la función. Ambos son 60 s: cuando una
invocación tarda 47 s o agota el timeout, el mensaje se hace visible antes o justo cuando la función
termina, y Lambda lo entrega otra vez. Falta el factor 6 recomendado. Causa 2: no está activado
ReportBatchItemFailures. Con batch-size 10, si el mensaje número 7 falla o el lote agota el
timeout, ninguno de los diez se borra y los otros nueve se reprocesan; eso explica los casos de tres
duplicados.
(b) Ambas causas dependen de la latencia del SMTP, que se degrada cuando se envían muchos correos a la vez. En valle la función tarda 900 ms y no se acerca a ningún límite; a partir de las 18:00 la concurrencia crece con la profundidad de la cola, el proveedor estrangula, la duración se dispara al p99 y las dos causas se activan a la vez. Es un fallo que no se reproduce en pruebas porque solo aparece con concurrencia real.
(c) Que la DLQ esté vacía significa que ningún mensaje agotó sus recepciones, no que todo vaya bien. Aquí el fallo es de doble entrega con éxito: el mensaje se procesa correctamente dos veces y se borra. Un duplicado exitoso nunca llega a la DLQ. La DLQ detecta trabajo que no se hizo, jamás trabajo que se hizo de más.
(d) VisibilityTimeout a 360 s (6 × 60); activar ReportBatchItemFailures y devolver
batchItemFailures; MaximumConcurrency=20 para no estrangular al SMTP; batch-size a 5 para acotar la
duración por invocación; idempotencia en el consumidor escribiendo PEDIDO#<id>#correo-confirmacion
en DynamoDB con escritura condicional y TTL de 24 h; y alarmas sobre Duration p99 y Throttles.
(e) Las cuatro primeras reducen la frecuencia: hacen los duplicados mucho más raros pero no imposibles, porque las colas estándar duplican por diseño. La única que elimina el problema de raíz es la idempotencia: recibir el mensaje dos veces pasa a ser inocuo. Esta es la tesis de 07-05, y el motivo de que la configuración correcta sea necesaria pero nunca suficiente.
Solución 3
(a) MessageGroupId = sku. El orden solo importa entre movimientos del mismo producto, y con
3.400 SKU hay paralelismo de sobra. Descartadas: un grupo fijo, que impone orden global innecesario y
un solo mensaje en vuelo; pedido_id, que garantiza el orden dentro de un pedido pero no entre
pedidos distintos del mismo SKU, que es justo donde está la condición de carrera; y categoria, que
deja una docena de grupos y crea grupos calientes en las categorías más vendidas —el mismo error que una
clave de partición caliente en DynamoDB (06-02)—.
(b) Un identificador determinista y único por movimiento lógico: f"{pedido_id}:{sku}:{motivo}". Si
el productor reintenta tras un tiempo de espera agotado, el mismo movimiento produce el mismo
identificador y SQS lo descarta dentro de la ventana de 5 minutos. ContentBasedDeduplication se deja
desactivado: el cuerpo incluye una marca de tiempo, con lo que el hash cambiaría entre reintentos; y
peor aún, dos movimientos legítimamente idénticos —dos reposiciones de 10 unidades del mismo SKU en la
misma ventana— se deduplicarían por error, perdiendo stock real.
(c) Es el escenario más grave de FIFO: como SQS no entrega el siguiente mensaje del grupo hasta que
el actual se borra, todo el grupo FRUT-FRES-011 queda bloqueado, y con retención de 4 días ese SKU
pasaría días sin actualizar stock mientras los demás funcionan —un fallo parcial silencioso—. Mitigación:
DLQ con maxReceiveCount=3; alarma sobre la DLQ; validación de esquema en el productor; distinguir
permanente de transitorio en el consumidor; y vigilar ApproximateAgeOfOldestMessage, que en FIFO
delata un grupo bloqueado aunque la profundidad total sea baja.
(d) Sí, y es gratuito. DeduplicationScope=messageGroup con
FifoThroughputLimit=perMessageGroupId levanta el límite de 300 msg/s por cola. Aunque 2.800
movimientos/hora son solo 0,8 msg/s, activarlo protege ante campañas o reposiciones masivas; la única
consecuencia es que la deduplicación pasa a ser por grupo, irrelevante porque el identificador ya
incluye el sku.
(e) Las colas FIFO no admiten temporizador por mensaje, solo el retardo a nivel de cola. Para
aplazar un movimiento concreto —liberar el stock de un pedido no pagado a los 15 minutos— hay tres
opciones: DelaySeconds de cola, descartada porque retrasaría también las reservas urgentes; devolver el
mensaje con ChangeMessageVisibility, descartada porque en FIFO bloquea el grupo entero; y la correcta,
sacar el aplazamiento de la cola y usar un estado Wait de Step Functions (07-04) o una regla
programada de EventBridge (07-03), de modo que el mensaje solo entre en la cola cuando haya que
aplicarlo.
Conclusión
Confirmar un pedido en MercadoFresco ha pasado de 2.893 ms de mediana a unos 400 ms en el percentil 95,
y de ocho puntos de fallo encadenados a dos: cobrar la tarjeta y escribir en Aurora. Lo demás vive ahora
en cola-mercadofresco-pedidos y cola-mercadofresco-correo, con cola-mercadofresco-stock.fifo para
lo único que necesitaba orden real y mercadofresco-pedidos-fallidos recogiendo lo que no se pudo
procesar. El ERP del almacén puede caerse dos horas sin que se pierda una sola venta.
En el camino has visto los conceptos que se repetirán todo el módulo: el modelo de sondeo, que
convierte la cola en amortiguador y da contrapresión gratis; el tiempo de espera de visibilidad,
fuente número uno de duplicados cuando se queda corto; el hecho de que no borrar es reprocesar, que
es a la vez la garantía de durabilidad y la razón de que los consumidores deban ser idempotentes; el
sondeo largo, que separa una factura de cero de una de cientos de euros; y las colas de mensajes
fallidos con su maxReceiveCount, su alarma obligatoria y su redrive.
Pero ha quedado una costura a la vista. cola-mercadofresco-pedidos es un canal punto a punto: cada
mensaje lo procesa un consumidor y desaparece. Con almacén, correo, reparto y analítica interesados en
el mismo hecho —«se ha confirmado un pedido»—, o la tienda envía cuatro mensajes a cuatro colas
distintas —y entonces vuelve a saber quiénes son sus consumidores, justo lo que queríamos evitar— o un
consumidor único reparte el trabajo y se convierte en el nuevo punto de fallo. Cuando el mes que viene
márketing pida enterarse también de los pedidos para su programa de fidelización, habrá que volver a
tocar el código de la tienda. El desacoplamiento está a medias.
En 07-02, «Amazon SNS», resolveremos esa mitad que falta: un tema al que la tienda publica una
vez y varias colas suscritas que reciben cada una su copia. Veremos el patrón fan-out SNS→SQS, por qué
conviene poner una cola entre el tema y cada consumidor en lugar de suscribir funciones Lambda
directamente, cómo filtrar por atributos para que la cola del reparto en 24 horas solo reciba lo que le
incumbe, y por qué el tema alertas-mercadofresco que usamos desde el módulo 5 es exactamente el mismo
mecanismo aplicado a las alarmas.
Curso de AWS
Módulo 1: Introducción a AWS
- ¿Qué es AWS?
- Configuración de tu cuenta de AWS
- Infraestructura global de AWS
- Consola de administración de AWS
- AWS CLI y SDKs
Módulo 2: Servicios principales de AWS
Módulo 3: Redes y entrega de contenido
- Amazon VPC
- Grupos de seguridad y listas de control de acceso
- Elastic Load Balancing
- Amazon CloudFront
- Route 53
Módulo 4: Seguridad e identidad
- AWS Identity and Access Management (IAM)
- AWS Key Management Service (KMS)
- Secrets Manager y Parameter Store
- AWS Shield
- AWS WAF
Módulo 5: Monitorización y gestión
- Amazon CloudWatch
- AWS X-Ray y trazabilidad distribuida
- AWS CloudTrail
- AWS Config
- AWS Trusted Advisor
Módulo 6: Bases de datos
- Cómo elegir la base de datos adecuada
- Amazon DynamoDB
- Amazon Aurora
- Amazon Redshift
- Amazon ElastiCache
Módulo 7: Integración de aplicaciones
- Amazon SQS
- Amazon SNS
- Amazon EventBridge
- AWS Step Functions
- Patrones de integración: idempotencia, reintentos y colas de mensajes fallidos
