为使用WebSocket构建的双向通信应用带来基于服务网格的全链路灰度

本文涉及的产品
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
简介: 介绍如何使用为基于WebSocket的云原生应用构建全链路灰度方案。

【阅读原文】戳:为使用WebSocket构建的双向通信应用带来基于服务网格的全链路灰度

流量泳道是一种将云原生应用中的多个服务根据版本(或其他特征)隔离成多个独立运行环境的能力,由服务网格通过精细化地控制服务间的东西向流量来实现,常用于全链路灰度发布、应用开发测试隔离环境构建等场景。目前宽松模式的流量泳道主要对于基于HTTP协议通信的服务有完善的支持,而只要对应用上下文的透传进行合理的配置,宽松模式的流量泳道也可以应用在WebSocket这样的双向通信应用上。本文主要介绍如何为基于WebSocket构建的双向通信应用带来宽松模式流量泳道,并基于此实现全链路灰度效果。

 

 

 

 

概述

 

 

 

在前文基于阿里云服务网格流量泳道的全链路流量管理(一):严格模式流量泳道基于阿里云服务网格流量泳道的全链路流量管理(二):宽松模式流量泳道中,我们介绍了流量泳道的概念、使用流量泳道进行全链路灰度管理的方案,以及阿里云服务网格ASM提供的严格模式与宽松模式的流量泳道:

 

流量泳道是将一个云原生应用中的多个服务根据服务版本(或其他特征)隔离成的多个独立的运行环境。

 

在严格模式下,每条流量泳道中包含应用的调用链路上的全部服务,对于应用程序则没有任何要求。

 

而在宽松模式下,您只需要确保创建一条包含调用链路中所有服务的泳道:基线泳道。其它泳道可以不包含调用链路上的全部服务。当一个泳道中的服务进行相互调用时,若目标服务在当前泳道中不存在,则请求将被转发到基线泳道中的相同服务,并在请求目标存在当前泳道中存在时将请求重新转发回当前泳道。宽松模式的流量泳道虽然可以实现灵活的全链路灰度,但要求应用程序必须包含一个能够在整条调用链路中透传的请求头(链路透传请求头)。

 

由于WebSocket基于HTTP协议进行通信、也可以在建联时加入HTTP请求头等元数据。socket.io是一个高性能的实时双向通信框架,它允许在客户端和服务器之间进行高效的、低延迟的数据交换,本文将以一个基于socket.io开发的示例服务为例,展示当WebSocket应用中存在调用链路时,如何使用宽松模式的流量泳道为此类应用带来全链路的灰度能力。

 

 

 

 

场景说明

 

 

 

本文将演示一个具有简单调用链路特征的基于socket.io构建的WebSocket应用,并为该应用创建宽松模式流量泳道来实现简单的全链路灰度效果。该应用的拓扑如下:

 

 

应用的主要逻辑如下:client_socket是集群外部的客户端,它通过ASM网关与集群内部的wsk服务通过socket.io进行双向通信。当wsk服务向与其建联的client_socket客户端发送消息时,会通过HTTP GET请求集群中的http服务helloworld,并将helloworld服务的响应内容作为消息发送给client_socket。

 

如图所示,helloworld服务当前后端部署了两个版本的工作负载:helloworld-v1和helloworld-v2。本例中期望实现的效果则是根据client_socket在与wsk服务建联时提供的请求头信息(version请求头)决定wsk后续请求的具体工作负载版本。

 

这是一个比较典型且简单的全链路灰度发布场景:当应用发布新的v2版本时,只发布了位于调用链路最后端的helloworld应用。此时希望wsk服务扔保持唯一的v1版本,并根据客户端发起连接时携带的请求头元数据来确定最终请求调用的helloworld目标版本。

 

 

 

 

前提条件

 

 

 

已创建ASM企业版或旗舰版实例,且版本为1.18.2.111及以上。具体操作,请参见创建ASM实例[1]

 

已添加ACK集群到ASM实例。具体操作,请参见添加集群到ASM实例[2]

 

已创建名称为ingressgateway的ASM网关,并创建30080端口。具体操作,请参见创建入口网关服务[3]

 

已创建名称为ws-gateway且命名空间为istio-system的网关规则。具体操作,请参见管理网关规则[4]

 

为default命名空间开启Sidecar自动注入。具体操作,请参考开启Sidecar自动注入[5]

 

apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
  name: ws-gateway
  namespace: default
