字节KV数据库(ABase)架构论文入选数据库顶会SIGMOD2025



时间回到2016年,字节跳动自研的高可用 KV 数据库 ABase 上线,以其极致的性能和易用性优势,在字节内部大受欢迎。然而,伴随着字节跳动业务的飞速发展,ABase用户量也爆炸式的增长,初代ABase的单租户架构越来越难堪重负。在当时我们遇到了以下难题:


  • 资源浪费: 每个业务独占集群,资源不能共享,CPU/磁盘资源大量闲置;

  • 弹性不足: 业务紧急扩容时,时间需要数小时乃至数天,用户体验断崖下跌


ABase 面临巨大压力,团队夜以继日地优化,始终治标不治本。 在2019 年,我们做了一个大胆的决定: 用多租户架构重构ABase,以期彻底解决资源、弹性问题。经过多年不懈的坚持,我们大幅度改善了以上问题,在大集群多租户能力的支撑下资源利用率大幅提高、支持秒级扩缩容。下面将为大家介绍一下这篇ABase论文的详细内容。


多租户Serverless的挑战


Serverless NoSQL数据库是大规模云环境中应对多样化和弹性负载的关键技术之一。它像水电一样按需供给,无需管理服务器,却能支撑起电商大促的秒杀洪流、社交媒体的爆款,甚至AI场景的海量动态数据吞吐。而这一切的核心,离不开多租户架构——通过让不同租户共享同一资源池,最大化资源利用率。


如表1所示,在字节跳动,ABase支撑着抖音、电商、搜索、广告、推荐、飞书、豆包等业务,它们对吞吐、存储、缓存命中率和TTL等需求多种多样。ABase 总计管理着 130亿峰值QPS 1EB数据容量 。其中单个租户的峰值QPS可达4.5亿,存储容量超11PB。多租户NoSQL数据库支持如此极致的业务需求非常有挑战。我们总结了多租户架构的四大挑战,并在ABase设计中逐一解决:

业务场景

负载类型

缓存命中率

读命令比例

平均KV大小

常用TTL

抖音评论

均衡型

54%

100%

0.1KB

-

抖音消息

容量型

74%

100%

1KB

-

电商元数据

吞吐型

92%

100%

1KB

-

搜索正排

吞吐型

99%

100%

1KB

-

广告消息拼接

均衡型

18%

25%

10KB

3 hours

推荐去重

吞吐型

76%

50%

2KB

15 days

大模型KVCache

均衡型

0%

85%

5MB

1 days

【表1:ABase不同业务场景的特点】


挑战1:缓存机制对多租户系统性能隔离的影响


NoSQL数据库中,缓存机制极其重要,是降低延迟、提升性能的关键。ABase采用Proxy和DataNode两级缓存:命中时直接从内存返回数据(仅消耗CPU/内存),未命中则需磁盘I/O(延迟更高)。这种差异会干扰多租户隔离——比如一个租户的缓存失效可能突然抢占磁盘带宽,影响其他租户。同时传统单租户架构下也难以解决用户的缓存命中率下降,导致无法提供标称QPS能力的问题。


挑战2:负载变化对多租户系统的影响


在真实业务中,租户负载可能瞬间飙升或剧烈波动。这给多租户NoSQL数据库带来了挑战。首先,租户的流量会发生变化,随着租户流量的增加,预应用的资源配额(称为quota)可能会耗尽,从而触发限制;相反,租户流量的减少通常会导致这些quota浪费。即使租户流量不变,访问分布的变化也可能导致缓存命中率急剧变化,进而导致对多租户系统的压力变化。


挑战3:租户需求差异对多租户系统资源利用率的影响


租户需求千差万别:有的高吞吐(RU)但存储小,有的海量存储但请求少,有的读写比例极端倾斜。若数据布局规划不当,会导致 CPU 或磁盘空间资源的闲置。


【图1:ABase租户的RU、存储用量、读比率分布图】


挑战4:热键问题对多租户系统的影响


在社交媒体和搜索等领域,热点事件通常会导致少量数据被大量访问,造成热键问题。在多租户架构中,热键问题被认为是“最后一公里”问题,因为系统必须用有限数量的DataNode来适应繁重的流量,并且无法通过数据分区和迁移来解决这个问题。如果应对不当,可能会导致单节点负载过高而延迟升高甚至宕机。


ABase解决方案


系统概要


