Kruise Rollout:基于 Lua 脚本的可扩展流量调度方案
作者:潘梦源
前言
Kruise Rollout [ 1] 是 OpenKruise 社区开源的渐进式交付框架。Kruise Rollout 支持配合流量和实例灰度的金丝雀发布、蓝绿发布、A/B Testing 发布,以及发布过程能够基于 Prometheus Metrics 指标自动化分批与暂停,并提供旁路的无感对接、兼容已有的多种工作负载(Deployment、CloneSet、DaemonSet)。
目前 Kruise Rollout 新增了流量调度支持自定义资源的能力 ,从而更好的支持渐进式发布中的流量调度。 本文将对 Kruise Rollout 所提出的方案进行介绍。
什么是渐进式发布?
**渐进式发布(Progressive Delivery)是一种软件部署和发布策略,旨在逐步将新版本或功能引入生产环境,以降低风险并确保系统的稳定性。一些常见的渐进式发布形式如下:
- 金丝雀发布: 在发布时会创建一个金丝雀版本的 Deployment 进行验证,当验证通过后,再进行全量的工作负载升级,并删除金丝雀版本的 Deployment。
- A/B 测试: 按照一定的规则将用户流量切分成 A、B 两个不相交通路,并将导入不同版本的 Pod 实例进行处理,以此来更好地观察、对比或者灰度新版本能力。
金丝雀发布、A/B 测试和蓝绿发布都是逐步测试和评估新功能或变更的策略,它们可以根据具体的需求和场景选择适合的部署和测试策略,并结合流量灰度等技术实现逐步发布和测试新版本或功能。
为什么需要对网关资源提供支持?
Kruise Rollout 目前已经对 Gateway API 提供了支持,那么为什么还需要对不同供应商的网关资源提供支持呢?在解释这个问题之前,我们先来简单介绍一下 Gateway API。
当前社区中不同的供应商都有自己的网关资源,并提出了自己的标准,而 Kubernetes 为了提供一个统一的网关资源标准,构建标准化的,独立于供应商的 API,提出了 Gateway API。目前,尽管 Gateway API 还处于开发阶段,但已经有很多项目表示支持或计划支持 Gateway API。包括:
- Istio 是最流行的服务网格项目之一,Istio 1.9 版本计划引入实验性的 Gateway API 支持。用户可以通过 Gateway 和 HTTPRoute 资源来配置 Istio 的 Envoy 代理。
- Apache APISIX 是一个动态、实时、高性能的 API 网关,APISIX 目前支持Gateway API 规范的 v1beta1 版本,用于其 Apache APISIX Ingress Controller。
- Kong 是一个为混合云和多云环境构建的开源 API 网关,Kong 在 Kong Kubernetes Ingress Controller (KIC) 以及 Kong Gateway Operator 中支持 Gateway API。
然而由于目前 Gateway API 并不能覆盖供应商所提出网关资源的所有功能,并且仍然有大量用户使用供应商提供的网关资源,虽然用户可以通过开发 Gateway API 对网关资源进行适配,但这样的工作量较大,所以仅仅为 Gateway API 提供支持是远远不够的,尽管随着 Gateway API 特性的不断丰富,在未来,使用 Gateway API 将成为一种更加推荐的方式。因此,虽然 Kruise Rollout 目前已经提供了对 Gateway API 的支持,如何对现有供应商多种多样的网关资源提供支持仍然是一个重要的问题。
如何兼容社区多样的网关方案?
当前社区中已经存在许多广泛使用的供应商提供的网关资源,比如:Istio、Kong、Apisix 等,然而正如前文所述,这些资源的配置并没有形成统一的标准,因此无法设计出一套通用的代码对资源进行处理, 这种情况给开发人员带来了一些不便和挑战。
argo-rollouts 与 flagger 兼容方案
为了能够兼容更多的社区网关资源,一些方案被提出,例如 flagger、argo-rollouts 为每一种网关资源都提供了代码实现。这些方案的实现相对简单,但也存在一些问题:
- 面对大量的社区网关资源时,需要消耗大量精力进行实现
- 每次实现都需要重新进行发布,自定义能力较差
- 在某些环境下用户可能使用定制的网关资源,在这种情况下难以适配
- 每一种资源都有不同的配置规则,配置较为复杂
- 每添加一个新的网关资源都需要为其实现新的接口,维护难度较大
argo-rollouts 不同资源配置
因此,需要一种支持用户定制,可以灵活插拔的实现方案,以适配社区以及用户定制的多种多样的网关资源,来满足社区不同的用户的需求,增强 Kruise Rollout 的兼容性和扩展性。
为此,我们提出了一种基于 Lua 脚本的网关资源可扩展流量调度方案。
Kruise Rollout:基于 Lua 脚本的可扩展流量调度
Kruise Rollout 使用基于 Lua 脚本的网关资源定制方案,本方案通过调用 Lua 脚本根据发布策略和网关资源原始状态来获取并更新资源的期待工作状态(状态包含 spec、labels 以及 annotations),可以使用户能够轻松地适配和集成不同类型的网关资源,而无需修改现有的代码和配置。
本方案对于网关资源的处理可以表示为上图,整个过程可以描述为:
- 用户定义了 Rollout 流量灰度规则、需要修改的资源等信息,开始金丝雀发布
- 根据 Rollout 配置获取指定资源
- 根据资源调用对应的 Lua 脚本
- 将资源当前状态转为字符串存入资源 annotation 中,并与发布策略一同输入 Lua 脚本
- 利用 Lua 脚本根据当前状态和发布策略处理得到新状态并更新资源
- 发布结束后,从 annotation 中获取资源的原始状态对资源进行恢复
通过使用 Kruise Rollout,用户可以:
- 定制处理网关资源的 Lua 脚本,可以自由的实现对资源的处理逻辑,为更多资源提供支持
- 利用一套通用的 Rollout 配置模版对不同资源进行配置,降低配置的复杂性,方便用户配置
同时,Kruise Rollout 采用的方案仅需要添加 5 个新接口即可实现对多种多样网关资源的支持。相比之下,其他方案例如 argo-rollouts 则为不同供应商的网关资源提供了不同的接口,对于 Istio 和 Apisix 来说,argo-rollouts 分别提供了 14 个和 4 个新的接口, 而且,该方案随着对更多网关资源的支持,接口数量还会持续增长。相比之下,Kruise Rollout 并不需要为新的网关资源提供新的接口,这使得 Kruise Rollout 成为一种更简洁、更易于维护的选择,而不会增加过多的接口负担。同时,编写 Lua 脚本相对于开发 Gateway API 对网关资源进行适配,可以大大减小开发人员的工作量。
以下展示了一个利用 Lua 脚本对 Istio DestinationRule 进行处理的的示例。
- 首先定义 rollout 配置文件:
apiVersion: rollouts.kruise.io/v1alpha1
kind: Rollout
...
spec:
...
trafficRoutings:
- service: mocka
createCanaryService: false # 使用原有service,不创建新的canary service
networkRefs: # 需要控制的网关资源
- apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
name: ds-demo
patchPodTemplateMetadata:
labels:
version: canary # 为新版本pod打上label
- 对 Istio DestinationRule 进行处理的 Lua 脚本为:
local spec = obj.data.spec -- 获取资源的spec,obj.data为资源的状态信息
local canary = {} -- 初始化一条指向新版本的canary路由规则
canary.labels = {} -- 初始化canary路由规则的labels
canary.name = "canary" -- 定义canary路由规则名称
-- 循环处理rollout配置的新版本pod label
for k, v in pairs(obj.patchPodMetadata.labels) do
canary.labels[k] = v -- 向canary规则中加入pod label
end
table.insert(spec.subsets, canary) -- 向资源的spec.subsets中插入canary规则
return obj.data -- 返回资源状态
- 处理完的 DestinationRule 为:
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
spec:
...
subsets:
- labels: # -+
version: canary # |- Lua脚本处理后新插入的规则
name: canary # -+
- labels:
version: base
name: version-base
Kruise Rollout 进行 Istio 资源流量调度实践
接下来介绍一个利用我们所提出方案对 Istio 进行支持的具体案例。
- 首先部署如下图所示的服务。该服务由以下几部分构成:
-
- 由 Ingress Gateway 作为外部流量网关
- 通过 VirtualService 和 DestinationRule 将流量调度至 nginx pod 中
- 利用 ConfigMap 作为主页 nginx pod 的主页
nginx 服务的 deployment 如下所示:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
version: base
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
volumeMounts:
- name: html-volume
mountPath: /usr/share/nginx/html
volumes:
- name: html-volume
configMap:
name: nginx-configmap-base # 挂载ConfigMap作为index
- 创建 rollout 资源,配置发布规则,该 rollout 分为两批发布:
-
- 第一批将 20% 的流量转发至新发布的 pod 中
- 第二批将带有 header version=canary 的流量转发至新版本 pod 中
apiVersion: rollouts.kruise.io/v1alpha1
kind: Rollout
metadata:
name: rollouts-demo
annotations:
rollouts.kruise.io/rolling-style: canary
spec:
disabled: false
objectRef:
workloadRef:
apiVersion: apps/v1
kind: Deployment
name: nginx-deployment
strategy:
canary:
steps:
- weight: 20 # 第一批转发20%的流量进入新版本pod
- replicas: 1 # 第二批将包含version=canary header的流量转发入新版本pod
matches:
- headers:
- type: Exact
name: version
value: canary
trafficRoutings:
- service: nginx-service # 旧版本pod使用的service
createCanaryService: false # 不创建新的canary service,新旧pod共用一个service
networkRefs: # 需要修改的网关资源
- apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
name: nginx-vs
- apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
name: nginx-dr
patchPodTemplateMetadata: # 为新版本pod打上version=canary的label
labels:
version: canary
- 修改 nginx 服务 deployment 中挂载的 ConfigMap 开始金丝雀发布。
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
...
volumes:
- name: html-volume
configMap:
name: nginx-configmap-canary # 挂载新的ConfigMap作为index
- 开始发布第一批,Kruise Rollout 自动调用定义的 Lua 脚本对 VirtualService 和 DestinationRule 资源进行修改,进行流量调度,将 20% 的流量转发至新版本 pod 中,此时整个服务的流量表示为下图所示:
- 执行命令 kubectl-kruise rollout approve rollout/rollouts-demo,开始发布第二批,Kruise Rollout 自动调用定义的 Lua 脚本对 VirtualService 和 DestinationRule 资源进行修改,进行流量调度,将包含 version=canary header 的流量转发至新版本 pod 中,此时整个服务的流量表示为下图所示:
- 执行命令 kubectl-kruise rollout approve rollout/rollouts-demo,发布结束,VirtualService 和 DestinationRule 资源恢复至发布前状态,所有流量路由至新版本 pod。
如何利用 Lua 脚本快速配置网关资源的流量调度
在调用 Lua 脚本获取资源状态新状态时,Kruise Rollout 支持两种 Lua 脚本调用方式,分别为:
- 自定义的 Lua 脚本:用户自定义的,以 ConfigMap 的形式定义并在 Rollout 中调用
- 已发布的 Lua 脚本:社区通用的、已经稳定的 Lua 脚本,随 Kruise Rollout 打包发布
Kruise Rollout 默认首先查找本地是否存在已发布的 Lua 脚本,这些脚本通常需要设计测试案例进行单元测试验证其可用性,具有更好的稳定性。 测试案例的格式如下所示,Kruise Rollout 利用 Lua 脚本根据 rollout 中定义的发布策略对资源原始状态进行处理,得到发布过程中每一步的资源新状态,并与测试案例中 expected 中定义的期待状态进行对比,以验证 Lua 脚本是否按照预期工作。
rollout:
# rollout配置
original:
# 资源的原始状态
expected:
# 发布过程中资源的期待状态
在资源的 Lua 脚本未发布的情况下,用户还可以快速的通过在 ConfigMap 中配置 Lua 脚本的方式由 Kruise Rollout 调用从而对资源进行处理。
apiVersion: v1
kind: ConfigMap
metadata:
name: kruise-rollout-configuration
namespace: kruise-rollout
data:
# 键以lua.traffic.routing.Kind.CRDGroup的形式命名
"lua.traffic.routing.DestinationRule.networking.istio.io": |
--- 定义Lua脚本
local spec = obj.data.spec
local canary = {}
canary.labels = {}
canary.name = "canary"
for k, v in pairs(obj.patchPodMetadata.labels) do
canary.labels[k] = v
end
table.insert(spec.subsets, canary)
return obj.data
详细的 Lua 脚本配置说明参见 Kruise Rollout 官网 [ 2] 。
未来规划
- 更多网关协议支持: Kruise Rollout 目前是以 Lua 脚本插件化的方式支持多类型的网关协议,我们后续会重点加大这方面的投入,但面对百花齐放的协议类型,单靠社区 Maintainer 的单薄力量还远远不够,希望更多的社区小伙伴加入我们,一起来不断完善这方面的内容。
- 全链路灰度支持: 全链路灰度是具有更加细粒度和全面的灰度发布模式,它涵盖了应用程序的所有服务,而不止对单一的服务进行灰度,可以更好的对新服务进行模拟和测试。目前可以通过社区的网关资源如 Istio 进行配置来实现,但人工配置往往需要消耗较大的精力。我们将对这一部分进行探索,从而实现对全链路灰度的支持。
社区参与
非常欢迎你通过 Github/Slack/钉钉/微信 等方式加入我们来参与 OpenKruise 开源社区。
你是否已经有一些希望与我们社区交流的内容呢?可以在我们的社区双周会 [ 3] 上分享你的声音,或通过以下渠道参与讨论:
- 加入社区 Slack channel [ 4] (English)
- 加入社区钉钉群:搜索群号 23330762 (Chinese)
- 加入社区微信群(新):添加用户 openkruise 并让机器人拉你入群 (Chinese)
相关链接:
[1] Kruise Rollout
https://github.com/openkruise/rollouts
[2] Kruise Rollout 官网
https://openkruise.io/rollouts/introduction
[3] 社区双周会https://shimo.im/docs/gXqmeQOYBehZ4vqo
[4] Slack channel
https://kubernetes.slack.com/?redir=%2Farchives%2Fopenkruise