Implementa sistemas basados en secuencias de eventos con Cloud Spanner

Con un patrón arquitectónico como ejemplo, en este artículo se explica cómo usar Spanner como un sistema de transferencia de eventos y Pub/Sub como un registro de eventos para crear un sistema que pueda realizar las siguientes acciones:

  • Escribir a una fuente de evento que tenga alta disponibilidad.
  • Publicar estas operaciones de escrituras como eventos para el uso por parte de otros sistemas.
  • Archivar eventos para su reproducción.
  • Cargar eventos en un sistema para elaborar estadísticas.
  • Filtrar eventos en un sistema para realizar consultas rápidas.

Este artículo está destinado a ingenieros de software que estén interesados en aprender acerca de los usos, las compensaciones y los componentes de un sistema basado en secuencias de eventos. Obtén más información sobre cómo crear una serie de aplicaciones y servicios mediante Cloud Functions para que sean compatibles con tu arquitectura basada en secuencias de eventos.

Casos prácticos y criterios de diseño

Puedes usar este patrón de arquitectura cada vez que necesites realizar una acción basada en escribir datos nuevos en una fuente de datos, como Spanner en este caso.

Casos prácticos

Este patrón resulta útil para administrar las siguientes situaciones:

  • Carritos de compra de comercio electrónico.
  • Administración de pedidos y cadena de suministro.
  • Billeteras, pagos y resoluciones de cargos.

En el siguiente diagrama se ilustra el flujo de un sistema de carrito de compras de comercio electrónico.

El flujo de eventos en un carrito de compras de comercio electrónico.

En sistemas complejos, como el comercio electrónico y los pagos, resulta útil realizar un seguimiento de las transacciones mediante las arquitecturas basadas en eventos. Por ejemplo, cuando un cliente agrega un artículo a su carrito de compras o procesa una tarjeta de crédito a la hora de realizar un pago, se recomienda activar varios procesos descendentes para verificar que haya artículos en stock y que la cuenta del cliente tenga fondos. Es importante asegurarse de que los clientes puedan realizar pedidos en todo momento, ya que tu negocio depende de ello.

Criterios de diseño

A continuación, se presentan algunos ejemplos de criterios de diseño que sugieren el uso de un sistema de arquitectura basado en secuencias de eventos:

  • Debe administrar la escritura de pedidos y pagos al sistema con alta disponibilidad.
  • Debe verificar que los clientes reciban los artículos que pidieron (y solo estos artículos) y que se les cobre el monto de dinero correcto por la compra.
  • Debe tener modos de falla deterministas (es decir, debes estar seguro de que tu escritura falló o tuvo éxito).
  • Debe incluir un mecanismo para notificar a los servicios dependientes cuando se realiza una operación de escritura que sea de su interés.

En este artículo, se describe un sistema que puede satisfacer todos estos requisitos y proporcionar flexibilidad para agregar funcionalidades más adelante.

Arquitectura del sistema

En primer lugar, necesitas un servicio que pueda aceptar operaciones de escritura con alta disponibilidad. Ese sistema también debe proporcionar modos de falla deterministas. El sistema debe saber cuándo una operación de escritura falla, de modo que el escritor pueda reintentar la operación de escritura sin miedo de duplicarla de forma entera o parcial.

La aplicación canónica de esta situación es una base de datos que admita transacciones con atomicidad, coherencia, aislamiento y durabilidad (ACID), pero lograr que las bases de datos tengan alta disponibilidad, sobre todo para las operaciones de escritura, es difícil. La replicación puede provocar incoherencias en los datos y, a su vez, incrementar el costo y la complejidad de tu arquitectura. Empeorar la complejidad es uno de los mayores riesgos de diseño cuando la alta disponibilidad es una prioridad.

Además, las configuraciones de bases de datos con alta disponibilidad no pueden lidiar con fallas en distintas zonas de disponibilidad sin generar demoras de replicación. A medida que aumentan esas demoras, también aumenta la probabilidad de que las fallas adicionales provoquen la pérdida de todas las operaciones de escritura que estén en tránsito a ese nodo de réplica en otra zona de disponibilidad. Estas fallas adicionales significan que las operaciones de escritura no se escriben por completo en la réplica antes de la falla en esa zona.

Diagrama de arquitectura

