【阅读原文】戳:为使用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/
我们是阿里巴巴云计算和大数据技术幕后的核心技术输出者。
获取关于我们的更多信息~