×
Community Blog End-to-end Canary Release Based on ASM for Bidirectional Communication Applications Built with WebSocket

End-to-end Canary Release Based on ASM for Bidirectional Communication Applications Built with WebSocket

This article describes how to implement traffic lanes in permissive mode for bidirectional communication applications built with WebSocket.

By Hang Yin

Traffic lanes are the capability that isolates multiple services in a cloud-native application into multiple independent runtime environments based on versions or other features. This isolation is achieved by Alibaba Cloud Service Mesh (ASM) through fine-grained control of east-west traffic between services. Traffic lanes are often used in scenarios such as end-to-end canary release and the construction of isolated environments for application development and testing. At present, traffic lanes in permissive mode mainly support services that communicate over the HTTP protocol. As long as the context pass-through of an application is properly configured, traffic lanes in permissive mode can also be applied to bidirectional communication applications built with WebSocket. This article describes how to implement traffic lanes in permissive mode for bidirectional communication applications built with WebSocket and achieve end-to-end canary release effects.

Overview

In Manage End-to-end Traffic Based on Alibaba Cloud Service Mesh (ASM): Traffic Lanes in Strict Mode and Manage End-to-end Traffic Based on Alibaba Cloud Service Mesh (ASM): Traffic Lanes in Permissive Mode, we introduced the concept of lanes, the solution of using lanes for end-to-end canary release management, and ASM traffic lanes in strict mode and permissive mode.

Traffic lanes isolate multiple services in a cloud-native application into multiple independent runtime environments based on the service version or other features.

• In strict mode, each traffic lane contains all services in relevant call chains, with no requirements for the application itself.

• In permissive mode, you need to only create a baseline lane that contains all services in relevant call chains. Other lanes are not required to contain all services in the relevant call chains. When services in one lane call each other, if the service to be called does not exist in the lane, requests are forwarded to the same service in the baseline lane. When the service to be called becomes available in the lane, requests are forwarded back to the lane. While traffic lanes in permissive mode can achieve flexible end-to-end canary releases, they require that the application includes a request header capable of being passed through the entire call chain (E2E pass-through request header).

Since WebSocket communicates over the HTTP protocol, it is also possible to add metadata such as HTTP request headers during connection establishment. Socket.io is a high-performance real-time bidirectional communication framework that allows efficient and low-latency data exchange between clients and servers. This article will use a sample service developed with Socket.io as an example to demonstrate how to bring the end-to-end canary release capability to WebSocket applications through traffic lanes in permissive mode when there is a call chain in such applications.

Scenario Description

This article will demonstrate a WebSocket application built with Socket.io that features a simple call chain and will show how to create traffic lanes in permissive mode for the application to achieve basic end-to-end canary release effects. The topology of the application is as follows:

1

The main logic of the application is as follows: client_socket is a client outside the cluster. It communicates with the wsk service inside the cluster through the ASM gateway using Socket.io. When the wsk service sends a message to the client_socket client that is connected to it, it makes an HTTP GET request to the helloworld HTTP service within the cluster, and then sends the response from helloworld as a message to the client_socket.

As shown in the figure, helloworld currently has two versions of workloads deployed at the backend: helloworld-v1 and helloworld-v2. In this example, the expected effect is to determine the specific workload version of subsequent wsk requests based on the request header information (version request header) provided by client_socket when the wsk service is established.

This is a typical and simple end-to-end canary release scenario: when the v2 of the application is released, only the helloworld application at the end of the call chain is released. In this case, we want the wsk service to remain at the sole version 1 and determine the target version of helloworld to be called based on the request header metadata that the client provides when initiating the connection.

Prerequisites

• An ASM instance of Enterprise Edition or Ultimate Edition is created and the version of the instance is 1.18.2.111 or later. For more information, see Create an ASM instance.

• The ACK cluster is added to the ASM instance. For more information, see Add a cluster to an ASM instance.

• An ASM gateway named ingressgateway is created and a 30080 port is created. For more information, see Create an ingress gateway.

• An Istio gateway named ws-gateway is created in the istio-system namespace. For more information, see Manage Istio gateways.

• Enable automatic sidecar proxy injection for the default namespace. For more information, see Enable automatic sidecar proxy injection.

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

Step 1: Deploy sample applications

Connect to the ACK cluster through kubectl and run the following command to deploy the sample application:

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

The v1 and v2 of the helloworld service are deployed in the cluster. The service responds to HTTP GET requests sent to /hello endpoint and returns its version information. The v1 of the wsk service is also deployed. This service is a sample Socket.io service. It responds to messages from the client by calling the helloworld service and returns the output of the helloworld service to the client. The code is as follows:

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(); // or response.json() if you need JSON-formatted data
    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);

