Tutorial: como criar um serviço de chat WebSocket para o Cloud Run

Neste tutorial, você verá como criar um serviço de chat em tempo real com várias salas usando WebSockets com conexão persistente para comunicação bidirecional. Com o WebSockets, o cliente e o servidor podem enviar mensagens uns para os outros sem procurar atualizações no servidor.

Embora o Cloud Run escalone automaticamente o número de instâncias de contêiner para exibir todo o tráfego, ele não fornece afinidade de sessão. Isso significa que qualquer nova solicitação pode ser encaminhada para uma instância de contêiner diferente. Consequentemente, as mensagens do usuário no serviço de chat precisam ser sincronizadas em todas as instâncias do contêiner, não apenas entre os clientes conectados a uma instância do contêiner.

Visão geral do design

Este serviço de chat de amostra usa uma instância do Memorystore para Redis para armazenar e sincronizar mensagens de usuários em todas as instâncias de contêiner. O Redis usa um mecanismo Pub/Sub para não confundir com o produto Cloud Pub/Sub, para enviar dados a clientes inscritos conectados a qualquer instância de contêiner, para eliminar a pesquisa de HTTP para atualizações.

No entanto, mesmo com atualizações push, qualquer instância de contêiner ativada receberá apenas novas mensagens enviadas ao contêiner. Para carregar mensagens anteriores, o histórico de mensagens precisaria ser armazenado e recuperado de uma solução de armazenamento permanente. Neste exemplo, usamos a funcionalidade convencional do Redis de um armazenamento de objetos para armazenar em cache e recuperar o histórico de mensagens.

Diagrama de arquitetura
O diagrama mostra várias conexões de cliente para cada instância de contêiner do Cloud Run. Cada instância de contêiner se conecta a uma instância do Memorystore para Redis por meio de um conector de acesso VPC sem servidor.

A instância do Redis é protegida da Internet usando IPs privados com acesso controlado e limitado a serviços em execução na mesma rede privada virtual que a instância do Redis. Portanto, é necessário um conector de acesso VPC sem servidor para que o serviço do Cloud Run se conecte ao Redis. Saiba mais sobre o acesso VPC sem servidor.

Limitações

  • Neste tutorial, não mostramos a autenticação do usuário final ou o armazenamento em cache da sessão. Para saber mais sobre a autenticação de usuários finais, consulte o tutorial do Cloud Run sobre autenticação do usuário final.

  • Neste tutorial, não implementamos um banco de dados como o Firestore para armazenamento e recuperação indefinidos do histórico de mensagens do chat.

  • São necessários elementos adicionais para que este serviço de amostra esteja pronto para produção. Uma instância do Redis de nível padrão é recomendada para fornecer alta disponibilidade usando a replicação e o failover automático.

Objetivos

  • Gravar, criar e implantar um serviço do Cloud Run que usa WebSockets.

  • Conecte-se a uma instância do Memorystore para Redis para publicar e assinar novas mensagens em instâncias de contêiner.

  • Conecte o serviço do Cloud Run com o Memorystore usando conectores de acesso VPC sem servidor.

Custos

Neste tutorial, há componentes faturáveis do Google Cloud, entre eles:

Use a calculadora de preços para gerar uma estimativa de custo com base no uso previsto.

Você já pode concluir este projeto como os créditos do teste gratuito.

Usuários novos do Cloud Platform podem se qualificar para um teste gratuito.

Antes de começar

  1. Faça login na sua conta do Google Cloud. Se você começou a usar o Google Cloud agora, crie uma conta para avaliar o desempenho de nossos produtos em situações reais. Clientes novos também recebem US$ 300 em créditos para executar, testar e implantar cargas de trabalho.
  2. No Console do Google Cloud, na página do seletor de projetos, selecione ou crie um projeto do Google Cloud.

    Acessar o seletor de projetos

  3. Verifique se o faturamento está ativado para seu projeto na nuvem. Saiba como confirmar se o faturamento está ativado para o projeto.

  4. No Console do Google Cloud, na página do seletor de projetos, selecione ou crie um projeto do Google Cloud.

    Acessar o seletor de projetos

  5. Verifique se o faturamento está ativado para seu projeto na nuvem. Saiba como confirmar se o faturamento está ativado para o projeto.

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

    Ative as APIs

  7. Instale e inicie o SDK do Cloud.

Configuração dos padrões gcloud

Para configurar a gcloud com os padrões do serviço do Cloud Run, realize as etapas a seguir:

  1. Defina seu projeto padrão:

    gcloud config set project PROJECT_ID

    Substitua PROJECT_ID pelo nome do projeto que você criou para este tutorial.

  2. Configure a gcloud para a região escolhida:

    gcloud config set run/region REGION

    Substitua REGION pela região compatível do Cloud Run.

