环境
部署两个服务 mocka 和 mockb,服务 mocka 会调用服务 mockb,服务只会保留流量中的 header my-request-id 而去除其它 header。

istio:1.23.2
k8s:1.26.15
kruise-rollout: 0.5.0
步骤
创建应用
apiVersion: v1
kind: Service
metadata:
name: mocka
labels:
app: mocka
service: mocka
spec:
ports:
- port: 8000
name: http
selector:
app: mocka
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mocka-base
labels:
app: mocka
spec:
replicas: 1
selector:
matchLabels:
app: mocka
template:
metadata:
labels:
app: mocka
version: base
spec:
containers:
- name: default
image: registry.cn-beijing.aliyuncs.com/aliacs-app-catalog/go-http-sample:1.0
imagePullPolicy: Always
env:
- name: version
value: base
- name: app
value: mocka
- name: upstream_url
value: "http://mockb:8000/"
ports:
- containerPort: 8000
---
apiVersion: v1
kind: Service
metadata:
name: mockb
labels:
app: mockb
service: mockb
spec:
ports:
- port: 8000
name: http
selector:
app: mockb
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mockb-base
labels:
app: mockb
spec:
replicas: 1
selector:
matchLabels:
app: mockb
template:
metadata:
labels:
app: mockb
version: base
spec:
containers:
- name: default
image: registry.cn-beijing.aliyuncs.com/aliacs-app-catalog/go-http-sample:1.0
imagePullPolicy: Always
env:
- name: version
value: base
- name: app
value: mockb
ports:
- containerPort: 8000

创建gateway
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: sample-gateway
spec:
selector:
istio: ingressgateway
servers:
- hosts:
- "*"
port:
name: http
number: 80
protocol: HTTP
创建VirtualService
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: vs-mocka
spec:
gateways:
- sample-gateway
hosts:
- "*"
http:
- route:
- destination:
host: mocka
subset: version-base
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: vs-mockb
spec:
hosts:
- mockb
http:
- route:
- destination:
host: mockb
subset: version-base
创建DestinationRule
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: dr-mocka
spec:
host: mocka
trafficPolicy:
loadBalancer:
simple: ROUND_ROBIN
subsets:
- labels:
version: base
name: version-base
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: dr-mockb
spec:
host: mockb
trafficPolicy:
loadBalancer:
simple: ROUND_ROBIN
subsets:
- labels:
version: base
name: version-base

访问链路如下:

测试流量访问
kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="http2")].nodePort}'
kubectl get po -l istio=ingressgateway -n istio-system -o jsonpath='{.items[0].status.hostIP}'
curl http://GATEWAY_IP:PORT

配置Rollout
Rollout 以及 TrafficRouting 配置的策略如下:
- 添加了匹配
my-request-id=canary的 header 规则,包含指定 header 的流量会走灰度环境 - 为发布的 pod 添加了 label
istio.service.tag=gray以及version=canary
为什么要添加这两个label?
- 在配置中为新版本 pod 打上了两个 label,其中
istio.service.tag=gray的目的是为了在 DestinationRule 中指定包含该 label 的 pod 作为一个 subset,lua 脚本会为 DestinationRule 自动添加该 Subset。 - 添加
version=canary的目的是为了覆盖原始版本中的 version=baselabel,如果不覆盖该 label,原始 DestinationRule 也会将稳定版本的流量导入新版本 pod 中。
apiVersion: rollouts.kruise.io/v1alpha1
kind: Rollout
metadata:
name: rollouts-a
annotations:
rollouts.kruise.io/rolling-style: canary
rollouts.kruise.io/trafficrouting: mocka-tr
spec:
objectRef:
workloadRef:
apiVersion: apps/v1
kind: Deployment
name: mocka-base
strategy:
canary:
steps:
- replicas: 1
pause: {}
patchPodTemplateMetadata:
labels:
istio.service.tag: gray
version: canary
---
apiVersion: rollouts.kruise.io/v1alpha1
kind: Rollout
metadata:
name: rollouts-b
annotations:
rollouts.kruise.io/rolling-style: canary
rollouts.kruise.io/trafficrouting: mockb-tr
spec:
objectRef:
workloadRef:
apiVersion: apps/v1
kind: Deployment
name: mockb-base
strategy:
canary:
steps:
- replicas: 1
pause: {}
patchPodTemplateMetadata:
labels:
istio.service.tag: gray
version: canary
配置TrafficRouting
apiVersion: rollouts.kruise.io/v1alpha1
kind: TrafficRouting
metadata:
name: mocka-tr
spec:
strategy:
matches:
- headers:
- type: Exact
name: my-request-id
value: canary
objectRef:
- service: mocka
customNetworkRefs:
- apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
name: vs-mocka
- apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
name: dr-mocka
---
apiVersion: rollouts.kruise.io/v1alpha1
kind: TrafficRouting
metadata:
name: mockb-tr
spec:
strategy:
matches:
- headers:
- type: Exact
name: my-request-id
value: canary
objectRef:
- service: mockb
customNetworkRefs:
- apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
name: vs-mockb
- apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
name: dr-mockb

