背景
近几年,随着有赞用户的迅速增长和业务的快速发展,对业务开发人员要求越来越高,一方面要求为用户提供稳定的服务,一方面要求进行快速业务迭代。然而,随着公司业务复杂度和服务化整体规模的增长,单个业务功能涉及的微服务接口数、服务化调用链路长度都在迅速增加,业务的回归测试越来越难以覆盖到所有的调用链路和业务逻辑,通过仅在测试环境进行业务测试的方式来保证系统稳定性的难度越来越高。
基于系统稳定性和快速业务迭代的综合考虑,业务应用开发团队采取了新版本服务灰度上线的方式,即新版本服务并非全量发布到线上环境,而是发布少数几个实例进行灰度验证,没有问题后再全量发布。在部分核心服务进行接口升级和逻辑迁移时,还会通过在业务逻辑代码中增加黑白名单或者流量百分比控制的方式,逐步将旧版本接口实现迁移至新版本接口实现。该方式较好地权衡了服务稳定性和业务迭代效率,但仍存在以下问题:
- 需要业务开发人员在服务接口中编码大量与业务逻辑无关的黑白名单或流量百分比控制代码,对业务入侵比较大;
- 调整黑白名单或流量控制百分比,需要业务团队发布代码或者接入动态配置中心,过程复杂、可复用性差、操作灵活性差;
- 当新版本接口出现波动或异常,通常需要业务团队通过紧急修改代码和紧急发布,来更新黑白名单和流量百分比控制策略,时间较长,业务影响较大。
为解决以上痛点,有赞基础保障团队于2018年开始立项实现流量控制系统以及灰度发布产品,主要目标如下:
- 将业务通过黑白名单或者百分比方式控制新、旧版本接口流量的逻辑,整体下沉到服务化基础组件和Http入口Nginx层;
- 提供完整的流量控制方案,除了上面提到的黑白名单和百分比流量控制策略,项目还将深挖业务需求,提供更丰富的动态流量控制策略;
- 与Ops运维管理系统打通,实现易于使用和管理的灰度发布及其他服务治理产品。
流量控制系统
协议
实现灰度发布产品,首先需要实现底层的流量控制系统。而实现流量控制系统,首先面临问题是如何确定流量控制协议。虽然我们最初的目标是为了支持灰度发布,但我们的系统设计必须充分考虑未来的可扩展性,除了流量路由,还期望支持熔断、限流、错误恢复等服务治理能力。我们在进行方案选型时,有如下几个目标:
- 协议应当是完备的,支持各种服务治理功能。包括但不限于:服务熔断、限流、A/B Test等。后续的服务治理功能应当都能够通过该控制协议实现;
- 可读性好,易于解析。控制协议应该是易读且易于解析的,便于理解和方便中间件实现,尽量不要引入复杂协议或新的编码和序列化方式;
- 避免重复造轮子,尽量使用业界已经成熟的优秀设计。
根据上面的设计目标以及充分的调研,最终确定使用业界广泛关注的Service Mesh框架Istio的流量控制协议。实际上,Istio使用Envoy作为data plane,也就是支持了Envoy的API。当时立项时,Envoy已经在业界非常多的实际生产场景中部署使用。我们最初采用的是Envoy v1 API,其路由控制协议支持json编码,语意丰富且易于解析和阅读(目前广泛使用的是基于gRPC的v2 API,我们也在向v2 API 迁移中)。同时,Istio很好的支持K8s容器化部署,符合有赞运维平台容器化的发展趋势。
需要额外说明的是,Istio使用的路由控制协议,七层协议功能最完备的是Http路由协议,而我们内部广泛使用的是RPC协议:Java应用之间使用Dubbo协议,跨语言调用使用Nova协议(自研),需要进行相应的适配。而值得庆幸的是,Dubbo和Nova RPC调用协议的语意,在整体上与Http请求有一定的相似性,适配起来比较方便,如service(interface)与method可对应Http的path,attachment(metadata)可以对应Http的header。此外,原始的Istio路由控制协议语义丰富而复杂,主要为了多维度的控制Http请求的路由、超时重试、限流、熔断等。我们短期内不需要支持完整的协议,仅支持经过微小适配后的协议子集即可,保留后续对其扩展的能力。
协议的详细细节我们在这里不再展开,具体可参考Istio和Envoy的文档。这里以一个抽象简化的示例来介绍简单描述一下流量控制协议(目前主要是路由控制,也称为路由规则)。示例如下:
{
"name":"java-demo-rule",
"domains":[
"java-demo"
],
"routes":[
{
"headers":[
{
"name":"userid",
"value_match":"123"
}
],
"cluster":"java-demo|version=v2"
},
{
"weighted_clusters":{
"clusters":[
{
"name":"java-demo|version=v2",
"weight":10
},
{
"name":"java-demo|version=v1",
"weight":90
}
]
}
}
]
}
上述规则描述的是java-demo应用的规则,有两条规则。每个规则主要分为两部分:一部分描述请求匹配条件,请求匹配条件包括请求path、请求header等,匹配方式包括等值匹配、正则匹配、范围匹配、列表匹配等;另一部分描述请求目标集群,目标集群可以是一个集群,也可以是带权重的多个集群;其中请求匹配条件可省略,即匹配所有请求。上述示例描述的是,当请求中的header字段“userid”值完全等于“123”的时候,路由到携带标签“version=v2”的实例上;其他请求10%路由到携带标签“version=v1”的实例上,90%路由到携带标签“version=v1”的实例上。实例携带什么样的标签,是发布系统控制的,实例启动的时候将标签信息同其它服务元数据一起写入服务注册中心,消费者通过服务发现就知道了每个实例及其标签信息。当有多条规则的时候,按顺序匹配,最先匹配的规则生效。
架构
有赞的流量控制系统涉及到多个基础组件:Http统一接入网关Nginx、服务化代理Tether、服务化框架Dubbo、流量控制中心Istio Pilot、运维管理系统Ops等。核心架构如下所示: 所有流量控制相关的产品入口都是集成在Ops运维管理系统的,Ops将产品层面的上层控制输入转化为底层路由规则推送到Nginx和Istio Pilot。Nginx独自管理路由规则;Istio Pilot为内部服务化系统管理与下发路由规则,规则以CRD的方式存储在K8s中。服务化基础组件包含Tether和Dubbo,都是周期性地通过轮询的方式从Istio Pilot拉取路由规则(后续将升级到Envoy v2 API,通过gRPC Push机制更新)。Tether是一个服务化代理,是有赞自研的Service Mesh框架Sidecar组件(参见有赞技术博客)。当Http请求到达统一接入层Nginx时,Nginx将根据请求特征、路由规则、Upstream信息进行匹配,决定将Http请求转发到哪些Upstream实例,即Node应用实例;Node应用收到Http请求后,向后端服务化系统发起Http Rest RPC请求,请求发送到本机部署的Tether,Tether首先将Http Rest RPC请求转化为Dubbo RPC请求,然后根据请求特征、路由规则、服务发现信息进行匹配与路由,将Dubbo请求转发到匹配的后端Dubbo实例。而Dubbo接收到请求向其他后端发起请求时,同样根据请求特征、路由规则、服务发现信息进行匹配与路由。其中请求特征分为全局特征与局部特征,全局特征会随着整个请求链路透传,由各个基础组件共同支持;而局部特征仅存在于一个请求跨度中。有赞的流量基本都是与用户所访问的商家店铺绑定的,因此,我们内部将店铺id作为一个全局特征,会在整个请求链路中透传,店铺id也是在有赞进行流量控制的一个核心维度。
至此,我们大概阐述了有赞底层流量控制系统的整体设计和架构。在此流量控制系统的基础上,我们在上层产品化出了有赞的“灰度发布”和“蓝绿发布”系统,在保证业务快速迭代的同时,最大程度的维护系统的稳定性。
灰度发布
什么是灰度发布
灰度发布,是在生产环境稳定集群之外,额外部署一个小规模的灰度集群,并通过流量控制,引入部分流量到灰度集群,进行生产全量发布前的灰度验证。如果验证失败,可立刻将所有流量切回至稳定集群,取消灰度发布过程;如果验证成功,则将新版本进行全量发布升级至生产环境稳定集群,完成灰度发布过程。 如上图所示,应用A是consumer,应用B是provider,应用B稳定集群的版本为v1,有3个实例。(a)应用B额外部署了一个灰度集群,版本为v2,有1个实例。(b)通过流量控制,将5%的请求路由到应用B灰度集群,进行小规模验证。(c)如果在灰度验证中发现了故障,则通过流量控制,将全部流量切回应用B稳定集群,实现快速回滚。(d)如果灰度验证没有发现任何故障,则应用B的灰度发布过程完成,并进行全量发布,稳定集群版本所有实例升级至v2。
发布流程
下面从产品层面介绍一下有赞灰度发布的流程,包括:灰度发布开始、灰度初始化、灰度验证、灰度取消或全量发布、灰度发布结束。
(1) 灰度发布开始。用户从Ops管理系统,进入应用的发布页面,选择发布类型“灰度发布”,开始灰度发布。
(2) 灰度初始化。底层运维系统进行灰度集群部署,灰度实例上线时服务注册会注册标签“canary=true”(稳定集群默认为“canary=false”),此时灰度实例的上线不会接收到任何流量,因为我们在底层流量控制系统做了特殊保护,在没有路由规则的情况下不会把流量路由到灰度实例,避免灰度集群在初始化没有完成或灰度集群全部挂掉的情况下请求处理失败。目前,我们灰度集群部署的规模为稳定集群的10%,即若稳定集群有100个实例,灰度集群部署10个实例。
(3) 灰度验证。初始化完成后,用户可推送灰度路由规则,将部分请求路由至灰度集群进行验证。目前,支持两种灰度规则:店铺列表和请求百分比。店铺列表规则是指如果对应的请求访问的是列表中的店铺,则请求路由至灰度集群。店铺id是有赞内部所有请求header或metadata中都会携带的一个字段,我们一般会选择一些内部测试店铺或小流量店铺进行灰度验证。请求百分比规则是指按一定百分比概率将请求路由至灰度集群。由于灰度集群部署规模仅为稳定集群规模10%,因此,请求百分比规则最大也只能设置为10%。如果灰度验证失败,进入流程4;成功则进入流程5。 店铺列表灰度规则示例如下:
{
"name":"java-demo-rule",
"domains":[
"java-demo"
],
"routes":[
{
"headers":[
{
"name":"shopid",
"list_match":[
"123",
"456"
]
}
],
"cluster":"java-demo|canary=true"
},
{
"cluster":"java-demo|canary=false"
}
]
}
(说明:店铺id为123和456的请求,进入灰度集群;其余请求保留在稳定集群)
百分比灰度规则示例如下:
{
"name":"java-demo-rule",
"domains":[
"java-demo"
],
"routes":[
{
"weighted_clusters":{
"clusters":[
{
"name":"java-demo|canary=true",
"weight":10
},
{
"name":"java-demo|canary=false",
"weight":90
}
]
}
}
]
}
(说明:随机10%请求进入灰度版本服务集群)
(4) 灰度取消。如果在灰度验证过程中发现了问题,灰度取消可秒级将全部流量切回稳定集群,具体包括两个步骤:删除路由规则和灰度集群下线。当然,用户也可先通过修改规则的方式(如店铺列表置空或请求百分比置0),先将流量切回稳定集群,然后保留现场,方便进行问题排查。
(5) 全量发布。如果灰度验证没有发现问题,那么就可以进行新版本的全量发布了。包括:稳定集群全量发布、删除灰度路由规则和灰度集群下线。
(6) 灰度发布结束。
上述流程描述可简化成如下流程图:
蓝绿发布
什么是蓝绿发布
蓝绿发布,是在生产环境稳定集群之外,额外部署一个与稳定集群规模相同的新集群,并通过流量控制,逐步引入流量至新集群直至100%,原先稳定集群将与新集群同时保持在线一段时间,期间发生任何异常,可立刻将所有流量切回至原稳定集群,实现快速回滚。直到全部验证成功后,下线老的稳定集群,新集群成为新的稳定集群。 如上图所示,应用A是consumer,应用B是provider,应用B稳定集群的版本为v1,有3个实例。(a)应用B额外部署了一个新集群,版本为v2,同样有3个实例。(b)通过流量控制,将所有请求路由到应用B新集群,进行全量验证,同时原稳定集群继续保持在线。(c)如果在验证中发现了故障,则通过流量控制,将全部流量切回应用B原稳定集群,实现快速回滚。(d)如果验证没有发现任何故障,则应用B的蓝绿发布完成,v2版本集群成为新的稳定集群。
为什么还需要蓝绿发布
有了灰度发布之后,为什么还需要蓝绿发布呢?主要有如下几点考虑:
- 应用在生产环境全量发布后,发现故障时回滚时间慢。当线上核心应用存在几十上百的服务实例时,应用实例分批滚动回滚,部分业务应用启动时间需要几分钟,导致整个回滚过程的时间可能超过十分钟,甚至几十分钟。
- 灰度集群容量有限,灰度流量可能超过灰度集群容量。目前限定最大灰度流量为10%,但是对于店铺id控制方式目前很难完全控制灰度流量低于10%。如果灰度店铺id列表中包含大商家,或者遇到列表中的商家搞秒杀活动,则流量可能会超过灰度集群容量,导致灰度服务实例负载过高,甚至不可用。
- 灰度发布期间能发现的问题有限。如数据库慢查问题、死锁问题等,10%流量很难发现,可能只会在100%流量中才容易暴露。
- 灰度发布成功后,仍然需要进行全量发布,在此过程中仍有较多不确定性,如因一些未预料的异常导致发布失败等。
对于上面几个问题,使用蓝绿发布系统都可以较好地解决:
- 蓝绿发布期间,流量全部切至新集群时,原稳定集群继续保持在线,若新集群有问题,可通过流量控制秒级切回至原稳定集群,没有应用启动以及其他等待时间。
- 蓝绿发布期间,新集群规模与原稳定集群规模一致,即使是瞬时大流量也没有问题。
- 蓝绿发布期间,新集群承载全站流量,容易验证各种场景,如数据库死锁等并发问题。
- 蓝绿发布新集群验证完成后,已经处于正常服务状态,不会再引入不确定性的变更操作。
理论上,灰度发布的需求都可以用蓝绿发布解决,而且可以更好地解决。但是我们不能完全用蓝绿发布替代灰度发布,因为应用蓝绿发布期间两个集群同时在线,占用平时两倍的服务器资源,成本很高,而灰度发布只需要很小一部分服务器资源就可以验证大部分问题。
发布流程
下面从产品层面介绍一下有赞蓝绿发布的流程,包括:蓝绿发布开始、蓝绿初始化、蓝绿验证、蓝绿取消或完成上线、蓝绿发布结束。
(1) 蓝绿发布开始。用户从Ops管理系统,进入应用的发布页面,选择发布类型“蓝绿发布”,开始蓝绿发布。
(2)蓝绿初始化。首先推送路由规则,控制全部流量在老集群,保证新集群部署启动期间不会接收任何流量。我们通过实例的BlueGreenVersion标签来识别是蓝集群还是绿集群。如果老集群BlueGreenVersion为blue,则新集群BlueGreenVersion为green;反之亦然。该标签是发布系统在发布时注入实例环境变量中,实例启动时注册到注册中心的。推送完规则后,就可以部署新集群了。
假设老集群BlueGreenVersion为blue,则推送的规则示例如下:
{
"name":"java-demo-rule",
"domains":[
"java-demo"
],
"routes":[
{
"cluster":"java-demo|BlueGreenVersion=blue"
}
]
}
(3) 蓝绿验证。初始化完成后,用户可推送路由规则,将部分请求流量或全部流量路由至新集群进行验证。目前,支持两种蓝绿规则:店铺列表和请求百分比。同灰度规则类似,不再赘述。验证过程中如果没有问题,可以不断将流量迁移至新集群,直至所有流量都在新集群。如果蓝绿验证失败,进入流程4;成功则进入流程5。
假设老集群BluGreenVersion为blue,店铺列表蓝绿规则示例如下:
{
"name":"java-demo-rule",
"domains":[
"java-demo"
],
"routes":[
{
"headers":[
{
"name":"shopid",
"list_match":[
"123",
"456"
]
}
],
"cluster":"java-demo|BlueGreenVersion=green"
},
{
"cluster":"java-demo|BlueGreenVersion=blue"
}
]
}
(说明:店铺id为123和456的请求,进入新版本服务集群;其余请求保留在旧版本服务集群)
百分比蓝绿规则示例如下:
{
"name":"java-demo-rule",
"domains":[
"java-demo"
],
"routes":[
{
"weighted_clusters":{
"clusters":[
{
"name":"java-demo|BlueGreenVersion=green",
"weight":50
},
{
"name":"java-demo|BlueGreenVersion=blue",
"weight":50
}
]
}
}
]
}
(说明:随机50%请求进入新版本集群)
(4) 蓝绿取消。如果在蓝绿验证过程中发现了问题,蓝绿取消可秒级将全部流量切回老集群,具体包括三个步骤:推送路由规则控制所有流量到老集群、新集群下线和删除路由规则。当然,用户也可先通过修改规则的方式(如店铺列表置空或请求百分比置0),先将流量切回老集群,然后保留现场,方便进行问题排查。
(5) 完成上线。如果蓝绿验证没有发现问题,那么就可以完成蓝绿发布上线了。只有当全部流量在新集群时才能操作完成上线。包括两个步骤,老集群下线和删除路由规则。完成上线后,如果再有需要回滚,那只能走普通的回滚流程了。
(6) 蓝绿发布结束。
上述描述可简化成如下流程图:
可观测性与可运维性
一个好的系统或产品,仅实现其基本功能是远远不够的,可观测、易运维也是必不可少的。有赞在灰度与蓝绿发布产品中,可观测性与可运维性方面也做了不少工作。主要包括发布应用监控、发布事件通知、全局发布状态以及周期统计报表。下面介绍一下有赞蓝绿发布产品在可观测性与可运维性方面的一些工作。
发布应用指标监控
蓝绿发布期间新老两个集群运行两个应用版本,需要能够对新老集群的出、入流量的QPS、耗时、失败率等指标进行监控,通过对比新旧两个版本集群的实时监控数据,用户可以快速的发现问题。如下图所示:
发布事件通知
发布事件通知是为了在发布过程中的一些重点事件发生时,及时的告知到对应的运维或开发人员,比如蓝绿发布开始、蓝绿发布取消、蓝绿发布完成上线、蓝绿发布验证过长等事件。这些事件通知通过企业办公IM发送到对应的人员。如下图所示:
全局发布状态
全局发布状态主要是通过一些关键指标实时反映蓝绿发布系统的整体使用状况,可以方便蓝绿发布运维人员根据实时状态来做出一些人为的管理和干预。如下图所示:
周期统计报表
统计报表主要是统计一个周期内的一些指标,方便全局视角的观察和分析,不断改进蓝绿发布系统。对于某个周期支持当前周期的统计与上一周期的统计的同时显示,方便进行对比,周期可以是周、月、季度、年。如下图所示:
未来规划
本文主要介绍了有赞的灰度发布和蓝绿发布实践,目前只局限于单应用的控制。然而,新产品或项目的上线往往会涉及到多个应用,这些应用一般会同时发布,如果各个应用独立进行灰度或蓝绿发布、独立验证,那么发布不仅非常费时费力,而且在出现问题时也难以做到快速回滚。如下图所示:应用A、B、D共同参与了项目001的新功能开发,新版本为v2,线上稳定版本为v1,这3个应用需要同时上线,我们期待通过一个规则来控制进入A-v2、B-v2、D-v2的流量,且经过A-v2的流量必然到B-v2、D-v2,经过B-v2的流量必然到D-v2,这样会可以极大地降低涉及多个应用的复杂项目整体上线的难度,保障项目上线的稳定性。未来我们会实现这方面的需求。
另外,有赞有不少业务依赖消息队列,当前如果请求链路中有消息队列,由于消息队列的一些限制,我们目前还没能够实现对消息消费的灵活控制,这一部分在一定程度上会影响整体的流量控制,未来我们也期望在这方面做一些改进。
本文所有内容结束。感谢您的阅读。