Locais do Cloud Run

O Cloud Run é regional, o que significa que a infraestrutura que executa seus serviços do Cloud Run está localizada em uma região específica e é gerenciada pelo Google para estar disponível de maneira redundante em todas as zonas da região.

Atender aos seus requisitos de latência, disponibilidade ou durabilidade são os principais fatores para selecionar a região em que seus serviços do Cloud Run são executados. Geralmente, é possível selecionar a região mais próxima de seus usuários, mas considere a localização dos outros produtos do Google Cloud usados pelo serviço do Cloud Run. O uso de produtos do Google Cloud em vários locais pode afetar a latência e o custo do serviço.

O Cloud Run está disponível nas regiões a seguir:

Sujeitas aos preços do nível 1

  • asia-east1 (Taiwan)
  • asia-northeast1 (Tóquio)
  • asia-northeast2 (Osaka)
  • europe-north1 (Finlândia) Ícone de folha Baixo CO
  • europe-west1 (Bélgica) Ícone de folha Baixo CO
  • europe-west4 (Países Baixos)
  • us-central1 (Iowa)Ícone de folha Baixo CO
  • us-east1 (Carolina do Sul)
  • us-east4 (Norte da Virgínia)
  • us-west1 (Oregon) Ícone de folha Baixo CO

Sujeitas aos preços do nível 2

  • asia-east2 (Hong Kong)
  • asia-northeast3 (Seul, Coreia do Sul)
  • asia-southeast1 (Singapura)
  • asia-southeast2 (Jacarta)
  • asia-south1 (Mumbai, Índia)
  • asia-south2 (Déli, Índia)
  • australia-southeast1 (Sydney)
  • australia-southeast2 (Melbourne)
  • europe-central2 (Varsóvia, Polônia)
  • europe-west2 (Londres, Reino Unido)
  • europe-west3 (Frankfurt, Alemanha)
  • europe-west6 (Zurique, Suíça) Ícone de folha Baixo CO
  • northamerica-northeast1 (Montreal) Ícone de folha Baixo CO
  • northamerica-northeast2 (Toronto)
  • southamerica-east1 (São Paulo, Brasil) Ícone de folha Baixo CO
  • us-west2 (Los Angeles)
  • us-west3 (Salt Lake City)
  • us-west4 (Las Vegas)

Se você já criou um serviço do Cloud Run, poderá ver a região no painel do Cloud Run no Console do Cloud.

Como recuperar a amostra de código

Para recuperar a amostra de código para uso, siga estas etapas:

  1. Clone o repositório de amostra na máquina local:

    Node.js

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

    Outra alternativa é fazer o download da amostra como um arquivo ZIP e extraí-lo.

  2. Mude para o diretório que contém o código de amostra do Cloud Run:

    Node.js

    cd nodejs-docs-samples/run/websockets/

Noções básicas sobre o código

O Socket.io é uma biblioteca que permite a comunicação bidirecional em tempo real entre o navegador e o servidor. Embora o Socket.io não seja uma implementação WebSocket, ele une a funcionalidade para fornecer uma API mais simples para vários protocolos de comunicação com recursos adicionais, como maior confiabilidade, reconexão automática e transmitir para todos ou um subconjunto de clientes.

Integração no lado do cliente

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

O cliente instancia uma nova instância de soquete para cada conexão. Como este exemplo é renderizado no servidor, o URL do servidor não precisa ser definido. A instância de soquete pode emitir e ouvir 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');
});

Integração no servidor

No servidor, o servidor Socket.io é inicializado e anexado ao servidor HTTP. Semelhante ao lado do cliente, uma vez que o servidor Socket.io faz uma conexão com o cliente, uma instância de soquete é criada para cada conexão que pode ser usada para emitir e ouvir mensagens. Ele também oferece uma interface fácil para criar "salas" ou um canal arbitrário em que os soquetes podem entrar e sair.

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

O Socket.io também fornece um adaptador Redis para transmitir eventos para todos os clientes, independentemente de qual servidor está veiculando o soquete. O Socket.io usa apenas o mecanismo Pub/Sub do Redis e não armazena dados.

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

O adaptador Redis do Socket.io pode reutilizar o cliente Redis usado para armazenar o histórico de mensagens da sala. Cada contêiner criará uma conexão com a instância do Redis, e o Cloud Run terá no máximo 1.000 instâncias. Isso está dentro das 65.000 conexões compatíveis com o Redis. Se você precisa oferecer suporte a essa quantidade de tráfego, também precisa avaliar a capacidade do conector de acesso VPC sem servidor.

