ClickHouse v21.7.2.7-stable 版本更新解析:1 项改进与 10 项缺陷修复全解读
【免费下载链接】ClickHouseClickHouse® is a real-time analytics database management system项目地址: https://gitcode.com/GitHub_Trending/cli/ClickHouse
本篇文章以 ClickHouse 官方变更日志(v21.7.2.7-stable changelog)为蓝本,逐条剖析该稳定版相对 v21.7.1.7283-prestable 发布的所有改动:从clickhouse-client工作目录兼容性改进,到时区转换、PREWHERE 断言、TTL 列 ALTER、ReplicatedMergeTree DROP PART、ARM 平台异常处理、后台任务退避、外部字典、查询分析器死锁、Map 类型 JSON 序列化与 Join 线程估计共 10 项缺陷修复。读者将理解每项修复背后的技术成因、受影响的使用场景,以及这些逻辑在当前仓库源码中的对应实现。
版本背景:从 prestable 到 stable 的收敛
ClickHouse 的版本演进遵循"prestable → stable"双轨机制:功能首先进入 prestable(预发布)分支接受社区与集成测试的检验,随后将验证通过的修复以 backport(回移植)方式收敛进 stable 分支。v21.7.2.7-stable 正是这样一个稳定化快照,其对比基线为 v21.7.1.7283-prestable。
从变更日志结构可以清晰看到两个分类维度:
- Improvement(改进):1 项,聚焦于客户端工具的使用体验;
- Bug Fix(缺陷修复):10 项,覆盖类型系统、查询执行、存储引擎、平台兼容、后台调度、外部字典、格式化与查询优化等多个子系统。
值得注意的是,日志末尾还保留了NOT FOR CHANGELOG / INSIGNIFICANT(不纳入变更日志/次要改动)分区,收录了两条内部修复(ExpressionCache 析构修复、Map 到 JSON 序列化的正确修复),这体现了 ClickHouse 项目对变更日志严谨性的要求——并非所有代码改动都会出现在面向用户的更新说明中。
Improvement:允许在不可读工作目录下启动 clickhouse-client
原始描述:Allow to start clickhouse-client with unreadable working directory.(#25817)
问题成因:clickhouse-client在启动时会读取当前工作目录相关信息(例如用于确定配置文件相对路径、history 文件位置等)。当工作目录自身权限不足(如目录x权限缺失、被挂载为无读权限)时,客户端会直接启动失败,这在某些容器化、受限 shell 或自动化脚本场景下会形成硬性障碍——用户往往只需要执行一条简单查询,却被工作目录权限卡住。
修复效果:客户端在遇到不可读工作目录时不再整体拒绝启动,而是退化为不依赖工作目录的功能路径。这一改动的本质是"降级而非报错"的健壮性策略,与 ClickHouse 一贯的"尽力而为"哲学一致。
Bug Fix 详解:10 项缺陷修复的技术分析
1. Date 与 DateTime 之间 CAST 的时区丢失
原始描述:CASTfromDatetoDateTime(orDateTime64)was not using the timezone of theDateTimetype.(#24129,关闭 issues/24128)
技术成因:Date本身是无时区语义的"日历日",而DateTime/DateTime64携带时区。在旧实现中,Date → DateTime的转换未读取目标DateTime类型的时区信息,导致转换结果在展示层出现时区偏差。更严重的是,这一缺陷会级联污染类型推断:当Date与DateTime出现在同一表达式中求公共类型时(典型场景包括if函数分支、数组构造[date_col, datetime_col]、以及两者直接比较),推断出的公共类型同样不带正确时区,最终影响查询结果正确性。
源码佐证:类型转换的核心逻辑位于 src/Functions/FunctionsConversion.h 与 src/Functions/FunctionsConversion.cpp。从当前仓库代码可以看到,时间相关类型转换对时区的处理在convertFieldToType/executeConvert链路中逐步完善,且 FunctionsConversion.h 中时区处理逻辑 对 Date/Date32 从字符串解析的场景明确标注了时区的"虚拟性"(dummy),说明 Date 语义与 DateTime 时区语义的边界在实现中是被反复斟酌的。
修复影响:涉及CAST、Date/DateTime比较、if、数组构造四类操作。对依赖跨日期时间类型计算的分析型查询(如按天分桶后与时间戳列对齐)有实际正确性意义。
2. PREWHERE 非 uint8 类型触发断言
原始描述:Fix assertion in PREWHERE with non-uint8 type(#25484,关闭 issues/19589)
技术成因:PREWHERE是 ClickHouse 的延迟物化优化——先读取过滤条件涉及的列进行粗筛,再按需读取其余列。内部实现中 PREWHERE 的过滤结果以类布尔掩码(bitmask)形式传递,旧实现默认掩码为uint8列。当用户对非uint8类型的列执行 PREWHERE(或谓词下推产生非 uint8 结果)时,会在部分代码路径触发断言失败(assertion),导致查询直接崩溃而非优雅报错。
源码佐证:PREWHERE 的分析与优化分散在 src/Interpreters/ExpressionAnalyzer.cpp、src/Interpreters/TreeRewriter.cpp、src/Interpreters/LogicalExpressionsOptimizer.cpp 等文件中,过滤条件下的物化时机与数据类型校验正是在这一分析阶段完成的。
修复影响:修复后 PREWHERE 对非 uint8 类型的处理与主过滤链路保持一致,消除了崩溃类故障。建议升级后对存量使用 PREWHERE 的宽表查询做回归验证。
3. ALTER MODIFY COLUMN 与 TTL 表达式的冲突
原始描述:FixALTER MODIFY COLUMNof columns, which participates in TTL expressions.(#25554)
技术成因:MergeTree 家族表的 TTL 表达式(行级 TTL、列级 TTL)会在后台被编译为独立的表达式树并持久化。当用户对"参与了 TTL 表达式"的列执行ALTER TABLE ... MODIFY COLUMN改变其类型时,旧实现未同步失效/重建 TTL 表达式的元数据,导致 TTL 表达式仍引用旧类型信息,进而在 TTL 后台任务或下一次元数据加载时发生错误。
修复影响:涉及三类常见操作——修改列类型后原 TTL 仍可正确执行、TTL 元数据与列元数据保持一致、避免后台 TTL 任务在类型变更后异常。对维护长期运行、带数据生命周期策略的大表(典型如按event_time做 TTL 的日志表)尤为重要。
4. ReplicatedMergeTree DROP PART 的罕见竞态错误
原始描述:Fix rare bug withDROP PARTquery forReplicatedMergeTreetables which can lead to error messageUnexpected merged part intersecting drop range.(#25783)
技术成因:DROP PART在 MergeTree 中被实现为"删除一个覆盖指定数据范围(drop range)的 part"。复制表场景下,DROP PART与后台 merge(合并)并发时,若 merge 恰好产出一个与 drop range 相交的新 part,旧实现会直接抛出LOGICAL_ERROR级别的错误并中断操作。问题在于:该错误信息本身是用于兜底保护逻辑一致性的,但真实场景中"merge 产出相交 part"是合法的并发交错结果,应该被安全处理而非当作不可恢复的逻辑错误。
源码佐证:该错误信息的检查逻辑至今仍保留在当前仓库的 src/Storages/MergeTree/MergeTreeData.cpp 中,两处抛出点位于 L7141 与 L7155,均以ErrorCodes::LOGICAL_ERROR抛出:
throw Exception(ErrorCodes::LOGICAL_ERROR, "Unexpected merged part {} intersecting drop range {}", ...);修复影响:修复后,DROP PART与后台 merge 的交错场景会被正确处理,错误信息Unexpected merged part intersecting drop range不再作为面向用户的常规失败路径出现。对频繁执行分区/部分删除操作的高写入复制表集群意义重大。
5. ARM 平台非默认页大小下的异常处理
原始描述:Fix ARM exception handling with non default page size.(#25854,同时关闭 issues/25512、25044、24901、23183、20221、19703、19028、18391、18121、17994、12483 共 11 个关联问题)
技术成因:ARM 架构支持多种页大小(典型为 4KB,部分平台为 16KB/64KB 等 large pages / 非默认页大小配置)。ClickHouse 的信号处理与异常栈展开逻辑(涉及 src/Common/phdr_cache.cpp、src/Common/SignalHandlers.cpp 等)依赖对内存页边界的精确判断。当页大小非默认值时,栈展开、信号帧遍历等操作会越界或错位,表现为 ARM 平台上的偶发崩溃或错误 unwind。
修复影响:一次性收敛了 11 个关联 issue,是典型的"一个根因、多症状"修复。对 ARM 服务器(如 AWS Graviton、华为鲲鹏等)上运行 ClickHouse 的部署有直接价值。
6. 后台任务池满载时的极长退避
原始描述:Fix extremely long backoff for background tasks when the background pool is full.(#25893,关闭 issues/25836)
技术成因:MergeTree 的后台任务(merge、mutation、TTL 等)由共享的 background pool 调度。当池满载时,新任务进入等待队列并采用指数退避(backoff)重试。旧实现的退避上限计算存在缺陷,在持续满载场景下退避间隔会指数级膨胀到极值(extremely long),导致池恢复空闲后任务仍长时间"沉睡",表现为 merge/mutation 长时间停滞、parts 数量堆积。
修复影响:修复后退避上限被合理约束,池满载解除后任务能够及时恢复调度。对高吞吐写入且并发 merge 密集的集群,可显著降低 parts 堆积与Too many parts风险。
7. dictGet() 参数错误导致崩溃
原始描述:Fix crash on call dictGet() with bad arguments.(#25913)
技术成因:dictGet()/dictGetOrDefault()等外部字典函数在参数校验不充分时(如字典键类型不匹配、属性名不存在、属性类型与目标列不兼容),旧实现可能走到底层解引用崩溃,而不是抛出可读的错误信息。崩溃而非报错意味着查询无法被 catch、服务进程可能受影响,且排障困难。
源码佐证:外部字典函数的实现位于 src/Functions/FunctionsExternalDictionaries.cpp 与 src/Functions/FunctionsExternalDictionaries.h,参数解析与属性解析逻辑均在此处完成;相关稳定性测试也覆盖于 src/Functions/tests/gtest_functions_stress.cpp。
修复影响:修复后非法参数会以标准异常形式(如BAD_ARGUMENTS/TYPE_MISMATCH)被拒绝。对依赖字典做维度补全(典型如dictGet('geo_dict', 'country', code))的查询链路,错误输入从"进程级风险"降级为"可捕获的查询错误"。
8. Query Profiler 栈展开期间的死锁
原始描述:Fix possible deadlock during query profiler stack unwinding.(#25970,关闭 issues/25968)
技术成因:Query Profiler(查询采样剖析器)通过定时信号中断线程并抓取调用栈。当采样信号恰好在某线程持有某个内部锁期间触发栈展开(stack unwinding),而展开过程又需要获取同一把锁时,便形成死锁——采样线程与业务线程互相等待。这类死锁只在特定时序下发生,难以复现但会造成查询永久挂起。
源码佐证:Query Profiler 的实现位于 src/Common/QueryProfiler.cpp 与 src/Common/QueryProfiler.h,其生命周期由 src/Common/ThreadStatus.cpp 管理;信号处理入口在 src/Common/SignalHandlers.cpp。
修复影响:修复后栈展开路径不再与采样信号路径竞争同一把锁,消除了该时序死锁。对长期开启query_profiler_real_time_period_ns/query_profiler_cpu_time_period_ns的集群(即依赖剖析功能做慢查询诊断的生产环境)是重要的稳定性提升。
9. Map 类型整数键序列化到 JSON 的格式错误
原始描述:Fix formatting of typeMapwith integer keys toJSON.(#25982,并由 #26048 在 NOT FOR CHANGELOG 分区补充了正确修复)
技术成因:Map(K, V)类型在 ClickHouse 内部以两个并行数组存储键与值。当K为整数类型(如Map(UInt64, String))并输出到 JSON 格式时,旧实现将键序列化为带引号的字符串({"1":"a"}之类)或存在转义不一致,导致 JSON 输出无法被下游(如 JSON 解析器、前端)正确消费。日志中NOT FOR CHANGELOG分区标注的"Proper fix of serialization of type Map to JSON"表明该修复经历了一次修正迭代。
修复影响:涉及FORMAT JSON、FORMAT JSONEachRow等 JSON 家族输出格式下 Map 列的呈现。对使用 JSON 接口对外提供数据的场景(如 HTTP 查询接口直接对接前端),此修复保证输出符合 JSON 规范。
10. 右子查询 Join 的线程数估计偏差
原始描述:Fix wrong thread estimation for right subquery join in some cases.(#26052,关闭 issues/24075)
技术成因:Join 执行前 ClickHouse 会根据参与方规模估算并行度(线程数)。当右表为子查询(right subquery)时,旧实现对其行数的估计在某些场景(如子查询内部存在聚合、去重或过滤下推)下失真,导致并行度设置不当——要么线程过少浪费 CPU,要么过度调度造成调度开销,均表现为查询执行计划与耗时偏离预期。
修复影响:涉及JOIN右端子查询的并行度推导。对大量使用"大表 JOIN 子查询"模式的 OLAP 场景(典型如先聚合再关联的宽查询),修复后执行计划更贴合数据实际规模。
变更日志的工程方法论:读 changelog 的视角
这份 changelog 本身也体现了可借鉴的工程实践,对使用 ClickHouse 的团队有实际参考价值:
- 回溯机制:每条修复都带有
Backported in #XXXXX标注,将 stable 分支的修复与 prestable 分支的原始提交关联,运维人员可据此追溯修复来源与验证范围; - 问题闭环:多数条目标注了"Closes / Fixes #XXXXX",形成"issue → PR → backport → release"的完整追踪链;
- 诚实分区:
NOT FOR CHANGELOG / INSIGNIFICANT分区明确区分"面向用户的修复"与"内部正确性修补",避免内部细节污染用户可见的更新说明,同时也提醒升级者在解读 changelog 时留意此类隐含修复; - 根因收敛:单条修复(如 ARM 异常处理)一次性关闭 11 个 issue,提示排查相似症状时优先检查已知根因。
升级建议与验证要点
v21.7.2.7-stable 属于 2021 年 7 月的稳定版本线,对仍运行在 21.7 系列(或从 21.7 早期版本升级)的部署,建议在验证后升级。升级后的回归验证建议重点覆盖以下场景:
- 涉及
Date/DateTime混合计算(if、数组构造、CAST)的查询结果一致性; - 使用
PREWHERE且过滤列类型非UInt8的查询; - 对参与 TTL 表达式的列执行
ALTER MODIFY COLUMN; ReplicatedMergeTree上高并发写入与DROP PART并行的稳定性;- 输出
Map整数键列到 JSON 格式的查询; - 右端子查询 JOIN 的执行计划(可通过
EXPLAIN观察线程数与行数估计)。
对于仍在使用 21.7 分支的部署,该版本修复的 10 项缺陷均已在后续长期支持分支中合入,可结合自身升级窗口选择合适的落点;对于新部署,建议直接选用当前仓库所代表的新版本线,并参考 docs/changelogs 目录下的更新说明规划升级路径。
【免费下载链接】ClickHouseClickHouse® is a real-time analytics database management system项目地址: https://gitcode.com/GitHub_Trending/cli/ClickHouse
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考