En el siguiente diagrama, se ilustra una arquitectura basada en secuencias de eventos con Spanner diseñada para abordar los problemas de complejidad y costo asociados con las bases de datos tradicionales con alta disponibilidad.

Diagrama arquitectónico basado en secuencias de eventos con Spanner.

Esta arquitectura basada en secuencias de eventos se apoya en los siguientes componentes, que puedes crear con Cloud Functions. Estos componentes consisten en apps y Cloud Functions.

  • App de sondeo: Sondea Spanner, convierte el formato de registro en Avro y publica en Pub/Sub.
  • archiver: Obtiene eventos activados por mensajes publicados en un tema de Pub/Sub y escribe esos registros en un archivo global en Cloud Storage.
  • bqloader: Se activa mediante registros escritos en Cloud Storage y los carga en una tabla de BigQuery correspondiente.
  • janitor: Lee todas las entradas escritas en el archivo global a una velocidad fija y, luego, las comprime para el almacenamiento a largo plazo.
  • replayer: Lee los registros del almacenamiento a largo plazo en orden, los descomprime y los carga en una nueva transmisión de Pub/Sub.
  • App de materialización: Filtra los registros escritos en Pub/Sub y, luego, los carga en una base de datos de Redis (vista materializada) correspondiente para facilitar el acceso a las consultas.

Crea un servicio de sondeo

Una vez que hayas establecido un sistema que acepte operaciones de escritura, debe notificarse a los servicios descendentes cada vez que se escriba algo en el sistema.

Las bases de datos tradicionales hacen esto de diferentes maneras, pero, en general, con alguna variación de escucha en el registro de escritura por adelantado (WAL) o la transmisión de la captura de datos modificados (CDC) de una base de datos. Estas soluciones no se encuentran en un formato útil para la legibilidad. El formato se diseñó para representar y transmitir los cambios realizados al registro. No se diseñó para informar a un sistema descendente sobre eventos y pasar el contexto relevante de esos eventos. Contar con una representación binaria solo de los cambios, y no con un registro completo, no resulta útil en la mayoría de los casos. Otra desventaja de ese formato es que no es legible por humanos, lo que dificulta en gran medida la depuración y la auditoría del flujo.

En su lugar, puedes crear una solución que sondee la base de datos para las nuevas entradas y que las pase al sistema descendente. Los servicios de sondeo son una manera común de procesar nuevos registros escritos en una base de datos y presentan las siguientes ventajas:

  • Son fáciles de comprender y escribir.
  • Presentan bajas sobrecargas cuando se escriben de forma correcta.
  • Son independientes y estables.
  • Toleran los errores, tanto para consultas como para el análisis de los registros.
  • Resultan flexibles en cuanto a la forma de filtrarse y representar los cambios.

Entre las compensaciones por escribir un servicio de sondeo en lugar de usar un WAL o una CDC convencionales, se incluyen las siguientes cuestiones:

  • Sondear una base de datos de a intervalos cortos (menos de un segundo) puede agregar cargas a tu base de datos.
  • Según el diseño de la tabla, la consulta que usas para sondear y la cantidad de apps que también sondean la base de datos en este momento, un servicio de sondeo podría crear una contención de recursos (bloqueos) con otras apps.
  • El sondeo puede requerir que uses máquinas de bases de datos de mayor tamaño y almacenamiento más costoso (como SSD) para administrar la carga adicional y la contención de recursos.

Si deseas ayudar a mitigar algunas de estas compensaciones en Spanner, usa transacciones de solo lectura para tus lecturas de sondeo y asegúrate de aplicar las prácticas recomendadas de SQL para realizar consultas eficientes y eficaces.

Para buscar todos los registros nuevos de un período determinado, puedes usar la función marca de tiempo de confirmación en Spanner. La marca de tiempo de confirmación se basa en la tecnología TrueTime y le proporciona a Spanner una representación coherente a nivel global del momento en que se confirmó una operación de escritura en la base de datos. Spanner puede aceptar operaciones de escritura en numerosas regiones de todo el mundo, y tu aplicación de sondeo puede crear un registro que contenga una representación exacta de los eventos en orden.

Usa Cloud Functions para representar tareas