ABase支持Redis协议,以方便熟悉Redis的用户使用,并实现最终一致性。如图2所示,ABase系统包括一系列资源池,每个资源池管理一组租户。租户可以创建多个Key-Value Table,每个Table由多个items组成,每个item由唯一的Key来标识。属于同一个租户的数据会按照Hash key被统一分配到几个连续不相交的分片(Partition)中。每个Partition跨多个可用区域(Availability Zones)生成多个副本,增强可用性和健壮性。多租户共享一块资源池,可以利用工作负载多样性,让具备不同资源需求的租户共存,共享空闲资源,从而提高机器资源利用率和弹性。


【图2:ABase多租户架构图】


ABase由三部分组成:控制平面、数据平面和代理平面:

  • 控制平面:其中Meta Server负责管理集群元数据,监控集群健康状况等;Predictive Autoscaler负责收集租户流量和存储利用率,根据这些信息做出扩缩容决策;Intra-Pool Inter-Pool Rescheduler根据负载信息在资源池内和资源池外进行资源调度;

  • 数据平面包含多个资源池,每个资源池中包含多个DataNode。每个DataNode都分配一块物理磁盘和对应的CPU资源,并且管理不同租户的Partition。DataNode根据租户特定Partition quota(Tenant-specific Partition Quota)来处理Partition层的流量控制,并且配备了一个大小感知的LRU(Size-Aware LRU,SA-LRU)和细粒度权重公平队列(Weighted Fair Queueing,WFQ)模块,共同保证多租户环境中的服务质量(Quality of Service,QoS);

  • 代理平面由属于不同租户的Proxy Groups组成。每个租户独占一组Proxy Group。收到用户客户端发出的请求后,Proxy通过从MetaServer获取的集群元数据来路由请求到不同的DataNode中。Proxy会根据每个租户的Proxy qutoa来进行代理层流量控制,并且配备了基于active-update的LRU(AC-LRU)。


缓存感知的多租户性能隔离(挑战1 性能隔离问题)


为了应对挑战1,ABase需要找到一个缓存感知的归一化请求流量表示,通过一定的限制策略限制单租户的流量,并且考虑不同租户在同一台机器上的资源使用公平性。


我们首先设计了一个缓存感知的请求单元(Request Unit,RU),将缓存命中率纳入RU计算。RU广泛应用于Serverless数据库中,能够将底层硬件的资源消耗抽象出来,将用户的注意力专注到请求吞吐量需求。而在ABase中,RU不仅对计费至关重要,还是多租户性能隔离的关键组件。下面介绍一下ABase如何针对不同的请求类型定制RU计算,以确保RU反映的是实际资源消耗,同时考虑缓存机制对资源消耗的影响。


首先定义一些变量:

  • :用户请求的value size

  • :unit大小,通常为2KB

  • :ABase中为这个租户设置的数据副本数量

  • :最后k个读请求读取value size的移动平均值

  • :最后k个读请求缓存命中率的移动平均值


写操作: 总消耗为 ,其中 ;


读操作: 预估消耗为 。通过这个预估消耗进行流量控制,但最后计费会根据实际消耗来收费;


Scan操作: 每次SCAN如果不超过10个key,并且请求结果不超过2KB,会被统计为1RU;如果SCAN超过了10个key,则以10个key为1RU,不足10个Key的,按1RU计算;如果SCAN请求超过2KB,则以2KB为1RU,不足2KB的按1RU算;


复杂数据结构请求: 对于ZSET/SET/HASH/LIST等复杂数据结构,会先按照读写或SCAN类型的请求的RU计算逻辑先计算一下,然后再乘以对应命令的RU复杂度。


【图3:缓存感知性能隔离机制图】


有了RU的计算和预估规则后,又该怎么实现请求限制,使请求不会超过每个租户的请求配额呢?


如图3所示,我们实现了分层的请求限制策略,分为Proxy层和Partition层。


在Proxy层, Proxy的主要职责是防止RU总数超过租户配额。ABase Proxy采用了一种异步流量控制策略来减少Proxy和MetaServer之间的依赖。首先,每个Proxy都会定义一个特定的 Proxy_quota,该配额是简单地通过租户配额除以Proxy数量计算得到,同时允许它们自主处理该配额的两倍请求。除此之外,为了将租户在所有Proxy的总流量保持在租户配额内,MetaServer会持续监控每个Proxy的流量,如果超过总数超过配额,则会通知Proxy恢复其标准的proxy_quota。这种限速能够保护DataNodes上的租户免受DataNode上其他租户突发流量的影响。例如,当某些租户的流量显著升级时,该租户的Proxy可以拒绝请求,从而阻止请求到达DataNode,减少DataNode处理或拒绝这些请求时的大量资源消耗,从而保护其他租户的稳定性。除此之外,Proxy还会限制租户单个Partition的流量最高为DataNode的拒绝请求速率,防止Proxy放过的请求超过DataNode拒绝请求能力的上限(DataNode拒绝请求的能力比处理请求能力高,但是也有上限)。