spec:
  selector:
    istio: ingressgateway
  servers:
    - hosts:
        - '*'
      port:
        name: http
        number: 30080
        protocol: HTTP

 

 

 

 

步骤一:部署示例应用

 

 

 

通过kubectl连接到ACK集群,并执行以下指令部署示例应用:

 

kuebctl apply -f- <<EOF
apiVersion: v1
kind: Service
metadata:
  name: wsk-svc
spec:
  selector:
    app: wsk
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 10
  ports:
    - protocol: TCP
      port: 5000
      targetPort: 5000
      name: http-ws
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wsk-deploy
  labels:
    app: wsk
    version: v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: wsk
      version: v1
  template:
    metadata:
      labels:
        app: wsk
        track: stable
        version: v1
      annotations:
        instrumentation.opentelemetry.io/inject-nodejs: "true"
        instrumentation.opentelemetry.io/container-names: "websocket-base"
    spec:
      containers:
        - name: websocket-base
          image: "registry-cn-hangzhou.ack.aliyuncs.com/dev/asm-socketio-sample:669297ea"
          imagePullPolicy: Always
          ports:
          - name: websocket
            containerPort: 5000
---
apiVersion: v1
kind: Service
metadata:
  name: helloworld
  labels:
    app: helloworld
spec:
  ports:
  - port: 5000
    name: http
  selector:
    app: helloworld
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: helloworld
  labels:
    account: helloworld
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: helloworld-v1
  labels: 
    apps: helloworld
    version: v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: helloworld
      version: v1
  template:
    metadata:
      labels:
        app: helloworld
        version: v1
    spec:
      serviceAccount: helloworld
      serviceAccountName: helloworld
      containers:
      - name: helloworld
        image: registry-cn-hangzhou.ack.aliyuncs.com/ack-demo/examples-helloworld-v1:1.0
        imagePullPolicy: IfNotPresent 
        ports:
        - containerPort: 5000
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: helloworld-v2
  labels: 
    apps: helloworld
    version: v2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: helloworld
      version: v2
  template:
    metadata:
      labels:
        app: helloworld
        version: v2
    spec:
      serviceAccount: helloworld
      serviceAccountName: helloworld
      containers:
      - name: helloworld
        image: registry-cn-hangzhou.ack.aliyuncs.com/ack-demo/examples-helloworld-v2:1.0
        imagePullPolicy: IfNotPresent 
        ports:
        - containerPort: 5000
EOF

 

在集群中部署了v1和v2两个版本的helloworld、该服务会响应发往/hello的HTTP GET请求,并返回自己的版本信息。同时还部署了v1版本的wsk服务,该服务是一个示例的socket.io服务,会响应客户端的消息、响应消息时调用helloworld服务,并将helloworld服务的输出返回给客户端,其代码如下:

 

import os from 'os';
import fetch from 'node-fetch';
import http from 'http'
import socketio from "socket.io";
const ifaces = os.networkInterfaces();
const makeRequest = async (url, baggage) => {
  try {
    let headers = {}
    if (!!baggage) {
      headers['baggage'] = baggage
    }
    const response = await fetch(url, {
      headers
    });
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const data = await response.text(); // 或者 response.json() 如果你需要 JSON 数据
    return data;
  } catch (error) {
    console.error('Error fetching the data:', error);
    return 'error';
  }
}
const privateIp = (() => {
  return Object.values(ifaces).flat().find(val => {
    return (val.family == 'IPv4' && val.internal == false);
  }).address;
})();
const randomOffset = Math.floor(Math.random() * 10);
const intervalOffset = (30+randomOffset) * Math.pow(10,3);
// WebSocket Server
const socketPort = 5000;
const socketServer = http.createServer();
const io = socketio(socketServer, {
  path: '/'
});
// Handlers
io.on('connection', client => {c
  console.log('New incoming Connection from', client.id);
  client.on('test000', async (message) => {
    console.log('Message from the client:',client.id,'->',message);
    const response = await makeRequest('http://helloworld:5000/hello', client.handshake.headers['baggage']);
    client.emit("hello", response, resp => {
      console.log('Response from the client:',client.id,'->',resp);
    })
  })
});
const emitOk = async () => {
  const response = await makeRequest('http://helloworld:5000/hello');
  let log0 = `I am the host: ${privateIp}. I am healty. Hello message: ${response}`;
  console.log(log0);
  io.emit("okok", log0);
}
setInterval(() => {
  emitOk();
}, intervalOffset);
// Web Socket listen
socketServer.listen(socketPort);

 

