ClickHouse v20.8.5.45-lts 版本发布解读:复制表、分布式查询与内存安全修复全解析

原创2026-09-09 19:19:331,814 阅读
文章标签:数据库列式数据库OLAP大数据数据分析实时分析

ClickHouse v20.8.5.45-lts 版本发布解读:复制表、分布式查询与内存安全修复全解析

本篇文章基于当前仓库 docs/changelogs/archive/v20.8.5.45-lts.md 的发布说明,围绕 ClickHouse 20.8 LTS 分支的补丁版本 v20.8.5.45-lts 展开解读。该版本相比 v20.8.4.11-lts,集中修复了 ReplicatedMergeTree 复制协议、Distributed 表查询改写、MATERIALIZED VIEW 数据写入、内存分配与哈希表等核心路径上的缺陷,并新增了集群 DDL 与分片裁剪相关的两个实用改进。读完本文,你将了解这一 LTS 补丁版本修复了哪些实际问题、每个修复背后的底层机制,以及如何在当前仓库源码中定位这些实现细节。

版本概览

v20.8.5.45-lts 是 ClickHouse 20.8 LTS(长期支持)分支上的一个补丁版本,变更基线为 v20.8.4.11-lts。整个变更清单包含:

  • 2 项改进(Improvement):集群 DDL 与分片裁剪的可用性提升;
  • 26 项 Bug 修复(Bug Fix):覆盖复制表、物化视图、分布式查询、RBAC、字典、内存管理等模块;
  • 1 项其他变更(Other):ReplicatedMergeTree 参数宏展开。

这些修复大多是从后续版本或主分支 backport(反向移植)到 20.8 LTS 分支的,说明它们属于社区确认过的高价值修复。

改进一:允许在集群上执行 ALTER ... ON CLUSTER,不再受 internal_replication 约束

变更内容:此前,当集群配置中开启了 <internal_replication> 时,执行 ALTER ... ON CLUSTER 会被限制;本次修复后,无论 <internal_replication> 设置如何,都允许执行集群级 DDL。对应 PR 为 #16075。

背景与原理:在 ClickHouse 的分布式架构中,Distributed 表只是逻辑视图,真正的数据存储在底层分片表(通常为 ReplicatedMergeTree)中。<internal_replication> 决定分布式写入时数据分发方式:

  • 当 internal_replication = true 时,写入端只向每个分片的一个副本插入,由复制表自身(通过 ZooKeeper/Keeper 协调)将数据复制到其余副本;
  • 当 internal_replication = false 时,写入端向每个分片的所有副本都插入数据。

对于数据写入而言两者各有适用场景,但对于 ALTER 这类 DDL 操作,能否在 ON CLUSTER 下执行本不应依赖 internal_replication 的取值——DDL 的复制语义与数据插入的复制语义是正交的。本次改进正是消除了这一不合理的耦合,使运维人员可以统一通过 ON CLUSTER 语法管理整个集群的 schema。

使用方式(当前版本语义):

ALTER TABLE cluster_all.sharded_table ON CLUSTER my_cluster ADD COLUMN new_col UInt32 DEFAULT 0;

修复后,无论 my_cluster 配置中是否开启 internal_replication,上述语句都能正常在集群所有分片/副本上执行。

改进二:新增 allow_nondeterministic_optimize_skip_unused_shards,允许分片键中使用非确定性函数

变更内容:新增设置项 allow_nondeterministic_optimize_skip_unused_shards,允许在分片键(sharding key)中使用 rand()、dictGet() 等非确定性函数。对应 PR 为 #16105。

源码实证:该设置项定义在 src/Core/Settings.cpp:

DECLARE(Bool, allow_nondeterministic_optimize_skip_unused_shards, false, R"(
Allow nondeterministic (like `rand` or `dictGet`, since later has some caveats with updates) functions in sharding key.

Possible values:

- 0 — Disallowed.
- 1 — Allowed.
)", 0) \

默认值与作用:默认值为 0(禁用)。其配套设置是 optimize_skip_unused_shards(见 src/Core/Settings.cpp 附近),后者用于在分布式查询中对每个分片执行「分片裁剪」(shard pruning)——即根据 WHERE 条件中的分片键表达式判断哪些分片不可能包含匹配数据,从而只把查询发送到必要的分片,减少网络与计算开销。

