环境
部署两个服务 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 协议生成路由。