直播混沌工程之故障演练实践总结 | 助力S12全球总决赛
The following article is from 哔哩哔哩技术 Author 董小兵&王旭
1.1 背景
近年来,随着系统架构逐渐向微服务架构演进,开发效率以及系统扩展性大幅提高,服务之间的依赖所带来的不确定性也成指数级增长,在这样的服务调用网中,任何一环出现的正常或者异常的变化,都有可能对其他服务造成类似蝴蝶效应一般的影响。传统的测试方法已经不能全面理解和覆盖系统所有可能的行为,测试的有效性被大打折扣,为此Netflix从混乱猴子开始,从主动出击的思维方式衍生出混沌工程,让系统在每一次失败中获益,然后不断进化,促使开发者在开发软件时必须选择将防御性内建在系统中。
1.2 实践原理
设定稳态假设:向系统注入的事情不会导致系统稳定状态发生明显的变化,以直播进房场景来说,当每个时间点的每秒成功进去直播间的次数大于一个阈值时,该服务处于稳定状态。
控制爆炸半径:将线上实验影响控制在最小范围,并且能够根据稳态的变化实时自动停止实验,即最小化爆炸半径。
自动化持续运行:通过每天自动运行这样的实验,确保任何后续对系统的变更都不会引入新的系统行为盲点。
故障事件
互联网微服务中,自顶向下故障发生的概率越来越小,但是故障影响面是越来越大,具体我们在生产环境选择重放什么类型的事件时,需要结合业务的具体实现,通常考虑的事件有以下几类:
异地多活,某个机房不可用,备用机房是否可以承载业务流量
资源耗尽:比如DB连接数打满、消息队列积压、缓存热点
硬件故障,比如某个k8s prod突然down掉
网络延迟
下游依赖故障
历史故障等等
1.3 价值所在
混沌工程的业务价值并不适合用过程指标(比如模拟了多少种实验场景、发起了都少次实例)来衡量。比如前期可以选择对历史故障进行复现,确保故障改进的有效性;中期可以选择监控发现率,验证故障发现能力和监控的完备程度;后期可以考虑引入一些复杂的MTTR(Mean time to repair)度量指标,从故障“发现-定位-恢复时长“这种综合性指标,最终的目标还是希望通过混沌工程来提供系统弹性。
1.4 混沌工程和故障演练的区别
混沌工程的思维方式是主动去找故障,是探索性的,不知道摘掉一个节点、关闭一个服务会发生故障,虽然按计划做好了降级预案,但是关闭节点时引发了上游服务异常,进而引发雪崩,这不是靠故障注入能发现的。而故障演练首先是知道会发生故障,然后一个个的注入,然而在复杂分布式系统中,想要穷举所有可能得故障,本身就是一种奢望。故障演练更像平时做的测试,在测试中需要进行断言:给定一个特定的条件,系统会输出一个特定的结果,验证这个结果是真还是假,从而判定测试是否通过,这个并不能让我们发掘出系统未知的或尚不明确的认知。
02 故障注入系统设计
故障注入测试FIT(Failure Injection Testing)在业务正常处理之前,通过切面在系统层面加了一层拦截器,请求在正常处理之前,都将先尝试匹配拦截器,对不匹配的业务是无感知的,系统设计的时候主要考虑如下三个方面:
轻量:轻量级接入,极低的接入成本
高性能:在线上演练时不增加内存占用和延迟
低耦合:SDK与控制面低耦合,SDK负责底层能力,控制面负责高层应用场景
P.S.
S12技术保障内幕揭秘-如何实现“喝茶保障”2.5.3章节混沌工程也有关于FIT架构的描述,感兴趣可通过链接前往。2.1 实现细节
待测服务启动时初始化故障演练SDK,只需要改动一行代码就可以接入:
# main.go
func main() {
flag.Parse()
log.Init(nil)
defer log.Close()
fault.Init(nil) // 此行:初始化故障演练模块
//... other logic
}
初始化后,故障演练SDK将会劫持所有业务组件的处理过程,根据平台配置的匹配规则进行故障注入。
为了使平台操作更快的下发给客户端节点,组件选择了采用双向gRPC streaming的方案,客户端实时的将演练信息上报给服务端,服务端也将配置通过gRPC通道实时下发。
在与服务端的通信协议上:我们定义了标准化的故障声明,将一次故障注入分成目标、匹配条件、行为、参数几种基本属性,以满足不同故障、不同场景下的需求,受益于统一协议,与控制面约定好支持的行为后,不同组件可以并行开发迭代。同时客户端使用的SDK版本作为元信息上报给服务端,服务端会针对不同的客户端版本做好故障注入前的检查工作。
message Fault {
// 目标: redis/mc/mysql/bm/warden
TARGET target = 1;
// 匹配类型: 某个端口 某个sql类型等
map<string, string> matchers = 2;
// 行为名称 如: ecode timeout
string action = 3;
//参数 具体错误码和超时等配置
map<string, string> action_args = 4;
}
2.1.1 故障匹配流程
用户流量进入后,首先在入口侧被故障注入模块拦截,根据当前用户、接口的信息进行故障匹配,判断当前是否有需要进行注入的故障,如匹配到,则将故障信息注入到请求上下文中去,在业务逻辑执行到各个子组件后,使用上下文信息对组件行为做更细粒度的操控。
P.S.
目前已实现的拦截器有:
gRPC客户端和服务端
HTTP客户端和服务端
Redis和Memcached
异步框架Fanout
消息队列
MySQL、ES、Taishan(KV)
2.1.2 数据上报
在请求处理结束,业务侧接口会实时返回,而此次故障注入的细节会进入数据上报模块,在模块内部的线程池中,综合请求信息和各个子组件的信息会形成一个详细的注入行为数据,这份数据会在内存暂存一会,用于合并同类项以及等待旁路的线程处理结束,随后会将演练数据压缩上报给服务端,用以在平台实时展示故障影响范围和相关细节。
2.2 故障配置演示
配置故障
控制爆炸半径
03 S12赛事故障演练实践
从今年8月份开始S12赛事的质量保障工作时,故障演练是作为一个大的主题来展开的,在前期方案制定上,我们从uat染色环境小范围的验收故障演练工具是否能有效的控制最小化爆炸半径,再到线上环境进行常规故障注入测试,再扩展为线上红蓝演练,评估开发者是否能够在规定的时候内发现故障并且定位故障,以及恢复时长来评估整个演练的结果。
业务场景选择
针对此次S12赛制,设置了多个场景的保障预案,因故障演练需要占用研发和测试人员的大量时间,所以从最核心几个业务场景出发,优先演练业务优先高并且人力有保障的场景,最终确定的场景如下:
直播首页
直播进房
直播弹幕
直播营收
故障注入实验设计
确定了场景后,接下来就要梳理各场景的业务逻辑,然后根据业务场景,梳理出故障场景(核心场景应至少覆盖L0/L1接口),以下列举了部分故障场景:
P.S.
报警配置一般都是一段时间内的错误阈值是不是已经到了预期,在测试的时候如果依赖人工一次次请求,效率会大受影响,所以一般都会结合压力工具设置一个合理的QPS范围;
整个故障演练遵循从小范围uat环境测试,然后在生产环境运行并且控制好爆炸半径,越接近生产环境,对实验外部有效性的威胁就越少,对实验结果的信心就越足。
线上问题汇总
送礼服务缓存超时,查缓存不回源时,礼物面板不展示
送礼下游创建订单order服务不可用【强依赖】,金瓜子、银瓜子、包裹送礼都正常,预期金瓜子送礼失败
android ios web三端不一致:获取用户钱包余额信息服务不可用时,送礼成功,但是客户端不展示电池、银瓜子数;Web端正常展示
ios粉版进房后,显示报错信息,高能榜tab点击会显示接口500
进房服务获取初始化数据失败
android进房场景未加载背景图
直播首页服务出现故障,直播首页不展示我的关注模块
送礼下游服务不可用,web端不展示大航海专属的默认图,Android不展示icon
...
P.S.
发现的每一个问题,会有负责的同学来评估是否修改,修改的排期是哪个版本,如果不修改的话,原因是因为什么。整个演练结束后,总发现问题22例,影响面大的问题一共有3例:分别是热门房间缓存无法构建、送礼服务缓存超时,查缓存不回源时,礼物面板不展示、进房场景下,ios和android双端不一致,严重问题占比14%,剩下的问题在正常的项目迭代中进行修复和验证。
红蓝演练
红蓝演练按照上面介绍的混沌工程的MTTR(Mean time to repair)的思想来实践的,从故障“发现-定位-恢复时长“来衡量整个系统的弹性。前期通过故障演练已经在生产环境进行了小范围测试,整个测试阶段从场景设计到预期结果研发都参与了,但是我们知道线上的故障都是不可预期的,我们进行故障演练无法覆盖方方面面,那如果在s12赛事期间,线上有用户反馈或者告警后,研发人员怎么能快速的止损了,使故障带来的影响降到最低,所以我们考虑通过红蓝演练的模式,线上随机的触发故障(故障评估无大的级联风险,范围可控),在定义的评估时间范围内看研发同学是否能准确的定位以及恢复故障。经过平时的演练,真的有故障发生的时候,不会手忙脚乱,处理的毫无章法。
红蓝规则定义:
红方:攻守方,由业务同学担任
蓝方:攻击方,由测试同担任,在预定时间内,随机从故障池注入故障
判定标准:蓝方注入故障后若红方1分钟内收到准确无误的告警并且5分钟内定位到故障原因,10分钟内解决故障,则红方胜,反之蓝方胜出
演练位置:红蓝双方选择一个会议室来进行,防止线上出了意外情况时,蓝方可以第一时间停止故障
演练内容:研发同学不知道会有什么故障,演练内容对研发是保密的,能这样做的前提是在故障演练阶段我们已经积累了足够的信心,由蓝方从故障池随机选择故障
下图显示了在红蓝过程中,实时记录的数据,只截取了其中部分数据,演练结束后,结果是红方胜利,符合设置的判定标准。
3.1 后续计划
自动分析应用强弱依赖
在微服务的场景下,一次请求可能涉及到几十个不同的后端服务,十多种组件依赖,靠人工梳理费时费力不说,辛苦梳理的数据也常随着业务代码的迭代而过期,为此我们正在实现一套更加完善的的依赖分析、自动故障注入功能,首先会用在分析业务应用的强弱依赖。
在规范的业务接口请求处理的场景下,强弱依赖的定义是比较明确的,强依赖会导致接口报错和返回特定数据,弱依赖会忽略或者使用一些降级数据,不会对接口本身造成影响,根据这个特征,我们就可以实现强弱依赖的自动识别,相对的如果业务逻辑不是那么规范也可以加以人工辅助确认的环节。
流程:
在故障注入上按一次请求的粒度进行,以至多几次请求报错为代价,实现线上环境的真实依赖验证。
故障演练自动化测试
故障演练完成一次,需要耗费很多时间去梳理业务逻辑,涉及测试方案,以及到最后的执行,如果能将整个过程随时随地的执行,那将节省大量的时间。例如强弱依赖的场景最合适做自动化,因为结果都是可预期的,强依赖的服务有故障,但最终表象肯定是某个接口会返回错误或者兜底数据,并且测试结果后可以自动停止故障,就跟业务测试的自动化一样的。
故障注入作为常规测试
故障演练绝非是每次大型活动前的质量保障专利,更应该作为一种常规的测试手段应用于平时的业务测试中。根据的时序图所示,客户端的请求到达服务A,服务A是我们的待测服务,服务A弱依赖服务B,所以即使服务B返回报错或者超时,都不影响服务A继续请求服务C,并能正常返回正确的数据。如果没有故障注入工具,要测试强弱依赖,我们是怎么样做的呢?
业务伪代码如下:
func (s *serviceA)Process(ctx context.Context,userID int64,roomID int64) (rsp *RoomRsp,err error) {
rsp=&RoomRsp{}
//服务B 弱依赖
if num,err:=rpc_B.Process(ctx, roomID);err!=nil{
log.Errorf(ctx,"err:%+v",err)
return nil,err
}
//服务C
if num,err:=rpc_C.Process(ctx, roomID);err!=nil{
log.Errorf(ctx,"err:%+v",err)
return nil,err
}
/*
其他业务逻辑
*/
return rsp,nil
}
按照上述说的,服务B是一个弱依赖的错误,代码不应该直接给上游返回报错信息。要测试这种场景,你可以直接关闭服务B,让网络不可达,但是在实际测试工作中,有可能多个业务都在使用服务B,所以简单粗暴的关闭服务B,会影响其他人员的使用,那有了故障注入工具,我们可以直接在平台上注入rpc超时故障,并且控制爆炸半径只影响这一个用户就可以了。
04 总结
分布式系统天生包含大量的交互、依赖点,可能出错的地方数不胜数。硬盘故障,网络不同,流量激增压垮某些组件...,可以不停的列举下去。这都是每天要面临的常事,任何一次处理不好就有可能导致业务停滞、性能低下,所以尽可能多的识别出导致这些异常的、系统脆弱、易出故障的环节,就可以针对性的对系统进行加固、防范,不断打造更具弹性的系统,从而避免故障发生时所带来点的严重后果。
参考文献:
[1] 混沌工程-NetFlix系统稳定性之道
参考阅读:
本文由高可用架构转载。技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