Tutoriel sur la création d'un service WebSocket Chat pour Cloud Run

Ce tutoriel explique comment créer un service de chat en plusieurs parties et en temps réel à l'aide de WebSockets avec une connexion persistante pour la communication bidirectionnelle. Avec WebSockets, le client et le serveur peuvent envoyer des messages sans interroger le serveur.

Bien que Cloud Run effectue l'autoscaling du nombre d'instances de conteneur pour diffuser tout le trafic, il n'offre pas d'affinité de session. Cela signifie que toute nouvelle requête peut être acheminée vers une autre instance de conteneur. Par conséquent, les messages des utilisateurs du service de chat doivent être synchronisés entre toutes les instances de conteneur, et pas seulement entre les clients connectés à une instance de conteneur.

Présentation de la conception

Cet exemple de service de chat utilise une instance Memorystore pour Redis afin de stocker et de synchroniser les messages des utilisateurs sur toutes les instances de conteneur. Redis utilise un mécanisme Pub/Sub, à ne pas confondre avec le produit Cloud Pub/Sub, pour transmettre les données aux clients abonnés connectés à une instance de conteneur, et ainsi éliminer les interrogations HTTP pour les mises à jour.

Toutefois, même avec les mises à jour push, toute instance de conteneur créée ne reçoit que les nouveaux messages envoyés au conteneur. Pour charger les messages précédents, l'historique des messages doit être stocké et récupéré à partir d'une solution de stockage persistant. Cet exemple utilise les fonctionnalités traditionnelles de Redis d'un magasin d'objets pour mettre en cache et récupérer l'historique des messages.

Schéma de l'architecture
Le schéma illustre plusieurs connexions clientes vers chaque instance de conteneur Cloud Run. Chaque instance de conteneur se connecte à une instance Memorystore pour Redis via un connecteur d'accès au VPC sans serveur.

L'instance Redis est protégée contre Internet à l'aide d'adresses IP privées avec un accès contrôlé et limitée aux services exécutés sur le même réseau privé virtuel que l'instance Redis. Par conséquent, un connecteur d'accès au VPC sans serveur est nécessaire pour que le service Cloud Run se connecte à Redis. En savoir plus sur l'accès au VPC sans serveur.

Limites

  • Ce tutoriel ne montre pas l'authentification de l'utilisateur final ni la mise en cache de session. Pour en savoir plus sur l'authentification des utilisateurs finaux, consultez le tutoriel Cloud Run sur l'authentification des utilisateurs finaux.

  • Ce tutoriel ne met pas en œuvre une base de données telle que Firestore pour le stockage et la récupération illimités de l'historique des messages de chat.

  • Des éléments supplémentaires sont nécessaires pour que cet exemple de service soit prêt pour la production. Une instance Redis de niveau standard est recommandée pour fournir une haute disponibilité à l'aide de la réplication et du basculement automatique.

Objectifs

  • Écrire, créer et déployer un service Cloud Run utilisant WebSockets

  • Connectez-vous à une instance Memorystore pour Redis pour publier de nouveaux messages et vous y abonner sur plusieurs instances de conteneur.

  • Connectez le service Cloud Run à Memorystore à l'aide de connecteurs d'accès au VPC sans serveur.

Coûts

Ce tutoriel utilise les composants facturables suivants de Google Cloud :

Obtenez une estimation des coûts en fonction de votre utilisation prévue à l'aide du simulateur de coût. Les nouveaux utilisateurs de Google Cloud peuvent bénéficier d'un essai gratuit.

