ClickHouse v20.8.17.25-lts 维护版本解析:子查询常量折叠、ReplicatedMergeTree 变更同步与 ZooKeeper 请求稳定性修复
【免费下载链接】ClickHouseClickHouse® is a real-time analytics database management system项目地址: https://gitcode.com/GitHub_Trending/cli/ClickHouse
本指南聚焦 ClickHouse 20.8 LTS 分支的维护版本 v20.8.17.25-lts 中三个关键修复点:分析阶段子查询常量折叠的正确性修复、ReplicatedMergeTree 表引擎变更(mutation/alter)跨副本等待语义的修复,以及 ZooKeeper 请求在 OOM 场景下可能挂起的问题。文章以官方变更记录为骨架,结合当前仓库源码(src/Interpreters/、src/Storages/、src/Common/ZooKeeper/)验证实现细节,帮助读者理解这些问题在什么场景下发生、修复如何落地,以及如何通过配置规避相关风险。
版本背景:一次 LTS 分支的 Bug Fix 维护迭代
v20.8.17.25-lts 是 ClickHouse 20.8 LTS(Long Term Support)系列中的维护版本,其发布说明位于仓库的 docs/changelogs/archive/v20.8.17.25-lts.md。它基于上一版本 v20.8.16.20-lts 迭代而来,变更内容集中在两个类别:
- Bug Fix(缺陷修复):共 3 项,涉及查询分析阶段的常量折叠、ReplicatedMergeTree 变更等待、ZooKeeper 请求挂起;
- Build/Testing/Packaging Improvement(构建/测试/打包改进):共 1 项,更新时区数据库(timezones)信息到 2020e。
作为 20.8 分支的早期维护版本,它体现了 LTS 分支“不引入新功能、只做稳定性和正确性修复”的迭代策略。20.8 是 ClickHouse 历史上首个 LTS 版本,后续的 21.8、22.8 等 LTS 均沿用这一发布节奏。当时的维护版本尚未使用_lts之外的独立命名,而是直接以.lts后缀标识。
需要说明的是,当前仓库(/data/web/disk1/git_repo/GitHub_Trending/cli/ClickHouse)的docs/changelogs/archive/目录下保存的是 ClickHouse 官方发布历史存档,源码则为演进后的主线版本。因此,下面用于佐证的源码可能包含较 20.8 更新的修改,但其核心逻辑路径仍然成立,且更能在“修复之后”的形态上说明问题的本质。
修复一:禁用子查询在分析阶段无法计算的常量折叠
问题描述
第一条修复(对应官方 PR #18446)指出:在分析(analysis)阶段,当子查询(subquery)的结果无法计算时,必须禁用常量折叠(constant folding)。
在 ClickHouse 中,常量折叠是查询优化器的一项常见优化:当一个表达式节点的所有子节点都能在分析期确定常量值时,优化器会把该表达式直接折叠为一个常量(COLUMN节点),从而避免在运行期重复计算。然而,当表达式涉及**标量子查询(scalar subquery)**时,其值需要执行子查询才能得到。如果分析阶段出于某种原因无法执行该子查询(例如子查询依赖的数据尚未就绪、或该子查询在分析期不可执行),却仍然强行折叠,就会得到错误的结果或产生异常。
源码级验证:常量折叠的开关路径
在主线源码中,常量折叠由ActionsDAG(Action Directed Acyclic Graph,动作有向无环图)负责,核心实现位于 src/Interpreters/ActionsDAG.cpp。
removeUnusedActions系列函数接收allow_constant_folding参数,只有在该参数为真时才执行折叠逻辑:
// src/Interpreters/ActionsDAG.cpp (主线形态) /// Constant folding. if (allow_constant_folding && !node->children.empty() && node->column) { node->type = ActionsDAG::ActionType::COLUMN; node->children.clear(); node->is_deterministic_constant = !non_deterministic_nodes.contains(node); }其语义是:当一个节点的子节点都已变为常量(node->column非空)时,将节点本身替换为COLUMN常量节点并清空其子节点,从而在后续执行中直接使用常量值。
而在 src/Interpreters/ExpressionAnalyzer.cpp 中,allowEarlyConstantFolding()会先检查enable_early_constant_folding设置,再遍历ActionsDAG节点,判断是否存在不适合常量折叠的函数:
// src/Interpreters/ExpressionAnalyzer.cpp bool allowEarlyConstantFolding(const ActionsDAG & actions, const Settings & settings) { if (!settings[Setting::enable_early_constant_folding]) return false; for (const auto & node : actions.getNodes()) { if (node.type == ActionsDAG::ActionType::FUNCTION && node.function_base) { if (!node.function_base->isSuitableForConstantFolding()) return false; } } return true; }对应地,在 PREWHERE 表达式处理中,代码明确以allow_constant_folding = false调用removeUnusedActions,因为“常量可能在查询的其他部分被使用,不能在此处移除”:
// src/Interpreters/ExpressionAnalyzer.cpp(PREWHERE 构建路径) tmp_actions_dag.removeUnusedActions( NameSet{prewhere_column_name}, /* allow_remove_inputs= */ true, /* allow_constant_folding= */ false);这印证了 v20.8.17.25-lts 修复的思路:在无法保证子查询可计算的分析阶段,显式关闭常量折叠,让子查询进入运行期正常执行,而不是在分析期被错误折叠。
对用户的影响与建议
此问题通常不影响绝大多数查询,但如果用户的查询在 WHERE/PREWHERE 中使用标量子查询、且子查询在分析期不可求值,则升级到 v20.8.17.25-lts 后查询结果会更正确。相关可调设置enable_early_constant_folding(布尔值,默认开启)在 src/Interpreters/ExpressionAnalyzer.cpp 中声明为SettingsBool,如有必要可在服务端配置或查询级设置中关闭它。
修复二:ReplicatedMergeTree 变更操作跨副本等待语义
问题描述
第二条修复(对应 PR #22669)指出:在多个副本上等待变更(mutation)完成时,mutation/alter 查询可能在变更实际在其他副本执行完成之前就提前结束。
ReplicatedMergeTree 是 ClickHouse 的复制表引擎,其 DDL/DML 变更(如ALTER TABLE ... UPDATE、DELETE、MODIFY COLUMN)以 mutation 形式写入 ZooKeeper,由各副本的 background 任务异步执行。当用户希望同步等待时,通过mutations_sync设置(对应早期版本的alter_sync)控制等待行为。
源码级验证:waitMutation 的副本枚举逻辑
修复后的等待逻辑位于 src/Storages/StorageReplicatedMergeTree.cpp 的waitMutation():
// src/Storages/StorageReplicatedMergeTree.cpp void StorageReplicatedMergeTree::waitMutation(const String & znode_name, size_t mutations_sync) const { if (!mutations_sync) return; /// we have to wait auto zookeeper = getZooKeeper(); Strings replicas; /// Value 3 (wait only for active replicas) is a `SharedMergeTree` mode that `ReplicatedMergeTree` /// does not have; here it waits for all replicas, like value 2. if (mutations_sync == 2 || mutations_sync == 3) /// wait for all replicas { replicas = zookeeper->getChildren(fs::path(zookeeper_path) / "replicas"); /// This replica should be first, to ensure that the mutation will be loaded into memory for (auto it = replicas.begin(); it != replicas.end(); ++it) { if (*it == replica_name) { std::iter_swap(it, replicas.begin()); break; } } } else if (mutations_sync == 1) /// just wait for ourself replicas.push_back(replica_name); waitMutationToFinishOnReplicas(replicas, znode_name); }关键点解读:
mutations_sync = 0(默认):不等待,变更异步执行,查询立即返回;mutations_sync = 1:只等待当前副本自身完成;mutations_sync = 2(以及等价的 3):等待所有副本完成,先通过zookeeper->getChildren(.../replicas)枚举全部副本,并确保当前副本排在列表首位,以保证“mutation 先被当前副本加载进内存”这一前置条件;- 若修复前枚举副本或判断完成条件的逻辑存在遗漏(例如未正确等待所有副本),就会出现“查询返回了但其他副本还没执行完变更”的一致性问题。
此外,同一文件中还有针对“在副本上发起同步变更”的防御性检查:当mutations_sync > 0且当前查询在某个副本上执行时,会提示“同步 mutation 不受支持或可能导致挂起”,建议改用mutations_sync = 0或换到主节点发起:
// src/Storages/StorageReplicatedMergeTree.cpp if (query_context->getSettingsRef()[Setting::mutations_sync] > 0 && ...) { throw Exception( "Synchronous mutation (mutations_sync = {}) is not supported on a replica with the ...", ...); }mutations_sync本身是SettingsUInt64(src/Storages/StorageReplicatedMergeTree.cpp),可在查询级别使用SETTINGS mutations_sync = N或全局配置指定。
对用户的影响与建议
- 若你的应用依赖“ALTER/MUTATION 完成后才继续”的强一致语义(例如先改 schema 再写入),应显式设置
mutations_sync = 2,并升级到 v20.8.17.25-lts 及以上; - 在设置同步等待时,应通过
system.mutations表观察变更进度;注意同步等待会阻塞发起连接的查询线程,变更量大时应评估耗时; - 非复制场景(MergeTree 引擎)由
IStorage::waitForMutation(src/Storages/IStorage.cpp 中的空实现)提供,可结合MergeTreeSettings.cpp中的相关设置一起了解。
修复三:ZooKeeper 请求在 OOM 异常下的挂起问题
问题描述
第三条修复(对应 PR #22684,修复 issue #22438)指出:在 OOM(Out Of Memory,内存耗尽)异常发生时,ZooKeeper 请求可能出现挂起(hang)。
ClickHouse 与 ZooKeeper(或 Keeper,ClickHouse 自研的协调服务)的交互通过ZooKeeper客户端完成。当内存受限、请求分配失败时,如果客户端没有妥善处理异常路径,请求可能永久阻塞在等待队列或 future 上,进而拖垮依赖 ZooKeeper 的所有后台任务(如复制队列、变更分发)。
源码级验证:ZooKeeper 根节点检查不再挂起
在 src/Common/ZooKeeper/ZooKeeper.cpp 中,对 ZooKeeper root 存在性检查有一段与本次修复直接相关的注释:
// src/Common/ZooKeeper/ZooKeeper.cpp /// Here we check that zk root exists. /// This check is clumsy. The reason is we do this request under common mutex, and never want to hung here. /// Otherwise, all threads which need zk will wait for this mutex eternally. /// /// Usually, this was possible in case of memory limit exception happened inside zk implementation. /// This should not happen now, when memory tracker is disabled. /// But let's keep it just in case (it is also easy to backport). auto future = asyncExists("/"); auto res = future.wait_for(std::chrono::milliseconds(args.operation_timeout_ms)); if (res != std::future_status::ready) throw KeeperException::fromMessage(Coordination::Error::ZOPERATIONTIMEOUT, "Cannot check if zookeeper root exists.");这段代码的要点:
- 该检查在公共互斥锁(common mutex)下执行,如果阻塞等待没有超时保护,所有需要 ZooKeeper 的线程都会等这把锁,造成全局性挂起;
- 修复的关键是给
future.wait_for加上operation_timeout_ms超时:一旦超时未就绪,立即抛出ZOPERATIONTIMEOUT异常而不是无限期等待; - 注释明确说明该场景历史上正是由“zk 实现内部的内存限制(memory limit)异常”触发的——即 OOM 场景,与 v20.8.17.25-lts 的修复描述完全对应;
- 修复在内存追踪器(memory tracker)禁用后虽然不再频繁触发,但仍保留以增强健壮性,并注明“易于 backport”,说明其作为 20.8 维护修复被刻意保留为可移植形态。
另外,在 src/Common/ZooKeeper/IKeeper.h 中可以看到协调协议对内存限制的显式错误码:
// src/Common/ZooKeeper/IKeeper.h ZOUTOFMEMORY = -10, /// Keeper has reached soft memory limit这从协议层印证了“协调服务自身也可能因达到软内存上限返回 OOM 类错误”的现实场景。
对用户的影响与建议
- 该修复面向所有通过
ZooKeeper客户端与协调服务通信的表引擎(复制表、ReplicatedMergeTree、分布式 DDL 队列等),属于稳定性收益; - 用户侧无需额外配置;若曾在高内存压力下观察到“ZooKeeper 请求挂起、后台任务停滞”,升级到 v20.8.17.25-lts 即可消除该类无限等待;
- 运维上仍应监控内存使用与
operation_timeout_ms配置,确保该超时值不过大,以免掩盖协调服务异常。
修复四:时区数据库更新到 2020e
第四条变更为构建/测试/打包类改进:将 timezones(IANA 时区数据库)信息更新到 2020e(对应 PR #18531)。
时区数据影响所有与时区相关的日期时间函数与类型转换(如toDateTime、timeZone()相关行为)。2020e 版本包含该年度各国/地区关于夏令时调整和时区规则变更的最新信息。此更新通过contrib/下的 cctz 等时区组件随构建链编译进 ClickHouse,属于“升级即生效”的改动,无需额外配置。对于强时区敏感的业务(如跨时区报表、定时任务),建议关注后续 LTS 版本中持续更新的时区数据版本。
升级建议与验证方式
升级路径
v20.8.17.25-lts 属于 20.8 LTS 分支的早期维护版本。若你当前运行在 20.8 分支的旧版本(如 v20.8.16.20-lts 或更早),可直接升级到该版本获得上述 3 项缺陷修复;若运行在其他主版本,请遵循对应 LTS 分支的迁移文档,避免跨大版本直接升级带来的兼容性问题。
验证方式
- 变更等待修复:对复制表执行
ALTER TABLE ... UPDATE/DELETE ... SETTINGS mutations_sync = 2,观察语句返回后各副本system.mutations中对应 mutation 的is_done状态是否都已为 1; - 常量折叠修复:构造含标量子查询的 WHERE 条件查询,确认结果正确且
EXPLAIN输出中无异常提前折叠; - ZooKeeper 稳定性:在高内存压力或降低 Keeper 内存上限的场景下观察复制队列任务是否仍能正常推进;
- 时区数据:对比新老版本对特定夏令时日期(如 2020 年规则变化涉及的日期)的时区换算结果。
小结
v20.8.17.25-lts 作为 20.8 LTS 分支的维护版本,体现了该分支“稳定优先、只修不增”的迭代哲学:3 项缺陷修复分别覆盖查询优化正确性(子查询常量折叠)、复制表数据一致性(变更跨副本等待)、系统健壮性(ZooKeeper OOM 挂起),另有 1 项时区数据同步更新。结合当前仓库源码可以看到,这些修复的落点分别是ActionsDAG的常量折叠开关、StorageReplicatedMergeTree::waitMutation的副本枚举与mutations_sync语义、以及ZooKeeper根检查的超时保护。
对于运行 20.8 LTS 的用户,本版本属于值得跟进的维护升级;相关机制(enable_early_constant_folding、mutations_sync、operation_timeout_ms)在更新版本中继续沿用,理解其语义有助于长期运维 ClickHouse 复制集群。
延伸阅读(仓库内资源)
- 变更记录原文:docs/changelogs/archive/v20.8.17.25-lts.md
- 常量折叠实现:src/Interpreters/ActionsDAG.cpp、src/Interpreters/ExpressionAnalyzer.cpp
- 变更等待与副本同步:src/Storages/StorageReplicatedMergeTree.cpp、src/Storages/IStorage.cpp
- ZooKeeper 客户端与错误码:src/Common/ZooKeeper/ZooKeeper.cpp、src/Common/ZooKeeper/IKeeper.h
【免费下载链接】ClickHouseClickHouse® is a real-time analytics database management system项目地址: https://gitcode.com/GitHub_Trending/cli/ClickHouse
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考