分片裁剪的正确性依赖分片键表达式的确定性:只有当给定行号时其取值可复现,才能安全推断某个分片一定不含结果。因此默认情况下,ClickHouse 会拒绝在分片键中使用 rand()、dictGet() 这类非确定性或依赖外部状态的函数。本次改进提供显式开关,让明确知道自身分片键行为(例如接受随机分片、或字典更新不影响正确性)的用户解除限制。相关查询改写逻辑位于 src/Interpreters/ClusterProxy/executeQuery.cpp(optimize_skip_unused_shards_nesting 对嵌套查询的传递控制也在该文件中)。

启用方式:

SET allow_nondeterministic_optimize_skip_unused_shards = 1;

Bug 修复:复制表(ReplicatedMergeTree)相关

复制表是 ClickHouse 高可用的基石,本次版本围绕它修复了多项问题,其中有两项与副本可用性直接相关,值得重点说明。

提升 max_replicated_logs_to_keep 默认值,减少副本丢失

变更内容:修复了 max_replicated_logs_to_keep 设置项默认值过低导致副本频繁丢失的问题;同时改进了丢失副本的恢复流程——选择数据最新的副本作为克隆来源,并且对丢失副本上的旧数据分片从「删除」改为「分离(detach)」。对应 PR 为 #15701。

源码实证:该设置项定义于 src/Storages/MergeTree/MergeTreeSettings.cpp:

DECLARE(NonZeroUInt64, max_replicated_logs_to_keep, 1000, R"(
How many records may be in the ClickHouse Keeper log if there is inactive
replica. An inactive replica becomes lost when when this number exceed.

Possible values:
- Any positive integer.
)", 0) \

机制解读:ReplicatedMergeTree 依赖 ZooKeeper/ClickHouse Keeper 中的复制日志(log)协调各副本。当某个副本长期离线,其落后的日志条目数超过 max_replicated_logs_to_keep 时,该副本会被标记为「lost」(丢失),必须从其他副本克隆全部数据才能恢复,代价高昂。日志清理线程的实现位于 src/Storages/MergeTree/ReplicatedMergeTreeCleanupThread.cpp:当日志条目数超过该阈值时,仅保留最近 max_replicated_logs_to_keep 条,其余被清理。

若该值过小,短暂离线的副本也会因日志被提前清理而被迫进入全量克隆流程。本次修复将默认值提升为 1000,显著降低了副本「丢失」的概率。同时配套的改进包括:

  • 选择最更新的副本作为克隆源:全量克隆时优先选择数据最完整的副本,缩短恢复时间;
  • 旧数据分片 detach 而非删除:丢失副本上已有的旧 part 被分离而不是直接删除,为后续可能的排查与数据恢复留有余地。

同文件还定义了配套的 min_replicated_logs_to_keep(默认 10,见 src/Storages/MergeTree/MergeTreeSettings.cpp),用于即使条目过时也保底保留最近若干条日志,便于诊断。

复制表参数校验:多余参数不再被静默忽略

变更内容:向 ReplicatedMergeTree 传入多余参数时,此前会静默忽略,本次修复改为直接抛出错误。对应 PR 为 #15516。

背景:ReplicatedMergeTree 引擎的参数数量是固定的(例如 ReplicatedMergeTree('/clickhouse/tables/{shard}/table', '{replica}'))。用户一旦写错参数个数,静默忽略很容易造成配置与预期不符的隐患(例如把日志路径或副本名写错却不报错)。修复后,参数数量不匹配会立即报错,帮助用户在创建表阶段就发现配置错误。

