ScyllaDB 性能隔离机制:调度组、控制器与跨节点 RPC 隔离的实现解析
2026/9/14 6:07:06 网站建设 项目流程

ScyllaDB 性能隔离机制:调度组、控制器与跨节点 RPC 隔离的实现解析

【免费下载链接】scylladbNoSQL data store using the Seastar framework, compatible with Apache Cassandra and Amazon DynamoDB项目地址: https://gitcode.com/GitHub_Trending/sc/scylladb

ScyllaDB 的每个 shard 在同一时刻并行承担多种职责:作为协调节点处理 CQL 请求并分发给副本,作为副本执行读写,将 memtable 刷入 SSTable,执行 compaction,以及运行 repair、gossip 等后台任务。本文基于官方设计文档 docs/dev/isolation.md,讲解 ScyllaDB 如何利用 Seastar 的调度组(scheduling groups)实现 CPU 与 I/O 层面的性能隔离、如何通过控制器(controllers)动态调节各组件的资源份额,以及如何在跨节点 RPC 调用中保持隔离语义,并结合仓库源码给出可验证的实现细节。

为什么 shard 内部需要性能隔离

ScyllaDB 是共享架构(shared-everything)设计:一个 shard 内,协调路径、副本读写路径、内存表刷盘、SSTable 合并、repair 与 gossip 等所有工作都运行在同一组 reactor 线程上。如果不做资源划分,就会出现典型的相互干扰问题:

  • 后台 compaction 跑得越快,用户查询的吞吐和延迟波动越大(compaction 进行中查询性能下降,结束后又回升,形成锯齿状曲线);
  • 但盲目放慢 compaction 又会积累 compaction backlog,反过来拖累后续读性能。

因此官方文档为性能隔离机制确立了两个目标:

  1. 隔离(isolate):每个组件的吞吐与延迟不应依赖其他组件的实现细节(如其并行度、I/O 量);
  2. 可控(control):Scylla 能够主动决定每个组件分得多大的资源份额,而不是写死一个静态比例。

以 compaction 为例,理想状态是后台合并"刚好够用":既要保证新任务出现之前完成旧任务,又不能比必要速度更快,否则用户查询性能会被周期性拉低。

实现上,ScyllaDB 复用了 Seastar 现有的隔离特性——调度组(用于 CPU 时间隔离)与 I/O 调度(用于 I/O 带宽隔离)。本文聚焦于 ScyllaDB 如何组织这些 Seastar 特性,而非 Seastar 本身的调度算法细节。

调度组(CPU 调度器):全局资源份额的划分

ScyllaDB 定义了一批 Seastar 调度组,作用域是全局的(不是 per-table、per-keyspace)。这些组在 main.cc 的启动流程中创建,并保存到 database 对象的database_config结构中(定义见 replica/database.hh)。

官方文档中列出的调度组及初始份额(shares)如下:

名称(database_config::*_scheduling_group初始份额
default(即 system)1000
memtable1000
memtable_to_cache1000
compaction1000
memory_compaction1000
statement1000
gossip1000
streaming(即 maintenance)200

从源码看:实际创建的调度组比文档表格更多

当前版本的 main.cc 中实际创建的调度组(名称、缩写、初始份额)包括:

  • compaction(缩写comp,1000 份额)——常规 compaction;
  • maintenance_compactionmanc,200,隶属 maintenance supergroup)——低优先级的维护性合并;
  • mem_compaction(1000)——内存 compaction;
  • logstor_compaction(1000)——log 型存储(logstor)段的合并;
  • streamingstrm,200,隶属 maintenance supergroup)——数据流传输,同时用于 repair 等维护场景;
  • maintenancemant,200)——maintenance 超级组(supergroup,总量 200 份额)下的通用后台任务组;
  • statementstmt,1000,隶属 user supergroup)——CQL 语句执行;
  • memtablemt,1000)——内存表刷盘;
  • memtable_to_cachemt2c,200)——写入行缓存的路径;
  • gossipgms,1000);
  • commitlogclog,1000)与schema_commitlogsclg,1000)——提交日志落盘;
  • backupbckp,200,隶属 maintenance supergroup)——SSTable 备份/克隆;
  • 此外还有一个低份额的background_reclaimbgre,50,见 main.cc)专门用于空闲时的 LSA 内存回收/碎片整理。

