Instructivo sobre compilación de un servicio de chat basado en WebSocket para Cloud Run


En este instructivo, se muestra cómo crear un servicio de chat multiusuario y en tiempo real mediante WebSockets con una conexión persistente para la comunicación bidireccional. Con WebSockets, el cliente y el servidor se pueden enviar mensajes entre sí sin sondear las actualizaciones del servidor.

Aunque puedes configurar Cloud Run para usar la afinidad de sesión, esto proporciona una afinidad de mejor esfuerzo, lo que significa que, potencialmente, cualquier solicitud nueva se puede seguir enrutando a una instancia diferente. Como resultado, los mensajes de los usuarios en el servicio de chat deben sincronizarse entre todas las instancias, no solo entre los clientes conectados a una instancia.

Descripción general del diseño

Este servicio de chat de muestra usa una instancia de Memorystore para Redis a fin de almacenar y sincronizar mensajes de usuarios en todas las instancias. Redis usa un mecanismo Pub/Sub, que no debe confundirse con el producto Cloud Pub/Sub, para enviar los datos a los clientes suscritos conectados a cualquier instancia a fin de eliminar el sondeo HTTP para obtener actualizaciones.

Sin embargo, incluso con las actualizaciones push, cualquier instancia que se inicie solo recibirá mensajes nuevos enviados al contenedor. Para cargar mensajes anteriores, el historial de mensajes tendría que almacenarse y recuperarse desde una solución de almacenamiento persistente. En este ejemplo, se usa la funcionalidad convencional de Redis de un depósito de objetos para almacenar en caché y recuperar el historial de mensajes.

Diagrama de arquitectura
En el diagrama, se muestran varias conexiones de clientes a cada instancia de Cloud Run. Cada instancia se conecta a una instancia de Memorystore para Redis a través de un conector de Acceso a VPC sin servidores.

La instancia de Redis está protegida de Internet mediante direcciones IP privadas con acceso controlado y limitado a servicios que se ejecutan en la misma red privada virtual que la instancia de Redis. Por lo tanto, se necesita un conector de acceso a VPC sin servidores para que el servicio de Cloud Run se conecte a Redis. Obtén más información sobre el Acceso a VPC sin servidores.

Limitaciones

  • En este instructivo, no se muestra la autenticación de usuario final ni el almacenamiento en caché de la sesión. Si deseas obtener más información sobre la autenticación de un usuario final, consulta el instructivo de Cloud Run para la autenticación de usuarios finales.

  • En este instructivo, no se implementa una base de datos como Firestore para almacenamiento indefinido y recuperación del historial de mensajes de chat.

  • Se necesitan elementos adicionales para que este servicio de muestra esté listo para la producción. Se recomienda una instancia de Redis de nivel Estándar para proporcionar alta disponibilidad mediante la replicación y la conmutación por error automática.

Objetivos

  • Escribir, compilar e implementar un servicio de Cloud Run que use WebSockets

  • Conéctate a una instancia de Memorystore para Redis a fin de publicar y suscribirte a mensajes nuevos en todas las instancias.

  • Conectar el servicio de Cloud Run con Memorystore mediante un conector de Acceso a VPC sin servidores

Costos

En este documento, usarás los siguientes componentes facturables de Google Cloud:

Para generar una estimación de costos en función del uso previsto, usa la calculadora de precios. Es posible que los usuarios nuevos de Google Cloud califiquen para obtener una prueba gratuita.

Antes de comenzar

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  3. Make sure that billing is enabled for your Google Cloud project.

  4. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  5. Make sure that billing is enabled for your Google Cloud project.

  6. Enable the Cloud Run, Memorystore for Redis, Serverless VPC Access, Artifact Registry, and Cloud Build APIs.

    Enable the APIs

  7. Instala e inicializa la CLI de gcloud

Roles obligatorios

Si quieres obtener los permisos que necesitas para completar el instructivo, pídele a tu administrador que te otorgue los siguientes roles de IAM en tu proyecto:

Para obtener más información sobre cómo otorgar roles, consulta Administra el acceso a proyectos, carpetas y organizaciones.

También puedes obtener los permisos necesarios mediante roles personalizados o cualquier otro rol predefinido.

Configura los valores predeterminados de gcloud

A fin de configurar gcloud con los valores predeterminados para el servicio de Cloud Run, sigue estos pasos:

  1. Configura el proyecto predeterminado:

    gcloud config set project PROJECT_ID

    Reemplaza PROJECT_ID por el nombre del proyecto que creaste para este instructivo.

  2. Configura gcloud en la región que elegiste:

    gcloud config set run/region REGION

    Reemplaza REGION por la región de Cloud Run compatible que prefieras.

