锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

直播系统解决高并发下缓存热key的业务实践

时间:2023-04-24 10:37:01 cp114差压变送器

直播是人类智慧的极好体现。通过技术数字化模拟数据,打破自然光和声传播的刚性约束,实现隔空重播,摆脱物理约束,自然放大信息的价值。

在手机 APP、在浏览器等终端上,我们可以通过窗口轻松看到隔空重放的信息,或者生动的人,或者热闹的戏剧或者一堆精美的物品,商业上的人,场, 货 它们都可以呈现在这个虚拟载体上。

我们通常称这个载体为直播间或房间。还有很多熟悉的词,比如进房间出房间开启房间等等。

直播场景自然以人为本,场地以人为本,货 因人而存在,可以是商品、声音、荷尔蒙甚至陪伴。正因为如此,商业上会想尽办法围绕人创造热点。房间越热越好,影响力越大越好。通过热门房间连接更多的人,多一双眼睛,就会有更多的价值。同时,马太效应自然会出现在人的内容基础上,大部分流量都是少数人产生的。

无论是自然属性还是人为干预,直播业务都必然呈现出明显的冷热分布。

业务爱热点,观众爱热点,几乎每个人都爱热点。除了做技术的弟弟妹妹。 因为容易出问题。

热点

目前的技术系统之所以能够为大量用户提供服务,是因为不同用户制造的访问压力被技术系统分散在不同的服务器上。机器的数量随人而来 (数据量) 随着访问压力和数据量的增加,只要访问压力和数据量能够近似均匀地分配到不同的机器上,同时为大型用户群服务就不是问题。

然而,热点自然会打破这一规律。在正常情况下,数据分散存储在服务器中,但当大量用户访问相同的数据时,访问压力将集中在某些服务器上。一旦超过服务器的承载能力,业务将面临瘫痪的风险。

在直播业务中,房间信息是热点重灾区,以下是业务依赖示意图:

房间信息提供标题封面、状态、标签等一系列数据,在直播的各种核心业务场景中得到广泛和深入的依赖。一方面,它强烈依赖于许多业务场景。一旦房间信息出现故障,所涉及的业务将直接不可用,典型的场景是进房间,房间故障后不能播放。另一方面,它将在业务流程中被依赖,并可能在依赖链中再次被依赖。

这直接导致房间信息服务面临业务流量几倍~几十倍的要求数量扩大,在此基础上保证足够的可用性。

比如刚刚过去的 S10[1]单个房间通常同时在线数百万。在此期间,一个活动和一个开幕式的背后是热门流量的洪峰。S10 房间可以暴露在各种流量和业务场景中。最后,房间信息 QPS 峰值达数十万,其中热点相关数据几乎占一半。

热点问题的本质是热点引起的访问压力没有均匀分散。一旦洪峰足够强大,服务就很容易瘫痪,这不是简单的堆叠机器所能解决的。

这就需要房间信息服务,根据自己的业务场景,建立一套探索和解决热点的机制。

理论支撑

在思考如何解决问题之前,我们先寻求一些理论参考,把知识串起来。

几乎所有具体的技术都是以理论为基础,根据自身的实际情况、综合成本和选择来实现的。方法和方法差异很大,但一切都是不可分割的。理论给了我们两个角色:

  • 提供设计理念,确定优化方向
  • 提供一套框架来分析和理解其他技术设计

CAP 定理是定位在分布式环境中,每个节点需要相互通信和共享数据的场景。本文讨论的要点有一定的相关性,或多或少可以借鉴。

CAP 三大概念:

  • C 一致性。对于指定的客户端,阅读操作确保返回最新的写作操作结果。 从客户端的角度要求数据完全一致。
  • A 可用性。非故障节点在合理时间内恢复合理响应。 及时返回合理数据 (不一定是最新的)。
  • P 分区容忍度。当网络分区出现时,系统可以继续提供服务。 在节点之间的数据复制和通信出现问题后,整个系统也可以提供外部服务 (系统不瘫痪)。

CAP 定理非常简单,即在分布式系统中 (节点相互通信,相互分享),只能同时保证以上两个。

由于分布式系统依赖网络进行相互通信,网络不能保证绝对可靠,分布式系统可能会不时出现 分区,即节点间相互通信失败。当部分节点通信失败时,系统实际上正常工作。