Avant de commencer

  1. Connectez-vous à votre compte Google Cloud. Si vous débutez sur Google Cloud, créez un compte pour évaluer les performances de nos produits en conditions réelles. Les nouveaux clients bénéficient également de 300 $ de crédits gratuits pour exécuter, tester et déployer des charges de travail.
  2. Dans Google Cloud Console, sur la page de sélection du projet, sélectionnez ou créez un projet Google Cloud.

    Accéder au sélecteur de projet

  3. Assurez-vous que la facturation est activée pour votre projet Cloud. Découvrez comment vérifier que la facturation est activée pour votre projet.

  4. Dans Google Cloud Console, sur la page de sélection du projet, sélectionnez ou créez un projet Google Cloud.

    Accéder au sélecteur de projet

  5. Assurez-vous que la facturation est activée pour votre projet Cloud. Découvrez comment vérifier que la facturation est activée pour votre projet.

  6. Activer les API Cloud Run, Memorystore for Redis, Serverless VPC Access, Artifact Registry, and Cloud Build .

    Activer les API

  7. Installez et initialisez le SDK Cloud.

Configurer les valeurs par défaut pour gcloud

Pour configurer gcloud avec les valeurs par défaut pour votre service Cloud Run, procédez comme suit :

  1. Définissez le projet par défaut :

    gcloud config set project PROJECT_ID

    Remplacez PROJECT_ID par le nom du projet que vous avez créé pour ce tutoriel.

  2. Configurez gcloud pour la région choisie :

    gcloud config set run/region REGION

    Remplacez REGION par la région Cloud Run compatible de votre choix.

Emplacements Cloud Run

Cloud Run est régional, ce qui signifie que l'infrastructure qui exécute vos services Cloud Run est située dans une région spécifique et gérée par Google pour être disponible de manière redondante dans toutes les zones de cette région.

Lors de la sélection de la région dans laquelle exécuter vos services Cloud Run, vous devez tout d'abord considérer vos exigences en matière de latence, de disponibilité et de durabilité. Vous pouvez généralement sélectionner la région la plus proche de vos utilisateurs, mais vous devez tenir compte de l'emplacement des autres produits Google Cloud utilisés par votre service Cloud Run. L'utilisation conjointe de produits Google Cloud dans plusieurs emplacements peut avoir une incidence sur la latence et le coût de votre service.

Cloud Run est disponible dans les régions suivantes :

Soumis aux tarifs de niveau 1

Soumis aux tarifs de niveau 2

  • asia-east2 (Hong Kong)
  • asia-northeast3 (Séoul, Corée du Sud)
  • asia-southeast1 (Singapour)
  • asia-southeast2 (Jakarta)
  • asia-south1 (Mumbai, Inde)
  • asia-south2 (Delhi, Inde)
  • australia-southeast1 (Sydney)
  • australia-southeast2 (Melbourne)
  • europe-central2 (Varsovie, Pologne)
  • europe-west2 (Londres, Royaume-Uni)
  • europe-west3 (Francfort, Allemagne)
  • europe-west6 (Zurich, Suisse) Icône Feuille Faibles émissions de CO2
  • northamerica-northeast1 (Montréal) Icône Feuille Faibles émissions de CO2
  • northamerica-northeast2 (Toronto) Icône Feuille Faibles émissions de CO2
  • southamerica-east1 (São Paulo, Brésil) Icône Feuille Faibles émissions de CO2
  • southamerica-west1 (Santiago, Chili)
  • us-west2 (Los Angeles)
  • us-west3 (Salt Lake City)
  • us-west4 (Las Vegas)

Si vous avez déjà créé un service Cloud Run, vous pouvez afficher la région dans le tableau de bord Cloud Run de Cloud Console.

Récupérer l'exemple de code

Pour récupérer l’exemple de code à utiliser, procédez comme suit :

  1. Clonez l'exemple de dépôt sur votre ordinateur local :

    Node.js

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

    Vous pouvez également télécharger l'exemple en tant que fichier ZIP et l'extraire.

  2. Accédez au répertoire contenant l'exemple de code Cloud Run :

    Node.js

    cd nodejs-docs-samples/run/websockets/

Comprendre le code