Ubicaciones de Cloud Run

Cloud Run es regional, lo que significa que la infraestructura que ejecuta los servicios se ubica en una región específica, y Google la administra para que esté disponible de manera redundante en todas las zonas de esa región.

El cumplimiento de los requisitos de latencia, disponibilidad o durabilidad es el factor principal para seleccionar la región en la que se ejecutan los servicios de Cloud Run. Por lo general, puedes seleccionar la región más cercana a los usuarios, pero debes considerar la ubicación de los otros productos de Google Cloud que usa el servicio de Cloud Run. Si usas productos de Google Cloud en varias ubicaciones, la latencia y el costo del servicio pueden verse afectados.

Cloud Run está disponible en las siguientes regiones:

Sujetas a los Precios del nivel 1

Sujetas a los Precios del nivel 2

  • africa-south1 (Johannesburgo)
  • asia-east2 (Hong Kong)
  • asia-northeast3 (Seúl, Corea del Sur)
  • asia-southeast1 (Singapur)
  • asia-southeast2 (Yakarta)
  • asia-south2 Delhi (India)
  • australia-southeast1 (Sídney)
  • australia-southeast2 (Melbourne)
  • europe-central2 (Varsovia, Polonia)
  • europe-west10 (Berlín) ícono de hoja Bajo nivel de CO2
  • europe-west12 (Turín)
  • europe-west2 (Londres, Reino Unido) ícono de hoja Bajo nivel de CO2
  • europe-west3 (Fráncfort, Alemania) ícono de hoja Bajo nivel de CO2
  • europe-west6 (Zúrich, Suiza) ícono de hoja Bajo nivel de CO2
  • me-central1 (Doha)
  • me-central2 (Dammam)
  • northamerica-northeast1 (Montreal) ícono de hoja Bajo nivel de CO2
  • northamerica-northeast2 (Toronto) ícono de hoja Bajo nivel de CO2
  • southamerica-east1 (São Paulo, Brasil) ícono de hoja Bajo nivel de CO2
  • southamerica-west1 (Santiago, Chile) ícono de hoja Bajo nivel de CO2
  • us-west2 (Los Ángeles)
  • us-west3 (Salt Lake City)
  • us-west4 (Las Vegas)

Si ya creaste un servicio de Cloud Run, puedes ver la región en el panel de Cloud Run en la consola de Google Cloud.

Recupera la muestra de código

A fin de recuperar la muestra de código para su uso, haz lo siguiente:

  1. Clona el repositorio de muestra en tu máquina local:

    Node.js

    git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git

    De manera opcional, puedes descargar la muestra como un archivo ZIP y extraerla.

  2. Ve al directorio que contiene el código de muestra de Cloud Run:

    Node.js

    cd nodejs-docs-samples/run/websockets/

Comprende el código

Socket.io es una biblioteca que permite la comunicación bidireccional en tiempo real entre el navegador y el servidor. Si bien Socket.io no es una implementación de WebSocket, sí ajusta la funcionalidad a fin de proporcionar una API más simple para varios protocolos de comunicación con funciones adicionales, como una mayor confiabilidad, una reconexión automática y hacer una transmisión a todos los clientes o a un subconjunto de ellos.

Integración del cliente

<script src="/socket.io/socket.io.js"></script>

El cliente crea una instancia de socket nueva para cada conexión. Debido a que este ejemplo es el procesamiento del servidor, no es necesario definir la URL del servidor. La instancia de socket puede emitir y escuchar eventos.

// Initialize Socket.io
const socket = io('', {
  transports: ['websocket'],
});
// Emit "sendMessage" event with message
socket.emit('sendMessage', msg, error => {
  if (error) {
    console.error(error);
  } else {
    // Clear message
    $('#msg').val('');
  }
});
// Listen for new messages
socket.on('message', msg => {
  log(msg.user, msg.text);
});

// Listen for notifications
socket.on('notification', msg => {
  log(msg.title, msg.description);
});

// Listen connect event
socket.on('connect', () => {
  console.log('connected');
});

Integración del servidor

Del lado del servidor, el servidor de Socket.io se inicializa y se adjunta al servidor HTTP. Al igual que en el lado del cliente, una vez que el servidor Socket.io establece una conexión con el cliente, se crea una instancia de socket para cada conexión que se puede utilizar para emitir y escuchar mensajes. Socket.io también proporciona una interfaz sencilla para crear “salas” o un canal arbitrario al que los sockets pueden unirse y salir.