Reconexão

O Cloud Run tem um tempo limite máximo de 60 minutos. Portanto, você precisa adicionar a lógica de reconexão para possíveis tempos limite. Em alguns casos, o Socket.io tenta se reconectar automaticamente após eventos de desconexão ou erro de conexão. Não há garantia de que o cliente se reconectará à mesma instância de contêiner.

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

As instâncias de contêiner persistirão se houver uma conexão ativa até que todas as solicitações sejam encerradas ou expirem. Como não há afinidade de sessão, as novas solicitações serão balanceadas por carga para contêineres ativos, o que permite que os contêineres sejam escalonados. Se você estiver preocupado com um grande número de contêineres que persistem após um pico de tráfego, diminua o valor máximo de tempo limite para que os soquetes não utilizados sejam limpos com mais frequência.

Como enviar o serviço

  1. Crie uma instância do Memorystore para Redis:

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

    Substitua INSTANCE_ID pelo nome da instância, ou seja, my-redis-instance, e REGION_ID pela região de todos os seus recursos e serviços, ou seja, us-central1.

    A instância receberá automaticamente um intervalo de IP do intervalo de rede de serviço padrão. Neste tutorial, 1 GB de memória é usado para o cache local de mensagens na instância do Redis. Saiba mais sobre Como determinar o tamanho inicial de uma instância do Memorystore para seu caso de uso.

  2. Configure um conector de acesso VPC sem servidor:

    Para se conectar à instância do Redis, o serviço do Cloud Run (totalmente gerenciado) precisa acessar a rede VPC autorizada da instância do Redis.

    Cada conector VPC requer a própria sub-rede /28 para colocar instâncias de conector. Ele não pode sobrepor nenhuma reserva de endereço IP atual na rede VPC. Por exemplo, 10.8.0.0 (/28) funcionará na maioria dos novos projetos ou é possível especificar outro intervalo de IP personalizado não utilizado, como 10.9.0.0 (/28). É possível ver quais intervalos de IP estão reservados no Console do Cloud.

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

    Substitua CONNECTOR_NAME pelo nome do conector.

    Esse comando cria um conector na rede VPC padrão, igual à instância do Redis, com tamanho de máquina e2-micro. Aumentar o tamanho da máquina do conector melhora a capacidade do conector, mas também aumenta o custo. O conector também precisa estar na mesma região da instância do Redis. Saiba mais sobre Como configurar o acesso VPC sem servidor.

  3. Defina uma variável de ambiente com o endereço IP da rede autorizada da instância do Redis:

     export REDISHOST=$(gcloud redis instances describe INSTANCE_ID --region REGION --format "value(host)")
  4. Crie uma conta de serviço para servir como a identidade de serviço. Por padrão, isso não tem privilégios além da associação ao projeto.

    gcloud iam service-accounts create chat-identity

    Esse serviço não precisa interagir com mais nada no Google Cloud. Portanto, nenhuma outra permissão precisa ser atribuída a essa conta de serviço.

  5. Crie e implante a imagem do contêiner no 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

    Responda a todas as solicitações para instalar as APIs necessárias respondendo ao y quando solicitado. Você só precisa fazer isso uma vez para um projeto. Responda a outras solicitações fornecendo a plataforma e a região, se você não tiver definido os padrões delas, conforme descrito na página de configuração. Saiba mais sobre como implantar a partir do código-fonte.

Testar

Para testar o serviço completo:

  1. Navegue até o URL fornecido pela etapa de implantação acima.

  2. Adicione seu nome e uma sala de chat para fazer login.

  3. Envie uma mensagem para a sala.

Se você optar por continuar desenvolvendo esses serviços, lembre-se de que eles têm acesso restrito do gerenciamento de identidade e acesso (IAM, na sigla em inglês) ao restante do Google Cloud e precisarão receber mais papéis do IAM para acessar muitos outros serviços.

Discussão sobre custos

Exemplo de detalhamento de custos para um serviço de chat, hospedado em Iowa (us-central1) com uma instância do Redis de 5 GB e um conector de acesso VPC sem servidor.

Produto Custo por mês
Redis Custo = capacidade provisionada (5 GB) * Preço de nível regional (us-central1)

Nível Standard: 5 GB * US$ 0,054 GB/hora * 730 horas/mês = US$ 197
Nível básico: 5 GB * US$ 0,027 GB/hora * 730 horas/mês = US$ 99
Acesso VPC sem servidor Custo = preço da máquina * número de instâncias (o mínimo de instâncias é 2)