Socket.io est une bibliothèque qui permet une communication bidirectionnelle en temps réel entre le navigateur et le serveur. Bien que Socket.io ne soit pas une implémentation WebSocket, il encapsule la fonctionnalité pour fournir une API plus simple pour plusieurs protocoles de communication avec des fonctionnalités supplémentaires, telles qu'une fiabilité améliorée, une reconnexion automatique et la diffusion à tous ou à un sous-ensemble de clients.

Intégration côté client

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

Le client instancie une nouvelle instance de socket pour chaque connexion. Comme cet exemple est côté serveur, il n'est pas nécessaire de définir l'URL du serveur. L'instance de socket peut émettre et écouter des événements.

// 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');
});

Intégration côté serveur

Du côté du serveur, le serveur Socket.io est initialisé et associé au serveur HTTP. Comme pour le client, une fois que le serveur Socket.io établit une connexion avec le client, une instance de socket est créée pour chaque connexion pouvant être utilisée pour émettre et écouter des messages. Socket.io fournit également une interface simple pour créer des "salles" ou un canal arbitraire que les sockets peuvent rejoindre et quitter.

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

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

// 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 fournit également un adaptateur Redis pour diffuser des événements à tous les clients, quel que soit le serveur qui diffuse le socket. Socket.io n'utilise que le mécanisme Pub/Sub de Redis et ne stocke aucune donnée.

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

L'adaptateur Redis de Socket.io peut réutiliser le client Redis utilisé pour stocker l'historique des messages de la salle. Chaque conteneur crée une connexion à l'instance Redis et Cloud Run dispose d'un maximum de 1 000 instances. Ce nombre est bien inférieur aux 65 000 connexions que Redis peut prendre en charge. Si vous devez gérer cette quantité de trafic, vous devez également évaluer le débit du connecteur d'accès au VPC sans serveur.

Reconnexion

Le délai avant expiration maximal de Cloud Run est de 60 minutes. Vous devez donc ajouter une logique de reconnexion pour les délais d'expiration possibles. Dans certains cas, Socket.io tente automatiquement de se reconnecter après les événements de déconnexion ou d'erreur de connexion. Rien ne garantit que le client se reconnectera à la même instance de conteneur.

// 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);
  }
});

Les instances de conteneur seront conservées si une connexion active est établie jusqu'à ce que toutes les requêtes soient fermées ou expirent. Comme il n'y a pas d'affinité de session, la charge des nouvelles requêtes est équilibrée sur les conteneurs actifs, ce qui permet aux conteneurs d'évoluer. Si vous craignez qu'un grand nombre de conteneurs persistent après un pic de trafic, vous pouvez réduire la valeur du délai maximal pour que les sockets inutilisés soient nettoyés plus fréquemment.