从代码中可以看到,应用中配置了当向客户端发送消息时、会调用helloworld服务,此时会读取客户端建联时提供的baggage请求头、并将请求头传递到调用helloworld服务的HTTP请求中;通过这种方式,socket.io应用实现了上下文在调用链路中的透传。我们可以利用这个上下文来实现宽松模式的流量泳道。

 

有关Baggage是什么?

 

Baggage是OpenTelemetry推出的一种标准化机制,旨在实现分布式系统调用链路中跨进程传递上下文信息。它通过在HTTP头部增加名为“Baggage”的字段实现,字段值为键值对格式,可传递租户ID、追踪ID、安全凭证等上下文数据。例如:

 

baggage: userId=alice,serverNode=DF%2028,isProduction=false

 

Baggage是OpenTelemetry社区提出的调用链路上下文透传标准化机制,因此我们也推荐您基于这种方式来配置宽松模式的流量泳道。

 

 

接下来我们部署网关上向wsk服务引流的虚拟服务,执行如下指令:

 

kubectl apply -f- <<EOF
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: ws
  namespace: default
spec:
  gateways:
    - default/ws-gateway
  hosts:
    - '*'
  http:
    - route:
        - destination:
            host: wsk-svc
            port:
              number: 5000
EOF

 

这条虚拟服务声明了一条在网关上的路由规则,通过这条虚拟服务,ASM网关就可以将客户端的socket.io请求转发到后端的wsk socket.io服务。

 

 

 

 

步骤二:部署流量泳道

 

 

 

在示例中,我们将wsk->hwlloworld服务调用链路的v1、v2两个版本分隔成两条泳道,三条泳道组成的集合称为泳道组。在服务网格ASM中,可以通过简单的配置完成泳道组和泳道的创建。

 

登录ASM控制台[6],在左侧导航栏,选择服务网格 > 网格管理。

 

在网格管理页面,单击目标实例名称,然后在左侧导航栏,选择流量管理中心 > 流量泳道。

 

在流量泳道页面,单击创建泳道组,在创建泳道组面板,配置相关信息,然后单击确定。

 

 

接下来在泳道组内创建v1、v2泳道,分别对应服务的v1、v2版本。

 

在流量泳道页面的流量规则定义区域,单击创建泳道。

 

在创建泳道对话框,配置相关信息,然后单击确定。

 

 

其中v1版本的泳道包含wsk-svc和helloworld两个服务,而v2版本的泳道仅包含helloworld服务(宽松模式的流量泳道允许一条泳道中不包含全部的服务,v1版本的wsk服务将作为服务的基线版本)。

 

 

 

 

步骤三:测试泳道效果

 

 

 

使用如下内容创建client-socket.js文件(将ASM网关IP替换成实际ip地址):

 

import uuid from 'uuid';
import io from 'socket.io-client';
const client = io('http://{ASM网关IP}:30080', {
  reconnection: true,
  reconnectionDelay: 500,
  transports: ['websocket'],
  extraHeaders: {
    'version': 'v2'
  }
});
const clientId = uuid.v4();
let disconnectTimer;
client.on('connect', function(){
  console.log("Connected!", clientId);
  setTimeout(function() {
    console.log('Sending first message');
    client.emit('test000', clientId);
  }, 500);
  // clear disconnection timeout
  clearTimeout(disconnectTimer);
});
client.on('okok', function(message) {
  console.log('The server has a message for you:', message);
})
client.on('hello', (arg, callback) => {
  console.log('the server respond to your test000: ' + arg);
  callback("got it");
})
client.on('disconnect', function(){
  console.log("Disconnected!");
  disconnectTimer = setTimeout(function() {
    console.log('Not reconnecting in 30s. Exiting...');
    process.exit(0);
  }, 10000);
});
client.on('error', function(err){
  console.error(err);
  process.exit(1);
});
setInterval(function() {
  console.log('Sending repeated message');
  client.emit('test000', clientId);
}, 5000);

 

执行client_socket.js来查看双向通信结果,预期结果如下:

 