其他复制表修复

  • 修复 ALTER MODIFY ... ORDER BY 在 ReplicatedVersionedCollapsingMergeTree 上挂起(PR #16011):DDL 与复制队列协调不当会导致查询长时间不返回。
  • 修复复制错误在空闲一段时间后导致副本挂起 5-10 分钟(PR #15987):长时间无复制活动后发生错误,副本的恢复逻辑被卡住。
  • 修复 DROP TABLE IF EXISTS、DROP/DETACH DATABASE 与并发重命名/删除时的竞争(PR #15934):涉及 Atomic 数据库引擎下 DDL 并发执行的竞态,包括 Table ... doesn't exist 误报与罕见的死锁。
  • 修复向/从 MaterializedView 写入读取时并发 DROP 目标表导致的罕见段错误(PR #15984)。

分离只读副本时正确递减 ReadonlyReplica 指标

变更内容:当分离(detach)只读副本时,ReadonlyReplica 系统指标此前未递减,导致指标失真;本次修复补上了递减逻辑,对应 issue #15598、PR #15592。

机制解读:ReadonlyReplica 是 ClickHouse 记录当前处于只读状态副本数量的指标。副本进入只读状态(例如 ZooKeeper 会话超时、表被置为只读)时指标递增,但 detach 表时若忘记递减,system.metrics 中该值会虚高,误导监控告警。修复确保了 detach 只读表后指标回到正确值。

Bug 修复:分布式查询与优化器

Distributed 表查询在 WHERE + PREWHERE + GLOBAL IN 组合下返回错误空结果

变更内容:修复 Distributed 表在查询同时包含 WHERE、PREWHERE 和 GLOBAL IN 时可能错误返回空结果的问题(issue #15792、PR #15933)。

机制解读:GLOBAL IN 需要把右端子查询的结果分发到所有远程分片,而 PREWHERE 会改变列的计算顺序。三者组合时,远程分片上的查询改写存在缺陷,导致过滤条件被错误应用而返回空集。这一修复涉及 src/Interpreters/ClusterProxy/executeQuery.cpp 中的分布式查询规划逻辑——该文件同时承载了 force_optimize_skip_unused_shards、optimize_skip_unused_shards 等分片裁剪设置在嵌套查询间的传播控制。

修复 SELECT ... ORDER BY DESC 在 ALTER MODIFY COLUMN 后的结构不匹配异常

变更内容:修复 SELECT ... ORDER BY DESC 在 ALTER MODIFY COLUMN 之后执行时报 Block structure mismatch 异常的问题(issue #15800、PR #15852)。列类型修改后,排序执行器与下游 pipeline 的列结构元数据未同步刷新。

修复 ALTER MODIFY COLUMN ... DEFAULT 与列类型不兼容时报错

变更内容:当 ALTER MODIFY COLUMN ... DEFAULT ... 指定的默认值表达式与列类型不兼容时,现在会直接抛出异常,而不是在后续使用中暴露问题(issue #15854、PR #15858)。

修复 GROUP BY 与 WITH TOTALS/ROLLUP/CUBE 组合下 min/max 聚合异常

变更内容:修复 GROUP BY 带 TOTALS/ROLLUP/CUBE 修饰符、且对分组键执行 min/max 聚合时结果异常的问题(issue #16393、PR #16397)。

异步分布式写入:prefer_localhost_replica=0 与 internal_replication 组合修复

变更内容:修复异步 Distributed INSERT 在 prefer_localhost_replica=0 且开启 internal_replication 时的行为(PR #16358)。

机制解读:prefer_localhost_replica=0 意味着写入时不优先选择本机副本;配合 internal_replication 时,异步写入路径需要为每个分片挑选一个副本投递数据。修复前该组合下副本选择逻辑存在缺陷。

Bug 修复:物化视图、虚拟列与字典

MATERIALIZED VIEW 写入时 ARRAY JOIN 导致 Cannot find column

变更内容:修复向 MATERIALIZED VIEW 插入数据时,若 MV 查询包含 ARRAY JOIN 会报 Cannot find column 错误的问题(PR #15717)。ARRAY JOIN 生成的临时列在写入链路中未正确保留,导致下游查找列失败。

仅查询虚拟列时报 Not found column _nothing in block

变更内容:修复仅选择虚拟列(virtual columns)的查询可能抛出 Not found column _nothing in block 异常的问题(issue #12298、PR #15756)。当查询没有普通列而只有虚拟列时,生成的空块结构未正确处理。

Buffer 表与目标表结构不一致时的空管道错误

变更内容:修复从 Buffer 表读取、且 Buffer 表结构与目标表不同、目标表返回空结果时,报 Cannot add simple transform to empty Pipe 错误的问题(issue #15529、PR #15662)。

字典相关修复

  • 允许 complex key 使用 direct 布局(PR #16007):此前 direct 字典布局不支持复杂键,本次放开限制。
  • 修复 dictGet 在分片键等持久化函数上下文中使用时的缺陷(PR #16205):函数上下文被永久缓存(如 sharding key)时,dictGet 的求值可能错误。
  • 修复 dictGet 函数在字典加载出错时异常路径的双重释放(double free)(PR #16429)。

Bug 修复:RBAC 与 SQL 解析

修复 RBAC 中可能的死锁

变更内容:修复基于角色的访问控制(RBAC)中可能的死锁问题(PR #15875)。权限检查涉及多把锁的嵌套获取,特定并发顺序下可能互相等待。

修复设置配置文件解析歧义:SETTINGS profile readonly

变更内容:修复 CREATE USER ... SETTINGS profile readonly 的解析歧义(issue #15628、PR #15982)。此前该语句可能被误解析为「名为 profile 的设置项附加 readonly 约束」,修复后明确按「引用名为 readonly 的 profile」处理。

修复排序规则/字符集解析器,支持字符串类型 length = 0

变更内容:修复 collate 名称与字符集名称的解析器,并支持字符串类型 length = 0 的声明(PR #16008)。

修复 indexOf 与 LowCardinality 参数组合的误报

变更内容:修复 indexOf([1, 2, 3], toLowCardinality(1)) 被错误禁止的问题(PR #16038)。这是此前某次回归引入的,本次恢复了对该合法组合的支持。

Bug 修复:内存与资源管理

修复内存可能超出限制被过度分配的问题

变更内容:修复在某些场景下内存分配可能无视 max_memory_usage 等限制被过度分配的问题(issue #14560、PR #16206)。内存跟踪器与实际分配之间存在窗口期,特定并发路径下可绕过限制。

修复 GROUP BY 字符串键的内存泄漏

变更内容:修复 GROUP BY 使用字符串键时,TwoLevelStringHashTable 实现中的错误导致的内存泄漏(PR #16264)。两级字符串哈希表在内存重分配或迁移过程中未正确释放旧桶内存。

修复数据库创建失败时的崩溃

变更内容:修复数据库创建失败时进程崩溃的问题(PR #15954)。创建流程的部分初始化失败后,清理路径未正确处理未完全初始化的对象。

S3 表函数 glob 模式下的 region 配置修复

变更内容:修复 S3 表函数使用 glob(通配符)模式时,从 URL 解析出的 region 未应用到 S3 客户端配置的问题(PR #15646)。使用 glob 时客户端配置的初始化走的是另一条路径,region 参数因此丢失。

其他变更:复制表参数中的宏展开

变更内容:在创建表时,展开 ReplicatedMergeTree 参数中的 {database}、{table}、{uuid} 宏(PR #16159)。

机制解读:这使得用户可以在引擎参数中直接写:

CREATE TABLE default.t ON CLUSTER my_cluster
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{database}/{table}', '{replica}')

创建时 {database} 与 {table} 会被替换为实际库名与表名,{uuid} 在启用 UUID 时展开。这样复制日志路径与表名强绑定,避免了表被重命名或在不同库中同名时路径冲突的问题,也让集群内多表配置更统一、更不易出错。

运维建议小结

针对 v20.8.5.45-lts 的变更,结合源码给出以下运维要点:

  1. 关注复制日志阈值:max_replicated_logs_to_keep 默认已提升至 1000(定义见 src/Storages/MergeTree/MergeTreeSettings.cpp)。如果你的副本经常因为离线而被标记为 lost,可在表设置中进一步调大该值,并确认 min_replicated_logs_to_keep 的取值适合你的诊断习惯。
  2. 分片裁剪开关按需开启:若分片键确实包含 rand() 或 dictGet() 等非确定性表达式,且你能接受其语义,可显式设置 allow_nondeterministic_optimize_skip_unused_shards = 1(默认 false,见 src/Core/Settings.cpp)来恢复分片裁剪能力。
  3. 升级后验证 DDL 与监控指标:升级到本版本后,可验证 ALTER ... ON CLUSTER 在 internal_replication=true 集群上的执行,以及 detach 只读副本后 ReadonlyReplica 指标(system.metrics)是否正确回落。
  4. 关注复制恢复行为变化:丢失副本的恢复流程已调整为「选择最新副本克隆 + 旧 part 分离而非删除」,建议在升级后观察一次真实的副本恢复过程,确认监控与告警阈值仍适用。

总结

v20.8.5.45-lts 作为一个 LTS 补丁版本,修复密度高且针对性强:复制表相关的三项修复(日志阈值、恢复流程、参数校验)直接关乎生产环境的数据可靠性与可用性;分布式查询与优化器的几项修复消除了 GLOBAL IN、ORDER BY DESC、GROUP BY ... WITH TOTALS/ROLLUP/CUBE 等常见查询形态下的错误结果;内存相关的两项修复则降低了 OOM 与泄漏风险。对于 20.8 LTS 分支的用户,建议结合上文列出的源码定位(MergeTreeSettings.cpp、Settings.cpp、ClusterProxy/executeQuery.cpp、ReplicatedMergeTreeCleanupThread.cpp)逐项核对自身环境,平稳完成升级。

登录后查看全文
ClickHouse