Cloud Functions es una plataforma de procesamiento sin servidores controlada por eventos en Google Cloud. Estas funciones son fragmentos de código sin estado que se ejecutan en respuesta a un activador, como una solicitud HTTP o un activador de eventos. En el caso de los sistemas basados en secuencias de eventos, Cloud Functions suele representar las tareas individuales asociadas con un evento que se publica. Debido a que se trata de funciones sin servidores, se escalan con el volumen de solicitudes y no requieren la intervención de operaciones adicionales.

A continuación, se enumeran algunas compensaciones de Cloud Functions:

  • Los tiempos de respuesta pueden ser incoherentes.
  • El registro y el seguimiento pueden ser más difíciles debido a la naturaleza efímera del inicio y la ejecución.
  • Deben ser funciones sin estado y también idempotentes, ya que el reintento en caso de falla es generalmente automático.
  • Puede ser difícil depurar y reproducir errores de manera local, según el estado del entorno de registro.

Usa Pub/Sub como registro

Este registro es de solo anexar y registra los eventos publicados en un bus de eventos o una cola de mensajes de algún tipo. Para suscribirte o ponerte a la escucha de un evento, puedes suscribirte a un bus de eventos en particular o a un tema de cola de mensajes; como opción, puedes filtrar la colección de todos los mensajes por el sustantivo, el verbo o los metadatos en particular que te interesen.

En este patrón, el registro es un solo tema de Pub/Sub. Puedes usar el sistema de eventos para activar Cloud Functions cada vez que se escribe un registro en el flujo.

Asegúrate de que tus suscriptores no confirmen la recepción del mensaje, de modo que permanezca disponible para otras funciones interesadas. Además, existe una ventana de retención de mensajes limitada para los mensajes en tu cola, por lo que debes crear una Cloud Function que cree una copia de seguridad de esos mensajes en un archivo. Cuando desees reproducir mensajes archivados, puedes usar una Cloud Function a fin de leer mensajes archivados y publicarlos en un tema de Pub/Sub nuevo para su consumo.

Usa una aplicación de sondeo para consultar la base de datos

El trabajo de la aplicación de sondeo es consultar la base de datos en intervalos fijos y pedir todos los registros que se produzcan después de un momento en particular, ordenados según las marcas de tiempo de confirmación y en orden ascendente (el registro más antiguo en primer lugar).

Requisitos de la aplicación de sondeo

Este diseño implica que debes realizar un seguimiento de la última marca de tiempo que vio la aplicación de sondeo y que debes iniciar el sistema para la primera ejecución. Para realizar un seguimiento de esta marca de tiempo, puedes almacenarla en el estado de aplicación, escribirla en otra base de datos o pedirle al registro su entrada más reciente y analizar la marca de tiempo a partir de ella.

Almacenar el registro en otra base de datos puede agregar complejidad adicional, ya que crea una dependencia en otro sistema que agrega otro punto de falla. Es mejor mantener la marca de tiempo del último proceso en el almacenamiento local de la app y solo recurrir a la consulta del registro si la aplicación falla o se reinicia por algún motivo y se pierde el estado interno.

Los requisitos de la aplicación de sondeo son los siguientes:

  • El intervalo de sondeo debe ser fijo y coherente.
  • El intervalo de sondeo debe ser menor de 1 segundo.
  • Ejecuta un bootstrap del sistema la primera vez que se ejecuta.
  • Consulta todos los registros que ocurren después de la marca de tiempo registrada previamente.
  • Serializa cada registro en un registro individual de Avro.
  • Publica cada registro de Avro en un registro de eventos basado en Pub/Sub.
  • Se suspende hasta que haya transcurrido el intervalo de sondeo.
  • Si se produce una falla, lo reintenta automáticamente dentro de su intervalo fijo.
  • Si se produce un error, cuando se reinicia la aplicación de sondeo, esta consulta la transmisión de Pub/Sub para obtener la última marca de tiempo registrada. Si este proceso falla, puedes iniciar la aplicación de sondeo con una marca de tiempo configurada de manera manual. También puedes obtener la marca de tiempo de los archivos de Cloud Storage.

Diseña la aplicación de sondeo

A continuación, debes diseñar la aplicación de sondeo. Existen al menos tres maneras diferentes de diseñarla, cada una con sus compensaciones.

Puedes usar Cloud Scheduler para programar una Cloud Function que sondee la base de datos, puedes iniciar tu aplicación de sondeo en un clúster de Kubernetes como un trabajo cron y programarla según el intervalo que prefieras, o bien puedes tener un servicio que se ejecute de manera continua en un pod de Kubernetes y que sondee la base de datos de a intervalos fijos, para conservar y actualizar la última marca de tiempo procesada en la memoria.

