全链路多重灰度发布
1. 什么是灰度发布
让我们从最简单的情况开始。灰度发布又叫金丝雀发布,是一种发布技术,用于减少软件新版本发布的风险。其想法是首先向少数用户发布新版本的软件,然后逐步扩大用户比例。例如,在这个图中,我们先测试10%的用户,然后逐渐将更多的用户转移到新版本,最后,当所有的用户都使用了新版本,那么旧版本就可以被清除并下线。
在整个测试过程中,我们可以给请求流量贴上各种业务标签,如:用户的设备:Android设备、用户的地理位置:Atlantis,等等。还要注意的是,用户标签不应该使用IP地址,因为这是不准确和不一致的。
然后我们可以指定灰度流量规则,将用户的某一部分流量调度/路由到某个灰度版本上来,例如,来自Atlantis的Android用户被调度到服务A的2.0灰度版本。
2. 什么是全链路灰度发布
单个服务的灰度发布的场景仍然是有限的。在现实中,更常见的是全链路灰度发布。例如,一个用户客户端不能直接通过路由器转发到服务的Canary版本上。这是因为该服务在整个请求链条中非常靠后,被其他服务隔开。
如图所示,我们已经为配送服务发布了灰度版本,但是用户和订单服务在配送服务的前面。在这种情况下,我们需要做两件事,以确保流量被正确调度。
- 第一件事是在整个调用链中传递用户标签,一旦丢失就无法调度。
- 第二件事是在所有上游调用方能够将流量路由到下游服务的正确版本。
如果在这里稍微考虑一下实现层面,我们会发现有两类方法来做全链路灰度发布;要么通过逐个改变每个服务里面的服务调用的代码的逻辑实现,要么通过非侵入式的平台级整体解决方案。而改变代码可能是非常繁琐而复杂,且容易出现bug。
3. 案例研究:现实世界中的灰度发布
有了刚才描述的问题模型,让我们用另一个实际的例子来说明多灰度发布的难度。例如,现在有两个服务Order v1.0和Email v2.0,服务Order调用服务Email,服务Email使用第三方的Email提供商A。
而我们决定在Order对象中增加一些信息,但先让Android用户灰度测试。由于对Order对象的改变影响到了有关联两个服务,我们准备发布Order v1.1和Email v2.1来应用新的Order对象。
然后,另一个团队决定在电子邮件服务中用电子邮件提供商B取代电子邮件提供商A,所以我们添加了电子邮件v2.0.1,只为测试来自Atlantis的用户。
而如果这两个灰度发布同时上了生产环境,我们就会发现问题来了。
如上图所示,淡蓝色的灰度发布需要的是Android用户,绿色的灰度需要的是Atlantis用户,如果在淡蓝色这边的Android用户也是Atlantis用户,那么,我是不是还要把他们调度到绿色的灰度上来?反之也一样。如果我们实现这种逻辑,那么就需要两个灰度发布互相能够感知到对方。这种在实际中是很难做到的。
这个问题让我们处于两难的境地,就是Android用户和Atlantis用户是重叠的,有用户即是Android用户又是Atlantis用户,于是导致了在两个灰度发布中进行流量调度的复杂度。而这种流量的复杂性会导致用户所不期望的不一致性,这势必会导致混乱的运维和用户流量管理,而灰度发布的流量也不可控。
4. 多重灰度发布的挑战
让我们退一步,分析一下灰度发布的不同情况。让我们首先看下图。
例如,A
和B
依赖Z
,而A'
测试Android流量,B'
测试 iPhone 流量,他们测试的是两个不同的用户群,如果他们都依赖Z'
来测试,Z'
承担了两个不同的灰度流量,将成为混乱的根源。
那么有两种更好的方法,从下图可以看出。
左边的是把A'
的流量调度Z
而不是Z’
,右边的是分别把A'
和B'
的流量调度到Z'
和Z''
。因此,使许多事情变得更容易的原则是:
【一个灰度发布的服务只能在一个灰度规则中!!】
即使解决了上述问题,仍然存在流量规则可能重叠的问题。前面的例子是Android和iPhone的用户流量,但如果一个灰度发布测试Android,另一个灰度发布测试Atlantis,两个灰度发布的流量规则将有一个共同的子集。
因此,如果流量来自一个共同的子集,例如来自Atlantis的Android设备的流量,此时我们应该如何路由这些流量?
这最终成为两个集合问题的数学抽象。
- 集合匹配:用户流量是一个集合,灰度发布的流量规则是一个集合。
- 多重匹配问题:多个灰度流量规则被匹配上,应该选择哪一个。
5. 案例分析 - 多个灰度规则匹配问题
那么,让我们用一个例子来说明前面提到的所有问题,例如,有一个送餐应用的后端服务栈。该服务由三个微服务组成。订单服务,餐厅服务,和配送服务。
- 订单服务没有灰度版本
- 餐厅服务有两个灰度版本
- 第一个灰度是针对来自Atlantis的Android流量
- 第二个灰度是针对所有的Android流量
- 配送服务也有两个灰度版本
- 第一个灰度是来自Atlantis的流量
- 第二个灰度是来自Android的流量
5.1 完美匹配
当系统收到带有Atlantis用户标签的流量时,它与配送服务Atlantis灰度的路由规则相匹配,流量遵循绿色虚线路径。
另一边,带有Android用户标签的流量与灰度的餐厅和配送的路由规则相符。该流量遵循蓝色虚线路径。
5.2 多重匹配
但是,对于同时包含Android和Atlantis标签的流量会发生什么?它与所有三个灰度流量规则相匹配,而且没有明确的方法来引导流量。
从数学集合来看,Atlantis流量和Android流量分别在绿色和蓝色的灰度上匹配,而Atlantis&Android流量可以在所有灰度规则上匹配。
从数学上看,如果灰度流量是用户流量的一个子集,那么灰度规则就是匹配成功的。那么,我们应该如何处理多重匹配的问题呢?
上图显示了,是 Android 也是 Atlantis 的用户同时属于三个集合,这就是为什么他们会匹配三条灰度规则的原因。
5.3 匹配优先级
解决这个问题的一个简单易行的方法是指定灰度规则的优先级。每个流量规则都有一个数字表示优先级,数字越小优先级越高。从图中可以看出,Traffic Atlantis & Android 匹配了三个灰度规则,但由于餐厅灰度的 Atlantis & Android的优先级最高,即优先级为1,因此红色Canary被选中。
5.4 匹配规则遮蔽问题
即使优先权解决了多重匹配的问题,但仍然存在着配置误用的问题。匹配规则遮蔽问题。在下面这个例子中,红色规则被蓝色规则所遮蔽,因为蓝色的优先级更高,而蓝色的集合是红色集合的超集。这意味着没有流量被路由到Atlantis & Android这个规则上来。
这个跟C++/Java中的异常捕捉的道理是一样的,如果范围更大的异常在前面的话,那么后面所定义的异常就会永远捕捉不到。
6. 技术实现
实现整个无侵入式的全链路多重灰度发布并不容易,不过,我们已经在我们的开源项目中做到了这一点 - EaseMesh
在这里我们解释一下EaseMesh的技术细节,并概述一下我们是如何实现多重灰度发布的。
首先,我们所有的服务都在Kubernetes Pods中运行。这里的三个服务对应于EaseMesh中的三个服务,请注意同一服务下的不同版本也是一个服务的一部分。
所以一个Mesh Service会有多个版本同时运行。为了将流量路由到正确的版本,需要完成两件事。
首先要保证的是流量里通过服务时带有的用户标签信息,在整个服务调用链中不丢失。
这需要Sidecar和业务服务协同合作。Sidecar自然知道所有灰度的流量规则和用户标签,比如一些特定的HTTP Headers,它将与流量一起转发。
同时,业务应用本身也需要传递用户标签给Sidecar,这可以由EaseMesh 官方提供的JavaAgent组件与Sidecar合作完成(这两个组件同样开源EaseAgent 和 Easegress),用户无需做任何代码的修改,完全透明。Sidecar会通过JavaAgent来传递所有和全链路灰度相关的信息。至于其他语言如Golang,由于没有字节码技术,只需要一个专用的SDK来配合完成即可。
所以EaseMesh在这个高级功能上也支持多种语言,只需要透传用户标签即可。
EaseMesh 灰度版本的第二个要求是流量路由。
所有的组件,包括EaseMesh的IngressController,以及每个服务Pod中的Sidecar都有能力将灰度流量路由到下一个服务的相应的灰度版本。我们可以看到,上图中所有的服务组件,无论是接收请求还是发送请求,都会通过Sidecar。而在向外发送请求时,Sidecar会根据流量特性决定是否需要将流量路由给下一个服务的某个灰度版本。而这些都是由Sidecar自动完成,不需要JavaAgent或SDK的参与。
7. 总结
现在我们来总结一下平台的设计原则,以及其使用的最佳实践。
7.1 设计原则
- 一个灰度服务版本最多属于一个灰度规则。
- 一个请求最多只能被路由到一个灰度规则。
- 灰度规则对传入的流量进行明确的选择。
- 不符合灰度规则的正常流量将走非灰度的正常版本。
7.2 最佳实践
- 标记流量必须使用用户侧的具有业务属性的信息,而客户的IP地址不是一个好的属性。
- 当标记的流量重叠时,使用明确的优先级来指导流量进行路由。
- 应该将范围更小的灰度规则赋予更高的优先级。