❯ node client_socket.js
Connected! ffcff9b2-2e41-4334-b64c-60512bdb7c7a
Sending first message
the server respond to your test000: Hello version: v2, instance: helloworld-v2-7b94944d69-wcckk
The server has a message for you: I am the host: 192.168.1.60. I am healty. Hello message: Hello version: v1, instance: helloworld-v1-978dbcf9-vsvw8
Sending repeated message
the server respond to your test000: Hello version: v2, instance: helloworld-v2-7b94944d69-wcckk
Sending repeated message
the server respond to your test000: Hello version: v2, instance: helloworld-v2-7b94944d69-wcckk
Sending repeated message
the server respond to your test000: Hello version: v2, instance: helloworld-v2-7b94944d69-wcckk

 

从预期结果可以看到,服务端给客户端的每一次响应都调用了Helloworld服务的v2版本。这是因为客户端在发起连接时使用了version: v2的额外信息,通过在wsk向helloworld服务发起请求时还原请求链路上下文,服务网格可以让请求发往正确的helloworld版本,达成全链路灰度的效果。

 

除了向特定的客户端回复消息外,此示例中还实现了服务端向所有客户端的消息广播,我们可以在客户端的log中定期看到这样的内容:

 

The server has a message for you: I am the host: 192.168.1.60. I am healty. Hello message: Hello version: v1, instance: helloworld-v1-978dbcf9-vsvw8

 

此消息是服务端对客户端的广播消息、所有连接到该服务端的客户端都会定期收到它。可以看到这条广播消息实际上调用了helloworld的v1版本,这是因为广播消息实际上没有具体的客户端上下文,所以调用时使用了helloworld的基线版本(即v1版本)。

 

 

 

 

FAQ:为什么不使用OpenTelemetry auto-instrumentation?

 

 

 

auto-instrumentation即自动插装[7],该能力使用OpenTelemetry Operator为应用程序注入自动插装能力来实现对链路ID请求头的透传,而无需修改应用程序代码。要完成自动插装,您需要跟随上述社区文档完成OpenTelemetry Operator的安装、自动插装的配置,以及为应用Pod加入annotation的一系列步骤。OpenTelemetry自动插装支持多种常见的分布式链路上下文透传标准(如W3C Baggage、B3等)。

 

对于socket.io来说,自动插装的语义实际上会变得有些不同,虽然socket.io基于HTTP(WebSocket),但主要实现的是服务之间的双向通信,客户端和服务端实际上可以实时地双向互相发送消息。因此,社区实现的socket.io自动插装实际上是在消息级别去实现的。而在HTTP的层级上,整个客户端和服务端的连接本身就是一个大的HTTP请求,我们主要是需要根据请求建联时客户端提供的额外请求头信息来确定调用链路上下文,对于这个场景来说,社区自动插装提供的上下文透传实际上没有太大意义、需要对代码进行轻度的改造。详情可以参考社区对socket.io自动插装的介绍文章:Instrument Your Node.js Socket.io Apps with OpenTelemetry Like a PRO

 

相关链接:

 

[1] 创建ASM实例

https://help.aliyun.com/zh/asm/getting-started/create-an-asm-instance#task-2370657

 

[2] 添加集群到ASM实例

https://help.aliyun.com/zh/asm/getting-started/add-a-cluster-to-an-asm-instance-1#task-2372122

 

[3] 创建入口网关服务

https://help.aliyun.com/zh/asm/user-guide/create-an-ingress-gateway#task-2372970

 

[4] 管理网关规则

https://help.aliyun.com/zh/asm/user-guide/manage-istio-gateways

 

[5] 开启Sidecar自动注入

https://help.aliyun.com/zh/asm/user-guide/configuring-a-sidecar-injection-policy#task-1962690

 

[6] ASM控制台

https://servicemesh.console.aliyun.com/#/auth

 

[7] 自动插装

https://opentelemetry.io/docs/kubernetes/operator/automatic/





我们是阿里巴巴云计算和大数据技术幕后的核心技术输出者。

欢迎关注 “阿里云基础设施”同名微信微博知乎

获取关于我们的更多信息~