Con el conocimiento de que debes consultar la base de datos cada un intervalo de tiempo fijo, puedes considerar usar Cloud Scheduler para programar una Cloud Function a fin de sondear la base de datos y, luego, enviar los registros nuevos a una transmisión de Pub/Sub.

Este enfoque funciona, pero conlleva dos compensaciones. En primer lugar, debes idear una manera de preservar el estado de la aplicación debido a que las Cloud Functions no tienen estado, por definición. Preservar el estado de la aplicación implica ingresar una base de datos adicional y agregar incoherencias en la latencia entre los eventos que se escriben y los eventos que se agregan al registro. A veces, las Cloud Functions pueden tardar más de lo esperado en generarse y ejecutarse. Esta demora se incrementa a medida que se reduce el intervalo de sondeo.

Si todos tus consumidores descendentes previsibles pueden tolerar una latencia variable que podría ser mayor que un segundo de forma ocasional, una Cloud Function programada podría ser la mejor opción para el diseño del sistema. En ese caso, Cloud Function rastrea la última marca de tiempo procesada cuando se consulta la transmisión de Pub/Sub, o mantiene ese estado en Cloud Storage. Si bien se reduce la complejidad de administrar sistemas como Kubernetes, otra compensación que se debe considerar en el caso de la Cloud Function es que esas consultas adicionales agregan latencia debido a la llamada adicional para obtener la marca de tiempo, por lo que se puede agregar otro punto de falla en el sistema de sondeo. Si puedes tolerar la latencia de esa llamada adicional, puedes usar una Cloud Function en este caso.

También tienes la opción de iniciar tu aplicación de sondeo en un clúster de Kubernetes como un trabajo cron y programarla según el intervalo que prefieras. Por desgracia, este enfoque tiene compensaciones semejantes a las de Cloud Scheduler, con la complejidad adicional de agregar Kubernetes. Sin embargo, puedes iniciar el trabajo como un servicio en Kubernetes y hacer que se suspenda durante un intervalo establecido que controlas en el bucle de eventos principales de la app. Puedes mantener el estado y tener un control completo sobre la latencia de sondeo y las semánticas de reintento. Si bien esta opción agrega la complejidad de Kubernetes, que puedes mitigar gracias a Google Kubernetes Engine (GKE), también te proporciona el máximo control sobre los siguientes aspectos:

  • El estado entre sondeos (la última marca de tiempo).
  • La latencia entre sondeos.
  • La semántica de reintento y la duración si falla la operación de lectura de Spanner o la operación de escritura de Pub/Sub.
  • Reinicio automático del servicio de sondeo si se cierra o finaliza inesperadamente.

Ejecuta un bootstrap de la aplicación de sondeo

La primera vez que ejecutes la aplicación de sondeo, esta configura todos los componentes requeridos para ejecutar el sistema basado en secuencias de eventos. Esto incluye la transmisión de Pub/Sub con el nombre correcto (lo ideal sería el nombre de la tabla) y el depósito de Cloud Storage en el que el archiver debe publicar. Una vez configurados todos los componentes del sistema, el proceso de inicio de la aplicación de sondeo realiza un escaneo de tabla inicial y procesa todos los datos existentes. Cuando la aplicación de sondeo finaliza, se mueve al intervalo de sondeo fijo y pasa la última marca de tiempo de confirmación procesada para que el sistema de sondeo la use en su primera ejecución de procesamiento.

Usa un registro para interpretar datos

Una vez que tengas la aplicación de sondeo programada y que comience a recopilar los últimos datos, necesitarás representar esos datos en el registro. Debido a que esta es una solución generalizada (es decir, puedes reutilizar esta aplicación de sondeo para varias tablas distintas con esquemas diferentes), escribir aplicaciones de sondeo y registros de consumidores específicos de una tabla no resulta una solución escalable. También deberías poder agregar distintos consumidores al registro sin que estos conozcan las variaciones del esquema con versión de antemano.

Estos son algunos casos prácticos potenciales:

  • Archivo a largo plazo de todas las transacciones.
  • Carga de las transacciones en un almacén de datos para elaborar estadísticas.
  • Carga de las transacciones en una base de datos NoSQL para alimentar modelos de aprendizaje automático y si es posible almacenar en caché las respuestas a las Preguntas frecuentes más cerca del usuario.