// Initialize Socket.io
const server = require('http').Server(app);
const io = require('socket.io')(server);

const {createAdapter} = require('@socket.io/redis-adapter');
// Replace in-memory adapter with Redis
const subClient = redisClient.duplicate();
io.adapter(createAdapter(redisClient, subClient));
// Add error handlers
redisClient.on('error', err => {
  console.error(err.message);
});

subClient.on('error', err => {
  console.error(err.message);
});

// Listen for new connection
io.on('connection', socket => {
  // Add listener for "signin" event
  socket.on('signin', async ({user, room}, callback) => {
    try {
      // Record socket ID to user's name and chat room
      addUser(socket.id, user, room);
      // Call join to subscribe the socket to a given channel
      socket.join(room);
      // Emit notification event
      socket.in(room).emit('notification', {
        title: "Someone's here",
        description: `${user} just entered the room`,
      });
      // Retrieve room's message history or return null
      const messages = await getRoomFromCache(room);
      // Use the callback to respond with the room's message history
      // Callbacks are more commonly used for event listeners than promises
      callback(null, messages);
    } catch (err) {
      callback(err, null);
    }
  });

  // Add listener for "updateSocketId" event
  socket.on('updateSocketId', async ({user, room}) => {
    try {
      addUser(socket.id, user, room);
      socket.join(room);
    } catch (err) {
      console.error(err);
    }
  });

  // Add listener for "sendMessage" event
  socket.on('sendMessage', (message, callback) => {
    // Retrieve user's name and chat room  from socket ID
    const {user, room} = getUser(socket.id);
    if (room) {
      const msg = {user, text: message};
      // Push message to clients in chat room
      io.in(room).emit('message', msg);
      addMessageToCache(room, msg);
      callback();
    } else {
      callback('User session not found.');
    }
  });

  // Add listener for disconnection
  socket.on('disconnect', () => {
    // Remove socket ID from list
    const {user, room} = deleteUser(socket.id);
    if (user) {
      io.in(room).emit('notification', {
        title: 'Someone just left',
        description: `${user} just left the room`,
      });
    }
  });
});

Socket.io también proporciona un adaptador de Redis para transmitir eventos a todos los clientes, sin importar qué servidor entrega el socket. Socket.io solo usa el mecanismo Pub/Sub de Redis y no almacena ningún dato.

const {createAdapter} = require('@socket.io/redis-adapter');
// Replace in-memory adapter with Redis
const subClient = redisClient.duplicate();
io.adapter(createAdapter(redisClient, subClient));

El adaptador de Redis de Socket.io puede reutilizar el cliente de Redis que se usa para almacenar el historial de mensajes de la sala. Cada contenedor creará una conexión a la instancia de Redis, y Cloud Run puede crear una gran cantidad de instancias. Esto está muy por debajo de las 65,000 conexiones que admite Redis. Si necesitas admitir esta cantidad de tráfico, también debes evaluar la capacidad de procesamiento del conector de Acceso a VPC sin servidores.

Reconexión

Cloud Run tiene un tiempo de espera máximo de 60 minutos. Por lo tanto, debes agregar la lógica de reconexión para los posibles tiempos de espera. En algunos casos, Socket.io se vuelve a conectar automáticamente después de los eventos de error de conexión o de desconexión. No hay garantía de que el cliente se volverá a conectar a la misma instancia.

// Listen for reconnect event
socket.io.on('reconnect', () => {
  console.log('reconnected');
  // Emit "updateSocketId" event to update the recorded socket ID with user and room
  socket.emit('updateSocketId', {user, room}, error => {
    if (error) {
      console.error(error);
    }
  });
});
// Add listener for "updateSocketId" event
socket.on('updateSocketId', async ({user, room}) => {
  try {
    addUser(socket.id, user, room);
    socket.join(room);
  } catch (err) {
    console.error(err);
  }
});

Las instancias se conservarán si hay una conexión activa hasta que todas las solicitudes se cierren o se agote el tiempo de espera. Incluso si usas la afinidad de sesión de Cloud Run, es posible que las cargas de las solicitudes nuevas se balanceen en contenedores activos, lo que permite que los contenedores reduzcan la escala. Si te preocupa que haya una gran cantidad de contenedores persistentes después de un aumento repentino de tráfico, puedes reducir el valor de tiempo de espera máximo para que los sockets sin usar se borren con más frecuencia.

