阿里妹导读
灰度很重要,灰度的策略也需要结合实际情况进行灵活的调整,本文跟大家分享了一个前些时间发现的灰度设计bug。
一、案例分享
跟大家分享一个前些时间发现的灰度设计bug,这个bug蛮有意思,看似完美方案,但因为未考虑到一些技术特性,导致存在缺陷。为更通顺的说明,先进行一些名词介绍。
灰度发布:灰度发布是指在黑与白之间,能够平滑过渡的一种方式。AB-test就是一种灰度发布方式,让一部分用户继续用A,一部分用户开始用B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。(引用Wiki百科)
安全生产环境:(简称SPE)为保障线上稳定性提供灰度流量生产环境。发布从预发到生产环境前,需要经过SPE进行流量验证。
1.1 案例简述
同一个应用,需要消费同一个topic消息两次,因不同环境的配置不一致,导致消息存在消费遗漏。
1.2 案例背景
需求的主要修改的点是将历史各异的结算模型改成统一结算模型,新老模型相差较大。在notify消息的处理上,原本是想在同一个consumer处理类中改写,但成本较大,且考虑到老模型后续是需要下线的,从代码整洁的角度出现,新写了一个消息处理类,使用另一个group组订阅,最终的结果就是同一个topic消息会被两个group组订阅,并由两个consumer类去消费。
1.3 方案描述
原灰度方案如下:设定一个时间作为灰度生效时间,再通过消息中买家的尾号作为切流条件。当一个消息被接收时,会先判断当前时间是否大于灰度时间,若满足大于灰度时间的条件,则判断买家尾号是否在灰度切流范围中,均满足时,走新的结算流程。若两个条件有一个条件不满足,则走老结算流程。
1.4 方案缺陷分析
上述方案,一般而言,是一个很不错也很完备的方案,但结合本次结算模型迁移的背景,则存在一个缺陷。缺陷的由来即是“双消息”。那么双消息处理是怎么引发问题的呢?原因在于灰度配置文件在安全生产和线上生产间阶梯推进时,需要等待1小时,这一小时会导致部分消息被遗漏。假设当前SPE和线上生产已同步推进至10%,此时SPE的配置推进至20%,线上生产依旧保留10%,那么双消息一条走到SPE,另一条走到安全生产时,即会出现问题,因为SPE的老group实现类只关心80%-100%的流量,而线上生产环境只关心0-10%的流量,中间存在消息遗漏。逻辑如下图:
1.5 案例的特殊之处
1.结算模型的升级,结合业务考虑,引入了双消息消费的方式。
2.新老consumer无差别监听消息进行消费,造成消费遗漏。
3.SPE安全生产停留1小时的机制。
1.6 改进策略
1.灰度时间总开关不变
2.买家尾号的灰度策略改为“尾号为K,区间生产时间为V”的KV对,其中V是一个未来时间,且“V值>系统时间+安全生产停留时间”。如下,从50%流量推至100%,100%的流量配置生效时间是一个未来时间。
[ {"rate":5000,"whiteList":[ ],"activeTime":1682928000000 }, {"rate":10000,"whiteList":[ ],"activeTime":1683795376000 //未来时间 }]
在跟进完上述问题后,我查阅了过去一年部门故障复盘文档,基本所有的故障和资金事件复盘,均有提及灰度手段。而像本案例中,于“不确定行为”进行灰度的控制,是新人们常见的误区。因此,结合自己在交易线的一些经验,谈下我认知中的灰度,并将阐述我理解的MVP版灰度方案,至少需要些什么,灰度设计中需要注意什么。
二、设计一个MVP版的灰度方案
2.1灰度方案是什么?
在聊how的问题前,有必要先讲一下WHAT和WHY。先聊一下WHY,假设没有任何管控流程,代码发布后立即全量上线,又恰巧不幸地出现系统、数据或逻辑等方面的问题,那一场灾难也就随之而来,而且业务体量越大,风险&舆情&资损的敞口也就越大,损失越不可挽回。因此,发布流程需要必要的管控和监督,将风险控制在有限范围内,这样的发布流程即是灰度。平滑过渡是灰度重要特征,这个特征也决定了灰度发布的作用至少有两点:
1.降低发布带来的风险,让少部分用户先使用新功能新版本,提前小范围内发现bug或性能问题,及时做好修复,降低新功能新版本的影响。
2.通过新老版本对比,观察新功能带来的效果,更好起过渡效果。
2.2 MVP版的灰度方案( Minimum Viable Product最小可执行版本)

2.2.1 明确灰度维度

常见的灰度规则有用户尾号(买家或卖家),业务单据id(如商品、订单、结算单、运单)、黑白名单、人群圈选(如定向投放)。黑白名单和人群圈选有点类似A/B-test,能比较精准的用于线上功能测试。一般用于两种场景:
1.风险极大,而功能测试又无法完全覆盖的场景,可以使用白名单和人群圈选做第一步的灰度策略。
2.功能存在争议,beta版功能测试,可以使用此法去收集反馈。
而采用用户id或业务id作为灰度条件,是更为通用的方式,这两种方案的流转模式如下:
尾号id灰度策略一般会与白名单策略组合使用,更稳妥的达到管控效果,结合我目前的项目实践,有一种较为万能的灰度公式分享给大家。
“1.白名单(个别用户)--> 2.买家尾号灰度1% -->3. 尾号3% -->4. 尾号10% -->5. 尾号30% --> 6. 尾号灰度50%-->7. 尾号灰度70%-->8. 尾号灰度100%”。
多数场景下,这一套公式流程是比较适用的。也许有同学会有疑问,是否可以采用“卖家id”灰度维度,多数情况下是可以的,但以卖家id为灰度规则,更容易命中到超大商家,可能带来的影响有两个,1.集中的单据被影响,2.相较买家购买,卖家维度更容易命中爆品热度的商品,引发数据库热点。因此,灰度维度的选择,是一件需要深思的事,我认为有几个原则需要遵守。
1.样本避免以偏概全,尽量保证样本的随机性,近似均匀分布。
这一点很好理解,比如你想统计有多少人坐过杭州地铁,然后你跑到杭州地铁站去做问卷调查,除了得到100%的结果,你一定会得到更多的白眼。同样,在灰度维度选择上,也需要保持样本的随机性。如用户id、商品id,一般就可以较好的满足日常灰度的需要。而像业务的人群id,在筛选时就得严格关注一下样本的随机性。
2.筛选条件需要由严至宽,逐步放开。
举一个例子,是去年进行某支付退款业务的中台升级时,在完成所有验证后,灰度策略里包含了一条,即逐步放开退款单的金额 ,从“控制退款单1元-->退款单10元-->30元-->100元-->300元-->全量放开”。这就是一种典型的风控层面的灰度策略,由严至宽。

2.2.2 做好过程观察和推进

灰度是一个逐渐由白至黑的过程,在这个过程中,“灰度可观察”和“灰度流程管理”,都需要做好。
灰度可观察
灰度过程必须可观察,这样才能及时发现问题,真正发挥灰度的价值。我总结了四个点,是灰度可观察的最小条件集,包括完备的流量日志、核对告警、反馈渠道及时触达和性能关注。以下逐一阐述。
策略一:完备的流量日志
完备的日志对于灰度过程中的问题发现是非常有必要的,将详细的处理逻辑,尤其是报错和异常的日志,是灰度可监控的必要条件。一方面,技术同学可以通过观察日志的错误日志,进行流量健康度的观察,另一方面,也可以结合 sunfire的统计监控能力,对灰度过程的报错做阈值告警。
策略二:核对告警
灰度项目中,核对也是常用的观测策略,无论是新业务的逐步上新或是新老业务的逐渐切换。如一个重资金相关的新业务上线,在逐步的灰度过程中,使用核对是验证非法单据的出现。这也是我日常业务跟进中经常使用的策略,比如RP3迁移灰度时,对打上新逻辑标的订单做校验。完备的日志是从系统处理的角度进行观察,而监控核对是从数据的角度进行另一个维度的观察,相辅相成不可缺少。
策略三:内部反馈渠道&舆情关注
这种策略一般用于白名单灰度的情况,且选中的白名单是与集团服务同学平时沟通较多且友好的商家,在百分比切流前,进行白名单个别商家的试点,关注异常情况。白名单内测后,切流阶段,如果有不可规避的风险,需要技术同学时刻关注客服反馈,必要时需要给客服统一的话术。
策略四:关注灰度过程中的性能问题
灰度不仅应用于功能,也可用于性能观察。灰度过程的流量是逐步增大的,新老功能的差异带来的性能影响,也是逐步放大的。比如一次改动中,新老流量模型中,对于某个信息字段的获取走的不同信息渠道,那么新老模型的性能差异就需要关注。此时不仅是业务的日志监控,应用系统的监控也需要安排上,尤其是灰度范围扩大的一段时间里,尤其需要关注接口性能,包括依赖rt变高,自身rt变高,数据库热点等问题。
灰度流程管理
好的灰度流程,需要具备优雅、可靠的特征。我认为“有序的推进”和“可及时回退”是灰度流程中必要的考虑。
有序的推进
从0到1的灰度过程,是一个边观察边推进的过程,通过日志、核对、自动化等手段做到有效观察后,在积累一定量级的单据后,再进行逐步的放开。需要关注的是如果配置非法状态下,是否保证老逻辑是否仍能正常消费。举例说明,下方伪代码是根据用户的尾号进行判断,在灰度配置平台里进行尾号的数字配置,那么,这一段代码看似正常,但实际隐藏着较大的风险:
//预期从灰度配置文件中读取一个int型的值,但配置中grayRange设置了一个字符串型“50%”,int grayRange = GrayHanlder.getConfig("灰度配置id").getInteger("grayRange");3if(userId mod 100 < grayRange){//走新逻辑}else{//走老逻辑}
上述代码在执行时,会报错,导致新老逻辑都不会走到。当然,在实际业务需求中,很少发生这么低级的错误,但我的意思是,依赖于配置的灰度推进,需要确保灰度逻辑进行必要的验证,灰度的推进也需要极其的谨慎。一般而言灰度代码只是一个if+else,但其背后影响却极大。
灰度流程推进的前提,一定有效的流量观察之后,而不是形而上的依据灰度时长,请确保这一点。分享一个真实的故障案例:
案例名:某B系业务的会员子账号id写入错误导致无法处理退款。
故障原因:因修复一个会员登录bug,更新了一个冷门字段的获取逻辑,某B系业务使用此字段做权限控制,导致业务受到影响。
灰度过程:此案例中的发布应用,在发布过程中进行了灰度停留,但灰度时间是晚上,而被影响的B系业务特性决定了晚间时分是流量低谷期,导致灰度过程中的问题未被及时关注,灰度流程未发挥应有的效用。
灰度改进:涉及会员和商家操作的系统,灰度发布时间包含商家操作高峰期(上午8点--10点)。
灰度回退
当上线功能的表现不符合预期时,需要考虑控制灰度回退。一般在灰度的配置文件中,需要引入逻辑开关,值为true或false,命中true后,才会进入更细粒度的灰度命中。所以当结果观察不符合预期后,可以快速推进配置为false,必要时,走紧急审批。确保没有新流量走到灰度流程中。
灰度回退仅能阻止问题的扩大,但已出现问题的单据需要做好妥善处理,一般而言,有两种方案。
1.修订数据:新写修正接口或批量更改Db数据,但需要注意合规问题。
2.快速修复并发布新版本,利用升级来“回退”,覆盖上次灰度发布的修改。
2.3 灰度工具
这里仅讲一下服务端常用的灰度工具,一般都是轻量配置的平台,将配置内容脱离于代码之外,可以清晰快捷的推进,在灰度推进时,仅需做配置更新即可,不用发布代码。配置可以简单成一个String,也可以是一个json,举例如下。
{"flag": true, //总开关,true为开启,false为关闭恢复"buyerAccessFlow": 10, //用户尾号控制,当前为尾号 0-9用户进入恢复"amountLimit": 30000//控制金额,金额小于300元,才进入恢复 }
三、关于灰度其它常见的问题
3.1 机器分批发布不是严格意义上的灰度策略
许多同学对于灰度的认知还不多,认为分批发布也是一种灰度策略。其实严格意义上讲,应用的分批发布更多的是出于对系统稳定性的考虑,而不是灰度验证的考虑。从“可回退”的角度看,分批发布过程中,即使发现了问题,发现了脏数据,那数据也都是随机的,数据无法通过特征来找出数据,也就无法对脏数据进行订正或回滚,所以不是严格意义上的灰度策略。
3.2 灰度设计的一致性原则
灰度意味着线上存在两套处理规则或流程,一条业务单据在两套流程中处理的结果一般也是不一样的,因此确保单据在灰度过程中的一致性非常有必要,否则很可能引发线上问题,这里先举一个真实发生的例子
案例名:某电商业务增加一种退款打款渠道,灰度策略不合理导致双渠道出账的情况。
案例描述:RPC调用退款同意时,第一次命中尾号灰度,走进组合渠道中,但组合渠道里出现调用异常,但此时渠道会进行自身的重试。在重试期间,用户二次点击,此时灰度策略有变化,导致走到了另一个组合渠道,进行了打款并成功。
案例分析:发生的原因在于打款事项的幂等被破坏。两个渠道无法感知对方的打款行为。
案例解决:在申请退款时,即打上标,将灰度行为完全依赖于标识,后续所有的处理依标进行,避免不同调用引起的灰度不一致的行为。后续退款相关的灰度行为,都前移至了申请退款阶段,此时命中灰度规则后,即会给它打上一个灰度标,后续的行为完全按标行走。
上述案例,是一个非常经典的问题,即灰度过程中数据一致性未能做好保证,导致出现两种打款策略组,破坏了幂等。那么怎么样保证灰度一致性原则呢?我认为有以下三个原则需要遵循:
原则一:灰度命中处理只能被一次消费
如上述案例,如果将灰度的判定放在“同意退款”,那就非常容易出现前后两次调用时,走到不同处理流程的尴尬情况,反之,我们可以巧妙的将灰度判定前移至“申请退款”,并打上相应的标,后续“同意退款”则按标进行即可,保证灰度命中处理只被一次消息。“同意退款”是想灰度的功能,但“申请退款”才是真正的灰度对象。所以想灰度的功能跟实现灰度的对象,并不一定要一致。
原则二:确保不同环境的灰度一致性
许多带安全生产环境的应用,从预发到线上前,需要在安全生产环境观察1小时以上,包括应用代码发布或配置发布。这中间的时差,极其容易引发灰度的混乱,比如开头说的那一个case,由于notify无差别的往安全生产和线上生产环境发消息,应用在两个环境中的配置不一致,导致消息被过滤的问题。
这种问题,就像使用原则一的方法进行预先打标处理,也是无法规避的,比较好的处理方法是,制造一个时延周期,确保线上生产和安全生产的一致性。比如推送一个未来的生效时间,且确保生效时间晚于全量发布完成后的时间,这样即可确保两个环境的一致性。
原则三:确保不同应用的灰度一致性
如果灰度流程涉及多个应用,那么灰度逻辑需要确保一致。简而言之,一个形如“A-->B-->C”的链路中,要么保证B系统无视A系统的灰度条件,要么确保灰度逻辑仅在A系统中进行判断。
3.4 前端灰度策略
前文提到的灰度策略,我均是从一个服务端的视角考虑,其实在前端或web端也有一些常用的灰度技巧,这里简单聊一下。
1.CDN资源分流
前端资源放在CDN上,每次发布新版本,资源即增量的传到CDN并指定唯一版本号。在处理请求时,依据前端策略来分流不同用户使用不同版本的CDN,展示不同的样式。此时,相应的后端接口,需要依据参数来控制灰度策略,区分前端不同的请求。一般在类似于账单业务升级时,会用到这种策略。因为前后端都需要灰度,所以需要由前端控制灰度策略,后端进行参数兼容,保证账单的多样性。
2.客户端分流
客户端分流的策略与CDN资源一样,由客户端来控制灰度分流,根据客户端传来的参数和版本号,结合当前的放量策略来决定服务情况。客户端分流的策略也会更多,如用户设备系统、app版本号、app安装渠道、用户ID、设备ID。
四、写在最后
灰度很重要,灰度的策略也需要结合实际情况进行灵活的调整。本文中提到的策略、观点均是本人一得之见。引玉之砖,欢迎大家讨论。
阿里云开发者社区,千万开发者的选择
阿里云开发者社区,百万精品技术内容、千节免费系统课程、丰富的体验场景、活跃的社群活动、行业专家分享交流,欢迎点击【阅读原文】加入我们。
继续阅读
阅读原文