作者:旦酱、十眠
什么是全链路灰度?
在发布使用的进程中,咱们一般期望用少量特定流量来验证新版别的发布是否正常,以确保全体稳定性。这个进程被称为灰度发布。关于灰度发布,咱们经过逐步增加发布的规模,来验证新版别的稳定性。假如新版别出现问题,咱们也能及时发现,操控影响规模,确保全体的稳定性。
灰度发布一般具有以下特色:
- 逐步增加发布的影响规模,回绝一次性全部发布。
- 阶段性的发布进程,能够经过金丝雀发布方法当心验证,以验证新版别的稳定性;
- 可暂停、可回滚、可继续、可主动化状况流通,以便灵活地操控发布进程并确保稳定性;
据调研数据 70% 的线上问题都是由于改变导致,咱们常说安全出产三板斧,可灰度、可观测、可回滚,也是为了操控改变带来的风险与影响面。 经过采用灰度发布的方法,咱们能够愈加稳健地发布新版别,避免因发布进程中出现的问题而带来的损失。
全链路灰度是微服务场景下灰度发布计划的最佳实践,一般每个微服务都会有灰度环境或分组来承受灰度流量。咱们期望进入上游灰度环境的流量也能进入下游灰度的环境中,确保1个恳求始终在灰度环境中传递,然后构成流量“泳道”。在“泳道”内的流量链路中,即便这个调用链路上有一些微服务使用不存在灰度环境,那么这些微服务使用在恳求下游使用的时分仍然能够回到下游使用的灰度环境中。
全链路灰度为微服务发布保驾护航
这种方法能够依据服务的实际情况,能够对单个服务能够进行独立的发布和流量操控,也能够操控多个服务一起进行发布改变,然后确保整个体系的稳定性。一起,还能够采用主动化的布置方法,完结快速、可靠的发布进程,进步发布效率和稳定性。
Istio 全链路灰度技能解析
怎么经过 Istio 完结全链路灰度才能,想必咱们都很关怀这个论题,今天咱们就具体谈一下根据 istio 完结全链路灰度才能的几个要害技能细节。
流量标签全链路透传
在对微服务进行全链路灰度的进程中,一个最需求考虑的问题是流量中 header 透传的问题,一些微服务会仅保存特定的 header 进行透传,而去除其它 header,使用 Kruise Rollout,能够有用削减发布中对网关资源进行装备的复杂性,但却无法解决 header 透传的问题。怎么确保灰度标识能够在链路中一直传递下去呢?分布式链路追寻技能对大型分布式体系中恳求调用链路进行具体记载,中心思维就是经过一个全局唯一的 traceid 和每一条的 spanid 来记载恳求链路所经过的节点以及恳求耗时,其间 traceid 是需求整个链路传递的。凭借于分布式链路追寻思维,咱们也能够传递一些自界说信息,比如灰度标识。业界常见的分布式链路追寻产品都支撑链路传递用户自界说的数据,其数据处理流程如下图所示:
咱们能够凭借 Tracing Baggage 机制在全链路中传递对应染色标识,由于大部分 Tracing 结构都支撑 Baggage 概念及才能,如:OpenTelemetry、Skywalking、Jaeger 等等。咱们只需求在 Envoy outbound Filter 中讲指定的透传 key 如 x-mse-tag 从 Tracing 协议指定位置的 Baggage 中读出 x-mse-tag 对应的值,并塞入到 Http 的 Header 中,供 Envoy 进行路由。
流量路由
经过对服务下一切节点依照标签名和标签值不同进行分组,使得订阅该服务节点信息的服务消费端能够按需拜访该服务的某个分组,即一切节点的一个子集。服务消费端能够使用服务供给者节点上的任何标签信息,依据所选标签的实际意义,消费端能够将标签路由使用到更多的事务场景中。
在 Istio 中咱们能够经过 Istio Gateway、DestinationRule 和 VirtualService 装备路由和外部拜访;
节点打标
怎么给服务节点增加不同的标签?咱们只要在事务使用描述资源 Deployment 中的 Pod 模板中为节点增加标签即可。在使用 Kubernetes Service 作为服务发现的事务体系中,服务供给者经过向 ApiServer 提交 Service 资源完结服务暴露,服务消费端监听与该 Service 资源下相关的 Endpoint 资源,从 Endpoint 资源中获取相关的事务 Pod 资源,读取上面的 Labels 数据并作为该节点的元数据信息。
为什么选择 Kruise Rollout?
从上述技能细节中能够看出,完结根据 Istio 的全链路灰度操作非常复杂且本钱较高。首先,需求创建灰度的 Deployment 并打上灰度的节点标识。其次,还需求装备 Istio 的流量路由 CRD,包括每一跳恳求的 VirtualService 和 DestinationRule 规矩,并且这些流量规矩还需求与恳求标识相配合。假如仅仅纸上谈谈技能细节,或许还能牵强了解,但假如真要实践起来,本钱确实非常高。一起,假如在装备进程中出现错误,或许会导致出产流量出现问题,对事务造成重大影响。为了下降全链路灰度实践的本钱,不得不提 Kruise Rollout 了。
Kruise Rollout [ 1] 是 OpenKruise 社区开源提出的一个渐进式交付结构。其规划理念是供给一组能够将流量发布与实例灰度相结合,支撑金丝雀、蓝绿、A/B Testing 等多样化发布方法,以及支撑根据 Prometheus Metrics 等自界说 Metrics 完结发布进程主动化,无感对接、易扩展的旁路式规范 Kubernetes 发布组件。
从上图中咱们能够看到 OpenKruise Rollout 能够将复杂的灰度发布进程主动化,因而经过 OpenKruise Rollout 能够大幅度下降全链路灰度的实施本钱,关于使用者来说只需求装备 Kruise Rollout 的 CRD,然后直接进行使用发布,即可完结全链路灰度发布。
Kruise Istio 全链路灰度实践
谈完技能完结的细节,下面咱们就开端根据 Kruise Rollout 跟 Istio 的全链路灰度才能实践。
装备服务
首先布置两个服务 mocka 和 mockb,服务 mocka 会调用服务 mockb,⚠️服务只会保存流量中的 header my-request-id 而去除其它 header(咱们也能够经过接入 OpenTelemetry 完结动态流量标签透传), 整个服务的拜访能够表示为:
服务的装备文件为:
apiVersion: v1
kind: Service
metadata:
name: mocka
namespace: e2e
labels:
app: mocka
service: mocka
spec:
ports:
- port: 8000
name: http
selector:
app: mocka
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mocka-base
namespace: e2e
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
namespace: e2e
labels:
app: mockb
service: mockb
spec:
ports:
- port: 8000
name: http
selector:
app: mockb
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mockb-base
namespace: e2e
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
服务的 header 部分处理代码如下所示:
// All URLs will be handled by this function
m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
requestId := r.Header.Get("my-request-id")
fmt.Printf("receive request: my-request-id: %sn", requestId)
response := fmt.Sprintf("-> %s(version: %s, ip: %s)", app, version, ip)
if url != "" {
// 新恳求只发送my-request-id
content := doReq(url, requestId)
response = response content
}
w.Write([]byte(response))
})
之后布置 Istio Gateway、DestinationRule 和 VirtualService 装备路由和外部拜访:
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: gateway
namespace: e2e
spec:
selector:
istio: ingressgateway
servers:
- hosts:
- "*"
port:
name: http
number: 80
protocol: HTTP
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: dr-mocka
namespace: e2e
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
namespace: e2e
spec:
host: mockb
trafficPolicy:
loadBalancer:
simple: ROUND_ROBIN
subsets:
- labels:
version: base
name: version-base
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: vs-mocka
namespace: e2e
spec:
gateways:
- simple-gateway
hosts:
- "*"
http:
- route:
- destination:
host: mocka
subset: version-base
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: vs-mockb
namespace: e2e
spec:
hosts:
- mockb
http:
- route:
- destination:
host: mockb
subset: version-base
布置完结后整个结构如下图所示:
在本地集群中,能够经过运转如下指令获取 Gateway 进口 ip 以及 port 对服务进行拜访:
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 能够得到如下成果:
-> mocka(version: base, ip: 10.244.1.36)-> mockb(version: base, ip: 10.244.1.37)
装备 Rollout 和 TrafficRouting 规矩
之后布置 Rollout,分别对 mocka 和 mockb 进行操控。Rollout 以及 TrafficRouting 装备的策略如下:
- 增加了匹配 my-request-id=canary 的 header 规矩,包括指定 header 的流量会走灰度环境
- 为发布的 pod 增加了 label istio.service.tag=gray 以及 version=canary
⚠️pod 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
namespace: demo
annotations:
rollouts.kruise.io/rolling-style: canary
rollouts.kruise.io/trafficrouting: mocka-tr
spec:
disabled: false
objectRef:
workloadRef:
apiVersion: apps/v1
kind: Deployment
name: mocka-base
strategy:
canary:
steps:
- replicas: 1
patchPodTemplateMetadata:
labels:
istio.service.tag: gray
version: canary
---
apiVersion: rollouts.kruise.io/v1alpha1
kind: TrafficRouting
metadata:
name: mocka-tr
namespace: demo
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: Rollout
metadata:
name: rollouts-b
namespace: demo
annotations:
rollouts.kruise.io/rolling-style: canary
rollouts.kruise.io/trafficrouting: mockb-tr
spec:
disabled: false
objectRef:
workloadRef:
apiVersion: apps/v1
kind: Deployment
name: mockb-base
strategy:
canary:
steps:
- replicas: 1
patchPodTemplateMetadata:
labels:
istio.service.tag: gray
version: canary
---
apiVersion: rollouts.kruise.io/v1alpha1
kind: TrafficRouting
metadata:
name: mockb-tr
namespace: demo
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 开端发布。Kruise Rollout 将主动获取网关资源并进行修正,此刻检查 VirtualService 和 DestinationRule 能够得到,VirtualService 界说了路由规矩将带有 my-request-id=canary 的 header 的流量路由至 canary 版别。DestinationRule 增加了关于包括 label istio.service.tag=gray 新的 Subset。
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"networking.istio.io/v1beta1","kind":"VirtualService","metadata":{"annotations":{},"name":"vs-mocka","namespace":"demo"},"spec":{"gateways":["simple-gateway"],"hosts":["*"],"http":[{"route":[{"destination":{"host":"mocka","subset":"version-base"}}]}]}}
rollouts.kruise.io/origin-spec-configuration: '{"spec":{"gateways":["simple-gateway"],"hosts":["*"],"http":[{"route":[{"destination":{"host":"mocka","subset":"version-base"}}]}]},"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{"apiVersion":"networking.istio.io/v1beta1","kind":"VirtualService","metadata":{"annotations":{},"name":"vs-mocka","namespace":"demo"},"spec":{"gateways":["simple-gateway"],"hosts":["*"],"http":[{"route":[{"destination":{"host":"mocka","subset":"version-base"}}]}]}}n"}}'
creationTimestamp: "2023-09-12T07:49:15Z"
generation: 40
name: vs-mocka
namespace: demo
resourceVersion: "98670"
uid: c7da3a99-789c-4f1e-93a4-caaee41cbe06
spec:
gateways:
- simple-gateway
hosts:
- '*'
http:
# -- lua脚本主动增加的规矩
- match:
- headers:
my-request-id:
exact: canary
route:
- destination:
host: mocka
subset: canary
# --
- route:
- destination:
host: mocka
subset: version-base
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"networking.istio.io/v1beta1","kind":"VirtualService","metadata":{"annotations":{},"name":"vs-mockb","namespace":"demo"},"spec":{"hosts":["mockb"],"http":[{"route":[{"destination":{"host":"mockb","subset":"version-base"}}]}]}}
rollouts.kruise.io/origin-spec-configuration: '{"spec":{"hosts":["mockb"],"http":[{"route":[{"destination":{"host":"mockb","subset":"version-base"}}]}]},"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{"apiVersion":"networking.istio.io/v1beta1","kind":"VirtualService","metadata":{"annotations":{},"name":"vs-mockb","namespace":"demo"},"spec":{"hosts":["mockb"],"http":[{"route":[{"destination":{"host":"mockb","subset":"version-base"}}]}]}}n"}}'
creationTimestamp: "2023-09-12T07:49:16Z"
generation: 40
name: vs-mockb
namespace: demo
resourceVersion: "98677"
uid: 7c96ee2b-96ce-48e4-ba6d-cf94171ed854
spec:
hosts:
- mockb
http:
# -- lua脚本主动增加的规矩
- match:
- headers:
my-request-id:
exact: canary
route:
- destination:
host: mockb
subset: canary
# --
- route:
- destination:
host: mockb
subset: version-base
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"networking.istio.io/v1beta1","kind":"DestinationRule","metadata":{"annotations":{},"name":"dr-mocka","namespace":"demo"},"spec":{"host":"mocka","subsets":[{"labels":{"version":"base"},"name":"version-base"}],"trafficPolicy":{"loadBalancer":{"simple":"ROUND_ROBIN"}}}}
rollouts.kruise.io/origin-spec-configuration: '{"spec":{"host":"mocka","subsets":[{"labels":{"version":"base"},"name":"version-base"}],"trafficPolicy":{"loadBalancer":{"simple":"ROUND_ROBIN"}}},"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{"apiVersion":"networking.istio.io/v1beta1","kind":"DestinationRule","metadata":{"annotations":{},"name":"dr-mocka","namespace":"demo"},"spec":{"host":"mocka","subsets":[{"labels":{"version":"base"},"name":"version-base"}],"trafficPolicy":{"loadBalancer":{"simple":"ROUND_ROBIN"}}}}n"}}'
creationTimestamp: "2023-09-12T07:49:15Z"
generation: 12
name: dr-mocka
namespace: demo
resourceVersion: "98672"
uid: a6f49044-e889-473c-b188-edbdb8ee347f
spec:
host: mocka
subsets:
- labels:
version: base
name: version-base
# -- lua脚本主动增加的规矩
- labels:
istio.service.tag: gray
name: canary
# --
trafficPolicy:
loadBalancer:
simple: ROUND_ROBIN
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"networking.istio.io/v1beta1","kind":"DestinationRule","metadata":{"annotations":{},"name":"dr-mockb","namespace":"demo"},"spec":{"host":"mockb","subsets":[{"labels":{"version":"base"},"name":"version-base"}],"trafficPolicy":{"loadBalancer":{"simple":"ROUND_ROBIN"}}}}
rollouts.kruise.io/origin-spec-configuration: '{"spec":{"host":"mockb","subsets":[{"labels":{"version":"base"},"name":"version-base"}],"trafficPolicy":{"loadBalancer":{"simple":"ROUND_ROBIN"}}},"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{"apiVersion":"networking.istio.io/v1beta1","kind":"DestinationRule","metadata":{"annotations":{},"name":"dr-mockb","namespace":"demo"},"spec":{"host":"mockb","subsets":[{"labels":{"version":"base"},"name":"version-base"}],"trafficPolicy":{"loadBalancer":{"simple":"ROUND_ROBIN"}}}}n"}}'
creationTimestamp: "2023-09-12T07:49:15Z"
generation: 12
name: dr-mockb
namespace: demo
resourceVersion: "98678"
uid: 4bd0f6c5-efa1-4558-9e31-6f7615c21f9a
spec:
host: mockb
subsets:
- labels:
version: base
name: version-base
# -- lua脚本主动增加的规矩
- labels:
istio.service.tag: gray
name: canary
# --
trafficPolicy:
loadBalancer:
simple: ROUND_ROBIN
此刻运转 curl http://GATEWAY_IP:PORT 能够得到如下成果,一切流量均经过 base 版别服务。
-> mocka(version: base, ip: 10.244.1.36)-> mockb(version: base, ip: 10.244.1.37)
此刻运转 curl -H http://GATEWAY_IP:PORT -Hmy-request-id:canary 能够得到如下成果,一切流量均经过 canary 版别服务。
-> mocka(version: canary, ip: 10.244.1.41)-> mockb(version: canary, ip: 10.244.1.42)
此刻整个服务的流量能够表示为下图,包括 headermy-request-id=canary 的流量全部走灰度环境,其它流量全部走基线环境:
(可选)使用 EnvoyFilter 进行流量染色
咱们能够经过编写 EnvoyFilter 进一步简化流量染色的步骤,在之前的例子中,在恳求的时分包括了能够透传的 header my-request-id,假如想要完结愈加通用的 header 恳求规矩,例如包括 agent=pc 的流量全部走灰度环境,其它则走基线环境,则能够经过 EnvoyFilter 在进口网关进行染色。以下界说了一个 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 能够得到如下成果,一切流量均经过 base 版别服务。
-> mocka(version: base, ip: 10.244.1.36)-> mockb(version: base, ip: 10.244.1.37)
此刻运转 curl -H http://GATEWAY_IP:PORT -Hagent:pc 能够得到如下成果,EnvoyFilter 主动为该流量增加 headermy-request-id=canary,因而一切流量均经过新版别服务。
-> mocka(version: canary, ip: 10.244.1.41)-> mockb(version: canary, ip: 10.244.1.42)
完整的全链路灰度计划
Kruise Rollout 除了支撑开源的 Istio 完结全链路灰度外,还支撑与 MSE 完结完整的全链路灰度计划,咱们能够经过如下操作文档 [ 2] 快速完结体系化的全链路灰度才能。
MSE 全链路灰度能够有用地操控流量在前端、网关、后端各个微服务等组件中闭环,不仅仅是 RPC/Http 的流量,关于异步调用比如 MQ 流量也支撑契合全链路“泳道”调用的规矩。经过 Kruise Rollout 跟 MSE 能够帮助咱们愈加快捷地完结微服务全链路灰度发布,进步微服务场景下发布的效率和稳定性。
参加 OpenKruise 社区
最后,非常欢迎你经过 Github/Slack/钉钉/微信 等方法参加咱们来参与 OpenKruise 开源社区。
- 参加社区 Slack channel (English)
- 参加社区钉钉群:查找群号 23330762 (Chinese)
- 参加社区微信群(新):增加用户 openkruise 并让机器人拉你入群 (Chinese)
相关链接:
[1]Kruise Rollout
[2]操作文档