在Partition层, DataNode会给每个租户定义一个Partition_quota,该配额通过租户配额除以分片数量得到。为了防止租户请求不均匀而导致的配额使用不完,DataNode会确保单个分区的流量不会超过其Partition_quota的三倍,而不是一倍。


现在,我们每个租户都能够合理地使用其配额不会超限,并且在Proxy层已经挡住了一部分租户的超额请求。但不可避免的DataNode会处理不同租户的请求,我们该怎么保证不同租户在DataNode中请求处理的公平性,同时又考虑到缓存机制对资源消耗的影响呢?


我们设计了一种细粒度、双层加权公平队列(Dual-Layer WFQ)机制。首先,我们按照类型和大小把请求分成了四类,分别是Large/Small Read/Write,并且为每一种类型的请求构建了WFQ。除此之外,请求的资源消耗还取决于它们是否缓存命中。因此我们将WFQ分为了两层,分别是CPU-WFQ和I/O-WFQ。请求首先进入上层CPU-WFQ进行处理。如果请求命中缓存,则可以直接返回;否则,则继续到I/O-WFQ。


WFQ以自定义的虚拟完成时间(virtual finish time,VFT)为比较对象构造最小堆,每次从WFQ中取出一个请求来处理。ABase设计的VFT计算方法如下所示:



表示该请求的配额占该DataNode所有请求的配额的比率。意味着如果它占比越大, 越小,优先级越大。除此之外,同一租户的VFT是累积计算的,从而防止单个租户的请求始终优先级较高。


我们在实践中规定了以下规则:

  1. CPU-WFQ中的Cost基于RU,而I/O-WFQ的成本由IOPS确定;

  2. 在CPU-WFQ中,我们首先保证单个租户的并发限制,避免单个租户占据所有并发,然后再对DataNode所有读写请求也执行并发限制,并且对于写操作,我们还对总RU设置上限,以确保底层存储引擎中Compaction和垃圾回收期间写延迟的稳定性;

  3. 单个租户的请求最多可以占用90%的CPU-WFQ资源,以防止单个租户的流量爆发期间其他租户的请求出现严重延迟;

  4. 在I/O-WFQ线程池中的所有基本线程都被一个租户的请求占用时,我们会临时增加额外的线程来处理其他租户的请求。该策略建立在DataNode上的两个租户不太可能同时出现高流量的基础上。


预测扩缩容(挑战2 负载变化问题)


为了便于用户感知其所能使用到的容量上限,ABase使用预置容量限制。我们希望能够识别出用户的非预期流量增长和预期内流量增长。对于非预期流量增长,我们依据预置容量限制做限速,便于用户准确控制成本上限。同时我们对于预期内的流量增长进行提前的资源扩容, 减少用户容量管理成本。


为了保持弹性,我们动态调节资源池规模,确保其闲置资源始终超过任一租户的配额上限。实际操作中,资源池总容量至少维持为单租户配额的十倍以上,且保证至少20%的资源处于闲置状态。这种设计既能为所有租户提供充足的弹性扩容空间,又能将闲置资源控制在合理比例。为保障租户秒级弹性扩容能力,我们不仅在资源池全局层面预留闲置资源,更在单机层级保持显著余量。每个层级的闲置资源都远超单租户配额,使得任意租户短期内均可快速实现配额翻倍,从容应对流量突增。


我们是通过什么算法来预测未来的配额使用情况呢?


我们开发了一种 ensemble-based forecasting solution。


在预处理阶段,我们使用多指标协作降低噪音。如果使用量和配额同时达到峰值,则会被视为噪音并过滤掉。因为在实践中,这两种情况同时发生几乎不可能。此外,我们使用启发式算法来消除零星峰值,这些峰值可能是由于偶然事件造成的。例如过去10天内只出现一次的事件。我们还利用变点检测来识别趋势变化,从而使预测算法更加关注最近的数据变化。


在预测阶段,我们首先使用功率谱密度(Power Spectral Density,PSD)分析来确定时间序列的周期性。随后,我们采用Prophet模型和历史平均算法得出加权预测集合。其中Prophet模型对具有明确趋势和周期的事件序列有效,而历史平均算法能提供稳定的预测,特别适合当趋势变化较小时。对于一致的非周期性爆发,如果预测明显低于历史输入数据,我们直接使用最近时期的历史数据进行预测,以避免不必要的规模缩减。


