关于 ClickHouse v20.8.13.15-lts 变更日志的技术解读
【免费下载链接】ClickHouseClickHouse® is a real-time analytics database management system项目地址: https://gitcode.com/GitHub_Trending/cli/ClickHouse
<output_article>
ClickHouse v20.8.13.15-lts 补丁版本深度解析:TTL 兼容性收紧、Merge 表读取性能优化与 32 项关键修复
本篇文章以 docs/changelogs/archive/v20.8.13.15-lts.md 为核心依据,系统解读 ClickHouse 20.8 LTS 分支补丁版本 v20.8.13.15(相对 v20.8.12.2)包含的 1 项向后不兼容变更、1 项性能改进与 32 项 Bug 修复。文章结合当前仓库源码(src/与programs/)逐一验证每项变更的底层实现与影响范围,帮助 20.8 LTS 用户评估升级风险、理解修复原理,并为低版本升级到 v20.8.13.15 提供自查清单。
版本背景与发布定位
ClickHouse 采用稳定版(LTS,Long Term Support)与预发布版并行的版本策略。v20.8.13.15-lts属于 20.8 LTS 系列的一个补丁版本,其变更内容并非新功能开发,而是集中于稳定性、数据正确性与安全性:
- 1 项 Backward Incompatible Change(向后不兼容变更):涉及 MergeTree 旧建表语法下 Table TTL 的禁用;
- 1 项 Performance Improvement(性能改进):
Merge表引擎在覆盖海量MergeTree表时的读取性能; - 32 项 Bug Fix(缺陷修复):覆盖存储引擎、复制与 Mutation、查询优化器、字典、格式解析、MySQL 协议、ODBC 桥接、内存安全等多个子系统。
所有修复均以"Backported"(回溯移植)形式合入,即先在上游 master 分支修复,再以补丁形式移植到 20.8 LTS 分支,确保 LTS 用户在不升级大版本的前提下获得关键修复。以下按分类逐一解读,并给出当前仓库中的源码佐证。
一、向后不兼容变更:MergeTree 旧语法下 Table TTL 被正式禁止
变更内容
本次版本开始,不再允许使用旧语法创建带 Table TTL 的 MergeTree 表。此前这种行为虽然能成功执行,但 Table TTL 实际上被静默忽略,并不会产生任何过期删除/合并效果,属于"看似成功、实则无效"的误导性行为。本次变更将该场景从"静默无效"改为"显式报错"。
与此同时,ATTACH(附加)通过旧语法创建的此类表仍然是允许的,以保证已有数据的可访问性与平滑迁移。
什么是"旧语法"
MergeTree 家族在建表时有两种语法形态。旧语法将index_granularity等参数作为引擎参数直接传入,例如:
-- 旧语法:index_granularity 作为引擎构造参数 CREATE TABLE old_syntax_t ENGINE = MergeTree('2020-01-01', (CounterID, EventDate), (CounterID, Date), 8192) SETTINGS index_granularity = 8192;新语法则将这些内容统一放入SETTINGS与ORDER BY/PARTITION BY等子句中:
-- 新语法 CREATE TABLE new_syntax_t ENGINE = MergeTree PARTITION BY toYYYYMM(EventDate) ORDER BY (CounterID, Date) SETTINGS index_granularity = 8192;在 src/Storages/MergeTree/registerStorageMergeTree.cpp 中可以看到旧语法参数的解析逻辑:当第 4 个引擎参数是UInt64字面量时,它被识别为index_granularity并写回存储设置;紧接着对旧语法强制校验 Table TTL:
if (args.storage_def->ttl_table && args.mode <= LoadingStrictnessLevel::CREATE) throw Exception(ErrorCodes::BAD_ARGUMENTS, "Table TTL is not allowed for MergeTree in old syntax");即:只有在CREATE(而非ATTACH)且带 Table TTL 时才抛出BAD_ARGUMENTS,与变更日志中"Attach of old tables is still possible"完全对应。此外,同一批次还收紧了MODIFY TTL:对旧语法创建的 MergeTree 表执行MODIFY TTL此前"查询成功但无实际效果",本次起同样被禁止(参见该版本的对应修复条目)。
升级影响与处理建议
- 如果线上存在用旧语法创建且声明了 Table TTL 的表,升级后这些表仍可正常读取与附加,但建表语句会在新版本被拒绝;
- 如需让 Table TTL 真正生效,建议用新语法重新创建表(例如
CREATE ... ENGINE = MergeTree PARTITION BY ... ORDER BY ... TTL ...),再迁移数据; - 该变更同时涉及
ALTER TABLE ... MODIFY TTL的限制,运维脚本中若包含对旧语法表的此类操作,需要先重建表。
二、性能改进:Merge 表引擎读取海量 MergeTree 表时的性能修复
问题背景
Merge表引擎(StorageMerge)本身不存储数据,而是将同一数据库内(或通过正则匹配的)多个表聚合为一个逻辑视图,供查询统一访问。当被合并的表数量达到数千、数万级别时,每次读取都需要在所有源表上枚举并定位数据部分,开销随源表数量线性膨胀,性能出现明显劣化(原 issue #7748)。
修复与源码对照
修复思路围绕StorageMerge对源表枚举、读取器创建路径上的冗余工作进行优化。从 src/Storages/StorageMerge.h 可以看到其核心结构:
class StorageMerge final : public IStorage, WithContext { using DBToTableSetMap = std::map<String, std::set<String>>; ...DBToTableSetMap维护"数据库 → 表集合"的映射,而 src/Storages/StorageMerge.cpp 的getDatabaseIterators则负责按数据库/正则获取迭代器:
StorageMerge::DatabaseTablesIterators StorageMerge::getDatabaseIterators(ContextPtr context_) const { return database_name_or_regexp.getDatabaseIterators(context_); }本次优化减少了在为Merge表构造读取计划时对每个源表重复进行的元数据获取与校验,将原本与源表数量强相关的开销显著降低。对于"一个 Merge 视图下挂几百上千张分区表/日志表"的经典运维场景(如按天分表的日志查询),该修复能直接改善查询延迟。
三、Bug Fix 深度解读:按子系统分组
3.1 MySQL 协议:INSERT 返回真实受影响行数
现象:通过 MySQL 协议执行 INSERT 时,客户端收到的影响行数恒为 0,与预期不符(issue #16605)。
修复:在 src/Server/MySQLHandler.cpp 中,服务器在处理查询时注册了一个进度回调,累计Progress::written_rows:
std::atomic<size_t> affected_rows {0}; auto prev = query_context->getProgressCallback(); query_context->setProgressCallback(&, my_prev = prev { if (my_prev) my_prev(progress); affected_rows += progress.written_rows; });查询结束后(无结果集输出时)通过OKPacket将该计数返回给 MySQL 客户端(src/Server/MySQLHandler.cpp):
if (!with_output) packet_endpoint->sendPacket(OKPacket(0x00, client_capabilities, affected_rows, 0, 0));影响:对使用 MySQL 客户端/驱动(JDBC、Python、ORM 等)接入 ClickHouse 的应用,affected_rows语义与 MySQL 一致,写入类应用的状态反馈从此可信。
3.2 Mutation / ALTER 挂起类修复(两项)
本版本集中修复了两个与 Mutation 生命周期相关的挂起问题:
ALTER查询在对应 mutation 于其他副本被 kill 后挂起:复制表上,一个副本上KILL MUTATION后,另一个副本正在执行的ALTER可能永久等待,本次修复补全了该状态下的唤醒与终止路径(issue #16953);ALTER在 mutation 被 kill 后挂起(由线程 fuzzer 发现):进一步覆盖了与上述问题相关的竞态窗口,避免ALTER与KILL MUTATION交错时出现死锁(issue #17244 之外的另一条路径)。
另外还有一项极罕见的 mutation 挂起修复:在DROP / DETACH / REPLACE / MOVE PARTITION之后,mutation 可能因 partition 状态变化而停滞,该问题此前已由 #15537 部分修复,本次将其余场景补齐(issue #19443 相关)。
运维意义:对于依赖ALTER ... UPDATE / DELETE进行数据订正、并在异常时执行KILL MUTATION的团队,这三项修复显著降低了"清理任务卡死、system.mutations长期处于 waiting"的概率。
3.3 内存安全与崩溃修复(四类关键漏洞)
topK聚合函数可能的段错误:topK在特定数据分布下可能发生非法内存访问导致服务崩溃(issue #17404);bitmapAndnot函数段错误:bitmapAndnot(a, b)对特定输入触发 segmentation fault(issue #19668);arrayEnumerateUniq崩溃或死循环:传入非常规参数时可能导致进程崩溃或无限循环(issue #19787);- H3 库缓冲区溢出:修复了上游 Uber H3 库中一个可能的 buffer overflow(内存读取),ClickHouse 侧随之修复(issue #19219,上游见 uber/h3#392);
addMonth缓冲区溢出(内存读取):addMonth(date, n)以精心构造的参数调用时可能发生越界读取(issues #19441 / #19413)。
其中arrayEnumerateUniq与addMonth均由 ClickHouse 维护者 Alexey Milovidov 直接修复,体现了 20.8 分支对外部输入可触发的内存安全问题的重视——这类问题在多租户、公网暴露的 ClickHouse 部署中属于必须优先升级的范畴。
3.4 查询正确性类修复
- 谓词优化器与不确定函数:开启 predicate optimizer 时,含不确定函数(如随机数、时间函数)的过滤条件下推/重排导致结果不确定(issue #17244);
groupUniqArray对 Enum 参数的返回类型:此前对Enum类型参数可能返回错误的列类型(issue #17875);- JOIN 中非零默认值:当被连接类型的默认值非零(如部分
Enum值)时,JOIN 结果中的默认填充值错误(issue #18197); neighbor函数与LowCardinality:neighbor(column, offset)在参数为LowCardinality列时返回错误结果(issue #10333)。
neighbor的修复非常典型。在 src/Functions/neighbor.cpp 中,FunctionNeighbor声明不启用 LowCardinality 的默认实现,注释明确说明了原因:
/// We do not use default implementation for LowCardinality because this is not a pure function. /// If used, optimization for LC may execute function only for dictionary, which gives wrong result. bool useDefaultImplementationForLowCardinalityColumns() const override { return false; }即:neighbor是有状态、非纯函数(返回结果依赖行的遍历顺序),若按 LowCardinality 字典去重执行,将只对字典值求值而得到错误结果。该修复同时将函数标记为 deprecated(见 src/Functions/neighbor.cpp),建议改用规范窗口函数或显式开启allow_deprecated_error_prone_window_functions。
Date溢出行为统一:将Date类型的严格上限固定为2106-02-07,超过该值强制转换为 0,消除"同一年份不同溢出值"的不一致;- CREATE DICTIONARY 的 id 表达式:修复了字典创建语句中 id 表达式解析/生成错误(PR #19571);
- RANGE_HASHED 字典非法解引用:
RANGE_HASHED()字典在特定边界输入下可能发生无效内存解引用(PR #20345); - 字典查询缺失键的行为不一致:修复了查询字典中不存在键时结果不稳定的问题(PR #20578)。
3.5 格式与解析类修复
- ORC 格式无限读取:从 ORC 文件读取时可能陷入无限循环,该缺陷由早期 #10580 引入,本次修复(issue #19095);
- 损坏 JSON 导致内存异常:遇到损坏的 JSON 时,旧代码试图将整个文件读入内存,可能触发分配器异常(issue #19719)。修复后不再整体加载,避免大文件撑爆内存;
- S3 URL 解析
std::out_of_range:解析特定 S3 URL 时抛出basic_string越界异常(PR #18059)。
3.6 系统表与日志类修复
system.stack_trace在守护进程模式下为空:服务器以 daemon 方式运行时,该表无法获取线程栈信息,本次修复(PR #17630);system.settings_profile_elements填充错误:该表未能正确列出设置配置文件的元素(issue #18231);- Logger 参数数量不匹配:修复日志器在参数个数不一致时可能崩溃的问题(PR #18717)。
3.7 复制与后台线程类修复
ON CLUSTER后台线程挂起:执行ON CLUSTER查询的后台线程可能在已删除的复制表上等待,本次修复避免其永久阻塞(PR #19684);- 复制表
ALTER在 mutation 被其他副本 kill 后挂起:见 3.2 节。
3.8 表引擎与存储行为类修复
MergeTree 合并期禁用 AIO 写入:合并过程中使用异步 I/O(AIO)写入存在极小概率导致主键列数据损坏,本次默认禁用合并期间的 AIO 写路径。相关的 Direct I/O 行为可通过
min_merge_bytes_to_use_direct_io设置控制(src/Storages/MergeTree/MergeTreeSettings.cpp):合并数据时,若参与合并的数据总量超过
min_merge_bytes_to_use_direct_io(默认 10 GB),则使用O_DIRECT直读直写;设为 0 则禁用直接 I/O。该项属于"以极小的性能代价换取数据完整性保障"的取舍,对追求数据正确性的生产环境是重要改进;
Distributed表插入空间不足时段错误:向Distributed表写入时若磁盘空间不足,可能触发段错误而非优雅报错(PR #17737);*CollapsingMergeTree/ReplacingMergeTree的版本列保护:禁止对version列执行DROP/RENAME,此前操作可能破坏折叠/去重语义(PR #20300);MongoDB 表引擎懒连接:
MongoDB表引擎改为"真正读数据时才建连",ATTACH TABLE不再尝试连接 MongoDB,避免启动时因 MongoDB 不可用而失败(PR #20110);mutation 中转义文本序列化错误:
ALTER ... UPDATE col = CAST('foo', 'Enum8(\'foo\' = 1')这类含转义字符的 mutation 序列化不正确,导致执行失败或结果错误(issue #18878);MergeTree 旧语法 dedup 提示:与 TTL 变更同源,非复制 MergeTree 在旧语法下不支持去重,相关逻辑在 src/Storages/StorageMergeTree.cpp 有对应校验。
3.9 ODBC 桥接修复
clickhouse-odbc-bridge两项问题被一并修复:
- 在同时支持 IPv4/IPv6 双栈的主机上,服务器可能无法访问 ODBC bridge 进程;
- ODBC 字典更新可能使用格式错误的查询执行,或直接导致崩溃(关联 issue #14489)。
对依赖 ODBC 字典接入外部数据库(如 SQL Server、其他 MySQL 实例)的部署,建议确认升级后 ODBC 字典的更新与查询行为恢复正常。
四、升级建议与验证清单
对于运行在 20.8.x 的实例,升级到 v20.8.13.15-lts 前建议按以下清单自查:
- 扫描建表 DDL:找出所有使用旧语法
ENGINE = MergeTree(partition_key, order_by, index_granularity, ...)且带有TTL子句的表,升级前先用新语法重建(旧表升级后仍可 ATTACH 读取); - 检查
MODIFY TTL脚本:涉及旧语法表的ALTER TABLE ... MODIFY TTL操作在新版本会被拒绝; - 确认 AIO 配置:若自定义过与 merge 直接 I/O 相关的设置(
min_merge_bytes_to_use_direct_io),确认与新的 AIO 写禁用策略兼容; - 回归高风险函数:针对涉及
neighbor、topK、groupUniqArray、bitmapAndnot、arrayEnumerateUniq、addMonth的报表查询做回归,重点验证LowCardinality与Enum参数场景; - 验证 MySQL 协议客户端:检查依赖
affected_rows的应用行为是否与预期一致; - 监控 mutation 队列:升级后观察
system.mutations,确认历史遗留 mutation 均能正常推进。
总结
v20.8.13.15-lts 是 20.8 LTS 分支中一次以"正确性、稳定性、安全性"为主旨的重要补丁:它通过禁用旧语法 Table TTL消除了"静默无效"的隐患;通过Merge 表读取优化改善了海量分表场景的查询体验;并通过 32 项修复覆盖了内存安全、Mutation 生命周期、字典一致性、格式解析、MySQL 协议等多个子系统。结合 docs/changelogs/archive/v20.8.13.15-lts.md 与 src/Storages/MergeTree/registerStorageMergeTree.cpp、src/Server/MySQLHandler.cpp、src/Storages/StorageMerge.cpp、src/Functions/neighbor.cpp 等源码,可以确认每一项变更都有明确的实现与校验逻辑支撑。对于仍在 20.8 LTS 线上的用户,本版本是值得尽快跟进的关键补丁;对于高版本用户,文中涉及的语义收紧(如neighbor弃用)与安全修复思路同样具有参考价值。 </output_article>
【免费下载链接】ClickHouseClickHouse® is a real-time analytics database management system项目地址: https://gitcode.com/GitHub_Trending/cli/ClickHouse
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考