也就是说,分区容忍度自然存在,当分区发生时,系统自然可用。不管你愿不愿意,P 已经为你选择了。所以接下来,系统要么是 AP,要么是 CP。为了一致性 C,当分区 P 禁止写入,所以 CP 牺牲了 A;为了可用性 A,分区时,不同的节点数据会不一致,因此 AP 要牺牲一致性。

以上是 CAP 在节点之间存在相互通信复制数据的前提下,存在一些理论基本点,数据可以从多个节点吐出。它看起来不同于一般的商业场景。当然,这并不重要。核心是它是否能激发我们的理论并补充它。

后来诞生的 BASE,也就是说,软状态基本可用 (中间状态),最终一致性,就是这样的存在,做了衍生和补充。AP 虽然牺牲了 C,但只要最终一致性快速实现,恢复正常,系统实际上同时提供 C 和 A。

回到热点处理,当有热点高峰时,系统的核心重点是确保可用性,更加重视 AP,牺牲一些一致性。对于基本信息等非余额、库存等业务,一致性的要求自然更宽松。因此,理论上可以指出:

  • 对于非热点数据,压力可以通过扩展来解决。在正常情况下,系统可以同时确保 A 和 C。
  • 为了提高热点数据的可用性,牺牲一定的一致性,及时保证最终一致性,使系统完全正常。

如何处理热点

通过以上分析,我们可以知道牺牲部分一致性可以提高可用性。一致性问题的本质是数据有多个副本,副本之间的数据没有及时同步。 CDN 缓存和 Nginx 缓存实际上是可用性的一致性,直接复制数据。

一些解决暴力问题的方法是复制多个数据,并在阅读数据时随机获取其中一个,以分散压力。事实上,这种方法缺乏适应性。设置多少份副本是合理的?如何降低设置多份副本的复杂性?如何确保最终一致性?这些问题有点复杂。

更传统的方法是检测热点数据,只复制热点数据,然后进行有针对性的处理,以抵抗相当大程度的流量,并控制成本。它分为两部分:

  • 热点自动检测
  • 将热点复制到内存中,及时实现最终一致性

以房间信息逻辑与数据分离结构为例:

在逻辑层和数据层两个地方可以探测和处理热点:

  • 逻辑层 ID、主播 ID 同时处理数据进行维度。
  • 数据层 ID、主播 ID、缓存 key 可同时处理数据。

以 ID 为了探测对象,需要将点埋在业务代码中进行缓存,以侵入业务。 以缓存 key 为对象则可以在缓存基础库中集成,可以做到对业务无感知。

如上图所示,我们选择了 ID 作为检测对象,并在逻辑层处理缓存。基本考虑点是在当前团队成员和基础设施条件下,保持逻辑和数据分离,足够简单和轻。

热点自动检测

热点处理行业常用的算法有几种:

一、LRU。 这是一个简单的栈结构,简单轻量。但本身无热点识别能力,在 “SKU” 在更大的场景中很难工作。同时,基于我们 ID 探测策略,LRU 使用需要下游数据层每次返回房间的所有信息,成本很高。

二、LIRS[2]。例如,数据库中广泛使用内存缓冲淘汰场景 BufferPool,在缓冲空间有限的情况下,踢出冷数据为热数据提供空间。可以认为 LIRS 是 LRU 高级版本可以提供更好的适应性热点识别和数据淘汰。它本身就有一定的复杂性,同时也有一定的复杂性 LRU 一样,基于 ID 下游需要返回完整的数据进行检测。(有时候接口只想读房间标题)

三、 LFU。简单版的 LFU 它是对被访问对象的访问频率的计数。它本身可以根据访问次数来识别热点,但这种识别不能根据时间间隔进行 “老热点” 没有消除的能力 “SKU” 很大的时候,很容易出现其他问题。

基于实际的业务场景,我们选择了简单版本 LFU,根据其缺陷,根据时间间隔提供滑动窗口识别热点的能力。窗口的维护时间,即热点的探测周期,与热点结算和报告相对应,并通过优先队列获得结算 Top-K。

总结以下三点:

一、 LFU。简单统计访问 ID 实现的核心是访问频率 HashMap 并辅以统计。针对 LFU 大规模处理难 SKU在设计实现时,会设置统计对象数的上限,超过一定比例时会强制采样。(防止有 bug 把 hashMap 写爆了)(还未实现数据淘汰,目前只是一个简单的频率统计)