En estos casos prácticos, considera usar un formato de serialización, como Avro, JSON o Protobuf. BigQuery, la solución de almacenamiento de datos de Google, es compatible con la transferencia directa de datos desde los archivos Avro. Avro es el formato preferido para cargar datos en BigQuery. Cargar archivos Avro en vez de JSON presenta las siguientes ventajas:

  • Carga más rápida. Los datos se pueden leer en paralelo, incluso si los bloques de datos están comprimidos.
  • No requiere escritura o serialización.
  • Es más fácil analizar los archivos porque no existen los problemas de codificación inherentes que a veces se encuentran en otros formatos.

Cuando cargas los archivos de Avro en BigQuery, el esquema de la tabla se infiere de forma automática desde los datos de origen de descripción automática.

Protobuf representa una alternativa a Avro, pero en este caso práctico Avro presenta dos ventajas a destacar:

  • Es compatible con la transferencia directa en BigQuery.
  • El esquema está contenido en el objeto de datos.

La última ventaja permite que los consumidores del registro obtengan datos y los inspeccionen. No es necesario que deserialicen JSON y esperen que el formato no cambie, o que obtengan una versión de un registro de esquema para una versión determinada de ese objeto.

Por estos motivos, serializa tus tablas de Spanner a un objeto de Avro para cada transacción antes de ubicarlas en el registro. Para obtener más información, consulta cómo transformar una tabla de Spanner en un registro de Avro.

Escribe la aplicación de sondeo

Después de decidir el modelo de implementación y el formato de serialización, considera las opciones de lenguaje para escribir la app de sondeo. Como el objetivo es leer datos de Spanner y escribirlos en Pub/Sub, estás limitado a elegir uno de los lenguajes compatibles con las API de Google Cloud. Esos lenguajes son C#, Go, Java, Node.js, PHP, Python y Ruby.

De los lenguajes compatibles con Spanner en este momento, Avro admite de forma oficial C#, Java, Python, PHP y Ruby. Debido a que es recomendable que tengas el mayor control posible sobre la latencia de tu app y que proceses las tablas consultadas en varios subprocesos, Java es una buena opción.

Usa el flujo de eventos

La aplicación de sondeo es la app principal del servicio, pero hay otros consumidores de la transmisión. La primera app que necesitas se suscribe a la transmisión y archiva cada mensaje en Cloud Storage para la referencia histórica. Este sistema almacena cada registro como un archivo independiente en un depósito con el mismo nombre que la tabla.

A cada hora (o según tu frecuencia de transacción), habrá otro servicio que tome esos registros de transacciones independientes y los comprima en un archivo mayor. Según la frecuencia de tus transacciones y el tamaño de tus registros de transacciones, puedes considerar comprimir también los archivos Avro de cada transacción individual.

Una vez que hayas archivado todas tus transacciones históricas en Cloud Storage, puedes realizar las siguientes acciones:

  • Propagar BigQuery directamente con datos de transacciones para elaborar estadísticas
  • Reproducir registros históricos para entrenar o probar otros sistemas
  • Volver a compilar el contenido del sistema de registro (en este caso, la base de datos de Spanner) si se pierde o se daña
  • Crear una réplica histórica de solo lectura con fines de informes o auditoría
  • Crear una versión de la base de datos para entornos de etapas de pruebas

Cómo crear una Cloud Function archiver

Crea una Cloud Function llamada archiver que se active cada vez que se agregue una transacción a la transmisión. Debes crear una función y un activador nuevos por tema (una tabla en Spanner). Cada vez que se agrega una transacción al tema de Pub/Sub configurado, la Cloud Function archiver toma el registro de Avro y lo escribe en un depósito de Cloud Storage con el mismo nombre que la tabla. La Cloud Function denomina el archivo con el nombre de la tabla, captura la fecha y hora en milisegundos, más 4 cifras al azar. Este esquema de nombramiento crea un ID similar al identificador único universal (UUID).

Crea la Cloud Function bqloader

