资讯动态

Doris tablet与compaction原理及调优实战指南

发布时间:2026/9/12 7:20:57 来源:尧图企业网站定制
1. 为什么刚上手 Doris 就被 tablet 和 compaction 搞得焦头烂额Apache Doris 这个名字听起来很“轻量”但实际用起来很多团队在导入第一批数据后不到三天就发现 FE 日志里开始刷屏tablet not found、compaction task failedBE 节点 CPU 突然飙到 95%查询延迟从 200ms 涨到 8s监控面板上红色告警像过年挂灯笼一样密密麻麻。这不是你配置错了也不是集群太小——这是 Doris 的存储模型和 LSM-Tree 架构在真实负载下必然触发的“成长痛”。它不像 MySQL 那样写完就完事也不像 ClickHouse 那样靠暴力刷盘硬扛Doris 的核心设计哲学是用精细的 tablet 粒度控制 异步 compaction 机制在实时性、一致性与资源开销之间做动态平衡。而这个平衡点从来不是开箱即用就能自动找对的。我带过三个不同规模的 Doris 落地项目最小的是日增 500 万行订单明细最大的是某车联网平台日写入 42 亿事件点位。所有团队踩的第一个坑都出在对tablet的认知偏差上——很多人以为 tablet 就是“分片”类比 MySQL 的 partition 或 ES 的 shard于是按时间或 ID 哈希粗暴切分结果发现查询性能不升反降BE 内存持续泄漏FE 元数据压力暴涨。真相是tablet 是 Doris 最小的物理存储单元也是 compaction、副本同步、版本管理、内存映射的原子边界。一个 tablet 对应磁盘上一个独立目录如/doris/data/cluster_id/tablet_id/里面存着多个 rowset数据版本每个 rowset 又由多个 segment 文件组成。当你执行 INSERT INTODoris 不是直接追加到文件末尾而是生成新 rowset当多条 UPDATE 或 DELETE 发生旧 rowset 不会立即删除而是打上 delete bitmap 标记最终靠 compaction 合并这些碎片化的 rowset生成更紧凑的新版本。这个过程就像整理书架新书不断插进来INSERT旧书被贴上“作废”标签DELETE但真正把作废书抽走、把同类书归并重排compaction得等图书馆管理员BE 的 compaction thread有空且判断书架太乱了才动手。所以tablet数量不是越多越好也不是越少越省事。它直接决定三件事元数据压力FE 要为每个 tablet 维护其所有 rowset 的元信息大小、版本号、delete bitmap 位置等10 万个 tablet 和 100 万个 tabletFE 的 JVM heap 压力差 3~5 倍compaction 并发瓶颈BE 默认只启动 10 个 compaction thread如果单个 tablet 数据量小但数量爆炸大量小 rowset 争抢有限的 thread反而导致 compaction 积压查询扫描效率查询时 BE 需加载每个 tablet 的索引segment footer 中的 min/max/bloom filtertablet 过多意味着更多小文件 IO 和更重的索引加载开销。而compaction失败90% 的根源不在“磁盘满了”或“CPU 不够”而在于tablet 粒度失衡 写入节奏失控 compaction 参数未适配业务特征。比如某客户把用户行为表按 user_id mod 1024 分 1024 个 tablet结果发现 80% 的活跃用户集中在前 100 个 tablet这些 tablet 的 rowset 数量在 2 小时内就突破 200触发 BE 的max_compaction_score限流阈值默认 1000而其余 900 多个 tablet 几乎没数据——这就像让 10 个快递员同时挤在 100 平米的仓库里分拣 10 万件包裹而隔壁 900 平米的仓库空着没人管。compaction 不是“失败”是系统在主动拒绝低效调度。提示别急着调大min_compaction_score或max_compaction_concurrency。先看清楚你的 tablet 分布是否健康再决定是调整分桶数、改用 RANDOM 分桶还是引入分区裁剪策略。Doris 的 compaction 是“自适应”的但它的适应器需要你给它一个合理的输入空间。2. FE 高负载、BE 报错 “too many requests” 的底层链路拆解当你在 Doris Manager 或 Grafana 上看到 FE 的 QPS 突然飙升到 3000CPU 持续 90%同时 BE 日志里反复出现automatic compaction failed: too many requests, you have exceeded a secondary rate limit. please wait a few seconds这不是网络抖动或瞬时高峰而是FE-BE 协同机制中某个环节的请求背压已传导至临界点。这个错误信息里的 “secondary rate limit”指的不是 HTTP 接口限流而是 Doris 内部基于 RPC 请求队列深度和响应延迟的动态熔断保护。它背后是一条完整的请求生命周期链路Client SQL → FE Parser → FE Planner → FE Coordinator → BE RPC → BE Compaction Scheduler → BE Rowset Merger → Disk Write其中too many requests的触发点几乎总在FE Coordinator 向 BE 发送 compaction 任务分配请求这一环。我们来逐层还原这个雪崩过程2.1 FE 的元数据服务为何成为瓶颈FE 的核心职责是元数据管理Table/Partition/Tablet/Rowset 的状态、SQL 解析与计划生成、以及任务分发协调。当 tablet 数量超过 50 万且写入模式是高频小批量如每秒 1000 条 Kafka 消息每批 100 行 INSERTFE 的元数据更新压力会指数级上升。原因在于每次 INSERT 都会为涉及的每个 tablet 创建一个新的 rowset并更新 FE 中该 tablet 的rowset_list每个 rowset 在 FE 中占用约 2KB 内存含 version、size、path、delete_bitmap_ref 等字段若单表有 10 万个 tablet每秒写入 100 批每批影响 50 个 tablet则 FE 每秒需新增 5000 个 rowset 元数据即 10MB 内存写入 相应的 GC 压力更致命的是FE 的元数据操作是强一致的所有变更必须通过 Raft Log 同步到多数派节点高并发写入会导致 Raft Log queue 积压进而拖慢整个 FE 的响应速度。我曾帮一家电商客户诊断他们 FE 配置了 32C64G但jstat -gc显示 Old Gen 每 3 分钟就 Full GC 一次GC 后存活对象高达 45GB。根因就是他们的日志表用了DISTRIBUTED BY HASH(log_id) BUCKETS 1024而 log_id 是 UUID导致数据完全随机分布1024 个 tablet 全部被写入且无热点倾斜。解决方案不是加机器而是将建表语句改为DISTRIBUTED BY RANDOM BUCKETS 64配合PARTITION BY RANGE (dt) (让每天的数据只落在当天的 partition 下的 64 个 tablet 中。改造后tablet 总数从 120 万降到 18 万FE 的 QPS 从 2800 降到 320Full GC 消失。2.2 BE 的 “secondary rate limit” 是如何被触发的BE 的 compaction 调度器CompactionManager并非无脑接收任务。它内部维护两个关键队列pending_task_queue等待被调度的 compaction 任务按 score 排序running_task_set当前正在执行的 task ID 集合避免重复调度同一 tablet。而too many requests错误源于 BE 的TaskPool对 RPC 请求的速率控制。具体逻辑如下FE Coordinator 每秒向每个 BE 发送一批 compaction 任务分配请求RPC 类型为COMPACT_TASK_REQUESTBE 收到请求后先检查本地pending_task_queue长度是否超过config.max_pending_compaction_task_num_per_broker默认 1000若未超限则解析任务、校验 tablet 状态、加入队列若超限BE 立即返回TStatusCode::TOO_MANY_REQUESTS错误且该错误会被 FE 记录为 “compaction failed”FE 收到此错误后不会重试而是将该 tablet 的 compaction score 临时提高模拟“更紧急”并在下次调度周期默认 10s再尝试。这个机制的设计初衷是防止 BE 因任务积压而 OOM但它在实践中常被误读为“BE 太忙”于是运维同学第一反应是调大max_pending_compaction_task_num_per_broker。这是危险的因为真正的瓶颈往往在上游——如果 FE 分发的任务本身质量差比如大量小 rowset、大量 delete bitmap 未清理增大队列只是让垃圾堆积得更深。我们做过压测当max_pending_compaction_task_num_per_broker从 1000 改为 5000BE 的 pending queue 确实不报错了但compaction_task_duration_ms的 P99 从 1200ms 涨到 4800ms且be_disk_usage_percent在 3 小时内从 45% 涨到 92%因为小文件合并效率暴跌磁盘空间释放变慢。2.3 如何定位是 FE 还是 BE 的问题别猜用 Doris 自带的诊断工具链直接看证据查 FE 状态访问http://fe_host:8030/rest/v1/system?pathfe/status重点关注fe_status中的query_queue_size、rpc_queue_size、raft_log_queue_size若raft_log_queue_size 10000说明 Raft 同步已严重滞后查 BE 状态访问http://be_host:8040/rest/v1/system?pathbe/status看compaction_pending_task_num和compaction_running_task_num若前者长期 500 且后者 5说明调度器卡住了查 compaction 历史执行SHOW PROC /compactions观察STATE列PENDING/RUNNING/SUCCESS/FAILED和SCHEMA_HASH列相同 schema_hash 的 tablet 往往有共性问题查 tablet 分布ADMIN SHOW REPLICA DISTRIBUTION FROM db_name.table_name;输出表格中replica_num列若方差极大如有的 BE 有 12000 个 replica有的只有 3000说明数据分布严重不均。有一次客户抱怨“BE 总是报 too many requests”我们跑SHOW PROC /compactions发现 97% 的 FAILED 任务都集中在schema_hash123456789的 tablet 上。进一步查ADMIN SHOW REPLICA STATUS WHERE SCHEMA_HASH 123456789发现这些 tablet 的version_count普遍 300正常应 20rowset_num 150。根因是他们用 Stream Load 导入时timeout设为 3600 秒但 Kafka 消费端偶发网络抖动导致同一个批次被重复提交了 12 次每次生成一个新 rowset。解决方案是① Stream Load 加label去重② 设置max_filter_ratio0.1防止脏数据阻塞③ 对该表执行ALTER TABLE xxx SET (storage_mediumSSD)强制走 SSD compaction加速清理。注意too many requests是症状不是病因。它像汽车仪表盘上的“发动机故障灯”亮了要查机油、火花塞、氧传感器而不是直接换灯泡。3. Tablet 分桶数与分布策略的实战选型指南Doris 表的DISTRIBUTED BY子句决定了数据如何落到各个 tablet 上这是整个集群稳定性的基石。但官方文档里那句“建议 bucket 数为 2 的幂次通常 10~100 个”根本没法直接抄作业。我在落地项目中见过太多反例有人照搬 StarRocks 的经验设BUCKETS 256结果小表日增 10 万行的每个 tablet 平均才 400 行compaction 效率极低也有人为图省事全用RANDOM BUCKETS 16结果大宽表100 列的单个 tablet 达到 8GBBE 加载索引时 OOM。选型必须结合数据量级、查询模式、更新频率、硬件配置四维交叉判断。3.1 三种主流分桶策略的适用场景与陷阱策略语法示例适用场景关键优势隐性风险实测建议HASH 分桶DISTRIBUTED BY HASH(user_id) BUCKETS 64点查为主WHERE user_id ?、JOIN 关联ON user_id点查路由精准JOIN 本地化率高数据倾斜严重如 user_id 有 10% 热点、小表易产生海量小 tablet热点 ID 必须预处理如MD5(user_id) % 1000BUCKETS 数 ≤ 128RANDOM 分桶DISTRIBUTED BY RANDOM BUCKETS 32范围查询WHERE dt BETWEEN 2024-01-01 AND 2024-01-31、聚合分析GROUP BY tag数据分布绝对均匀tablet 大小可控点查需广播到所有 BEJOIN 无法本地化宽表列数 50必选BUCKETS 数按(日增行数 * 行平均字节) / (1GB ~ 2GB)取整COMPOUND 分桶DISTRIBUTED BY HASH(user_id, dt) BUCKETS 128混合负载既有 user_id 点查又有 dt 范围过滤平衡点查与范围查询性能复合键组合爆炸user_id × dt 组合数远超实际数据量tablet 数失控仅用于高基数维度组合BUCKETS 数需用EXPLAIN验证实际分布举个真实案例某广告平台的曝光日志表日增 8 亿行包含ad_id高基数、user_id超高基数、dt日期、city低基数等字段。最初用HASH(ad_id) BUCKETS 256结果发现ad_id前 100 个头部广告主占了 65% 流量导致 256 个 tablet 中 32 个承载了 80% 数据这些 tablet 的 compaction score 常年 5000而其余 224 个 tablet score 10。改成RANDOM BUCKETS 128后各 BE 的 replica 数标准差从 4200 降到 80但点查WHERE ad_id 12345的 P95 延迟从 120ms 涨到 480ms。最终方案是保留HASH(ad_id)作为主分桶但增加二级分区PARTITION BY RANGE(dt) (每天一个 partition每个 partition 内BUCKETS 32。这样既保证了 ad_id 点查的局部性只需查当天 partition 的 32 个 tablet又避免了跨 partition 的数据倾斜compaction score 全局稳定在 200~800 区间。3.2 BUCKETS 数量的黄金计算公式别死记硬背“10~100”用这个公式算Optimal Buckets ≈ (Daily Data Volume in Bytes) / (Target Tablet Size)其中Daily Data Volume in Bytes 日增行数 × 各列平均字节数之和× 压缩率Doris 默认 LZ4文本类压缩率约 3~5x数值类约 2~3xTarget Tablet Size推荐值SSD 环境1GB ~ 2GBcompaction 吞吐高IO 延迟低SATA HDD 环境500MB ~ 1GB避免单次 compaction 时间过长内存受限 BE 64GB RAM300MB ~ 500MB减少索引加载内存占用。例如一张用户画像表日增 2000 万行每行平均 200 字节LZ4 压缩后约 60 字节/行则日增体积 20000000 × 60 1.2GB。若 BE 用 NVMe SSD目标 tablet 大小设为 1.5GB则BUCKETS ≈ 1.2GB / 1.5GB ≈ 0.8 → 向上取整为 1显然不对因为 BUCKETS 是 per partition 的必须考虑分区数。假设按月分区12 个 partition则每个 partition 日增 1.2GB / 12 100MB此时BUCKETS 100MB / 1.5GB ≈ 0.067 → 取 1仍不合理。正确做法是先定分区粒度再算 per partition buckets。该表改为按天分区365 个 partition则 per partition 日增 1.2GB / 365 ≈ 3.3MB远小于 1GB此时BUCKETS 1即可即每个 partition 一个 tablet。但实际中为避免单 partition tablet 过少导致 BE 负载不均我们会设BUCKETS 16让每个 partition 有 16 个 tablet总 tablet 数 365 × 16 5840完全在 FE 承载范围内。3.3 动态调整 BUCKETS 的安全操作路径建表后想改 BUCKETS 数Doris 不支持ALTER TABLE ... SET BUCKETS。但你可以安全迁移新建表CREATE TABLE tbl_new LIKE tbl_old PROPERTIES(BUCKETS 64);停写旧表ADMIN STOP ROUTINE LOAD FOR tbl_old;若有 Routine Load导出数据INSERT INTO tbl_new SELECT * FROM tbl_old;注意设置query_timeout3600原子切换RENAME TABLE tbl_old TO tbl_old_bak, tbl_new TO tbl_old;验证 清理查SHOW TABLET FROM tbl_old确认新 tablet 分布均匀再DROP TABLE tbl_old_bak;。关键细节INSERT SELECT时务必在 session 中设置set enable_insert_strictfalse;否则 NULL 值可能被拦截若表有物化视图需在CREATE TABLE tbl_new LIKE ...后手动CREATE MATERIALIZED VIEW ... AS SELECT ... FROM tbl_new;切换前用EXPLAIN对比新旧表的查询计划确保SCAN NODE的 tablet 数量合理如从 1024 降到 128。我曾帮金融客户迁移一张 12TB 的交易流水表原BUCKETS 1024导致 FE 元数据达 18GB。按上述流程迁移到BUCKETS 256后FE heap 使用率从 92% 降到 65%且SHOW PROC /compactions中 PENDING 任务数从平均 2400 降到 120。整个过程耗时 4.5 小时含数据校验零业务中断。提示BUCKETS 数不是越小越好。太少会导致单 tablet 过大compaction 单次耗时过长 30min影响查询并发太多则元数据膨胀FE 成为瓶颈。找到那个“刚刚好”的数字需要你亲手算、亲手测、亲手调。4. Compaction 参数调优的七种武器与失效场景Doris 的 compaction 不是黑盒它暴露了 12 个可调参数但 90% 的团队只动min_compaction_score和max_compaction_concurrency。这就像只调油门不管变速箱车速上不去还容易爆缸。真正的调优是理解每个参数在 LSM-Tree 合并链路中的角色然后根据你的数据写入特征写入频次、更新比例、delete 密度组合使用。下面这七种“武器”是我从上百个集群中提炼出的实战组合。4.1 武器一min_compaction_score—— compaction 的“触发阈值”原理每个 tablet 有一个 compaction score计算公式为score (rowset_num - 1) * 10 delete_version_num * 5 (total_data_size / 1024 / 1024) * 0.1。当 score ≥min_compaction_score默认 1000该 tablet 进入 pending 队列。误区很多人以为调大它就能“减少 compaction”其实它只控制“何时开始”不控制“合并什么”。实战技巧对于写多读少、update/delete 频繁的表如实时风控表delete_version_num是主要得分项建议设min_compaction_score500让 delete bitmap 尽快合并避免查询时需遍历大量版本对于写少读多、append-only的表如日志归档表rowset_num是主因可设min_compaction_score2000减少不必要的合并延长 rowset 生命周期以提升查询缓存命中率绝对禁止设为 0 或负数这会导致 compaction 无限循环刚合并完立刻又触发。4.2 武器二max_compaction_concurrency—— BE 的“并发线程数”原理BE 上 compaction thread 的最大数量默认 10。每个 thread 同时处理一个 tablet 的 compaction。误区盲目加到 50 以为能加速结果 CPU 100%、IO util 95%查询延迟翻倍。真相compaction 是 CPU IO 密集型任务thread 数应 ≤min(可用 CPU 核心数 × 0.7, 磁盘 IOPS × 0.3)。例如32C BE NVMe SSDIOPS 50000理论最大 thread 数 min(32×0.722.4, 50000×0.315000) ≈ 22。但实测发现当 thread 16 时compaction_task_duration_msP99 开始劣化因为 OS 调度开销和锁竞争加剧。推荐值12~16。4.3 武器三base_compaction_interval_seconds—— “基础合并”的冷静期原理控制 base compaction将所有 rowset 合并为一个的最小间隔默认 3600 秒1 小时。base compaction 比 cumulative compaction只合并最近几个 rowset更重但能彻底清理 delete bitmap。适用场景当你的表有大量DELETE FROM ... WHERE ts xxx操作且 delete 条件覆盖历史数据时必须缩短此值。否则 delete bitmap 长期不清理查询时需扫描所有 rowset 的 bitmap性能雪崩。安全值180030 分钟是底线低于此值可能导致 compaction 风暴。某客户设为60010 分钟结果 BE 每小时 compaction 任务超 2 万磁盘写入带宽持续 95%最终触发disk_full保护。4.4 武器四cumulative_compaction_check_interval_seconds—— “累积合并”的心跳原理BE 每隔此秒检查一次是否需要触发 cumulative compaction默认 10 秒。它不直接控制并发而是影响调度灵敏度。调优逻辑写入节奏越快此值应越小。例如 Kafka 实时写入每秒 5000 行设为5离线 T1 导入每小时一批设为3005 分钟即可。副作用设得太小如 1 秒BE 会频繁轮询增加 CPU 无谓消耗设得太大如 300 秒则小 rowset 积压时间过长rowset_num持续攀升。4.5 武器五max_cumulative_compaction_num—— “累积合并”的宽度限制原理cumulative compaction 一次最多合并多少个 rowset默认 100。它决定了合并后的 rowset 大小。关键权衡值小如 10合并后 rowset 小查询索引加载快但 compaction 频次高值大如 500单次合并产出大 rowset减少后续 compaction 次数但合并耗时长且大 rowset 的 Bloom Filter 内存占用高。推荐对于 SSD 环境设200HDD 环境设50避免单次 IO 时间过长。4.6 武器六compaction_mem_limit_bytes—— compaction 的“内存红线”原理单个 compaction 任务允许使用的最大内存默认 2GB。超过则失败并重试。为什么重要Doris compaction 需将参与合并的 rowset 的索引segment footer全部加载到内存再进行排序、去重、bitmap 合并。若 tablet 有 200 个 rowset每个索引 1MB则需 200MB 内存远低于 2GB。但若 rowset 数达 2000且每个索引因宽表变大到 2MB则需 4GB必然失败。解决方案监控SHOW PROC /compactions中failed_reason是否含memory limit exceeded若频繁出现优先优化 tablet 分布减少 rowset_num其次调大此值如4294967296 4GB终极手段对宽表启用enable_unique_key_merge_on_writetrue让 compaction 时跳过部分索引加载。4.7 武器七storage_min_max_level—— “层级合并”的深度开关原理Doris 的 LSM-Tree 分 Level 0~NLevel 0 是最新写入的 rowsetLevel N 是最老的 base rowset。此参数控制 compaction 的最大层级默认2即只在 L0→L1 和 L1→L2 间合并。高级玩法设为3可强制推进到 L2→L3适用于超大表 10TB的冷数据归档让最老数据彻底固化减少查询时的版本遍历。但会显著增加 compaction 时间和 IO仅限离线分析集群使用。最后强调一个铁律任何参数调整后必须持续观察 72 小时。重点盯三个指标compaction_task_duration_ms的 P95 是否 300sbe_disk_usage_percent的增长斜率是否平缓日增 5%fe_query_response_time_ms的 P95 是否稳定波动 10%。如果调参后这三项任一恶化立即回滚。Doris 的稳定性永远比“看起来更快”更重要。注意没有银弹参数。我给客户的调参方案从来不是一份配置清单而是一张“参数-数据特征-硬件环境”的三维决策表。你得自己填自己试自己信。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价