brightnfeng,腾讯 QQ 音乐后台开发工程师
Without music, life would be a mistake. ― Friedrich Nietzsche
故障无处不在,而且无法避免。(分布式计算谬误)
在分布式系统建设的过程中,我们思考的重点不是避免故障,而是拥抱故障,通过构建高可用架构体系来获得优雅应对故障的能力。QQ音乐高可用架构体系包含三个子系统:架构、工具链和可观测性。
业内主流的容灾方案,包括异地冷备,同城双活,两地三中心,异地双活/多活等。
容灾架构的选型我们需要衡量投入产出比,不要为了预防哪些极低概率的风险事件而去投入过大的成本,毕竟业务的规模和收入才是重中之重。QQ音乐的核心体验是听歌以及围绕听歌的一系列行为,这部分业务以只读服务为主。而写容灾需要支持双写,带来的数据一致性风险较大,且有一定的实施成本。综合权衡,我们舍弃写容灾,采用一写双读的异地双活模式。
在原有深圳中心的基础上,建设上海中心,覆盖接入层、逻辑层和存储层,形成异地双中心。
异地容灾支持自动故障转移才有意义。如果灾难发生后,我们在灾难发现、灾难响应以及评估迁移风险上面浪费大量时间,灾难可能已经恢复。
我们最初的方案是,客户端对两地接入IP进行动态评分(请求成功加分,请求失败减分),取最优的IP接入来达到容灾的效果。经过近两年多的外网实践,遇到一些问题,动态评分算法敏感度高会导致流量在两地频繁漂移,算法敏感度低起不了容灾效果。而算法调优依赖客户端版本更新也导致成本很高。
后来我们基于异地自适应重试对容灾方案做了优化,核心思想是在API网关上做故障转移,降低客户端参与度。
方案主要有两点:
使用最新方案后,API网关重试比客户端调度更可控,双中心流量相对稳定,一系列自适应限流和熔断策略也抵消重试带来的请求量放大问题。接下来介绍方案细节。
API网关故障转移需要考虑重试流量不能压垮异地,否则会造成双中心同时雪崩。这里我们做了一个自适应重试的方案,在异地成功率下降的时候,取消重试。
自适应重试方案:
上述方案中的重试窗口,由探测及退避策略决定:
探测策略及退避策略图示:
探测策略及退避策略的算法描述:
// 设第 i 次探测的窗口为 f(i),实际探测量为 g(i),第 i 次探测的成功率为 s(i),第 i 次本地总请求数为 t。
// 那么第 i+1 次探测的窗口为 f(i+1),取值如下:
if s(i) = [98%, 100%] // 第 i 次探测成功率 >= 98%,探测正常
if g(i) >= f(i) // 如果第 i 次实际探测量等于当前窗口,增大第 i+1 次窗口大小
f(i+1) = f(i) + max (min ( 1% * t, f(i) ) , 1)
else
f(i+1) = f(i) // 如果第 i 次实际探测量小于当前窗口,第 i+1 次探测窗口维持不变
else
f(i+1) = max(1,f(i)/2) // 如果第 i 次探测异常,第 i+1 次窗口退避置 1
// 其中,重试窗口即 f(i) 初始大小为 1。算法中参数及细节,根据实际测试和线上效果进行调整。
自适应重试效果:
即使我们在容量规划上投入大量精力,根据经验充分考虑各种请求来源,评估出合理的请求峰值,并在生产环境中准备好足够的设备。但预期外的突发流量总会出现,对我们规划的容量造成巨大冲击,极端情况下甚至会导致雪崩。我们对容量规划的结果需要坚守不信任原则,做好防御式架构。
限流可以帮助我们应对突发流量,我们有几个选择:
我们采用的是滑动窗口计数器,主要考虑以下几点:
上图描述ServceA到ServiceB之间的RPC调用过程中的限流:
限流算法的选择,还有一种可行的方案是,框架提供不同的限流组件,业务方根据业务场景来选择,但也要考虑成本。社区也有Sentinel等成熟解决方案,新业务可以考虑集成现成的方案。
上一节的分布式限流是在Client-side限制流量,即请求量超出阈值后在主调直接丢弃请求,被调不需要承担拒绝请求的资源开销,可最大程度保护被调。然而,Client-side限制流量强依赖主调接入分布式限流,这一点比较难完全受控。同时,分布式限流在集群扩缩容后需要及时更新限流阈值,而全量微服务接入有一定的维护成本。而且分布式限流直接丢弃请求更偏刚性。作为分布式限流的互补能力,自适应限流是在Server-side对入口流量进行控制,自动嗅探负载、入口QPS、请求耗时等指标,让系统的入口QPS和负载达到一个平衡,确保系统以高水位QPS正常运行,而且不需要人工维护限流阈值。相比分布式限流,自适应限流更偏柔性。
指标说明:
指标名称 | 指标含义 |
---|---|
CPU usage | 当系统CPU使用率超过阈值启动自适应限流 |
inflight | 系统中正在处理的请求数量 |
qps | 窗口内每个桶的请求处理成功的量 |
MaxQPS | 滑动窗口中QPS的最大值 |
rt | 窗口内每个桶的请求成功的响应耗时 |
MinRt | 滑动窗口中响应延时的最小值 |
算法原理:
根据Little's Law,inflight = 延时 QPS。则最优inflight为MaxPass MinRt,当系统当前inflight超过最优inflight,执行限流动作。
用公式表示为:cpu > 800 AND InFlight > (MaxQPS * MinRt)
其中MaxQPS和MinRt的估算需要增加平滑策略,避免秒杀场景下最优inflight的估算失真。
限流效果:
在微服务系统中,服务会依赖于多个服务,并且有一些服务也依赖于它。如下图,“统一权限”服务,依赖歌曲权限配置、购买信息、会员身份等服务,综合判断用户是否拥有对某首歌曲进行播放/下载等操作的权限,而这些权限信息,又会被歌单、专辑等歌曲列表服务依赖。
当“统一权限”服务的其中一个依赖服务(比如歌曲权限配置服务)出现故障,“统一权限”服务只能被动的等待依赖服务报错或者请求超时,下游连接池会逐渐被耗光,入口请求大量堆积,CPU、内存等资源逐渐耗尽,导致服务宕掉。而依赖“统一权限”服务的上游服务,也会因为相同的原因出现故障,一系列的级联故障最终会导致整个系统宕掉。
合理的解决方案是断路器和优雅降级,通过尽早失败来避免局部不稳定而导致的整体雪崩。
传统熔断器实现Closed、Half Open、Open三个状态,当进入Open状态时会拒绝所有请求,而进入Closed状态时瞬间会有大量请求,服务端可能还没有完全恢复,会导致熔断器又切换到Open状态,一种比较刚性的熔断策略。SRE熔断只有打Closed和Half-Open两种状态,根据请求成功率自适应地丢弃请求,尽可能多地让请求成功请求到服务端,是一种更弹性的熔断策略。QQ音乐采用更弹性的SRE熔断器:
正常情况下,requests 等于 accepts,所以丢弃概率为0。随着正常处理的请求减少,直到 requests 等于 K * accepts ,一旦超过这个限制,熔断器就会打开,并按照概率丢弃请求。
超时是一件很容易被忽视的事情。在早期架构发展阶段,相信大家都有因为遗漏设置超时或者超时设置太长导致系统被拖慢甚至挂起的经历。随着微服务架构的演进,超时逐渐被标准化到RPC中,并可通过微服务治理平台快捷调整超时参数。但仍有不足,传统超时会设定一个固定的阈值,响应时间超过阈值就返回失败。在网络短暂抖动的情况下,响应时间增加很容易产生大规模的成功率波动。另一方面,服务的响应时间并不是恒定的,在某些长尾条件下可能需要更多的计算时间,为了有足够的时间等待这种长尾请求响应,我们需要把超时设置足够长,但超时设置太长又会增加风险,超时的准确设置经常困扰我们。
其实我们的微服务系统对这种短暂的延时上涨具备足够的容忍能力,可以考虑基于EMA算法动态调整超时时长。EMA算法引入“平均超时”的概念,用平均响应时间代替固定超时时间,只要平均响应时间没有超时即可,而不是要求每次都不能超时。主要算法:总体情况不能超标;平均情况表现越好,弹性越大;平均情况表现越差,弹性越小。
如下图,当平均响应时间(EMA)大于超时时间限制(Thwm),说明平均情况表现很差,动态超时时长(Tdto)就会趋近至超时时间限制(Thwm),降低弹性。当平均响应时间(EMA)小于超时时间限制(Thwm),说明平均情况表现很好,动态超时时长(Tdto)就可以超出超时时间限制(Thwm),但会低于最大弹性时间(Tmax),具备一定的弹性。
为降低使用门槛,QQ音乐微服务只提供超时时间限制(Thwm)和最大弹性时间(Tmax)两个参数的设置,并可在微服务治理平台调整参数。算法实现参考:https://github.com/jiamao/ema-timeout
我们做了很多弹性能力,比如限流,我们是否可以根据服务的重要程度来决策丢弃请求。此外,在架构迭代的过程中,有许多涉及整体系统的大工程,如微服务改造,容器化,限流熔断能力落地等项目,我们需要根据服务的重要程度来决策哪些服务先行。
如何为服务确定级别:
服务分级的应用场景:
API网关既是用户访问的流量入口,也是后台业务响应的最终出口,其可用性是QQ音乐架构体系的重中之重。除了支持自适应限流能力,针对服务重要程度,当触发限流时优先丢弃不重要的服务。
效果如下图,网关高负载时,2级、3级、4级服务丢弃,只有1级服务通过。
随着产品的迭代,系统不断在变更,性能、延时、业务逻辑、用户量等因素的变化都有可能引入新的风险,而现有的架构弹性能力可能不足以优雅地应对新的风险。事实上,即使架构考虑的场景再多,外网仍然存在很多未知的风险,在某些特殊条件满足后就会引发故障。一般情况下,我们只能等着告警出现。当告警出现后,复盘总结,讨论规避方案,进行下一轮的架构优化。应对故障的方式比较被动。
那么,我们有没有办法变被动为主动?在故障触发之前,尽可能多地识别风险,针对性地加固和防范,而不是等着故障发生。业界有比较成熟的理论和工具,混沌工程和全链路压测。
混沌工程通过在生产环境上进行实验,注入网络超时等故障,主动找出系统中的脆弱环节,不断提升系统的弹性。
TMEChaos 以ChaosMesh为底层故障注入引擎,结合TME微服务架构、mTKE容器平台打造成云原生混沌工程实验平台。支持丰富的故障模拟类型,具有强大的故障场景编排能力,方便研发同学在开发测试中以及生产环境中模拟现实世界中可能出现的各类异常,帮助验证架构设计是否合理,系统容错能力是否符合预期,为组织提供常态化应急响应演练,帮助业务推进高可用建设。
上一节的混沌工程是通过注入故障的方式,来发现系统的脆弱环节。而全链路压测,则是通过注入流量给系统施加压力的方式,来发现系统的性能瓶颈,并帮助我们进行容量规划,以应对生产环境各种流量冲击。
全链路压测的核心模块有4个:流量构造、流量染色、压测引擎和智能监控。
随着微服务架构和基础设施变得越来越复杂,传统监控已经无法完整掌握系统的健康状况。此外,服务等级目标要求较小的故障恢复时间,所以我们需要具备快速发现和定位故障的能力。可观测性可以帮助我们解决这些问题。
指标、日志和链路追踪构成可观测的三大基石,为我们提供架构感知、瓶颈定位、故障溯源等能力。借助可观测性,我们可以对系统有更全面和精细的洞察,发现更深层次的系统问题,从而提升可用性。
在实践方面,目前业界已经有很多成熟的技术栈,包括Prometheus,Grafana,ELK,Jaeger等。基于这些技术栈,我们可以快速搭建起可观测系统。
指标监控能够从宏观上对系统的状态进行度量,借助QPS、成功率、延时、系统资源、业务指标等多维度监控来反映系统整体的健康状况和性能。
我们基于Prometheus构建联邦集群,实现千万指标的采集和存储,提供秒级监控,搭配Grafana做可视化展示。
我们在微服务框架中重点提供四个黄金指标的观测:
QQ音乐Metrics解决方案优势:
随着业务体量壮大,机器数量庞大,使用SSH检索日志的方式效率低下。我们需要有专门的日志处理平台,从庞大的集群中收集日志并提供集中式的日志检索。同时我们希望业务接入日志处理平台的方式是无侵入的,不需要使用特定的日志打印组件。
我们使用ELK(ElasticSearch、Logstash、Kibana)构建日志处理平台,提供无侵入、集中式的远程日志采集和检索系统。
下图为音乐馆首页服务的远程日志:
在微服务架构的复杂分布式系统中,一个客户端请求由系统中大量微服务配合完成处理,这增加了定位问题的难度。如果一个下游服务返回错误,我们希望找到整个上游的调用链来帮助我们复现和解决问题,类似gdb的backtrace查看函数的调用栈帧和层级关系。
Tracing在触发第一个调用时生成关联标识Trace ID,我们可以通过RPC把它传递给所有的后续调用,就能关联整条调用链。Tracing还通过Span来表示调用链中的各个调用之间的关系。
我们基于jaeger构建分布式链路追踪系统,可以实现分布式架构下的事务追踪、性能分析、故障溯源、服务依赖拓扑。
下图为音乐馆首页服务的链路图:
想必大家都遇到过服务在凌晨三点出现CPU毛刺。一般情况下,我们需要增加pprof分析代码,然后等待问题复现,问题处理完后删掉pprof相关代码,效率底下。如果这是个偶现的问题,定位难度就更大。我们需要建设一个可在生产环境使用分析器的系统。
建设这个系统需要解决三个问题:
我们基于conprof搭建持续性能分析系统:
如下图,我们可以通过服务名、实例、时间区间来检索profile信息,每一个点对应一个记录。
传统的方式是在进程崩溃时把进程内存写入一个镜像中以供分析,或者把panic信息写到日志中。core dumps的方式在容器环境中实施困难,panic信息写入日志则容易被其他日志冲掉且感知太弱。QQ音乐使用的方式是在RPC框架中以拦截器的方式注入,发生panic后上报到sentry平台。
本文从架构、工具链、可观测三个维度,介绍了QQ音乐多年来积累的高可用架构实践。先从架构出发,介绍了双中心容灾方案以及一系列稳定性策略。再从工具链维度,介绍如何通过工具平台对架构进行测试和风险管理。最后介绍如何通过可观测来提升架构可用性。这三个维度的子系统紧密联系,相互协同。架构的脆弱性是工具链和可观测性的建设动力,工具链和可观测性的不断完善又会反哺架构的可用性提升。
此外,QQ音乐微服务建设、Devops建设、容器化建设也是提升可用性的重要因素。单体应用不可用会导致所有的功能不可用,而微服务化按单一职责拆分服务,可以很好地处理服务不可用和功能降级问题。Devops把服务生命周期的管理自动化,通过持续集成、持续测试、持续发布等来降低人工失误的风险。容器化最大程度降低基础设施的影响,让我们能够将更多精力放在服务的可用性上,此外,资源隔离,HPA,健康检查等,也在一定程度上提升可用性。
至此,基础架构提供了各种高可用的能力,但可用性最终还是要回归业务架构本身。业务系统需要根据业务特性选择最优的可用性方案,并在系统架构中遵循一些原则,如最大限度减少关键依赖;幂等性等可重试设计;消除扩容瓶颈;预防和缓解流量峰值;过载时做好优雅降级等等。而更重要的一点是,我们需要时刻思考架构如何支撑业务的长期增长。
新年红包封面福利
点击卡片进入公众号后台
回复:2023 即可参与封面抽奖。