Envía el servicio

  1. Crea una instancia de Memorystore para Redis

    gcloud redis instances create INSTANCE_ID --size=1 --region=REGION

    Reemplace INSTANCE_ID por el nombre de la instancia, es decir, my-redis-instance, y REGION_ID por la región de todos sus recursos y servicios, es decir us-central1.

    Se asignará un rango de IP del rango desde el rango de red del servicio predeterminado de forma automática a la instancia. En este instructivo, se usa 1 GB de memoria para la caché local de mensajes en la instancia de Redis. Obtén más información a fin de determinar el tamaño inicial de una instancia de Memorystore para tu caso de uso.

  2. Configura un conector de Acceso a VPC sin servidores.

    Para conectarte a tu instancia de Redis, tu servicio de Cloud Run necesita acceso a la red de VPC autorizada de la instancia de Redis.

    Cada conector de VPC requiere su propia subred /28 para colocar sus instancias. Este rango de IP no debe superponerse con ninguna reserva de dirección IP existente en la red de VPC. Por ejemplo, 10.8.0.0 (/28) funcionará en la mayoría de los proyectos nuevos o puedes especificar otro rango de IP personalizado sin usar, como 10.9.0.0 (/28). Puedes ver qué rangos de IP están reservados en este momento en la consola de Google Cloud.

    gcloud compute networks vpc-access connectors create CONNECTOR_NAME \
      --region REGION \
      --range "10.8.0.0/28"

    Reemplaza CONNECTOR_NAME con el nombre de tu conector.

    Este comando crea un conector en la red de VPC predeterminada, igual que la instancia de Redis, con el tamaño de máquina e2-micro. Aumentar el tamaño de la máquina del conector puede mejorar la capacidad de procesamiento del conector, pero también puede aumentar el costo. El conector también debe estar en la misma región que la instancia de Redis. Obtén más información sobre la configuración del Acceso a VPC sin servidores.

  3. Define una variable de entorno con la dirección IP de la red autorizada de tu instancia de Redis:

     export REDISHOST=$(gcloud redis instances describe INSTANCE_ID --region REGION --format "value(host)")
  4. Crea una cuenta de servicio para que funcione como la identidad del servicio. De forma predeterminada, esta no tiene otros privilegios más que la membresía del proyecto.

    gcloud iam service-accounts create chat-identity
    gcloud projects add-iam-policy-binding PROJECT_ID \
    --member=serviceAccount:chat-identity@PROJECT_ID.iam.gserviceaccount.com \
    --role=roles/serviceusage.serviceUsageConsumer
  5. Compila e implementa la imagen de contenedor en Cloud Run:

    gcloud run deploy chat-app --source . \
        --vpc-connector CONNECTOR_NAME \
        --allow-unauthenticated \
        --timeout 3600 \
        --service-account chat-identity \
        --update-env-vars REDISHOST=$REDISHOST

    Responde a cualquier solicitud para instalar las API obligatorias. Para ello, responde y cuando se te solicite. Solo debes hacer esto una vez en un proyecto. Responde a otras solicitudes suministrando la plataforma y la región si no configuraste los valores predeterminados de estas como se describe en la página de configuración. Obtén más información para Implementa a partir del código fuente.

Prueba

Para probar el servicio completo, haz lo siguiente:

  1. Dirige tu navegador a la URL proporcionada en el paso de implementación anterior.

  2. Agrega tu nombre y una sala de chat para acceder.

  3. Envía un mensaje a la sala.

Si eliges seguir desarrollando estos servicios, recuerda que tienen acceso restringido de la administración de identidades y accesos (IAM) al resto de Google Cloud y necesitarán tener funciones de IAM adicionales para acceder a muchos otros servicios.

Limpia

Si creaste un proyecto nuevo para este instructivo, bórralo. Si usaste un proyecto existente y deseas conservarlo sin los cambios que se agregaron en este instructivo, borra los recursos creados para el instructivo.

Borra el proyecto

La manera más fácil de eliminar la facturación es borrar el proyecto que creaste para el instructivo.

Para borrar el proyecto, sigue estos pasos:

  1. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

Borra los recursos del instructivo

  1. Usa este comando para borrar el servicio de Cloud Run que implementaste en este instructivo:

    gcloud run services delete SERVICE-NAME

    En el ejemplo anterior, SERVICE-NAME es el nombre del servicio que elegiste.

    También puedes borrar los servicios de Cloud Run desde la consola de Google Cloud.

  2. Quita la configuración de región predeterminada de gcloud que agregaste durante la configuración en el instructivo:

     gcloud config unset run/region
    
  3. Quita la configuración del proyecto:

     gcloud config unset project
    
  4. Borra otros recursos de Google Cloud que creaste en este instructivo:

¿Qué sigue?