二、 滑动窗。滑动窗的作用是辅以 LFU 实现基于时间间隔的热点统计,并在窗口滑动过程中包装结算、回调、数据报告等操作。基于房间场景的时间间隔 “SKU” 的统计 分析判断 (脑拍)单个窗口的保持时间是 3s,也就是说热点探测的反应时间最快是 3s。

三、 优先队列。目的是以最快的速度在数万~数十万之间 SKU 中,计算出 Top-K。常规全排序时间的复杂性是 O(N*LogN)。优先队列的时间消耗:

  • 1.构造大小为 K 的小顶堆, O(K) ;
  • 2.遍历所有 SKU,尽量写小顶堆,O(N)
  • 3.如果头部小于小顶堆,则丢弃,否则更换头部并重新堆放。O(log(K))

整体时间复杂: O(K) O(N*log(K)),当 K 最小时的时间复杂度近似 O(N)。

缓存和及时的最终一致性

  • 探测到热点后,基于热点 ID 为房间信息构建内存中的数据副本是全量字段 (数十个)
  • 异步基于热点 ID,不断请求下游,刷新缓存数据,实现及时最终一致性。(周期、有效期均可实时由配置跳针)
  • 业务接口根据 ID 优先从缓存副本中读取,miss 后才真正访问下游数据层。
  • 热点的处理规则、刷新周期、有效期等通过配置实时干预

整体结构示意图:

  • 通过 SDK 集成热点探测能力,并通过业务回调,定时回传 Top-K 热点
  • 业务根据 Top-K 热点对复制热点数据,并保证及时最终一致性
  • 探测、热点处理均基于本地实例 (可以很便捷演进为分布式模式)
  • 无任何外部依赖,轻量、简单、灵活性高、开发周期短、易于问题排查分析

实际效果

hits 为命中了缓存,miss 为未命中:

对集群整体压测时,下游数据层在 3s 左右后 (单实例探测周期为 3s),访问压力变成常数:

从业务表现和压测检验来看,热点探测让房间信息服务在热点洪峰下能对热点自适应探测,使得整体保持稳定。

场景分析

适用场景:

  • 流量洪峰相对,例如压测场景,表现良好;间隔性脉冲访问效果差。
  • 各个本地实例流量需近似均匀
  • 中小型节点规模。

以上均暂时符合我们的场景。

不足

1)一致性的报复

当热点处理彻底消灭了热点问题时,另一个被忽略的场景则出现问题,可简单总结为时序性依赖:

  1. 房间信息变更 -> 消息队列投递消息(例如房间关播)
  2. 某业务消费消息
  3. 业务消费消息后立即调用房间信息 (根据开播状态中踢掉房间)
  4. 房间信息还未及时保证一致性 (房间状态还是开播中)

1~3 时间消耗大约在 10ms 以内,这是小于房间信息及时最终一致性周期的,也就是说在这种消息变更依赖场景下,房间信息的一致性未能满足需求。成也萧何,败也萧何。

为了解决以上问题,我们提出了几个解决途径:

  • 消息投递时做一定延迟
  • 时序性状态依赖的特殊场景 (量很小无热点问题),避开缓存
  • 说服调用方不要时序型依赖 (理解业务)

2)复制成本稍高

以上基于 ID 探测的方案优点很明显,简单轻量无任何外部依赖。但其代价是业务有感知,并有一定份复制成本,无法无缝复制到其他业务场景中。

更理想的状态

技术的一般追求是,通过技术的方式降本提效,改善生产力,实现人肉不能实现的能力,并在此基础上提供近乎零成本的复制。

而具体的技术实现,则是在时间、成本、人力、场景、需求等综合因素作用下的产物,处于随时满足新的需求,并不断向往着理想的状态。

我们在心里默念理想的状态:

  • 基于热点探测、及时最终一致性更加极致的实现
  • 业务零感知、零耦合
  • 近乎零的复制成本: 无缝移植到其他场景
  • 良好的适应性: 灵活且符合大量场景的规则配置

这已经有开源方案作出了示例。[3]

应对攻击

暴露在公网的房间,极易被攻击或被爬虫遍历 (房间 ID/主播 ID)。这背后是热点攻击,热点处理能很好的应对,而暴力遍历攻击则相对令人头疼。

在被刷过数次后,房间信息也做了相应的策略应对。

非法 房间 ID 防御