相关文章
|
1月前
|
前端开发 JavaScript UED
探索Python Django中的WebSocket集成:为前后端分离应用添加实时通信功能
通过在Django项目中集成Channels和WebSocket,我们能够为前后端分离的应用添加实时通信功能,实现诸如在线聊天、实时数据更新等交互式场景。这不仅增强了应用的功能性,也提升了用户体验。随着实时Web应用的日益普及,掌握Django Channels和WebSocket的集成将为开发者开启新的可能性,推动Web应用的发展迈向更高层次的实时性和交互性。
75 1
|
24天前
|
JavaScript 前端开发 测试技术
前端全栈之路Deno篇(五):如何快速创建 WebSocket 服务端应用 + 客户端应用 - 可能是2025最佳的Websocket全栈实时应用框架
本文介绍了如何使用Deno 2.0快速构建WebSocket全栈应用,包括服务端和客户端的创建。通过一个简单的代码示例,展示了Deno在WebSocket实现中的便捷与强大,无需额外依赖,即可轻松搭建具备基本功能的WebSocket应用。Deno 2.0被认为是最佳的WebSocket全栈应用JS运行时,适合全栈开发者学习和使用。
|
2月前
|
JavaScript 前端开发 UED
WebSocket在Python Web开发中的革新应用:解锁实时通信的新可能
在快速发展的Web应用领域中,实时通信已成为许多现代应用不可或缺的功能。传统的HTTP请求/响应模式在处理实时数据时显得力不从心,而WebSocket技术的出现,为Python Web开发带来了革命性的变化,它允许服务器与客户端之间建立持久的连接,从而实现了数据的即时传输与交换。本文将通过问题解答的形式,深入探讨WebSocket在Python Web开发中的革新应用及其实现方法。
44 3
|
1月前
|
消息中间件 网络协议 安全
C# 一分钟浅谈:WebSocket 协议应用
【10月更文挑战第6天】在过去的一年中,我参与了一个基于 WebSocket 的实时通信系统项目,该项目不仅提升了工作效率,还改善了用户体验。本文将分享在 C# 中应用 WebSocket 协议的经验和心得,包括基础概念、C# 实现示例、常见问题及解决方案等内容,希望能为广大开发者提供参考。
101 0
|
4月前
|
前端开发 网络协议 JavaScript
在Spring Boot中实现基于WebSocket的实时通信
在Spring Boot中实现基于WebSocket的实时通信
|
25天前
|
开发框架 前端开发 网络协议
Spring Boot结合Netty和WebSocket,实现后台向前端实时推送信息
【10月更文挑战第18天】 在现代互联网应用中,实时通信变得越来越重要。WebSocket作为一种在单个TCP连接上进行全双工通信的协议,为客户端和服务器之间的实时数据传输提供了一种高效的解决方案。Netty作为一个高性能、事件驱动的NIO框架,它基于Java NIO实现了异步和事件驱动的网络应用程序。Spring Boot是一个基于Spring框架的微服务开发框架,它提供了许多开箱即用的功能和简化配置的机制。本文将详细介绍如何使用Spring Boot集成Netty和WebSocket,实现后台向前端推送信息的功能。
251 1
|
30天前
|
前端开发 Java C++
RSocket vs WebSocket:Spring Boot 3.3 中的两大实时通信利器
本文介绍了在 Spring Boot 3.3 中使用 RSocket 和 WebSocket 实现实时通信的方法。RSocket 是一种高效的网络通信协议,支持多种通信模式,适用于微服务和流式数据传输。WebSocket 则是一种标准协议,支持全双工通信,适合实时数据更新场景。文章通过一个完整的示例,展示了如何配置项目、实现前后端交互和消息传递,并提供了详细的代码示例。通过这些技术,可以大幅提升系统的响应速度和处理效率。
|
3月前
|
开发框架 网络协议 Java
SpringBoot WebSocket大揭秘:实时通信、高效协作,一文让你彻底解锁!
【8月更文挑战第25天】本文介绍如何在SpringBoot项目中集成WebSocket以实现客户端与服务端的实时通信。首先概述了WebSocket的基本原理及其优势,接着详细阐述了集成步骤:添加依赖、配置WebSocket、定义WebSocket接口及进行测试。通过示例代码展示了整个过程,旨在帮助开发者更好地理解和应用这一技术。
270 1
|
3月前
|
小程序 Java API
springboot 微信小程序整合websocket,实现发送提醒消息
springboot 微信小程序整合websocket,实现发送提醒消息
|
3月前
|
JavaScript 前端开发 网络协议
WebSocket在Java Spring Boot+Vue框架中实现消息推送功能
在现代Web应用中,实时消息提醒是一项非常重要的功能,能够极大地提升用户体验。WebSocket作为一种在单个TCP连接上进行全双工通信的协议,为实现实时消息提醒提供了高效且低延迟的解决方案。本文将详细介绍如何在Java Spring Boot后端和Vue前端框架中利用WebSocket实现消息提醒功能。
158 0