Logo Google Cloud con Eduardo

Google Cloud con Eduardo

Recibe el código de la lección en tu correo
Te enviaremos el enlace directo al archivo (.zip) y te sumaremos a nuestra newsletter de Google Cloud.

Información básica de protección de datos: Responsable: Eduardo Martínez Agrelo. Finalidad: Facilitar el recurso solicitado y enviar comunicaciones periódicas de valor sobre Google Cloud. Legitimación: Consentimiento del interesado (Art. 6.1.a RGPD). Destinatarios: Plataforma Odoo y proveedores de infraestructura técnica (Google Cloud / Firebase). Derechos: Acceso, rectificación y supresión en eduardomartinezagrelo@gmail.com.
✦ Guía Técnica & Arquitectura

Google Cloud Pub/Sub en Producción: Arquitectura Event-Driven, Exactly-Once y FinOps

Descubre cómo estructurar patrones de ingestión desacoplados, configurar suscripciones de alto rendimiento con Ordering Keys y Dead-Letter Topics, y controlar el gasto operativo evitando fugas de costes en Cloud Pub/Sub. Para procesar esos eventos con ventanas, watermarks y Apache Beam, continúa con Transforma tu Negocio.

En esta guía de arquitectura aprenderás:

✓ Semántica de entrega (At-Least-Once vs. Exactly-Once) y mitigación de mensajes fantasma.
✓ Configuración de Flow Control y políticas de Backoff en suscriptores streaming.
✓ Patrones de particionamiento lógico mediante Message Ordering Keys.
✓ Estrategias FinOps para evitar costes desorbitados por cross-region egress y reintentos infinitos.
Google Cloud Pub/Sub Cloud Pub/Sub Lite Event-Driven Architecture Python Streaming Pull Dead Letter Queues (DLQ) Exactly-Once Processing FinOps GCP

Matriz de Selección: Servicios de Ingestión y Streaming en GCP

Elegir el servicio de mensajería adecuado determina la escalabilidad, la complejidad operativa y la factura final en Google Cloud. La siguiente tabla compara las soluciones nativas y gestionadas en función de latencia, consistencia y casos de uso enterprise:

Servicio Garantía de Entrega Throughput & Particionado Latencia Típica Modelo de Coste Caso de Uso Recomendado
Cloud Pub/Sub (Standard) At-Least-Once (Exactly-Once opcional) Global, autoescalado elástico sin particiones fijas 10 - 25 ms Por volumen ingerido (GB) y operaciones de API Microservicios desacoplados, ingestión analítica global y pipelines CDC.
Cloud Pub/Sub Lite At-Least-Once (Orden por partición) Zonal/Regional con capacidad pre-aprovisionada (MiB/s) 15 - 50 ms Capacidad aprovisionada (throughput y almacenamiento) Cargas continuas y predecibles tipo Kafka de bajo coste (ahorro de hasta un 70%).
Cloud Tasks At-Least-Once (Deduplicación explícita) Colas con control estricto de concurrencia y rate limiting 50 - 150 ms Por llamada de tarea ejecutada Invocación asíncrona punto a punto con control de cadencia hacia endpoints HTTP.
Managed Kafka (GCP) At-Least-Once / Exactly-Once via Streams API Particionado estricto basado en topics y offsets manuales < 10 ms Nodos/Instancias dedicadas + almacenamiento adjunto Ecosistemas heredados de Kafka, stateful stream processing nativo en Flink/Kafka Streams.

Antipatrones Críticos y Fugas de Costes en Producción (FinOps)

Pub/Sub es un servicio serverless de escalabilidad prácticamente infinita, pero esa misma elasticidad puede ocultar errores graves de diseño que degradan el rendimiento y disparan la factura de GCP.

⚠️ Antipatrón: El Bucle de Veneno y la Inanición de Ack Deadline

El Problema: Cuando un suscriptor no puede procesar un mensaje (por fallo en deserialización, error 5xx en microservicio downstream o timeout no capturado), el mensaje vuelve a la cola tras expirar el ack_deadline_seconds. Si no existe un límite de reintentos con Dead Letter Topic, se genera un bucle infinito que:

  • Multiplica exponencialmente el volumen de datos transferidos y las peticiones API facturables.
  • Satura los hilos del consumidor (thread pool starvation), impidiendo procesar mensajes válidos.
  • En suscripciones con enable_message_ordering=True, bloquea por completo todos los mensajes sucesivos con la misma Ordering Key.