Transmettre le service

  1. Créer une instance Memorystore pour Redis :

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

    Remplacez INSTANCE_ID par le nom de l'instance, c'est-à-dire my-redis-instance, et REGION_ID par la région pour toutes vos ressources et tous vos services, c'est-à-dire us-central1.

    Une plage d'adresses IP de la plage de réseau de service par défaut sera automatiquement attribuée à l'instance. Ce tutoriel utilise 1 Go de mémoire pour le cache local de messages dans l'instance Redis. En savoir plus sur la détermination de la taille initiale d'une instance Memorystore pour votre cas d'utilisation.

  2. Configurer un connecteur d'accès au VPC sans serveur :

    Pour se connecter à votre instance Redis, votre service Cloud Run doit avoir accès au réseau VPC autorisé de votre instance Redis.

    Chaque connecteur VPC nécessite son propre sous-réseau /28 pour y placer des instances. Cette plage d'adresses IP ne doit pas chevaucher les réservations d'adresses IP existantes sur votre réseau VPC. Par exemple, 10.8.0.0 (/28) fonctionnera dans la plupart des nouveaux projets ou vous pourrez spécifier une autre plage d'adresses IP personnalisée inutilisée, telle que 10.9.0.0 (/28). Vous pouvez afficher les plages d'adresses IP actuellement réservées dans Cloud Console.

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

    Remplacez CONNECTOR_NAME par le nom de votre connecteur.

    Cette commande crée un connecteur dans le réseau VPC par défaut, identique à l'instance Redis, avec la taille de machine e2-micro. L'augmentation de la taille de la machine du connecteur peut améliorer son débit, mais également augmenter les coûts. Le connecteur doit également se trouver dans la même région que l'instance Redis. Découvrez comment configurer l'accès au VPC sans serveur.

  3. Définissez une variable d'environnement avec l'adresse IP du réseau autorisé de votre instance Redis :

     export REDISHOST=$(gcloud redis instances describe INSTANCE_ID --region REGION --format "value(host)")
  4. Créez un compte de service qui servira d'identité de service. Par défaut, il ne dispose d'aucun autre droit que l'abonnement au projet.

    gcloud iam service-accounts create chat-identity

    Ce service n'a pas besoin d'interagir avec autre chose dans Google Cloud. Par conséquent, aucune autorisation supplémentaire n'a besoin d'être attribuée à ce compte de service.

  5. Créez et déployez l'image de conteneur dans Cloud Run :

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

    Répondez aux invites pour installer les API requises en répondant y lorsque vous y êtes invité. Vous ne devez procéder à cette opération qu'une fois par projet. Répondez aux autres invites en fournissant la plate-forme et la région, si vous n'avez pas défini les paramètres par défaut pour celles-ci, comme décrit sur la page de configuration. En savoir plus sur le déploiement à partir de code source.

Essayez-le !

Pour tester le service complet :

  1. Accédez dans votre navigateur à l'URL fournie par l'étape de déploiement ci-dessus.

  2. Ajoutez votre nom et un salon de discussion pour vous connecter.

  3. Envoie un message au salon !

Si vous décidez de poursuivre le développement de ces services, n'oubliez pas qu'ils ont un accès IAM restreint au reste de Google Cloud. Des rôles IAM supplémentaires devront donc leur être attribués afin de pouvoir accéder à de nombreux autres services.

Discussion sur les coûts

Exemple de répartition des coûts pour un service de chat, hébergé dans l'Iowa (us-central1) avec une instance Redis de 5 Go et un connecteur d'accès au VPC sans serveur.

Produit Coût mensuel
Redis Coût = Capacité fournie (5 Go) x Prix du niveau régional (us-central1)

Niveau Standard : 5 Go x 0,054 $/h x 730 heures/mois = 197 $
Niveau de base : 5 Go x 0,027 $/h x 730 h/mois = 99 $
Accès au VPC sans serveur Coût = prix de la machine x nombre d'instances (nombre minimal d'instances défini sur 2)

f1-micro : 3,88 $ x 2 instances = 7,76 $
e2 micro : 6,11 $ x 2 instances = 12,22 $
e2-standard-4 : 97,83 $ x 2 instances = 195,66 $
Cloud Run Coût = Durée d'utilisation des ressources du service Cloud Run * prix x nombre d'instances

Mémoire : 0,5 Gio x 0,00000250 $ / Gio x 60 s/min x 60 min/h x 8 h x 30,5 jours x  4 instances = 4,39 $†
Processeur : 1 processeur virtuel x 0,00002400 $ / processeurs virtuels x 60 s/min x 60 min/h x 8 heures x 30,5 jours x 4 instances = 84,33 $†
Requêtes : 0,40 $ / million ~= 0 $
Mise en réseau : 0,085 $/Go/mois ~= 0 $
Total 197 $ + 12,22 $ + 89 $ ~= 298 $ par mois

† Consultez la section ci-dessous pour connaître le contexte du scénario.

