百度 Feed 稳定性架构实践
导读:百度 Feed 信息流推荐系统服务于手百、好看、全民、贴吧等公司绝大多数信息流业务场景,随着业务的高速发展,整个系统承载的流量已经高达数十亿,在庞大的流量规模背后是数百个微服务和数万台机器做支撑。如何保证整套系统对外的高可用性是整个系统能力建设的关键,也是我们团队的一个非常核心的工作方向。为了保障信息流推荐系统常态 5 个 9 的可用性目标, 本文将基于我们实际的工作经验分享介绍百度 Feed 在线推荐系统是如何建设高可用性架构的。
一、背景
百度 Feed 信息流推荐系统服务于公司绝大多数信息流业务场景,目前已经承载了手百、好看、全民、贴吧等多端产品的信息流推荐能力。如下是从资源漏斗维度给出的百度 feed 推荐系统的一个简化示意图。
系统简要说明如下:
资源索引库:基于倒排索引或是向量索引提供数千万资源的索引查询能力,通常会有数十甚至数百的不同的资源索引库,分别提供不同类型的资源索引能力。
召回层:从数千万的资源中召回跟用户相关的数十万的资源,通常与资源索引库强绑定,比如一些显式召回会在召回层基于用户的 tag 去相应倒排索引库找到用户相关的文档资源。
排序层:在大型推荐系统中,为了平衡算力和效果会构建粗排 &精排两层排序体系,基于统一的预估打分机制从数万的资源中找到用户最感兴趣的数百条资源送给混排层。
混排层:混排层从数百资源中根据用户上下文信息推荐出最终用户看到的十几条资源,通常也会耦合产品的干预能力,比如资源的多样性控制等。
用户模型:用户模型服务提供用户级别的特征获取能力,比如用户感兴趣的标签集合。
文档特征:文档特征服务提供文档级的全特征服务,通常基于文档 id 访问获取得到。
随着业务的高速发展,百度 Feed 推荐系统目前已经承载了数十亿的在线请求流量,在庞大的流量规模背后是数百个微服务和数万台机器做支撑。如何保证系统的高可用是整个系统架构设计的关键能力之一,也是我们整个团队工作的一个核心方向。
二、整体思路
为了保证在线推荐系统常态 5 个 9 的高可用目标,基于我们实际工作的不断抽象沉淀,形成了我们整个架构的柔性处理能力。
△图 2:多层级柔性处理能力
如图 2 所示,从对单请求的长尾超时异常到 IDC 级大规模故障,基于我们的实际工作总结,都形成了相应的架构设计方案来应对,接下来进行具体的介绍。
三、具体方案
1. 实例级故障解决方案
在线推荐系统是由数百个微服务所组成,并且在私有云上进行了大规模的混部, 对于常态可用性指标影响最大的因素主要有两个方面:
单实例故障所引起的可用性抖动,可能的引发原因包括机器死机、磁盘打满,混部导致的 cpu 过载等。
长尾请求超时所导致的失败, 这通常是由于系统的一些随机因子所导致,引发的原因多种多样,且很难被抽象归一,比如资源的竞争、接收流量的瞬时不均、周期性的内存清理、旁路日志操作等运维活动。
通常对于这类问题的解决方案是增加重试机制和对于异常实例的屏蔽探活机制,我们的解决思路类似,但在实现方面对可能引发的问题做了进一步演进。
1.1 动态重试调度
重试机制最大的问题是重试时间的设定和雪崩问题:
重试时间设置的短,意味着常态下重试流量的比例会相对较大,造成资源的浪费,同时会更容易引发下游服务的雪崩,比如一旦下游出现大面积的延迟退化,就会导致流量的整体翻倍,进而整个下游服务都将被拖垮。
重试时间设置的长,意味着整个服务的超时都会长,否则重试的效果就起不到有效的作用,在复杂的微服务调度链路上很容易引起超时的倒挂。
当然重试时间的设定,我们可以基于某个状态下去权衡利弊,给出一个近似的解来避免上面的问题, 但业务的快速迭代需要我们不断的基于新的条件去更新这个值,这会带来极大的运维成本。
基于上述问题的考虑,我们设计并实现了动态重试调度机制, 其核心思想是严格控制重试流量的比例(比如只允许 3%的流量触发重试),同时基于分位耗时的实时动态采集机制,将重试的机会分配给需要重试的流量,这同时也意味着我们不再需要手动去维护超时时间的设定了。
如下是一个实现对比图:
△图 3:动态重试调度
其核心实现包括:
后端分位耗时统计机制:将时间序列划分为周期性的间隔(比如 20s 一个周期),基于前一个周期的流量去统计分位耗时值,并结合配置化的重试比例来动态的确定 backup_request_ms 的值,基于 rpc 的请求级重试时间设定机制,就达成了我们动态超时调度的目标了。
熔断控制机制: 从分位耗时统计机制来看,可以发现这里有个周期的滞后, 但不会对我们效果产生大的影响,这是因为前后两个周期服务的延时变化不会发生很大的波动,只要能够处理极端情况下滞后所导致的流量超发(可能引发的雪崩)问题就可以了, 而这正是熔断控制机制所要解决的问题, 熔断控制机制通过实时统计重试请求数量,并基于更小时间窗口来控制请求的比例,同时会通过平滑的机制去处理小窗口带来的统计不准问题。
整个方案在推荐系统应用后,在常态下可用性相对能够提升 90%以上。
1.2 单实例故障处理
应对单实例故障通常采取的方案是屏蔽和探活机制,但在高可用架构上面还面临如下挑战:
识别能力上:这里包含几个维度的考量,准确率、召回率和时效性三个方面, 一方面基于单机视角的探活在时效性方面虽然能够做到实时的效果,但由于局部视角的原因准确率往往不高,误识别的概率较大,而基于全局信息的采集往往会带来时效性的大幅降低,同时在性能方面也会带来一定折损。
策略处理上:一方面探活流量会导致小部分流量的折损,同时非全局视角的屏蔽会导致可用集群的服务容量风险,从而带来整体的恶化。
基于上述问题的考虑,我们在动态重试调度的基础上,做了进一步的演进, 其核心思想在于实时动态的尽可能降低损失,异步准实时的做全局异常处理控制。
实时动态止损:动态重试机制应对的场景是无实例差异的长尾请求,为了应对单实例故障这个特定化的场景,这里有两个抽象的处理方案来解决,其一基于可用性和耗时反馈的权重调节机制,利用可用性可以感知到异常实例的存在,从而快速降低权重访问,同时在重试实例的选择上也会加以区分;利用耗时反馈的平滑调节,会基于压力去自适应的调节正常实例的权重,保证正常实例的可用性不会因为容量问题打垮。
全局准实时的止损:基于我们集成在 brpc 框架内部的组件能够快速的收集单机视角的实例级信息,然后周期级别的去上报给统一的控制汇聚层。 通过高效的代码实现以及两层的汇聚机制基本上可以做到对 client 的性能无影响。更重要的是我们基于与 pass 的联动来进一步控制我们对异常实例的控制策略,从而在保证可用性容量的前提下做彻底的止损。
△图 4:全局准实时止损方案
基于上述方案的实现,我们的系统基本上能够自动的应对单实例故障的问题,可用性抖动相较于单纯的动态重试方案,抖动区间更小,基本上在 5s 内就能得到收敛,同时在抖动区间可用性的降低幅度也相较于之前下降了 50%。
2. 服务级故障解决方案
这里讲的服务级故障指的是在微服务体系下某个子服务出现大规模不可用,基本上无法对外提供任何服务能力。目前基于云原生的微服务化架构是整个业界的发展趋势,设计良好的微服务体系能够极大的提高整个系统开发的迭代效率,同时在稳定性方向也提供了更多可探索的空间。在微服务架构的基础上,我们通过柔性架构的设计思想并结合多层级的容灾设计来应对这种大规模的服务故障场景。这里先给出其核心要点:
差异化对待异常:比如对于核心服务,我们需要有一定的机制应对可能潜在的异常;对于非核心的服务,我们要做的可能仅仅是控制它的异常不要扩散到核心链路。
多层级容灾机制:通过多层级的容灾机制,当大规模故障发生时,能够在一定的时间内保障用户体验的基本无损。
当然服务级故障的处理需要依托于具体的业务场景来做具体的设计,这里给出部分例子来说明在 Feed 推荐系统是如何来进行处理的。
2.1 多召回调度框架
在推荐系统中,由于召回方式的差异性,往往会设计成多召回框架,每路召回负责某种具体的召回能力,比如从资源层面看,我们可以分为图文召回,视频召回, 从召回算法层面,我们可以分为 itemcf 召回,usercf 召回等。就单召回路而言,对整体服务的影响取决于它的功能划分:
一级召回:比如运营控制相关的召回, 直接影响产品的体验感知, 不允许失败;
二级召回:决定了信息流分发效果,这些召回路如果失败,会导致大盘分发下降显著, 单路可允许短期失败,但多路不能同时失败;
三级召回:补充召回路,比如探索兴趣召回,对长期效果起到一定增益作用,短期对产品效果影响不显著,允许短期失败。
对于三级召回路,服务调用失败虽然短期对产品影响不显著,但大规模的超时失败会导致整体性能的延迟退化,进而影响到整体服务的可用性。一种直接粗暴的解决方案就是调小这些召回路的超时时间,使得其即使超时也不会影响到整体,但这显然会降低这些服务的常态可用性,不是我们想要的效果, 为了抽象统一的解决这个问题, 我们设计了多召回调度框架, 具体设计如下:
△图 5:多召回调度框架
其核心设计包括:
召回等级划分:一级召回不允许主动丢弃;二级召回路划分多个群组,目前我们按照资源类型划分,图文召回群组 &视频召回群组 &...;每个召回群组由对分发贡献最大的几个召回路组成, 群组内部允许超过 40%(可配)的召回路主动丢弃;三级召回路则允许主动丢弃;
丢层机制:当满足要求的召回路都返回后,剩下没有返回的召回路会主动丢弃,不在等待返回结果了;
补偿机制:丢弃对产品效果始终还是有影响的,只是影响较小而已, 为了将这部影响降低到最小,我们设计了旁路的 cache 系统,当主动丢弃发生后,利用上次的结果 cache 作为本次的返回,从而一定程度的降低损失。
基于上述方案的实现,通过我们实际的演练结果,目前我们整个系统能自动应对二/三级召回队列服务级故障的处理。
2.2 排序层故障解决方案
排序服务构建在召回之上,提供对多路召回数据的一个统一打分机制,从而完成资源的整体排序, 目前业界通常采用粗排 &精排的两层排序体系来解决算力和效果相矛盾的问题。
粗排通过双塔模型的设计能在一次用户请求中支持数万的资源排序预估,但效果相对来说偏弱;
精排通过更复杂的网络模型能提供效果更优的排序能力,但支持的预估数量级只有千级别;
召回 → 粗排 → 精排 的漏斗过滤体系在算力一定的情况下很好的提升了推荐效果。
可以看到,排序服务对整个推荐效果至关重要,一旦排序服务出现大规模异常,基本上就是在数万的资源里面进行随机推荐了。为了防止这类问题的发生,在设计之初我们也是充分考虑了故障的应对场景,其核心思路是建设层级的降级机制。
△图 6:排序层降级方案设计
其核心设计包括:
构建一层稳定的中间代理层 Router:改层逻辑简单,迭代少,稳定性高,提供异常发生时的降级能力;
粗排异常处理方案:旁路建设一路基于资源的点展比和时长信息的排序能力,数据由离线后延统计信息挖掘得到,当粗排服务发生大规模异常时,利用该路信息提供应急排序能力;
精排异常处理方案:粗排的排序效果相比精排要差,但可以作为精排异常的时候降级数据供整个系统使用,当精排服务发生大规模异常时,直接利用粗排服务进行降级。
相比于随机推荐来说,整套排序层故障解决方案能够大幅降低故障时对推荐效果的影响。
2.3 弹性容灾机制
基于排序层故障解决方案的进一步演进,我们构建了针对全系统的弹性容灾机制,其目标是用一套统一的架构支持异常的处理,当异常发生的时候,降低对用户的体验感知和策略效果的影响。
△图 7:弹性容灾新架构
其核心设计包括:
稳定的推荐系统入口模块 Front:该模块是推荐系统的入口层,定位是不耦合任何业务逻辑,只支持弹性容灾相关的能力;
分层的异常数据建设:个性化容灾数据 + 全局容灾数据, 个性化容灾数据通常基于跨刷新数据的缓存来构建, 全局容灾数据通过旁路挖掘来构建;
统一的异常感知机制:跨服务的异常透传能力,当上层 Front 感知到核心服务异常时,就优先获取个性化容灾数据来进行容灾,若无个性化容灾数据,即访问全局容灾数据来进行容灾。
我们通过信息流的置顶数据的异常处理来做说明。
全量容灾数据的挖掘:旁路周期性的将所有可用的置顶数据写入全局的容灾 cache 中;
个性化容灾数据的构建:当用户前一刷的时候,置顶召回服务会取出符合用户条件的最佳置顶数据集合,将这部分数据写入个性化容灾数据 cache;
服务失败感知:当置顶召回访问失败,或是从 Front 到置顶召回服务之间任何链路失败时, Front 接收到的数据返回包里面都没有标志置顶数据成功的标记;
容灾控制:Front 会基于返回数据做判断,如没有成功标记,则开始进行容灾处理,优先读取个性化置顶容灾数据,没有读取到,则开始利用全局容灾数据进行容灾。
该机制构建了统一的弹性容灾能力,一方面利用个性化数据在容灾时尽可能保证策略的损失最小,另一方面利用全局的容灾数据进行兜底容灾尽可能的保证用户体验的无感知。
3. IDC 级故障解决方案
IDC 级的故障解决方案通常采用异地多主方案来解决,即故障发生时通过 idc 切流来及时止损, 百度推荐系统也采用类似的方案来解决,这里我们通过下发历史存储服务的异地多主方案设计来做个介绍。
下发历史存储服务提供用户级别最近的下发、展现、点击过的资源。基于它提供跨请求的策略支持,比如多样性控制, 更重要的是防止重复资源的下发。一旦服务挂掉将整个推荐服务将无法对外提供服务。
其整体设计如下:
△图 8:下发历史存储服务多主架构
其核心设计包括:
每个地域维护一份全量的存储;
对于读请求,只允许读本地 idc;
对于写请求,同步写入本地机房,同时进行跨 idc 同步写入,若写入失败,则写入消息队列,由消息队列进行异步跨 idc 进行写操作更新。
当出现 idc 级故障时,基于异地多主多活架构能够快速支持切流止损。
四、总结
百度 Feed 推荐架构支持着业务的高速迭代发展,在整个架构演进过程,可用性建设一直是系统架构能力的关键指标,我们通过柔性架构的建设,保证了对外常态 5 个 9 的高可用目标,并具备应对常规大规模故障的能力。
△图 9:近一个月的出口可用性指标
本期作者 | windmill & smallbird
原文链接:https://mp.weixin.qq.com/s/sm8eehjoUEqvF_PS4VcRKg
评论