Ahora puedes crear una Cloud Function nueva llamada bqloader para cargar ese archivo de Avro directamente en BigQuery. La función se activa cuando el archiver sube el archivo a Cloud Storage. Cada vez que una transacción se carga en Cloud Storage, esta Cloud Function adjunta la entrada de transacción en la tabla de BigQuery correspondiente. Si deseas reducir aún más la latencia para tareas como la elaboración de estadísticas o el suministro de contenido a los modelos de aprendizaje automático, puedes transmitir tus datos a BigQuery de a un registro por vez mediante el método tabledata.insertAll. Este enfoque permite consultar datos sin la demora de ejecutar un trabajo de carga. Ten en cuenta las cuotas para cargar registros en BigQuery.

Crear la Cloud Function janitor

La Cloud Function archiver escribe el archivo de Avro en un depósito de Cloud Storage. A continuación, tendrás que crear otra Cloud Function llamada janitor a fin de comprimir todas las transacciones individuales en un solo archivo comprimido para el almacenamiento a largo plazo. janitor nombra este archivo que se acaba de crear con el nombre de la tabla, la fecha y el intervalo de tiempo incluidos en los archivos. Por ejemplo, si janitor está programado para ejecutarse a cada hora, el nombre del archivo podría ser table1-jan_1_2019_1200-1300.tar.gz. La Cloud Function janitor no es un requisito, pero ayuda a mantener los costos de almacenamiento bajos y los depósitos de Cloud Storage organizados.

Crea la Cloud Function replayer

Si deseas reproducir los archivos de Avro almacenados en Cloud Storage, necesitas otra Cloud Function, llamada replayer, que se activa por HTTP. La Cloud Function replayer toma los archivos almacenados del período que solicitaste para que se vuelvan a reproducir, los expande y los publica en orden en una transmisión de Pub/Sub nueva.

Esta Cloud Function se activa mediante una solicitud HTTP POST que proporciona el período que deseas volver a reproducir. La Cloud Function responde con el nombre de la transmisión de Pub/Sub después de que se la terminó, o con un código de error y una descripción si no pudo cargar de forma correcta todos los datos almacenados en la transmisión nueva.

Si tus archivos almacenados se vuelven demasiado grandes a fin de reproducirlos de manera coherente o si la reproducción se vuelve demasiado lenta para tu caso práctico, esta app podría requerir algo más sofisticado. Puedes dividir tus archivos en secciones más pequeñas o hacer una selección de lenguaje diferente para tu Cloud Function replayer.

Otra opción es un trabajo de Dataflow, para dividir el trabajo en tareas más pequeñas y ejecutarlas en paralelo. La implementación de un sistema como este no se incluye en este documento, pero hay excelentes ejemplos en el repositorio de GitHub de Google Cloud y en la documentación de Dataflow.

Crea la aplicación de materialización

Una vez que tengas estos eventos en el registro y que cada uno represente una sola transacción, puedes integrar distintas clases de servicios en el sistema de manera fluida sin necesidad de escribir código adicional y sin tener que coordinar entre los equipos de distintas apps.

Por ejemplo, esta app detecta todos los mensajes relacionados con un tema determinado, filtra por nombre de cliente y crea una vista materializada de los datos relevantes de ese cliente. Esta app es ideal cuando necesitas consultar información acerca de un usuario y acceder a ella con muy baja latencia.

Si puedes analizar los datos temprano, colocarlos en un almacenamiento rápido y mostrar una respuesta detallada, lograrás mejorar el rendimiento de las consultas que ejecutas con frecuencia.

Esta app de materialización puede consistir en una Cloud Function que se activa por un tema Pub/Sub, filtra la información por ID de cliente y, si el ID de cliente coincide con la que te interesa, la Cloud Function escribe los datos relevantes en Memorystore.

Conclusión

Compilar una arquitectura de sistema basado en secuencias de archivos permite crear funcionalidades nuevas y agregar flexibilidad a cualquier sistema que necesite reaccionar ante eventos o comprender la relación entre ellos. Cuando el orden determinista o la capacidad de procesamiento de transferencia elevada son importantes, usar Spanner como sistema de transferencia de eventos y Pub/Sub como registro de eventos puede crear una base sólida y confiable para la arquitectura basada en secuencias de eventos. Después de configurar una base según una secuencia de eventos, puedes descubrir las numerosas formas en las que los problemas que alguna vez fueron difíciles de resolver pueden convertirse en soluciones de suscripción de Pub/Sub o Cloud Functions simples.

Próximos pasos