由于房间 id 需要便于记忆,其生成规则是接近自增的,整体上房间 ID 的分布是接近连续的。此时业务上只要简单判断,请求中的 房间 ID 是否大于当前系统中最大的房间 ID,即可大致判断出该房间 ID 是否合法,将其以极低成本拒绝。

在房间服务中,定时获取当前最大房间 Maxid,并加入一定 buffer,当 requestId > Maxid + buffer 时,则直接丢弃。

非法主播 ID 防御

主播数量比较庞大,且增长区间跳跃、范围极广。对于房间信息来讲是一个无限集,无法像房间 ID 那样通过简单的策略做出判断。在面对大范围暴力遍历时面临挑战。

首先想到的策略是通过 bitmap 做映射,合法的 uid = 主播数  = 房间数,这是个有限集,将全量数据映射到 bitmap 中,如果某个 uid 在 bitmap 中无法找到映射,则可以肯定其不是主播,可以直接丢弃。 否则大概率是主播,因为 bitmap 长度问题,可能有些精度上的丢失,但不成问题。

布隆过滤器同理。

为何放弃 bitmap

在此想法的验证过程中,通过业界成熟且广泛使用的 RoaringBitmap[4] 进行落地,虽然其提供优异的压缩能力,在将所有主播写入时,对象的大小达到了数十 M,这对于要通过缓存系统加载到内存的对象来讲,太大了。如果进行分片,则 uid 的增量更新 (新增主播) 成本会变高,同时由于主播 ID 可能超过 32 位,这也超出了 RoaringBitmap 的范围 (32 位版本)。(如果 uid 增量更新没处理好,新增主播会找不到自己房间,业务会受损,因为 uid 被当作非法丢弃了)

且在主播数量进一步扩张的未来,无法提供好的适应能力。这大块数据面临维护成本:

  • 及时更新内存中的 bitmap,否则新增主播无法拿到自己房间
  • 整块数据需要在实例启动时加载到内存,成本高
  • 整块数据需要在缓存中及时维护一份全量数据,可靠性不高。一旦数据丢失全量构建一次成本很高

如果基于分布式缓存维护 bitmap,可解决一部分问题,但其性价比则极剧下降。基于以上分析,bitmap 方案带来的工程上的隐患稍显棘手。(布隆过滤器同理,且多份 bitmap 成本更高。)

惹不起就躲

基于业务场景,我们选择了更加低成本的方案:

  • 隐藏部分通过主播 ID 获取房间信息的入口 (通过合法服务端调用)
  • 限制部分通过 uid 获取房间信息的能力

这其实更加简单地解决了问题。

总结

从面临问题时的焦躁,到一步步抬起锄头东闯西撞地作出尝试,再到问题的基本解决。从名义上可美化为演进或迭代,但站在终点边缘回看,这都不是最佳的解决方式。往往在日常中,很多场景更喜欢有相关经验的人,因为他们见过做过,能更容易有基于记忆的一顺而下的解决方案。但又有谁能拥有足够多的经验呢?在变化中的行业能瞬间让经验作为过去时,这并不能意味着有解决问题的增量。

更加合理的方式是,关注到问题的本源,通过理论知识的推导,得出大致解决思路及尝试方向。在此基础上通过对技术要素的组合,构建解决问题的方案。这才是更快速解决问题的途径,并具备解决经验之外问题的能力。这背后需要做两个事情:

  • 关注问题的本源,寻求理论知识疏导、拆解、归类等
  • 积累足够丰富的技术要素。算法、数据结构、以及更朴素的计算机思想 (如时空互换、批处理等)

这是姿势的大改造。

代码实现

精简版可见: GitHub - EarlyZhao/hotDetect: 自动探测热点

延伸阅读

  • 直播 (上) -- 底层逻辑浅析
  • 直播 (中) -- 核心流程梳理
  • 直播 (下) -- 业务结构简介
  • 直播:弹幕系统的秘密

附录

  • [1] http://web.cse.ohio-state.edu/hpcs/WWW/HTML/publications/papers/TR-02-6.pdf
  • [2] hotkey: 京东App后台中间件,毫秒级探测热点数据,毫秒级推送至服务器集群内存,大幅降低热key对数据层查询压力
  • [3] http://github.com/RoaringBitmap/roaring
锐单商城拥有海量元器件数据手册IC替代型号,打造电子元器件IC百科大全!

相关文章