值得注意的是源码中已经引入了调度组超级组(scheduling supergroup)maintenance_supergroup = create_scheduling_supergroup(200)(main.cc),maintenance、streaming、backup、maintenance_compaction 等低优先级组都嵌套在其中——即这些组之间的份额竞争先发生在 200 份额的总量内部,再以 200 份额的总量与前台组竞争。这正是官方文档"Additional notes"一节中提到的方向:嵌套组让两个 statement 类的组彼此竞争而不与 compaction 竞争。

各调度组的用途与份额的动态调节

文档对每个组的用途留有说明性注释,结合源码可以确认其归属:

  • default(system):未显式指定调度组的通用工作(如 gossip 心跳之外的系统路径)使用的兜底组;
  • memtable:memtable 刷盘(flush)到 SSTable 的工作;
  • statement:CQL 语句在 replica 侧的执行上下文;
  • streaming / maintenance:SSTable 流式传输与 repair 等维护操作共享的低优先级路径;
  • compaction / memory_compaction / logstor_compaction:三类不同对象(常规 SSTable、内存 compaction、logstor 段)的合并工作。

"初始份额"只是启动时的值。这些组的 shares 之后会被**控制器(controllers)**动态修改:当某个组件落 behind(如 compaction backlog 增长)时调高其份额,当它完成得比必要更快(引起查询性能波动)时调低其份额。份额是 Seastar 调度器的配额,份额更高的组分得更多 CPU 时间片与 I/O 带宽。

另外,I/O 隔离与 CPU 隔离是成对配置的:例如 main.cc 中为 compaction 组挂了io_throughput_updater("compaction", ..., cfg->compaction_throughput_mb_per_sec),main.cc 为 streaming 组挂了stream_io_throughput_mb_per_sec限速——I/O 调度器的带宽上限与 CPU 调度组的份额共同构成一条组件的完整资源边界。

控制器(Controllers):动态份额调节

官方文档的 Controllers 一节指向 compaction_controller.md,其核心思想是:

  • 在控制器出现之前,各组份额在启动时静态确定——能公平,但不能最优;
  • 每个 compaction 策略(SizeTiered、TimeWindow 等)各自定义"backlog"(待完成合并工作量的估计值),每张表维护自己的 backlog,汇总成系统级 backlog;
  • 控制器根据 backlog 的大小周期性调整 compaction 组的份额:backlog 增长 → 提份额追进度;backlog 被快速清零 → 降份额,把"快速峰值 + 空档"拉平为"稳定高原",减少对前台查询的干扰。

实现上,backlog 的跟踪与份额更新分别落在 compaction/compaction_backlog_manager.hh、compaction/compaction_manager.cc 与顶层的 backlog_controller.hh;database在 replica/database.cc 中创建 flush 控制器(make_flush_controller),说明同一套"backlog 驱动份额"的思路也被用于 memtable 刷盘路径。

用户级隔离、多租户与 RPC 路径

Per-user 性能隔离与多租户

官方文档明确了两个边界:

  • Per user 性能隔离:文档中该节标记为 TODO,尚未形成完整设计;
  • 多租户(Multi-tenancy):当前版本不支持同一服务器内不同租户之间相互隔离的性能保证;若未来支持,将在此文档补充。

也就是说,当前的隔离是"组件级"(statement 对 compaction、对 streaming 等)而非"租户级"的;不同租户的请求共享 statement 组的份额。

跨 RPC 调用保持隔离:verb 到调度组的映射

ScyllaDB 节点间通信大量依赖基于 "verb" 的 RPC:每个 RPC 调用对应一个 verb,verb 关联一个 C++ 处理函数,并绑定一个固定的调度组。远端节点收到该 verb 后,在与之关联的调度组上下文中执行。这样,隔离语义不会在节点边界处丢失:例如 replica 发出的MUTATIONverb,在接收方同样跑在 statement 组的上下文里,而STREAM_BLOBREPAIR_*系列 verb 则跑在 maintenance/streaming 组的上下文里。