Ce tutoriel utilise une instance Redis de niveau de base autonome. Le niveau de service peut être mis à niveau vers Standard pour une haute disponibilité avec la réplication interzone activée automatiquement et le basculement automatique. La région et la capacité affectent également les tarifs de Redis. Par exemple, une instance de niveau standard de 5 Go en Iowa (us-central1) coûte 0,054 $ par Go et par heure. Le coût horaire est de 5 x 0,054 $, ce qui correspond à environ 0,27 $ par heure ou à 197 $ par mois. Déterminez la taille initiale d'une instance Memorystore et découvrez les tarifs de Redis.

Le connecteur d'accès au VPC sans serveur est facturé en fonction de la taille et du nombre d'instances, ainsi que de la sortie réseau. L'augmentation de cette taille et de ce nombre peut améliorer le débit ou réduire la latence des messages. Il existe trois tailles de machines : f1-micro, e2-micro et e2-standard-4. Le nombre minimal d'instances est de 2. Le coût minimal est donc le double de la taille de la machine.

Cloud Run est facturé en fonction de l'utilisation des ressources, arrondie à la centaine de millisecondes la plus proche pour la mémoire, le processeur, le nombre de requêtes et la mise en réseau. Ce tutoriel utilise les paramètres Cloud Run par défaut de 512 Mio et 1 processeur virtuel, qui coûtent 0,5 Gio x 0,00000250 $ / Gio-seconde et 1 processeur virtuel x 0,00002400 $/processeur virtuel-seconde, respectivement, soit un total de 0,091 $ par heure et par instance. La simultanéité a un impact considérable sur le coût total, bien que les limites et les recommandations varient selon les langues. L'augmentation de la simultanéité peut réduire le nombre d'instances de conteneur nécessaires. Cloud Run a un maximum de 250 requêtes simultanées. Avec un maximum de 1 000 instances de conteneur comportant 250 requêtes simultanées, vous pouvez diffuser jusqu'à 250 000 clients. Par exemple, pour un service desservant 1 000 employés avec une simultanéité de 250, vous aurez besoin d'au moins quatre instances. 4 instances pendant 1 mois à 8 heures par jour coûtent 0,091 $ x 4 instances x 8 heures x 30,5 jours = 89 $. Le nombre de requêtes et la mise en réseau entraînent des frais supplémentaires, sans doute au minimum.

Ce service de chat, hébergé dans l'Iowa (us-central1), coûte 197 $ pour une instance Redis Standard de 5 Go et 12,22 $ pour un connecteur d'accès au VPC sans serveur par défaut, et 89 $ pour le service Cloud Run (soit un total de 298 $par mois). Consultez l'estimation dans le simulateur de coût Google Cloud.

Nettoyer

Si vous avez créé un projet pour ce tutoriel, supprimez-le. Si vous avez utilisé un projet existant et que vous souhaitez le conserver sans les modifications du présent tutoriel, supprimez les ressources créées pour ce tutoriel.

Supprimer le projet

Le moyen le plus simple d'empêcher la facturation est de supprimer le projet que vous avez créé pour ce tutoriel.

Pour supprimer le projet :

  1. Dans Cloud Console, accédez à la page Gérer les ressources.

    Accéder à la page Gérer les ressources

  2. Dans la liste des projets, sélectionnez le projet que vous souhaitez supprimer, puis cliquez sur Supprimer.
  3. Dans la boîte de dialogue, saisissez l'ID du projet, puis cliquez sur Arrêter pour supprimer le projet.

Supprimer les ressources du tutoriel

  1. Supprimez le service Cloud Run que vous avez déployé dans ce tutoriel :

    gcloud run services delete SERVICE-NAME

    SERVICE-NAME est le nom de service que vous avez choisi.

    Vous pouvez également supprimer des services Cloud Run à partir de Google Cloud Console.

  2. Supprimez la configuration régionale gcloud par défaut que vous avez ajoutée lors de la configuration du tutoriel :

     gcloud config unset run/region
    
  3. Supprimez la configuration du projet :

     gcloud config unset project
    
  4. Supprimez les autres ressources Google Cloud créées dans ce tutoriel :

Étape suivante