ClickHouse v21.11.8.4-stable 版本修复深度解读:从复制队列挂起到稀疏哈希字典性能回归
【免费下载链接】ClickHouseClickHouse® is a real-time analytics database management system项目地址: https://gitcode.com/GitHub_Trending/cli/ClickHouse
本篇文章基于 ClickHouse 仓库中的 v21.11.8.4-stable 变更日志,深入解析该稳定版补丁相对于 v21.11.7.9-stable 修复的 7 个用户可见缺陷与 2 个内部改进。每项修复都对应一个真实的线上故障场景——复制队列卡死、DROP TABLE 并发崩溃、物化视图 LOGICAL_ERROR、字典查询性能骤降等。读者读完后,不仅能明确知晓这些 Bug 的表现与触发条件,还能借助仓库源码理解其底层成因与修复思路,为排查同类问题提供直接参考。
版本概览:一次聚焦线上稳定性与并发正确性的补丁发布
v21.11.8.4-stable 是 ClickHouse 21.11 稳定分支的第四次补丁发布,变更点全部集中在Bug Fix(用户可见的官方稳定版误行为)与NOT FOR CHANGELOG / INSIGNIFICANT(内部优化)两类,不包含任何新功能或破坏性变更。这意味着本次升级的核心价值在于消除已知的稳定性风险,建议受影响的用户优先评估升级。
修复清单速览:
| 修复项 | 影响模块 | 故障现象 |
|---|---|---|
| projection 意外移除 | MergeTree / 物化投影 | detach 数据 part 时投影被错误删除 |
| 复制队列条目挂起 | ReplicatedMergeTree | 临时目录残留导致复制队列阻塞最长 1 天 |
| sparse_hashed 字典性能骤降 | 字典(Dictionary) | 顺序键场景哈希函数错误,查询性能严重劣化 |
| 表生命周期竞态 | 存储层 | 并行 DROP TABLE 与 INSERT 导致 use-after-free |
| RabbitMQ 启动异常 | RabbitMQ 表引擎 | 启动时延迟创建 channel 后异常消失 |
| 物化视图 LOGICAL_ERROR | 物化视图 | 目标表为 JOIN/SET 表时报 LOGICAL_ERROR |
| fuzzBits 崩溃 | 随机函数 | 多个相同 FixedString 输入触发崩溃 |
修复一:Detach Parts 时投影(Projection)被意外移除
现象:在部分场景下,当用户 detach(分离)MergeTree 数据 part 时,原本附加在该 part 上的投影(projection)会被意外删除,造成数据语义丢失。该修复由 Amos Bird 提交,对应 PR #32067(由 issue #32679 回溯合入)。
背景知识:ClickHouse 的投影功能允许在 part 内部维护轻量级物化聚合结果,是加速GROUP BY等聚合查询的重要手段。投影的生命周期与 part 紧密绑定,一旦 part 在被 detach 的过程中错误清理了投影元数据,重新 attach 后投影将无法被查询优化器使用。
从源码看 detach 的清理路径:MergeTree 表的 detach 操作最终会进入 src/Storages/MergeTree/IMergeTreeDataPart.cpp 所定义的 part 生命周期管理逻辑。该修复的关键在于收紧「哪些资源在 detach 时应被清除」的判断条件,确保投影等重建成本高的附属对象被保留,而不是与临时清理对象一并移除。
实操建议:升级到 v21.11.8.4 及以上后,若此前曾因该 Bug 丢失投影,可通过重新执行ALTER TABLE ... MATERIALIZE PROJECTION重建受影响的投影;日常运维中建议在变更 part 状态(detach/drop)前先通过system.parts或system.projection_parts确认投影状态。
修复二:复制队列条目挂起与 temporary_directories_lifetime 的治理
现象:部分复制队列条目可能长时间挂起,持续报错形如Directory tmp_merge_<part_name>或Part ... (state Deleting) already exists, but it will be deleted soon,挂起时长可达temporary_directories_lifetime(默认1 天),对应 issue #29616,由 Alexander Tokmakov 修复(PR #32201,由 issue #32543 回溯合入)。
原因分析:MergeTree 在合并、突变等后台操作中会创建tmp_merge_<part_name>之类的临时目录。当进程异常中断或操作被取消时,这些临时目录可能残留;后台清理线程默认要等到temporary_directories_lifetime到期后才会回收它们,而复制队列中的后续操作恰好依赖同名 part 目录就绪,于是出现「队列条目干等临时目录过期」的连锁阻塞。
源码佐证——清理线程的真实逻辑:在 src/Storages/MergeTree/ReplicatedMergeTreeCleanupThread.cpp 中可以看到,复制表的后台清理线程iterate()每轮会执行clearOldPartsAndRemoveFromZK(),并在共享锁保护下调用storage.clearOldTemporaryDirectories(...),超时参数正是取自 MergeTree 设置项temporary_directories_lifetime(该设置定义于 src/Storages/MergeTree/MergeTreeSettings.cpp)。修复使清理线程能够及时识别并处理与复制队列冲突的残留临时目录,从而打破挂起。
实操建议:
- 临时目录的保留时长可通过 MergeTree 设置调整:
<merge_tree><temporary_directories_lifetime>3600</temporary_directories_lifetime></merge_tree>(单位秒)。 - 若线上已出现
Part ... already exists, but it will be deleted soon类报错,可结合system.replication_queue观察队列条目状态,并检查system.parts中是否存在残留的tmp_前缀目录。 - 建议定期巡检数据目录,避免异常中断(OOM、kill -9)后遗留大量临时目录挤占磁盘。
修复三:sparse_hashed 字典在顺序键下的性能回归(错误哈希函数)
现象:sparse_hashed布局的字典在处理顺序排列的键(如自增 ID)时性能明显劣化。根因是采用了不合适的哈希函数,由 Azat Khuzhin 修复(PR #32536,由 issue #32769 回溯合入)。
源码级解读——为什么是哈希函数的问题:ClickHouse 的HashedDictionary模板同时支持Hashed(紧凑 HashMap)与SparseHashed(内存优先)两种布局,见 src/Dictionaries/HashedDictionary.h:
Hashed使用内置HashMap<UInt64, Value, DefaultHash<UInt64>, ...>;SparseHashed根据键值对的大小在google::sparse_hash_map与PackedHashMap之间选择(判断函数useSparseHashForHashedDictionary()位于 src/Dictionaries/HashedDictionaryCollectionType.h)。
在 src/Dictionaries/HashedDictionaryCollectionType.h 的源码注释中明确写道:稀疏哈希表统一改用DefaultHash<>而非std::hash<>,原因是DefaultHash<>在处理「顺序键 + 随机访问」场景(如SELECT number FROM numbers(3000000) ORDER BY rand()式负载)时显著优于std::hash<>,差距可达10 倍以上。本次修复正是纠正了 sparse 布局中哈希函数的选择,使顺序键场景不再退化为病态分布。
延伸:PackedHashMap 为何存在:同文件注释还给出了各类哈希结构在 10 亿元素下的内存占用估算——google::sparse_hash_map约 26 GiB、内置HashMap约 35 GiB、PackedHashMap约 22 GiB、sparse_hash_map<packed_pair>约 17 GiB,同时说明google::sparse_hash_map因无法使用紧凑结构、且存在大量 realloc 导致 jemalloc 下 33% 碎片化等问题。因此当<K,V>对较小(sizeof(V) <= 8且键为 POD)时,PackedHashMap反而是更优选择,这也解释了useSparseHashForHashedDictionary()的判定逻辑。
实操建议:使用SPARSE_HASHED布局(或根据键值对大小自动退化为稀疏结构)的字典用户,升级后应复测以顺序键为主的高频查询;字典性能可通过system.dictionaries的bytes_allocated、query_count、hit_rate等指标观察。
修复四:并行 DROP TABLE 与 INSERT 引发的表生命周期竞态(use-after-free)
现象:在并行执行DROP TABLE与INSERT时,存在表对象生命周期管理缺陷,可能造成 use-after-free(悬空指针访问)。该问题对稳定版用户是潜在崩溃风险,由 Azat Khuzhin 修复(PR #32572,由 issue #32635 回溯合入)。
背景知识:ClickHouse 的表存储对象(StoragePtr)被查询、后台任务、DDL 等多个执行上下文共享持有。DROP TABLE会触发对象析构流程,而正在执行的INSERT若仍持有指向该对象的裸引用,就可能出现释放后再访问的竞态。此类问题在并发压力下表现为偶发段错误或std::bad_alloc类异常,排查难度高。
从源码结构看修复方向:该修复属于存储层对象生命周期管理范畴,核心思路是保证 DROP 流程与正在进行的写入流程对表对象的引用计数/锁持有关系保持一致,避免在对象析构后仍有执行路径访问其成员。由于本补丁不改变 SQL 语义,升级后无需调整使用方式,但建议 DROP 类操作与批量写入错峰执行,以降低并发竞态暴露面。
修复五:RabbitMQ 表引擎启动异常——延迟 channel 创建
现象:RabbitMQ 存储(表引擎)在启动阶段可能抛出异常,原因是在连接尚未就绪时过早创建了 channel。修复方式是延迟 channel 的创建时机,由 Kseniia Sumarokova 修复(PR #32584,由 issue #32633 回溯合入)。
背景知识:ClickHouse 的 RabbitMQ 表引擎用于将消息队列作为表读写。启动时会建立 AMQP 连接、创建 channel、声明队列并开始消费。若启动流程中 channel 创建早于底层连接完成握手,在断线重连等场景下会触发协议层异常。
实操建议:使用ENGINE = RabbitMQ的用户升级后需重启实例以加载新代码;如启动阶段仍观察到连接异常,可检查 RabbitMQ 服务端日志与system.tables中该表的元数据状态,并确认网络与鉴权配置(用户名、密码、vhost)无误。
修复六:物化视图目标为 JOIN / SET 表时的 LOGICAL_ERROR
现象:当物化视图(Materialized View)的目标表是JOIN或SET引擎表时,写入过程会抛出LOGICAL_ERROR(逻辑错误异常)。由 Raúl Marín 修复(PR #32669,由 issue #32891 回溯合入)。
背景知识:物化视图在插入数据时会同步向目标表写入;JOIN与SET引擎表属于内存型、非持久化的特殊表,其写入路径与普通 MergeTree 目标表存在差异。旧版本未对这两类目标做兼容处理,导致内部断言失败并被提升为LOGICAL_ERROR。
实操建议:
- 生产环境不建议将 JOIN/SET 引擎表作为物化视图目标(数据不落盘、重启即失),此类表更适合作为查询侧加速结构;
- 若已存在该组合,升级后写入不再报错,但请确认数据仅存在于内存中这一限制;
- 排查历史 LOGICAL_ERROR 时,可结合
system.query_log与物化视图定义(system.tables的create_table_query)定位涉及的目标表类型。
修复七:fuzzBits 在多个相同 FixedString 输入下崩溃
现象:当fuzzBits函数配合多个相同的FixedString输入使用时可能崩溃(关闭 issue #32737),由 SuperDJY 修复(PR #32755,由 issue #32792 回溯合入)。
源码级解读——fuzzBits 的实现与约束:fuzzBits定义于 src/Functions/fuzzBits.cpp,功能是对输入字符串的每一位以概率p进行翻转(XOR 掩码扰动):
- 签名与类型约束:
fuzzBits(s, p),第一个参数必须是String或FixedString,第二个参数必须是常量浮点数,取值范围限定为0.0到1.0(越界抛ARGUMENT_OUT_OF_BOUND),见 src/Functions/fuzzBits.cpp; - 随机性设计:内部使用
pcg64_fast伪随机数生成器,通过getXorMask()将随机数按位换算为翻转掩码(ptr_out[i] = ptr_in[i] ^ mask)。函数刻意声明为非确定性(isDeterministic()与isDeterministicInScopeOfQuery()均为 false),并强制第二个参数为常量,避免对常量列展开导致「一行结果复制到所有行」的错误语义,见 src/Functions/fuzzBits.cpp; - 修复点:此前的实现未充分处理多个相同 FixedString 输入时列数据的边界条件,导致内存访问越界。修复后 FixedString 路径通过
common::mulOverflow精确计算input_rows_count * n的缓冲区大小(src/Functions/fuzzBits.cpp),杜绝了溢出与越界写入。
使用示例(摘自该函数自带的文档化示例):
SELECT fuzzBits(materialize('abacaba'), 0.1) FROM numbers(3)每次执行结果不同,例如:
┌─fuzzBits(materialize('abacaba'), 0.1)─┐ │ abaaaja │ │ a*cjab+ │ │ aeca2A │ └───────────────────────────────────────┘实操建议:fuzzBits常用于数据脱敏、模糊测试与数据增强场景。使用FixedString输入时,概率参数建议按字节扰动需求设置在0.001~0.5之间,避免过高概率使数据完全失真。
内部改进:ProtobufSchemas 数据竞争与常量条件优化
除用户可见修复外,本版本还合入了两项内部改动(归入 NOT FOR CHANGELOG / INSIGNIFICANT):
- 修复 ProtobufSchemas 数据竞争(PR #27822,filimonov 提交):
ProtobufSchemas全局模式缓存的并发访问存在 data race,在多线程解析 Protobuf 格式(如Protobuf/ProtobufSingle输入输出格式)时可能产生未定义行为。该项为线程安全加固,不改变外部行为。 - 始终启用常量条件优化(PR #32858,Nikolai Kochetov 提交):将
const-condition-if优化由条件性应用改为始终应用。该优化会在查询分析阶段对if/multiIf中值为常量的分支进行折叠,减少执行期不必要的表达式求值,属于查询计划层面的稳定收益,对大量使用常量条件过滤的报表类 SQL 有正向影响。
升级建议与验证方式
- 升级路径:v21.11.x 用户可平滑升级至 v21.11.8.4-stable(补丁版本不引入破坏性变更);建议先在灰度环境执行
clickhouse-server滚动升级并观察系统日志与system.metric_log。 - 回归重点:结合本版本修复清单,建议重点回归以下场景——投影查询与 part detach/attach、复制表队列健康度(
system.replication_queue)、稀疏哈希字典查询延迟、fuzzBits随机函数、物化视图写入。 - 故障定位参考:本版本涉及的复制清理、字典实现、函数实现等模块源码分别位于 src/Storages/MergeTree/ReplicatedMergeTreeCleanupThread.cpp、src/Dictionaries/HashedDictionaryCollectionType.h、src/Functions/fuzzBits.cpp,出现同类问题时可直接对照排查。
说明:本文所有修复条目与 PR/issue 编号均摘自 v21.11.8.4-stable 变更日志,源码佐证来自当前仓库对应文件,升级决策请以官方稳定版发布渠道为准。
【免费下载链接】ClickHouseClickHouse® is a real-time analytics database management system项目地址: https://gitcode.com/GitHub_Trending/cli/ClickHouse
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考