ClickHouse v23.3.12.11-lts 补丁版本深度解析:异步插入去重、目录关停死锁与稀疏列等四项关键修复
【免费下载链接】ClickHouseClickHouse® is a real-time analytics database management system项目地址: https://gitcode.com/GitHub_Trending/cli/ClickHouse
v23.3 是 ClickHouse 的 LTS(长期支持)分支,v23.3.12.11-lts是该系列的第 12 个补丁版本,面向线上稳定运行场景提供向后兼容的关键修复。本文以该版本的官方变更日志(docs/changelogs/archive/v23.3.12.11-lts.md)为主体,逐一拆解其中 4 项用户可见的 Bug Fix 与 1 项内部修复,并结合当前仓库源码还原其底层机制。读完本文,你将理解异步插入在复制表上的去重语义、服务器关停阶段的锁序问题、稀疏列在 JOIN 中的行为差异,以及rows_before_limit_at_least在延迟源(DelayedSource)场景下的计数规则,从而更准确地评估本次升级的收益与风险。
版本背景:LTS 分支中的补丁发布节奏
在 ClickHouse 的版本体系中,LTS 分支(如 v23.3)与创新分支(如 v23.4+ 的常规发布)并行演进。LTS 分支长期接收小步幅的补丁修复,每个补丁版本通常只包含少量高价值的稳定性修复,不会引入新功能。v23.3.12.11-lts相对上一个补丁版本v23.3.11.5-lts(变更日志见 docs/changelogs/archive/v23.3.11.5-lts.md)而言,新增了 4 项被标记为 "Bug Fix (user-visible misbehavior in an official stable release)" 的修复——即官方稳定版中用户可见的异常行为,以及 1 项 "NOT FOR CHANGELOG / INSIGNIFICANT"(不进对外变更日志、仅内部用途)的修复。
这类补丁版本适合通过常规升级通道滚动部署,但部署前建议逐条核对修复点是否命中自身的线上场景,并利用 tests/queries 中的回归用例做验证。
修复一:ReplicatedMergeTree 上异步插入与去重算法的兼容问题(#51676)
变更日志条目:Fix async insert with deduplication for ReplicatedMergeTree using merging algorithms(修复使用合并算法的 ReplicatedMergeTree 的异步插入去重)。
问题场景
异步插入(async_insert = 1)是 ClickHouse 为降低高频小批量写入压力而提供的缓冲机制:客户端将多条小 INSERT 合并为更大的块后真正落盘。去重(deduplication)则保证同一数据块在重试、网络抖动等场景下不会被重复写入。当二者叠加在ReplicatedMergeTree系列引擎上,并通过"合并算法"路径(异步缓冲块被合并后参与复制日志提交)时,旧逻辑可能出现去重失效或误判,进而产生重复数据。
底层机制:同步与异步插入共享同一去重目录
从当前仓库源码看,复制表插入的去重逻辑集中在 ReplicatedMergeTreeSink.cpp:
, deduplicate( [&] { /// Sync and async inserts share the unified deduplication_hashes directory; /// replicated_deduplication_window governs both enabling and retention. return mt_settings[MergeTreeSetting::replicated_deduplication_window] != 0; }) , is_async_insert(async_insert_)即同步插入与异步插入最终都向统一的deduplication_hashes目录(存放在 ClickHouse Keeper 中)写入去重哈希,由replicated_deduplication_window统一控制开关与保留窗口。该窗口的语义在 MergeTreeSettings.cpp 中有明确说明:哈希覆盖整个插入块,因此只有整块数据与历史块完全一致时才判定为重复(适用于重试场景),而非逐行比对;窗口过大时插入会比较慢,因为需要比对更多条目。
与异步插入相关的两个旧设置replicated_deduplication_window_for_async_inserts与replicated_deduplication_window_seconds_for_async_inserts在最新源码中已被标记为 "Legacy setting retained for mixed-version rolling upgrades"(MergeTreeSettings.cpp):新写入统一使用replicated_deduplication_window与replicated_deduplication_window_seconds管理,旧设置只约束滚动升级期间由旧副本写入async_blocks目录的旧式异步哈希的保留数量与时长,并由当前 leader 负责清理。
工程验证
仓库为此保留了专门的回归测试 gtest_async_inserts.cpp,覆盖异步插入场景下的去重行为;异步块 ID 的内存缓存机制实现在 AsyncBlockIDsCache.cpp,通过detectConflicts在写入前检测与历史块的冲突,避免 Keeper 往返。
修复二:DatabaseCatalog 关停阶段的死锁(#51908)
变更日志条目:Fix deadlock on DatabaseCatalog shutdown(修复 DatabaseCatalog 关停时的死锁)。
问题场景
DatabaseCatalog是 ClickHouse 服务器中维护全部数据库、表、字典等对象注册信息的核心单例,几乎所有路径(查询、后台任务、DDL)都会访问它。服务器优雅关停(shutdown)时,需要按特定顺序释放各子系统持有的锁与资源;若此时某个后台线程恰好持有DatabaseCatalog内部锁并等待另一个已被关停的子系统,就会形成锁序反转导致的死锁,使进程无法按时退出,影响容器编排下的滚动发布(如 Kubernetes 优雅终止超时后被强制 kill)。
源码佐证
DatabaseCatalog定义于 src/Interpreters/DatabaseCatalog.h,对外提供静态关停入口static void shutdown(std::function<void()> shutdown_system_logs)(L125),内部实现为void shutdownImpl(std::function<void()> shutdown_system_logs)(L297)。关停逻辑需要与system.*_log等系统日志表的写路径协调(这也是shutdown接收shutdown_system_logs回调的原因)。本次修复即针对该过程中可能出现的死锁路径进行了调整,确保各类后台线程(复制任务、异步插入缓冲、日志刷新等)在目录关停期间的锁获取顺序一致。
修复三:稀疏列上的 JOIN 崩溃(#53548)
变更日志条目:Fix crash in join on sparse column(修复在稀疏列上执行 JOIN 时的崩溃)。
问题场景与稀疏列背景
稀疏列(sparse column)是 ClickHouse 针对大量默认值(如0、空串)场景的存储优化:以"值数组 + 偏移/默认值"的方式存储,显著压缩了默认值占比高的列。但由于其物理表示与稠密列不同,任何未显式处理稀疏布局的算子都可能触发崩溃或错误结果。JOIN 引擎涉及多路数据流的拼接与行号对齐,是稀疏列的高风险路径之一。
本版本修复了 JOIN 在稀疏列上可能触发的崩溃。从当前仓库代码看,稀疏列的表示与转换集中维护在 src/Core/Block.cpp(相关嵌套结构测试见 gtest_block_nested_sparse_structure.cpp),而多个数据变换算子(如 FilterTransform.cpp、DistinctTransform.cpp)都需要显式考虑列的稀疏属性,可以推断 JOIN 路径同样需要保证稀疏列在拼接、物化过程中的正确性。
值得关注的是,紧邻的前一个补丁版本v23.3.11.5-lts刚修复了稀疏列上的sorted distinct问题("Fix: sorted distinct with sparse columns",docs/changelogs/archive/v23.3.11.5-lts.md)。两个连续补丁均落在稀疏列处理上,说明该阶段 ClickHouse 正在系统性地收敛稀疏列特性在各算子上的行为差异,使用sparse_columns特性的用户应尤其关注这两个补丁版本。
修复四:DelayedSource 的 rows_before_limit_at_least 计数(#54122)
变更日志条目:Fix rows_before_limit_at_least for DelayedSource(修复 DelayedSource 场景下rows_before_limit_at_least的取值)。
何为 rows_before_limit_at_least
rows_before_limit_at_least是 ClickHouse 查询结果中的一个元信息字段:它表示在应用LIMIT之前、实际读取并处理过的行数(可能大于等于最终返回行数)。客户端与监控脚本常借助它判断"是否因 LIMIT 截断了数据"。该值由查询管线中的Limit相关处理器(如 LimitTransform.cpp、OffsetTransform.cpp)在流水线上游逐级累加,并通过RowsBeforeStepCounter(RowsBeforeStepCounter.h)在各处理器之间传递。
问题与修复
DelayedSource(DelayedSource.h)是一种"延迟管线计算直到开始执行"的处理器:首次需要数据时才会调用回调展开子管线并接入输入端口,如果主输出端口从未被消费,回调甚至不会执行。这种惰性机制用于避免无谓构建代价昂贵的子管线(典型场景是分布式查询中远程分片的读取)。
问题在于:在DelayedSource之前已经消费掉一部分行的场景下(例如远程读路径中先处理了部分数据才到达DelayedSource),旧实现没有把这段"在到达 DelayedSource 之前就已被消费的行数"计入rows_before_limit_at_least,导致该值偏小、无法反映真实扫描行数。当前源码中 ReadFromRemote.cpp 的注释明确写道:这些行在到达DelayedSource之前就已被消费,但rows_before_limit_at_least必须包含它们。本次修复正是补齐了这段计数的传递,保证分布式/延迟读取场景下该元信息的准确性。
内部修复:sorted distinct 稀疏回归测试(NOT FOR CHANGELOG)
变更日志末尾还有一条标记为 "NOT FOR CHANGELOG / INSIGNIFICANT" 的修复:
- Fix broken
02862_sorted_distinct_sparse_fix(修复损坏的02862_sorted_distinct_sparse_fix测试)。
它对应 ClickHouse 内部测试编号02862(测试脚本位于 tests/queries 目录,按编号组织),修复的是上一个版本新增的"排序去重 + 稀疏列"回归测试本身无法正确运行的问题。这类条目不进入面向用户的对外变更说明,但对保证 CI 中稀疏列相关回归覆盖的完整性有意义,也再次印证稀疏列修复是本阶段补丁的主线之一。
升级建议与验证路径
- 按命中场景决定优先级:如果你的线上环境启用了异步插入且表为
ReplicatedMergeTree系列,或依赖 JOIN 处理高默认值占比的稀疏列,或使用分布式查询并依赖rows_before_limit_at_least做数据完整性判断,本次补丁属于高优先级升级项。 - 关注混合版本滚动升级:若集群存在新旧副本混跑,异步插入去重相关旧设置(
replicated_deduplication_window_for_async_inserts等)仍会在过渡期生效,升级期间应保留其默认值,交由新 leader 逐步清理旧式哈希。 - 升级后回归验证:可通过 tests/queries 中的相关用例(如异步插入、JOIN、LIMIT 计数相关查询)验证行为一致性;对于
rows_before_limit_at_least,可执行带LIMIT的查询并在结果集中检查该元字段是否覆盖实际扫描行数。 - 查阅完整演进:本版本的变更日志位于 docs/changelogs/archive/v23.3.12.11-lts.md,其余 LTS 补丁的变更可参考 docs/changelogs 目录下的归档文件。
【免费下载链接】ClickHouseClickHouse® is a real-time analytics database management system项目地址: https://gitcode.com/GitHub_Trending/cli/ClickHouse
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考