La Solución Arquitectónica: Configurar siempre un Dead Letter Queue (DLQ) con max_delivery_attempts (típicamente entre 3 y 5) y asociar una política de Exponential Backoff para mitigar tormentas de reintentos sobre microservicios degradados.

Fuga de Costes: Tráfico Cross-Region y Cross-Zone Egress

Aunque Pub/Sub es un servicio global, la publicación y consumo a través de distintas regiones de GCP genera costes directos de Internet / Inter-region Network Egress. Si tus suscriptores en europe-west1 leen de forma intensiva de pipelines publicados en us-central1, el coste de red puede superar con creces el propio coste de ingestión de Pub/Sub. Implementa Message Storage Policies para confinar geográficamente el almacenamiento temporal de mensajes si tus consumidores operan en zonas específicas.

Implementación de Producción: Suscriptor Asíncrono Resiliente en Python

El siguiente bloque de código implementa un suscriptor streaming robusto en Python utilizando google-cloud-pubsub. Incluye control de flujo de memoria (Flow Control), política de extensión de ACK automática y gestión de excepciones para evitar el bloqueo del worker:

import os
import logging
from concurrent.futures import TimeoutError
from google.cloud import pubsub_v1
from google.api_core.exceptions import GoogleAPICallError

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")

PROJECT_ID = os.getenv("GCP_PROJECT_ID", "mi-proyecto-produccion")
SUBSCRIPTION_ID = os.getenv("GCP_SUBSCRIPTION_ID", "orders-sub-streaming")

def process_message_payload(data: bytes, attributes: dict) -> bool:
    """
    Lógica de negocio del microservicio/consumidor.
    Retorna True si el procesamiento fue exitoso, False en caso contrario.
    """
    try:
        payload_str = data.decode("utf-8")
        event_type = attributes.get("event_type", "UNKNOWN")
        logging.info(f"Procesando evento '{event_type}': {payload_str[:80]}...")
        # Simulación de procesamiento intensivo / escritura a base de datos
        return True
    except Exception as err:
        logging.error(f"Error procesando payload: {err}")
        return False

def message_callback(message: pubsub_v1.subscriber.message.Message) -> None:
    """
    Callback asíncrono ejecutado en el ThreadPool del suscriptor.
    """
    try:
        success = process_message_payload(message.data, dict(message.attributes))
        if success:
            # Confirmar procesamiento con ACK
            message.ack()
        else:
            # Enviar NACK explícito para acelerar el reintento o envío al DLQ
            logging.warning(f"Rechazando mensaje {message.message_id} vía NACK.")
            message.nack()
    except Exception as unhandled_err:
        logging.critical(f"Excepción no controlada en callback: {unhandled_err}")
        message.nack()

def run_resilient_subscriber():
    # 1. Configurar límites de Flow Control para proteger la memoria del contenedor
    flow_control = pubsub_v1.types.FlowControl(
        max_messages=500,               # Máximo de mensajes concurrentes sin ACK
        max_bytes=100 * 1024 * 1024,    # Límite máximo de 100 MiB en buffer
        max_lease_duration=600          # Extensión automática de ACK deadline (10 min max)
    )

    subscriber_client = pubsub_v1.SubscriberClient()
    subscription_path = subscriber_client.subscription_path(PROJECT_ID, SUBSCRIPTION_ID)

    streaming_pull_future = subscriber_client.subscribe(
        subscription_path,
        callback=message_callback,
        flow_control=flow_control,
        await_callbacks_on_shutdown=True
    )
    
    logging.info(f"Escuchando eventos en {subscription_path} con Flow Control activo...")

    with subscriber_client:
        try:
            # Mantener el hilo principal vivo
            streaming_pull_future.result()
        except KeyboardInterrupt:
            logging.info("Interrupción por teclado. Deteniendo streaming pull...")
            streaming_pull_future.cancel()
            streaming_pull_future.result()
        except TimeoutError:
            streaming_pull_future.cancel()
            streaming_pull_future.result()
        except GoogleAPICallError as api_err:
            logging.error(f"Error de API en Pub/Sub: {api_err}")
            streaming_pull_future.cancel()

if __name__ == "__main__":
    run_resilient_subscriber()

Patrones de Diseño Esenciales para Sistemas Streaming

1. Patrón Claim Check para Payloads Pesados