灰度发布
修改 mocka 和 mockb 中的环境变量为 version=canary 开始发布。Rollout自动创建了两个新的灰度deployment。

此时查看 VirtualService,lua脚本自动添加了将带有 my-request-id=canary 的 header 的流量路由至 subset=canary 版本。


查看 DestinationRule,lua脚本自动添加了对于包含 label istio.service.tag=gray 新的 subset。


再次测试流量访问
curl http://GATEWAY_IP:PORT -H "my-request-id:canary"

可以看到所有流量均通过 canary 版本服务。
pod日志如下,可以看到透传了header my-request-id: canary。

流量路径如图所示:

使用EnvoyFilter进行流量染色
要想实现全链路灰度有一个核心就是header透传。除了上面使用VirtualService+DestinationRule的方法外,还可以使用EnvoyFilter在入口网关进行染色。下面的 EnvoyFilter 定义了一个 Lua 脚本,会对包含 agent=pc 的流量染色,为其添加 my-request-id=canary,而其它流量则添加 my-request-id=base。
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: http-request-labelling-according-source
namespace: istio-system
spec:
workloadSelector:
labels:
app: istio-ingressgateway
configPatches:
- applyTo: HTTP_FILTER
match:
context: GATEWAY
listener:
filterChain:
filter:
name: "envoy.filters.network.http_connection_manager"
subFilter:
name: "envoy.filters.http.router"
patch:
operation: INSERT_BEFORE
value:
name: envoy.lua
typed_config:
"@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua"
inlineCode: |
function envoy_on_request(request_handle)
local header = "agent"
headers = request_handle:headers()
version = headers:get(header)
if (version ~= nil) then
if (version == "pc") then
headers:add("my-request-id","canary")
else
headers:add("my-request-id","base")
end
else
headers:add("my-request-id","base")
end
end
测试流量访问:
curl http://GATEWAY_IP:PORT -H "agent:pc"

lua脚本解释
envoy.lua
https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/lua_filter
envoy_on_request是 Envoy 用来处理请求的生命周期钩子函数之一。它在每个请求到达时被调用。request_handle是一个对象,允许访问请求的 header 和其他上下文信息。- 定义了
header = "agent" - 调用
request_handle:headers()方法获取请求的 header 集合,并将其赋值给变量 headers。这样就可以访问请求中的所有 header。 - 从 headers 表中获取名为 "agent" 的 header 的值,并将其赋值给变量 version。如果请求中没有 "agent" header,那么 version 的将为 nil。
DestinationRule/trafficRouting.lua
上面发布灰度版本后,dr和vs都会被lua脚本自动修改掉。就是这个脚本。
local spec = obj.data.spec
local canary = {}
canary.labels = {}
canary.name = "canary"
local podLabelKey = "istio.service.tag"
canary.labels[podLabelKey] = "gray"
table.insert(spec.subsets, canary)
return obj.data
- 从
obj.data这里就是 DestinationRule 中获取 spec 字段,并将其赋值给局部变量 spec。 - 定义一个名为 canary 的表,
canary.labels是一个子表,用于存储标签,为该子集设置名称 "canary"。 - 定义一个变量
podLabelKey,值为"istio.service.tag",将标签键值对istio.service.tag: gray添加到canary.labels中,表示灰度版本。 - 将 canary 表插入到
spec.subsets列表中。实现了向spec.subsets动态添加一个带有灰度标签的新子集。 - 返回修改后的
obj.data,包含了新增的 canary 子集。这样修改后的数据可以用于更新资源配置,使灰度路由规则生效。
VirtualService/trafficRouting.lua
代码可以动态生成和修改 VirtualService,以支持金丝雀发布策略。它通过检查服务的权重、匹配条件和请求头修改器,来决定如何将流量路由到不同的服务版本(稳定版本或金丝雀版本)。
- 如果
obj.canaryWeight为 -1,则将其设置为 100,并将obj.stableWeight设置为 0。这意味着如果没有指定金丝雀权重,则默认将所有流量引导到金丝雀版本。 GetHost(destination):从目标的 host 字段中提取主机名,去掉域名部分。GetRulesToPatch(spec, stableService, protocol):查找与稳定服务匹配的路由规则。CalculateWeight(route, stableWeight, n):计算每个路由的权重。如果路由已经有权重,则根据稳定权重进行计算;否则,均匀分配稳定权重。GenerateRoutesWithMatches(spec, matches, stableService, canaryService, requestHeaderModifier):生成带有匹配条件的路由,并将其插入到spec.http的开头。GenerateRoutes(spec, stableService, canaryService, stableWeight, canaryWeight, protocol):生成不带匹配条件的路由。查找与稳定服务匹配的规则,并为每个规则添加金丝雀路由。- 主逻辑:根据
obj.matches的存在与否,调用相应的路由生成函数。如果存在匹配条件,则调用GenerateRoutesWithMatches;否则,调用GenerateRoutes为 HTTP、TCP 和 TLS 协议生成路由。