B站动态outbox本地缓存优化
本期作者
窦晨超
哔哩哔哩资深开发工程师
问题的发现
动态综合页比较容易因为高热事件,引起大量用户持续消费feed流,导致线上拉取动态时间线feed流接口快速飙升至平时峰值2~3倍以上而大量超时,较多用户无法正常消费其feed流。从监控上发现outbox(用户发件箱)服务依赖的redis集群大量实例CPU使用率皆超过了95%甚至达到100%(如图1)。因此,瓶颈在于outbox redis集群压力太大,无法扛住过大的高热流量。而痛点在于redis集群无法高效快速扩容,因此,我们遇到此类情况通常只能被迫降级限流,以防情况进一步恶化。
图1 outbox-redis集群实例cpu使用率过载
瓶颈根因的分析
图2 动态时间线feed流推拉结合方案
解决方案的PK
方案的设计与上线后的效果
缓存哪些UP
既然决定采用本地缓存优化的方案,那么我们首先需要知道哪些UP是热的呢?从关系链的特点,我们推断大粉UP被访问的概率应该更高。我们通过统计历史动态时间线的UP流量分布也论证了我们的猜测。所以,我们定义了一个粉丝数阈值,将粉丝数达到该阈值及以上的UP作为热key,缓存他们一部分最新动态列表于本地(阈值的设定基于内存可以承受的空间),理论上可以获得较高的缓存命中率,并有效缓解“拉”outbox对redis集群的压力。
如何构建本地缓存
因为设定的粉丝数阈值比较高,所以热UP的数量变更不会特别频繁。基于此特点,我们给出的本地缓存整体方案是(如图3):从数据平台每天离线T+1地统计出所有粉丝数达标以及因掉粉粉丝数从达标变为不达标的UP名单,并通过kafka推送写入redis。当outbox服务实例启动时,会从redis拉取到全量名单,并从outbox redis分别拉取这些被缓存UP的最新动态列表,构建于本地缓存中。而启动后的outbox实例每当感知到来自数据平台的被缓存UP(包括需要删除的)名单推新时,也会拉取推新后的名单,但只构建当前未被缓存的新UP,并删除粉丝数低于阈值的UP缓存。当用户获取feed流“拉”其关注的UP的outbox时,优先从本地缓存获取UP的最新动态列表,未命中的UP才回源拉,以此缓解outbox redis的读扩散压力。
图3 outbox本地缓存整体方案
如何防止回源雪崩
被缓存UP的最新动态列表不是一直不变的,当UP发布或者删除动态后,需要及时回源outbox redis获取该up变更后的最新动态列表并重构其本地缓存。所以,该方案还需要考虑回源对outbox redis的压力问题。在万级别的热key个数加上百级别的实例规模场景下,如果我们采用简单、常规的对每个被缓存UP设置一个较短过期时间,过期后回源重构则容易造成大量key同时过期回源导致outbox redis集群瞬时压力过大,产生雪崩现象。因此,我们给出的回源重构方案是”变更广播+异步重构“(如图4)。在这个方案中,outbox实例本地缓存的UP最新动态列表是常驻不过期的。当某个被缓存的UP发布或删除动态时,会广播该UP的“动态列表变更”事件给所有outbox实例,outbox实例接收通知后异步回源并重构该UP的本地缓存。因为被缓存UP变更频次极少,所有这种回源重构方式对outbox redis的压力也很小。
图4 变更通知+异步回源重构
如何保证本地缓存与redis的一致性
图5 一致性检测与自动修复
上线后的效果
outbox本地缓存优化上线后,命中率达到了55%以上(如图6)。环比优化上线前后的周末高峰outbox redis压力情况:outbox redis的压力峰值降低了上线前的近44%(如图7~8),而outbox redis集群单实例CPU使用率峰值也降低了上线前的37.2%(如图9~10)。
图6 outbox本地缓存命中率
图7 本地缓存优化上线前周末outbox redis的压力峰值
图8 本地缓存优化上线后周末outbox redis的压力峰值
图9 本地缓存优化上线前redis的高峰期间cpu使用率
图10 本地缓存优化上线后redis的高峰期间cpu使用率
后续规划
以上是今天的分享内容,如果你有什么想法或疑问,欢迎大家在留言区与我们互动,如果喜欢本期内容的话,欢迎点个“在看”吧!
往期精彩指路