verb 到连接池(进而到执行上下文)的映射由 message/messaging_service.cc 中的do_get_rpc_client_idx()静态确定,从源码可以看到当前划分:

  • idx 0(拓扑无关连接组):gossip 系列(GOSSIP_DIGEST_SYN/ACK/ACK2GOSSIP_SHUTDOWN等)、GET_SCHEMA_VERSION、Raft 系列(RAFT_APPEND_ENTRIESRAFT_VOTE_REQUEST等)、JOIN_NODE_*DIRECT_FD_PING等"稀有且廉价"的 verb。注释特别强调:此组禁止放入数据路径上的"热" verb,且 gossip verb 永远留在组内(需要低延迟tcp_nodelay);
  • idx 1:streaming、repair 系列 verb(STREAM_BLOBREPAIR_*HINT_MUTATIONNODE_OPS_CMD、tablet 流式与克隆等);
  • idx 2:数据路径 verb——MUTATIONREAD_DATAREAD_DIGESTTRUNCATE、Paxos 轻量事务系列(PAXOS_PREPARE/ACCEPT/LEARN/PRUNE)、FORWARD_CQL_EXECUTE等;
  • idx 3MUTATION_DONE/MUTATION_FAILED回执;
  • idx 4MAPREDUCE_REQUEST(repair 的 map-reduce 阶段)。

编译期断言SCYLLA_ASSERT(tab[i] < PER_TENANT_CONNECTION_COUNT + PER_SHARD_CONNECTION_COUNT)(message/messaging_service.cc)保证新增 verb 必须显式归组,防止新 verb 落入错误的连接/调度上下文。

statement 组的 "tenants" 机制

有一类 RPC verb——statement 组——可以从多个调度组的上下文中发出。为支持这一点,statement RPC verb 引入了 "tenants" 概念(租户清单在 main.cc 配置,从源码可见$system对应 default 组、$maintenance对应 streaming 组等):

  • 每个 tenant 有自己的调度组与唯一标识,并拥有独立的 RPC 连接
  • 建立连接时,tenant 标识会告知远端节点"这条连接上的 statement verb 应该在哪个调度组中执行";
  • 发送侧在自己的调度组上下文内选择对应 tenant 的连接发出请求,接收侧按连接绑定的 tenant 还原执行上下文。

这套机制让"隔离"在 coordinator → replica 的 RPC 边界上得以保持:由不同调度组发起的语句转发,即使走同一批 verb,也会在接收节点落入各自的调度组与连接,避免低优先级的转发请求挤占高优先级的队列(同时GET_SCHEMA_VERSION单独归入 idx 0,就是为了避免与读写 verb 同连接产生死锁,见 message/messaging_service.cc 的注释)。

设计权衡与已知限制

  • 组数量与最坏延迟:官方文档指出,调度组越多,最坏情况延迟越高(轮转调度下每个组的唤醒间隔被拉长)。缓解方向是嵌套组结构(supergroup),让低优先级组先在组内竞争——当前源码中的maintenance_supergroupusersupergroup 正是这一思路的落地(main.cc)。
  • 隔离粒度是组件级而非租户级:per-user 隔离与多租户性能保证尚未实现,当前所有前台请求共享 statement 组份额。
  • 份额是全局初始值:表中数字是启动时的默认值,运行期会被控制器按 backlog 等指标调整,因此观察到的实际份额分配会与表格不同。

小结

ScyllaDB 的性能隔离体系可以概括为三层:

  1. 静态划分:在 main.cc 创建一组全局调度组(及 I/O 带宽上限),覆盖 statement、memtable、compaction、streaming/maintenance、gossip、commitlog、backup 等所有工作类别,并通过 replica/database.hh 的database_config注入各服务;
  2. 动态调节:控制器(如 compaction controller)依据各组件 backlog 周期性修改份额,在"追得上进度"与"不打扰前台查询"之间寻找平衡点;
  3. 跨节点保持:RPC verb 静态绑定连接池与执行调度组,statement verb 通过 tenant 机制在多调度组场景下仍能保持上下文一致。

这套机制使 ScyllaDB 能够在共享 shard 的架构下,为协调、副本执行与各类后台任务之间提供可预测、可调度的资源边界。

【免费下载链接】scylladbNoSQL data store using the Seastar framework, compatible with Apache Cassandra and Amazon DynamoDB项目地址: https://gitcode.com/GitHub_Trending/sc/scylladb

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询