完成了未来配额使用情况的预测后,我们会自动执行资源扩容。


当预测的配额使用量超过租户配额的上阈值(0.85倍)或低于下阈值(0.65倍),就会触发扩容或缩容。扩容后,如果Partition配额超过上限 ,还会触发Partition分裂;缩容后,我们会保证Partition配额不会低于下限 ,以适应租户偶尔爆发的流量。


在启用预测扩缩容机制后,我们的周均Oncall数量减少了约65%,大幅减少了人力消耗和业务限制的情况。


多租户负载均衡(挑战3 利用率问题)


为了应对挑战3,ABase集成了一个资源调度模块,由两个部分组成,分别是池内调度和池间调度。


池内调度算法主要由两个阶段组成。


  • 第一阶段的目标是平衡每个租户的副本分布,让副本尽可能均匀地在DataNode上分布;

  • 第二阶段的目标是平衡资源池内所有DataNode的资源利用率,从两个两个资源维度(RU和Disk)进行平衡,并且不会违反第一阶段建立的副本平衡。


两个阶段都使用了类似的算法,为了简洁起见,接下来我们重点介绍第二阶段。

【图4:负载均衡目标】


如图所示,第二阶段的目标是让不同负载类型租户分布在资源池内的不同DataNode中,以达成更高的资源利用率。

【图5:DataNode分类图】


我们会根据Disk和Throughout两种维度的负载占用率将DataNode分为三种不同的状态$S_L、S_M、S_H$,通过将 中的副本迁移到 中来实现负载均衡。我们会遍历 中的副本,计算出迁移每个副本的收益,选择收益最高的副本进行迁移。如图6所示,我们认为DataNode越靠近坐标系中心(最佳负载使用率),收益越高。经过负载再均衡后,理想状态是所有DataNode的两个维度的负载均靠近最佳负载率。


【图6:DataNode负载坐标系】


图7展示我们使用负载均衡策略的结果。可以看到,所有DataNode的负载点均向最佳负载点靠近,有效地缓解了资源倾斜度,促进了更好的资源利用并降低了与高负载数据节点相关的风险。

暂时无法在飞书文档外展示此内容


【图7:负载均衡结果图】


在池间重调度算法方面,它主要关注于在资源池之间重新分配DataNode,该算法可以很容易地从池内算法扩展。例如,为了平衡两个资源池 (负载较高)和 (负载较低)之间的资源利用率,我们倾向于从 腾出一部分DataNode并将它们重新分配给 。首先,我们从 中选择一些低利用率的DataNode,并将副本从这些选定的DataNode迁移到同一池( )中的其他DataNode。然后,我们将这些腾出的DataNode重新分配给 。最后,我们调用池内算法来重新平衡两个资源池内的负载。


双层缓存机制(挑战4 热键问题)


为了应对挑战4,一个朴素的想法是增加缓存机制。在DataNode实现缓存机制能够缓解Hot key问题。我们为ABase DataNode专门设计了一种Size-Aware LRU(SA-LRU) strategy,将请求按照Value size分为了多种请求,当请求插入LRU中时会同时插入到最底层和其Size对应层的LRU链表中。当需要淘汰请求时,我们会优先考虑淘汰掉Size更大的请求。在这种机制下,SA-LRU不仅优化了资源利用率,还提高了总体缓存命中率。


然而,由于DataNode节点个数的限制,处理热key的极限能力有限。 所以我们通过Proxy支持缓存功能来对极热key承载能力做补充。但是传统的缓存方法(如LRU)可能导致Proxy缓存的数据和DataNode上不一致,除此之外,客户端的随机路由算法会造成命中率的较低。


【图8:Proxy缓存机制和路由策略图】


如图所示,我们和字节搜索架构团队同学(论文作者Qingshuo Li同学)进行合作,在Proxy端提出了两种优化策略: AU-LRU缓存机制 limited fan-out hash 路由策略。


Proxy缓存模块实现了AU-LRU缓存机制。AU-LRU总的来说是一种带TTL的LRU,但是会通过主动更新机制来解决由于缓存条目过期而导致的缓存穿透,会在hot key接近到期时自动刷新,从而保持缓存数据的及时性和连续性。


我们提出了limited fan-out hash策略来路由请求。首先租户的N个Proxy被分为n组,每条请求都会使用哈希函数哈希到这n个代理组中,然后从这个Proxy组中随机选择一个Proxy来发送请求。那么我们就可以通过调整n来适应当前的key分布。假设当前用户的请求是hot key较多,为了避免多数请求都只被发送到少数Proxy,那么我们可以把n值调小,这样hot key请求就能在一个较大的Proxy组中随机选择Proxy发送请求。另一方面,假设当前用户的key分布广泛,那么我们可以将n调大,这样每个Proxy收到的请求就只有整个Hash范围内的1/n,有助于提高缓存命中率。


