
GreptimeDB common-runtime 深度解析基于令牌桶的 CPU 资源限制与多优先级运行时实践【免费下载链接】greptimedbThe open-source observability database. One columnar engine for metrics, logs, and traces, on object storage.项目地址: https://gitcode.com/GitHub_Trending/gr/greptimedb本篇技术指南围绕 GreptimeDB 仓库中的 common-runtime 子 crate 说明文档 展开系统讲解 GreptimeDB 如何在 Tokio 异步运行时之上构建可限流的ThrottleableRuntime通过限制 poll 次数的思路实现 CPU 资源约束并给出 5 种优先级 × 4 类负载的对比实验方法。读完本文你将掌握该运行时的设计原理、构建 API、限流参数含义以及如何在本地复现其多优先级性能实验。一、为什么数据库内核需要可限流的异步运行时GreptimeDB 是一个把指标metrics、日志logs与追踪traces统一在一套列式存储引擎上的可观测性数据库一个进程内同时承载大量并发查询query与数据写入ingest任务。这两类任务对 CPU 的诉求差异极大写路径任务通常短小、高频希望低延迟、及时落盘读路径查询任务可能长时间占用 CPU例如大范围聚合扫描、复杂 SQL 执行。如果所有任务无差别地共享 Tokio 工作线程池一个重量级查询就可能在 CPU 时间片上挤压写入任务造成写入延迟抖动。因此 GreptimeDB 在 src/common/runtime 子 crate 中实现了一套带优先级的运行时体系不仅把任务按优先级分层还通过**速率限制rate limiting**直接约束每个优先级每秒可以获得多少次 future poll 机会从而把 CPU 用量限制在预期范围内。该能力最初由 GreptimeTeam/greptimedb#3685issue提出、#4782PR落地即文档中标注的 Preliminary support cpu limitation。二、快速体验多优先级 × 多负载性能测试文档给出了一条最简单的验证命令在 common-runtime 子 crate 的工作区内以 release 模式运行自带的性能测试二进制# workspace is at this subcrate cargo run --release -- --loop-cnt 500这里的--loop-cnt是命令行参数对应 src/common/runtime/src/bin.rs 中通过 clap 解析的loop_cnt#[derive(Debug, Default, Parser)] pub struct Command { #[clap(long)] loop_cnt: usize, }该二进制crate 名common-runtime-bin见 Cargo.toml会遍历 5 种优先级VeryLow/Low/Middle/High/VeryHigh与 4 类负载的笛卡尔组合为每个组合创建独立运行时并压测。四类负载在 bin.rs 中定义def_workload_enum!( ComputeHeavily, // 轻量计算密集计算 π 小数位精度 10循环 3000 次 ComputeHeavily2, // 重量计算密集计算 π 小数位精度 30循环 2000 次 WriteFile, // 异步写文件tokio::fs 写入计算精度 50 SpawnBlockingWriteFile // 阻塞写文件spawn_blocking 中同步写计算精度 100 );ComputeHeavily/ComputeHeavily2是纯 CPU 密集负载通过compute_pi_str反复计算 π 的小数位并yield_now()让出调度WriteFile使用tokio::fs异步文件写入贴近真实 IO 型任务SpawnBlockingWriteFile通过tokio::task::spawn_blocking把同步写文件操作移入阻塞线程池模拟混合负载。每次压测前会先sleep(1s)让线程池稳定随后按loop_cnt批量 spawn 任务并等待全部完成bin.rs。由于构建是 release 模式最终 CPU 占用数据更能反映生产场景。三、运行时抽象RuntimeTrait、Builder 与默认运行时切换common-runtime 没有直接向业务代码暴露 Tokio 的Runtime而是定义了自己的抽象层。3.1 RuntimeTrait统一的运行时接口src/common/runtime/src/runtime.rs 定义了RuntimeTrait包含 4 个核心方法语义与 Tokio 对齐pub trait RuntimeTrait { /// Get a runtime builder fn builder() - Builder { Builder::default() } /// Spawn a future and execute it in this thread pool fn spawnF(self, future: F) - JoinHandleF::Output where F: Future Send static, F::Output: Send static; /// Run the provided function on an executor dedicated to blocking operations. fn spawn_blockingF, R(self, func: F) - JoinHandleR where F: FnOnce() - R Send static, R: Send static; /// Run a future to complete, this is the runtimes entry point fn block_onF: Future(self, future: F) - F::Output; /// Get the name of the runtime fn name(self) - str; }3.2 Builder链式配置线程池Builderruntime.rs封装了 Tokio 的RuntimeBuilder默认配置为多线程运行时priority默认为VeryHigh并提供链式方法方法作用默认值worker_threads(val)工作线程数系统可用核数max_blocking_threads(val)阻塞线程池上限用于spawn_blocking512thread_keep_alive(duration)阻塞线程空闲回收超时10 秒runtime_name(val)运行时名称runtime-{递增ID}thread_name(val)工作线程名称前缀default-workerpriority(priority)优先级仅ThrottleableRuntime生效VeryHigh构建时BuilderBuildtrait会挂载线程生命周期钩子on_thread_start/stop维护存活线程数on_thread_park/unpark维护空闲线程数见 runtime.rs供指标采集使用。还有一个值得注意的细节debug 构建下线程栈被强制设置为 8MBruntime.rs目的是避免 sqlness 集成测试在 debug 模式下因栈溢出而失败。运行时通过Dropper优雅停机构造时创建一对 oneshot channel并另起一个名为{thread_name}-blocker的线程block_on(recv_stop)当Dropper被 drop 时发送关闭信号运行时随之退出runtime.rs、L166-L171。该设计在注释中注明受到 databend 启发。3.3 默认运行时的切换开关文档明确指出默认使用的运行时可以通过 runtime.rs 中的一行类型别名切换// configurations pub type Runtime DefaultRuntime;DefaultRuntimesrc/common/runtime/src/runtime_default.rs是对 TokioHandle的薄封装spawn直接透传不带任何限流逻辑而把它替换为ThrottleableRuntime后同一套RuntimeTrait接口下所有spawn的任务都会被限流包装。这正是面向接口编程的体现——业务代码无需改动即可切换两种调度策略。四、ThrottleableRuntime以限制 poll 次数实现 CPU 资源约束4.1 设计思想文档对核心设计做了精炼概括采用速率限制rate limiting的概念来实现 CPU 资源约束。具体做法是当创建 future 时先用另一层 future 将其包装以便在运行时拦截 poll 操作。借助 ratelimit 库可以在特定时间窗口内当前令牌生成间隔设置为 10ms只允许某个优先级的一批任务执行有限次数的 poll。也就是说CPU 时间的主要消耗者是 future 被反复poll。只要控制每秒每 10ms能 poll 多少次就间接控制了该优先级任务占用的 CPU 比例。ThrottleFuture的实现位于 src/common/runtime/src/runtime_throttleable.rsenum State { Pollable, // 允许直接 poll 内层 future Throttled(PinBoxSleep), // 令牌不足睡眠等待补充 } #[pin_project::pin_project] pub struct ThrottleFutureF: Future Send static { #[pin] future: F, /// RateLimiter of this future handle: ArcRuntimeRateLimiter, state: State, }poll的逻辑是一个三态机若处于Throttled状态先 poll 内部的Sleep未就绪则返回Pending就绪则切回Pollable调用ratelimiter.try_wait()尝试取令牌若失败拿到等待时长wait进入Throttled状态并sleep(wait)后返回Pending令牌充足时才真正 poll 内层 future。多个任务共享同一个ArcRuntimeRateLimiter每个运行时一个令牌在整批任务间竞争从而在宏观上把该优先级的 poll 总量锁在令牌生成速率附近。4.2 优先级 → 限流参数映射每个Priority对应一个Ratelimiter的构建参数这是文档的核心代码段完整保留如下runtime_throttleable.rsimpl Priority { fn ratelimiter_count(self) - ResultOptionRatelimiter { let max 8000; let gen_per_10ms match self { Priority::VeryLow Some(2000), Priority::Low Some(4000), Priority::Middle Some(6000), Priority::High Some(8000), Priority::VeryHigh None, }; if let Some(gen_per_10ms) gen_per_10ms { Ratelimiter::builder(gen_per_10ms, Duration::from_millis(10)) // generate poll count per 10ms .max_tokens(max) // reserved token for batch request .build() .context(BuildRuntimeRateLimiterSnafu) .map(Some) } else { Ok(None) } } }参数含义与取值整理如下优先级gen_per_10ms每 10ms 生成的 poll 令牌数等效速率poll/秒说明VeryLow2000200k令牌桶容量上限均为max_tokens 8000Low4000400k同上Middle6000600k同上High8000800k同上VeryHighNone不限速不构建限流器任务直达 TokioRatelimiter::builder(gen_per_10ms, Duration::from_millis(10))令牌生成间隔固定为 10ms每次补充gen_per_10ms个令牌.max_tokens(8000)令牌桶容量上限文档注释为 reserved token for batch request——即允许突发burst一批同时到达的任务可以一次性消耗预留令牌避免瞬时请求被误伤VeryHigh返回None表示该优先级如内部管理、心跳等关键任务不做 CPU 限制。优先级枚举本身定义在 runtime.rs按VeryLow 0到VeryHigh 4递增#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum Priority { VeryLow 0, Low 1, Middle 2, High 3, VeryHigh 4, }构建ThrottleableRuntime时ThrottleableRuntime::new会调用priority.ratelimiter_count()生成对应的限流器runtime_throttleable.rs若限流器构建失败会通过BuildRuntimeRateLimiter错误上报见 src/common/runtime/src/error.rs。五、5 种优先级 × 4 类负载的实验结果5.1 实验设置回顾实验在 bin.rs 的test_diff_workload_priority中完成为每一对(workload, priority)创建 8 个工作线程、名为test的运行时loop_cnt次循环 spawn 对应负载任务记录完成耗时与 CPU 占用。5.2 实验结果解读文档在 This is the preliminary experimental effect so far: 后附上了实验图。图中横轴为 4 类负载ComputeHeavily、ComputeHeavily2、WriteFile、SpawnBlocking纵轴为 CPU 占用五根柱子对应 5 个优先级图中 Priority 0~4从图中可以观察到以下规律计算密集负载对优先级最敏感ComputeHeavily组中VeryLowPriority 0CPU 占用约 262VeryHighPriority 4约 592随优先级近乎线性递增ComputeHeavily2组趋势一致约 256 → 595。这说明限流器对纯 CPU 任务的约束效果显著低优先级任务的 CPU 份额被有效压低写文件负载几乎不受优先级影响WriteFile组各优先级占用都在 516~529 之间差异不足 5%。原因在于 IO 任务大部分时间在等待文件写完成而非消耗 CPUpoll 频率天然不高令牌很少成为瓶颈阻塞写文件负载介于两者之间SpawnBlocking组中VeryLow约 522明显低于中高优先级约 551~571说明即使阻塞操作被移入 blocking 线程池计算 π 的过程仍发生在被限流的 future 内限流依然部分生效。这组实验验证了设计的核心假设通过限制 poll 次数可以按优先级划分 CPU 份额且对 CPU 密集任务效果最好。需要注意的是这是文档标注的 preliminary experimental effect初步实验结果数值依赖具体机器与负载实现生产环境应结合自身负载重新测量。六、从实验到生产全局运行时的线程池规划实验用的test运行时只是验证手段生产环境中 GreptimeDB 通过 src/common/runtime/src/global.rs 规划多套全局运行时并暴露了可配置项RuntimeOptionsglobal.rspub struct RuntimeOptions { /// The number of threads for the global default runtime. pub global_rt_size: usize, /// The number of threads to execute the runtime for compact operations. pub compact_rt_size: usize, /// The maximum number of blocking threads for compact operations. pub compact_rt_max_blocking_threads: usize, /// The number of threads to execute datanode query operations. pub query_rt_size: usize, /// The number of threads to execute datanode ingestion operations. pub ingest_rt_size: usize, /// Experimental weighted scheduler for query and write workloads. pub experimental_workload_scheduler: WorkloadSchedulerOptions, }默认值按 CPU 核数推导global.rs并保证最少 2 个线程MIN_RUNTIME_THREADS防止单线程运行时在异步代码中死锁配置项默认值global_rt_sizenum_cpuscompact_rt_sizemax(num_cpus / 2, 2)compact_rt_max_blocking_threadsmax(num_cpus / 2, 2)query_rt_sizemax(num_cpus - 1, 2)ingest_rt_sizenum_cpus不同进程角色的初始化入口不同global.rsinit_global_runtimesfrontend / metasrv / flownode 使用query 与 ingest 共享 global 运行时init_standalone_runtimesstandalone 使用同样共享 global 运行时init_datanode_runtimesdatanode 使用query 与 ingest 各自独立线程池避免相互干扰。这些配置在 datanode 的示例配置文件中都有对应注释config/datanode.example.toml## The runtime options. # [runtime] ## The number of threads to execute the runtime for global read operations. # global_rt_size 8 ## The number of threads to execute compact operations. # compact_rt_size 4 ## The maximum number of blocking threads for compact operations. ## Defaults to max(num_cpus / 2, 2). # compact_rt_max_blocking_threads 4 ## The number of threads to execute datanode query operations. ## Defaults to max(num_cpus - 1, 2). # query_rt_size 7 ## The number of threads to execute datanode ingestion operations. # ingest_rt_size 8此外global.rs还引入了一个实验性的加权查询/写入任务调度器WorkloadSchedulerOptions默认关闭global.rs可在查询与写入同时积压时按query_weight : write_weight默认 2 : 8分配 poll 份额。这与ThrottleableRuntime的思路一脉相承但粒度更细按任务类别而非优先级目前属于实验特性相关开关与权重也可在 config/datanode.example.toml 中配置。它属于运行时体系的延伸与本文的限流主题互补。全局运行时对外暴露了一组便捷函数spawn_global/spawn_query/spawn_ingest/spawn_compact/spawn_hb及其spawn_blocking_*、block_on_*变体global.rs业务模块按负载类型选择合适的线程池。七、可观测性线程与调度指标运行时把线程生命周期钩子与 Prometheus 指标打通src/common/runtime/src/metrics.rsgreptime_runtime_threads_alive{thread_name...}存活工作线程数greptime_runtime_threads_idle{thread_name...}空闲park 状态线程数实验性调度器还会上报greptime_workload_scheduler_*系列指标enabled / active_polls / weight / queued_tasks / polls_total / admission_wait_seconds_total见 metrics.rs。在tokio_unstablefeature 下运行时还会注册tokio_metrics采集器暴露注入队列深度、强制 yield 次数、工作线程数等 Tokio 内部指标runtime.rs。这一点在 runtime.rs 的test_metric测试中也有验证——它断言 dump 出的指标文本包含runtime_threads_idle、runtime_threads_alive以及tokio_workers_count等字段。八、测试与验证仓库为运行时提供了多层次的测试佐证runtime.rs 的单元测试覆盖block_on_async、spawn_from_blocking、spawn_join等基础语义runtime_throttleable.rs 的测试对全部 5 种优先级分别验证简单 future 与文件写入 future 能正确返回结果确保限流包装不破坏任务语义global.rs 的测试验证了默认线程池大小推算、最小线程数兜底、compact 阻塞线程上限、以及实验性调度器在查询积压时不饿死写入场景下的行为。九、已知限制与后续规划文档在结尾明确列出了一个 TODOIntroduce PID to achieve more accurate limitation.即当前基于 poll 次数的限流只是间接度量 CPU 占用后续计划引入 PID比例-积分-微分控制器把实际 CPU 使用率作为反馈信号动态调整令牌生成速率从而实现更精确的 CPU 约束。这也是该机制被标注为 preliminary 的原因——在真实生产负载下poll 次数与 CPU 占用之间并非严格线性IO 等待、锁竞争等因素都会带来偏差。十、小结GreptimeDB 的 common-runtime 给出了一条务实的 CPU 资源隔离路径不依赖 cgroup 等操作系统机制而是在应用层通过future 包装 令牌桶限流控制 poll 频率从而按优先级切分 CPU 份额。其核心要点可以概括为通过RuntimeTraitBuilder抽象屏蔽 Tokio 细节pub type Runtime DefaultRuntime一处即可切换限流与否ThrottleFuture用Pollable/Throttled两态机配合ratelimit库在 10ms 粒度上精确控制每个优先级的 poll 预算VeryLow2000 →High8000VeryHigh不限速实验证明该方案对计算密集负载效果显著IO 密集负载影响有限结合 config/datanode.example.toml 的线程池配置与 global.rs 的初始化入口可在生产环境按角色规划运行时拓扑。若要进一步深入可以阅读 common-runtime 的说明文档 原文或直接阅读 runtime_throttleable.rs 与 bin.rs 中的实验代码在本地跑一次cargo run --release -- --loop-cnt 500复现上述结论。【免费下载链接】greptimedbThe open-source observability database. One columnar engine for metrics, logs, and traces, on object storage.项目地址: https://gitcode.com/GitHub_Trending/gr/greptimedb创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考