El límite físico de un mensaje individual en Cloud Pub/Sub es de 10 MB. Sin embargo, procesar mensajes de más de 1 MB reduce drásticamente el throughput e incrementa la latencia de serialización. Con el patrón Claim Check, el publicador almacena el archivo binario o JSON voluminoso en un bucket de Cloud Storage y publica en Pub/Sub un payload liviano con el puntero (gs://bucket/object_id) y metadatos relevantes. El suscriptor descarga el contenido únicamente cuando está listo para su procesamiento.

2. Idempotencia y Deduplicación en Consumidores

Debido a la naturaleza distribuida de Pub/Sub, en el modo estándar se garantiza At-Least-Once Delivery. Esto implica que, ante reinicios de red o extensiones de ack tardías, un mensaje puede entregarse más de una vez. Diseña siempre tus consumidores con capas de deduplicación idempotentes:

  • Usa un transaction_id o hash(payload) como clave de idempotencia almacenada en memoria rápida (Cloud Memorystore / Redis) con un TTL ajustado.
  • Aprovecha bases de datos relacionales con operaciones UPSERT o sentencias MERGE condicionales para asegurar que repeticiones no alteren el estado.

Framework de Implementación Empresarial en 5 Pasos

  1. Definición de Schemas (Avro / Protocol Buffers)

    Implementa Pub/Sub Schemas en el topic para forzar la validación de contratos entre microservicios y evitar la polución de datos malformados en capas tempranas.

  2. Configuración de DLQ y Políticas de Reintento

    Crea el Dead-Letter Topic secundario con una suscripción dedicada para alertas e inspección de payloads erróneos. Ajusta el límite de reintentos a un máximo de 5.

  3. Ajuste Fino de Flow Control y Ack Deadlines

    Calcula el tiempo medio del proceso downstream (P95/P99) y asigna un ack_deadline holgado pero realista (ej. 30 a 60 segundos), limitando el buffer de memoria del suscriptor.

  4. Gobernanza IAM y Cifrado CMEK

    Aplica el principio de mínimo privilegio con roles específicos (roles/pubsub.publisher y roles/pubsub.subscriber) sin otorgar roles de Admin a las cuentas de servicio de aplicación. Habilita Cloud KMS (CMEK) si tu sector exige control de claves de cifrado.

  5. Monitorización y SLIs Críticos en Cloud Monitoring

    Configura dashboards y alertas sobre las métricas: subscription/oldest_unacked_message_age, subscription/num_undelivered_messages y topic/send_request_count desglosado por código de respuesta.

Preguntas Frecuentes (FAQ)

¿Cuál es la diferencia técnica entre el ordenamiento global y el uso de Ordering Keys en Cloud Pub/Sub?

Cloud Pub/Sub es un sistema distribuido y masivamente escalable que no garantiza orden global por defecto para maximizar el throughput. Al habilitar Ordering Keys en la suscripción y publicar con una clave específica, Pub/Sub enruta los mensajes con la misma clave a la misma partición lógica y los entrega en estricto orden de recepción. No obstante, si el procesamiento de un mensaje falla y se agota su Ack Deadline, todos los mensajes posteriores con esa misma clave se bloquean hasta que se confirme o expire el reintento.

¿Cómo funciona la garantía de entrega Exactly-Once en Google Cloud Pub/Sub y cuál es su trade-off?

La funcionalidad Exactly-Once Delivery en suscripciones de Pub/Sub evita entregas duplicadas al sincronizar de forma estricta los ACKs y las extensiones de ACK deadline entre consumidores. El trade-off principal radica en un incremento en la latencia de confirmación del ACK y una mayor probabilidad de errores de tipo ACK_FAILURE si el suscriptor responde fuera de la ventana de sincronización, requiriendo un manejo de excepciones más granular en el cliente.

¿Cuándo se debe utilizar el patrón Claim Check en arquitecturas basadas en Pub/Sub?

El patrón Claim Check se utiliza cuando el payload del mensaje excede el límite máximo de Pub/Sub (10 MB) o cuando se busca reducir costes de transferencia y cuotas de red en payloads medianos (1 MB a 10 MB). Consiste en subir el payload completo a Cloud Storage y publicar en Pub/Sub únicamente un mensaje ligero con el URI del objeto, metadata y tokens de correlación.

Sobre el Autor: Eduardo Martínez Agrelo

AI & Data Architect

Especialista en el diseño e implementación de arquitecturas analíticas distribuidas, pipelines de datos en tiempo real y soluciones avanzadas de Inteligencia Artificial en entornos multicloud. Enfocado en la optimización del rendimiento, la escalabilidad resiliente y la eficiencia de costes (FinOps) para sistemas a escala petabyte.