本記事はこちらのブログを参考にしています。
翻訳にはアリババクラウドのModelStudio(Qwen)を使用しております。
トラフィックレーンは、クラウドネイティブアプリケーション内の複数のサービスをバージョンや他の特徴に基づいて複数の独立した実行環境に分離する機能です。この分離は、Alibaba Cloud Service Mesh (ASM) によってサービス間の東西トラフィックの細粒度な制御を通じて達成されます。トラフィックレーンは、エンドツーエンドのカナリーリリースやアプリケーション開発とテスト用の隔離された環境の構築などのシナリオでよく使用されます。現在、パーミッシブモードのトラフィックレーンは主にHTTPプロトコルで通信するサービスをサポートしています。アプリケーションのコンテキスト通過が適切に設定されている場合、パーミッシブモードのトラフィックレーンはWebSocketで構築された双方向通信アプリケーションにも適用できます。この記事では、WebSocketで構築された双方向通信アプリケーションに対してパーミッシブモードのトラフィックレーンを実装し、エンドツーエンドのカナリーリリース効果を達成する方法について説明します。
概要
Manage End-to-end Traffic Based on Alibaba Cloud Service Mesh (ASM): Traffic Lanes in Strict Mode および Manage End-to-end Traffic Based on Alibaba Cloud Service Mesh (ASM): Traffic Lanes in Permissive Mode で、レーンの概念、エンドツーエンドのカナリーリリース管理にレーンを使用するソリューション、およびASMの厳格モードとパーミッシブモードのトラフィックレーンについて紹介しました。トラフィックレーンは、クラウドネイティブアプリケーション内の複数のサービスをサービスバージョンや他の特徴に基づいて複数の独立した実行環境に分離します。
- 厳格モードでは、各トラフィックレーンには関連する呼び出しチェーン内のすべてのサービスが含まれ、アプリケーション自体には要求はありません。
- パーミッシブモードでは、関連する呼び出しチェーン内のすべてのサービスを含むベースラインレーンを作成するだけで十分です。他のレーンは関連する呼び出しチェーン内のすべてのサービスを含む必要はありません。あるレーン内のサービスが互いに呼び出す場合、呼び出されるサービスがそのレーンに存在しない場合、要求はベースラインレーンの同じサービスに転送されます。呼び出されるサービスがレーン内で利用可能になると、要求は再度そのレーンに転送されます。
パーミッシブモードのトラフィックレーンは柔軟なエンドツーエンドのカナリーリリースを達成できますが、アプリケーションが全呼び出しチェーン(E2E通過要求ヘッダー)を通過できる要求ヘッダーを含むことを要求します。WebSocketはHTTPプロトコルを介して通信するため、接続確立時にHTTP要求ヘッダーなどのメタデータを追加することも可能です。Socket.ioは高性能なリアルタイム双方向通信フレームワークであり、クライアントとサーバー間での効率的かつ低遅延のデータ交換を可能にします。この記事では、Socket.ioで開発されたサンプルサービスを例に、このようなアプリケーションに呼び出しチェーンがある場合に、パーミッシブモードのトラフィックレーンを通じてWebSocketアプリケーションにエンドツーエンドのカナリーリリース機能を導入する方法を示します。
シナリオの説明
この記事では、シンプルな呼び出しチェーンを持つSocket.ioで構築されたWebSocketアプリケーションを示し、そのアプリケーションに対してパーミッシブモードのトラフィックレーンを作成して基本的なエンドツーエンドのカナリーリリース効果を達成する方法を示します。アプリケーションのトポロジーは以下の通りです:
! 1
アプリケーションの主要なロジックは以下の通りです:client_socket
はクラスター外のクライアントで、ASMゲートウェイを介してクラスター内の wsk
サービスとSocket.ioで通信します。wsk
サービスが接続されている client_socket
クライアントにメッセージを送信するとき、クラスター内の helloworld
HTTPサービスに対してHTTP GET要求を行い、helloworld
からの応答をメッセージとして client_socket
に送信します。図に示すように、helloworld
には現在、バックエンドに2つのバージョンのワークロード(helloworld-v1
と helloworld-v2
)がデプロイされています。この例では、期待される効果は、wsk
サービスが接続を確立するときに client_socket
から提供される要求ヘッダー情報(バージョン要求ヘッダー)に基づいて、後続の wsk
要求の具体的なワークロードバージョンを決定することです。これは典型的でシンプルなエンドツーエンドのカナリーリリースシナリオです:アプリケーションのv2がリリースされるとき、呼び出しチェーンの末端にある helloworld
アプリケーションのみがリリースされます。この場合、wsk
サービスは単一のバージョン1のままであり、クライアントが接続を開始するときに提供する要求ヘッダーメタデータに基づいて、呼び出す helloworld
のターゲットバージョンを決定します。
必要条件
- エンタープライズエディションまたはアルティメットエディションのASMインスタンスが作成され、インスタンスのバージョンが1.18.2.111以降である。詳細については、ASMインスタンスの作成を参照してください。
- ACKクラスターがASMインスタンスに追加されている。詳細については、ASMインスタンスへのクラスターの追加を参照してください。
- 名前が
ingressgateway
のASMゲートウェイが作成され、ポート30080が作成されている。詳細については、イングレスゲートウェイの作成を参照してください。 -
istio-system
名前空間に名前がws-gateway
のIstioゲートウェイが作成されている。詳細については、Istioゲートウェイの管理を参照してください。 - デフォルト名前空間での自動サイドカー・プロキシ注入が有効になっている。詳細については、自動サイドカー・プロキシ注入の有効化を参照してください。
ステップ1: サンプルアプリケーションのデプロイ
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
helloworld サービスの v1 と v2 がクラスターにデプロイされます。このサービスは /hello エンドポイントへの HTTP GET リクエストに応答し、そのバージョン情報を返します。wsk サービスの v1 もデプロイされます。これはサンプルの Socket.io サービスで、クライアントからのメッセージに応答し、helloworld サービスを呼び出してその出力をクライアントに返します。コードは以下の通りです。javascript
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 => {
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」という名前のHTTPヘッダーを追加できます。Baggageヘッダーの値はキーと値のペア形式です。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 リク
Swimlane Services
Kubernetes Clustersドロップダウンリストから目的のACKクラスターを選択し、Namespaceドロップダウンリストからdefaultを選択します。次に、下のリストからwsk-svcとhelloworldサービスを選択し、をクリックして、選択したセクションにサービスを追加します。
次に、レーングループ内のv1とv2のレーンを作成して、サービスのv1とv2に対応させます。
- トラフィックレーンページのトラフィックルール定義セクションで、「スウィムレーンを作成」をクリックします。
- 「スウィムレーンを作成」ダイアログボックスで、必要なパラメータを設定し、「OK」をクリックします。
v1レーンにはwsk-svcとhelloworldサービスが含まれており、v2レーンにはhelloworldサービスのみが含まれています(許容モードのトラフィックレーンでは、レーンがすべてのサービスを含まないことが許可され、wskサービスのv1がサービスのベースラインバージョンとして機能します)。
ステップ3: レーンのパフォーマンスをテストする
次の内容を持つclient-socket.jsファイルを作成します(ASMゲートウェイのIPアドレスを実際のIPアドレスに置き換えてください):javascript
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);
// disconnection timeout clear
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を実行して、双方向通信の結果を確認します。期待される結果は以下の通りです:plaintext
❯ 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
期待される結果は、サーバーからの各レスポンスがクライアントに対してv2のhelloworldサービスを呼び出していることを示しています。これは、クライアントが接続を開始する際にversion: v2という追加情報を使用しているためです。wskを通じてhelloworldへのリクエストを送信するときにリクエストコンテキストをコールチェーン上に復元することで、ASMはリクエストを正しいバージョンのhelloworldサービスにルーティングし、エンドツーエンドのカナリアリリースを達成します。特定のクライアントへの応答だけでなく、この例ではサーバーからすべてのクライアントへのメッセージブロードキャストも実装されています。クライアントのログで定期的に以下のような内容を見ることができます:plaintext
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を使用しない理由
自動インストルメンテーションについて学ぶには、Injecting Auto-instrumentationを参照してください。この機能は、OpenTelemetry Operatorを使用してアプリケーションに自動インストルメンテーション機能を注入し、アプリケーションコードを変更せずにトレースIDリクエストヘッダーのパススルーを実現します。自動インストルメンテーションを実装するには、前述のコミュニティドキュメントに従ってOpenTelemetry Operatorをインストールし、自動インストルメンテーションを構成し、アプリケーションポッドに注釈を追加する必要があります。OpenTelemetry自動インストルメンテーションは、W3C BaggageやB3など、複数の一般的な分散コールチェーンコンテキストパススルースタンダードをサポートしています。
Socket.ioの場合、自動インストルメンテーションの意味合いが少し異なります。Socket.ioはHTTP(WebSocket)に基づいていますが、主にサービス間の双方向通信を実装しており、クライアントとサーバーはリアルタイムでメッセージを送受信できます。そのため、コミュニティによって実装されたSocket.ioの自動インストルメンテーションは、実際にはメッセージレベルで動作します。HTTPレベルでは、クライアントとサーバー間の接続は大きなHTTPリクエストであり、主に接続時にクライアントによって提供される追加リクエストヘッダーに基づいてコールチェーンコンテキストを決定する必要があります。このシナリオでは、コミュニティの自動インストルメンテーションによって提供されるコンテキストパススルーキャパビリティは限られた意味しかなく、コードの若干の修正が必要となります。詳細については、コミュニティの記事「Instrument Your Node.js Socket.io Apps with OpenTelemetry Like a PRO」を参照してください。