The code shows that the application is configured to call the helloworld service when a message is sent to the client. In this case, it reads the baggage request header provided by the client during connection establishment and passes the request header to the HTTP request that calls the helloworld service. In this way, the Socket.io application implements context pass-through in the call chain. We can use this context to implement traffic lanes in permissive mode.

What is Baggage?

Baggage is a standardized mechanism developed by OpenTelemetry to pass through context information across processes in call chains of a distributed system. You can add an HTTP header named Baggage to HTTP headers. The value of a Baggage header is in the key-value pair format. You can use Baggage headers to transfer context data such as tenant ID, trace ID, and security credentials. Examples:

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

Baggage is a standardized mechanism proposed by the OpenTelemetry community for context pass-through in the call chain. Therefore, we also recommend that you configure traffic lanes in permissive mode based on this mechanism.

Next, we deploy the virtual service on the gateway to redirect traffic to the wsk service and run the following command:

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

This virtual service declares a routing rule on the gateway. Through this virtual service, the ASM gateway can forward the Socket.io requests from the client to the backend wsk Socket.io service.

Step 2: Deploy traffic lanes

In the example, we separate the wsk->helloworld service call chain into two lanes for v1 and v2. The set of the three lanes is referred to as a lane group. In ASM, you can create lane groups and lanes through simple configurations.

• Log on to the ASM console. In the left-side navigation pane, choose Service Mesh> Mesh Management.

• On the Mesh Management page, click the name of the ASM instance. In the left-side navigation pane, choose Traffic Management Center> Traffic Lane.

• On the Traffic Lane page, click Create Swimlane Group. In the Create Swimlane Group panel, configure the following information and click OK.

Parameter Description
Name of swim lane group For this example, enter test.
Entrance Gateway Select ws-gateway
Lane Mode Select Permissive Mode
Pass-through Mode of Trace Context Select Pass Through Baggage Header, which is the method implemented in the application for context pass-through across the call chain.
Swimlane Services Select the desired ACK cluster from the Kubernetes Clusters drop-down list and select default from the Namespace drop-down list. Then, select wsk-svc and helloworld services in the list below, and click on icon to add the services to the selected section.

2

Next, create the v1 and v2 lanes in the lane group to correspond to the v1 and v2 of the service.

• In the Traffic Rule Definition section of the Traffic Lane page, click Create swimlanes.

• In the Create swimlanes dialog box, configure the required parameters and click OK.

3

The v1 lane contains the wsk-svc and helloworld services, while the v2 lane only contains the helloworld service (traffic lanes in permissive mode allow a lane to not contain all services, with the v1 of the wsk service serving as the baseline version of the service).

Step 3: Test the performance of lanes

Create a client-socket.js file with the following content (replace the IP address of the ASM gateway with the actual IP address):

import uuid from 'uuid';
import io from 'socket.io-client';
const client = io('http://{IP address of the ASM gateway}: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);

Execute the client_socket.js to view the bidirectional communication result. The expected result is as follows:

❯ 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

The expected result shows that each response from the server to the client calls the v2 of the helloworld service. This is because the client uses the extra information of version: v2 when initiating a connection. By restoring the request context on the call chain when sending requests to helloworld through wsk, ASM can route the request to the correct version of the helloworld service, achieving the end-to-end canary release.

In addition to replying to specific clients, this example also implements message broadcasting from the server to all clients. You can periodically see such content in the client logs:

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

This message is a server-to-client broadcast message that is periodically received by all clients connected to the server. The broadcast message actually calls the v1 of the helloworld service. This is because the broadcast message does not have a specific client context. Therefore, the baseline version of helloworld (that is, v1) is called.

FAQ: Why Don't We Use OpenTelemetry Auto-instrumentation?

To learn about auto-instrumentation, see Injecting Auto-instrumentation. This capability uses the OpenTelemetry Operator to inject the auto-instrumentation capability into applications, achieving the pass-through of trace ID request headers without modifying the application code. To implement auto-instrumentation, you must follow the preceding community documentation to install the OpenTelemetry Operator, configure auto-instrumentation, and add annotations to application pods. OpenTelemetry auto-instrumentation supports multiple common distributed call chain context pass-through standards, such as W3C Baggage and B3.

For Socket.io, the semantics of auto-instrumentation become somewhat different. Although Socket.io is based on HTTP (WebSocket), it mainly implements bidirectional communication between services. The client and server can send messages to each other in real time. Therefore, the auto-instrumentation of Socket.io implemented by the community actually operates at the message level. At the HTTP level, the connection between the client and the server is a large HTTP request. We mainly need to determine the call chain context based on additional request headers provided by the client during connection establishment. For this scenario, the context pass-through capability provided by community auto-instrumentation has limited significance and requires slight modification of the code. For more details, see the community's article on Socket.io auto-instrumentation: Instrument Your Node.js Socket.io Apps with OpenTelemetry Like a PRO.

0 1 0
Share on

Alibaba Container Service

180 posts | 32 followers

You may also like

Comments