f1-micro: US$ 3,88 * 2 instâncias = US$ 7,76
e2- micro: US$ 6,11 * 2 instâncias = US$ 12,22
e2-standard-4: US$ 97,83 * 2 instâncias = US$ 195,66
Cloud Run Custo = tempo de uso do recurso de serviço do Cloud Run * preço * número de instâncias

Memória:0,5 GiB * US$ 0,00000250 / GiB-s * 60s/min * 60 min/h * 8 horas * 30,5 dias * 4 instâncias = US$ 4,39 †
CPU: 1 vCPU * US$ 0,00002400 / vCPU-s * 60 s/min * 60 minutos/hora * 8 horas * 30,5 dias * 4 instâncias = US$ 84,33†
Solicitações: S$ 0,40/milhão ~= US$ 0
Rede: US$ 0,085/GB/mês ~= US$ 0
Total US$ 197 + US$ 12,22 + US$ 89 ~= $298 por mês

† Consulte a análise abaixo para ver o contexto do cenário.

Neste tutorial, usamos uma instância do Redis de nível básico independente. O nível de serviço pode ser atualizado para Padrão para alta disponibilidade com replicação de zona cruzada ativada automaticamente e failover automático. A região e a capacidade também afetam os preços do Redis. Por exemplo, uma instância de nível padrão de 5 GB em Iowa (us-central1) custa US$ 0,054 por GB por hora. O custo por hora é 5 x US$ 0,054, que é aproximadamente US$ 0,27 por hora ou US$ 197 por mês. Determinar o tamanho inicial de uma instância do Memorystore e saber mais sobre os preços do Redis.

O conector de acesso VPC sem servidor tem o preço por tamanho de instância e número, além da saída de rede. Aumentar o tamanho e o número pode melhorar a capacidade ou reduzir a latência nas mensagens. Há três tamanhos de máquinas: f1-micro, e2-micro e e2-standard-4. O número mínimo de instâncias é 2, portanto, o custo mínimo é o dobro do tamanho da máquina.

O Cloud Run tem o preço calculado por uso de recursos, arredondado para os 100 ms mais próximos, para memória, CPU, número de solicitações e rede. Neste tutorial, as configurações padrão do Cloud Run são de 512 MiB e 1 vCPU, que custam 0,5 GiB * US$ 0,00000250 / GiB-segundo e 1 vCPU * US$ 0,00002400 / vCPUs-segundo, respectivamente, ou um total de US$ 0,091 por hora por instância. A simultaneidade afeta o custo total, embora os limites e as recomendações variem de acordo com o idioma. Aumentar a simultaneidade pode reduzir o número de instâncias de contêiner necessárias. O Cloud Run tem um máximo de 250 solicitações simultâneas. Com um máximo de 1.000 instâncias de contêiner com 250 solicitações simultâneas, é possível atender até 250 mil clientes. Por exemplo, um serviço que atende 1.000 funcionários com simultaneidade de 250 precisará de pelo menos 4 instâncias. 4 instâncias por 1 mês de 8 horas de duração custam US$ 0,091 * 4 instâncias * 8 horas * 30,5 dias = US$ 89,00. O número de solicitações e a rede custarão mais, mas provavelmente será o mínimo.

Este serviço de chat, hospedado em Iowa (us-central1), custaria US$ 197 para uma instância padrão do Redis de 5 GB, US$ 12,22 para um conector padrão de acesso VPC sem servidor, um valor estimado de US$ 89 para o serviço do Cloud Run por um total de US$ 298 por mês. Veja a estimativa na Calculadora de preços do Google Cloud.

Limpeza

Se você criou um novo projeto para este tutorial, exclua o projeto. Se você usou um projeto atual e quer mantê-lo sem as alterações incluídas neste tutorial, exclua os recursos criados para o tutorial.

Como excluir o projeto

O jeito mais fácil de evitar cobranças é excluindo o projeto que você criou para o tutorial.

Para excluir o projeto:

  1. No Console do Cloud, acesse a página Gerenciar recursos:

    Acessar "Gerenciar recursos"

  2. Na lista de projetos, selecione o projeto que você quer excluir e clique em Excluir .
  3. Na caixa de diálogo, digite o ID do projeto e clique em Encerrar para excluí-lo.

Como excluir recursos do tutorial

  1. Exclua o serviço do Cloud Run que você implantou neste tutorial:

    gcloud run services delete SERVICE-NAME

    SERVICE-NAME é o nome escolhido do serviço.

    Também é possível excluir os serviços do Cloud Run no Console do Google Cloud.

  2. Remova a configuração da região padrão da gcloud que você adicionou durante a configuração do tutorial:

     gcloud config unset run/region
    
  3. Remova a configuração do projeto:

     gcloud config unset project
    
  4. Exclua outros recursos do Google Cloud criados neste tutorial:

A seguir