成果总结


利用率收益: 与单租户ABase相比,多租户ABase实现了更高的资源利用率。单租户ABase中机器的平均CPU、内存和磁盘利用率分别仅为17%、52%和27%。而多租户ABase这些比率分别是44%、63%和46%。

单租户ABase

多租户ABase

CPU利用率

17%

44%

内存利用率

52%

63%

磁盘利用率

27%

46%


弹性收益: 如图9所示,在电商大促期间,无论租户的流量或缓存命中率如何变化,ABase中所有租户的延迟仍然保持稳定,完全满足SLA要求。这得益于ABase的多租户设计,资源池的资源容量远远超过个体租户需求的变化,允许租户共享空闲资源,从而具有充足的资源来处理租户负载的变化。


【图9:字节电商大促流量变化图】


经验反思


业务类型越丰富、资源池规模越大,资源利用率优化潜力越高。但是我们建议严格限定单个资源池容纳的 最大租户数量及资源池规模上限 。ABase从事故中学习到的深刻教训是:保持适度的资源池数量和租户规模至关重要。当预期外的故障发生时,这能避免故障影响范围过大。鉴于资源池总配额应显著超过单个租户需求,我们相应设定了各租户的配额上限。


在多租户性能隔离中,我们遵循,对于可能触达共享资源限制的请求, 必须在这之前 有对单租户资源使用做限制。对于同一节点上无法实现的限制,通过前置组件来完成。例如proxy 模块上对单分片流量做限制,以保证到达DataNode的单租户流量不超过 DataNode拒绝能力的上限,然后再由DataNode对单租户流量/并发做限制,然后再由DataNode对整体流量/并发做限制。这样可以有效避免单租户占用共享资源过多而影响其他租户。


未来工作


AI 评估租户物理资源


当前ABase虽然基于RU和空间进行了多维度的负载均衡,但是在实践过程中,仍然存在RU不能充分表示租户CPU/Mem/IO实际消耗的情况。多租户场景下,获取指定租户的准确硬件资源使用很困难。RedisFamily团队和ByteBrain团队持续合作。通过机器学习的方法训练模型来预测一个租户的CPU/Mem/IO维度的资源消耗,更加精细化的做到资源池内/资源池之间的各个资源维度的负载均衡。


模型推理KVCache场景优化


对于大模型推理加速的KVCache缓存场景,用户的吞吐高,Value大。为了降低网络带宽消耗,ABase 提供直连模式。数据从DataNode直接返回给用户。这对ABase的多租户流量管理提出了更高的挑战。包括在直连情况下的流量控制与热key管理,特大Value场景的多租户性能隔离影响,用户流量如何在多个模型间切换等。也欢迎有兴趣的同学和我们讨论或加入我们的团队。


存算分离架构演进


通过资源池化,多租户混部,ABase已经大幅度提升了资源使用率。但是资源使用率仍然受整体业务的负载类型影响,无法做到极致。下一步我们会朝着存算分离架构(PrisDB)继续演进。依赖字节成熟的存储底座Bytestore系统,极致的提升资源使用率。同时进一步提升我们的弹性扩展能力。


团队介绍


RedisFamily团队介绍


RedisFamily是字节跳动的KV/缓存团队,开发和维护字节的Redis、ABase、PrisDB 等产品,致力于打造全球化部署的超大规模分布式KV/缓存数据库。为字节跳动集团业务和火山公有云业务提供统一的KV/缓存数据库解决方案。服务的业务包括抖音、TikTok、头条、小说、飞书、火山引擎、豆包等。RedisFamily团队正在招聘相关方向的研发工程师和实习生,联系方式:gaoliang6@bytedance.com


ByteBrain团队介绍


ByteBrain是字节跳动 AI for Infra / AI for System服务平台,旨在利用AI技术(机器学习、大模型、运筹优化等),对基础架构和系统的全生命周期进行自动优化。ByteBrain 优化对象包括:数据库、存储、大数据系统、虚机、容器、网络,主要方向为AIOPS、AI4DB、运筹优化、LLM4Infra,功能模块包括容量预测、资源调度、慢SQL优化、Text2SQL、数据冷热识别、参数优化、异常检测、根因分析、LLM-AGENT、智能问答等等。ByteBrain团队正在招聘相关方向的研究员和实习生,联系方式:tieying.zhang@bytedance.com



原文链接:,转发请注明来源!