ClickHouse Schema修改引发的Merge阻塞问题解析 1. ClickHouse Schema修改引发的Merge阻塞问题深度解析作为一名长期与ClickHouse打交道的工程师我最近遇到一个由Schema修改引发的Merge阻塞问题整个过程堪称教科书级的分布式系统故障排查案例。本文将详细记录从问题发现到最终解决的完整过程并深入分析背后的技术原理。1.1 问题背景与现象我们在生产环境中对emo_mdm_flowwork_pt1m_local表执行了添加5个String类型列的DDL操作ALTER TABLE default.emo_mdm_flowwork_pt1m_local ON CLUSTER cluster ADD COLUMN IF NOT EXISTS host String DEFAULT AFTER connType, ADD COLUMN IF NOT EXISTS path String DEFAULT AFTER host, ADD COLUMN IF NOT EXISTS referrer String DEFAULT AFTER path, ADD COLUMN IF NOT EXISTS title String DEFAULT AFTER referrer, ADD COLUMN IF NOT EXISTS referrerHost String DEFAULT AFTER title;操作看似简单却引发了连锁反应Mutation阻塞数据删除操作在部分分片上永久阻塞Merge失败后台Merge任务大量失败错误日志显示类型转换异常查询异常涉及新增列的查询会触发相同错误1.2 初步排查与应急处理1.2.1 资源池调整无效我们首先怀疑是MergeAndMutatePool资源不足导致。将background_pool_size从32调整到48后# 监控指标变化 ClickHouseMetrics_BackgroundMergesAndMutationsPoolSize: 64 → 96虽然部分分片恢复但仍有分片持续阻塞说明不是根本原因。1.2.2 错误日志分析关键错误信息反复出现Bad cast from type DB::ColumnNothing to DB::ColumnLowCardinality while reading from part /path/to/part堆栈显示问题发生在VerticalMergeStage且与复杂类型处理相关。特别值得注意的是报错的不是新增的5个String列问题只出现在添加列之前创建的part中涉及LowCardinality类型的复合列Map/Array/Tuple2. 深入诊断过程2.1 问题复现条件验证通过以下查询验证问题触发条件-- 不包含新增列的查询 → 正常 SELECT count(1) FROM table WHERE timestampMs BETWEEN... AND flowId IN(...) -- 包含新增列的查询 → 报错 SELECT count(1) FROM table WHERE timestampMs BETWEEN... AND referrer value2.2 Part元数据分析对比新旧part的列存储情况# 新part包含新增列 ls /path/to/new_part | grep referrer referrer.bin referrer.cmrk2 ... # 旧part缺少新增列 ls /path/to/old_part | grep referrer [无输出]2.3 系统表查询通过系统表定位受影响范围-- 查找所有报错的part SELECT database, table, partition_id FROM system.parts WHERE name IN ( SELECT DISTINCT result_part_name FROM system.merges WHERE last_exception LIKE %Bad cast% )结果显示问题集中在emo_mdm_flowwork_*系列表且只影响添加列前创建的part。3. 根本原因分析3.1 ClickHouse的Merge机制当执行Merge操作时ClickHouse会确定Merge算法Horizontal/Vertical读取源part数据处理缺失列新增列场景写入新part在Vertical Merge模式下列是按需读取的这触发了缺失列的默认值填充逻辑。3.2 默认值填充的两阶段过程当读取的part缺少表中存在的列时第一次补列fillMissingColumns()为简单类型填充类型默认值为复合类型准备offset信息// 示例为Map类型准备默认数据结构 ColumnPtr column data_type-createColumn(); column-insertDefault();第二次补列evaluateMissingDefaults()计算表达式默认值此处发生了ColumnNothing到ColumnLowCardinality的错误转换3.3 复合类型的Substream处理复杂类型的列如Map(LowCardinality(String), String)通过Substream机制处理SubstreamPath标识嵌套结构中的具体子列Serialization各类型实现自己的序列化逻辑SerializationMapSerializationArraySerializationTuple当处理缺失的复合列时类型系统未能正确处理LowCardinality子列的默认值构造。4. 解决方案与验证4.1 临时解决方案强制使用Horizontal Merge算法SET vertical_merge_algorithm_min_rows_to_activate 2000000000; SET vertical_merge_algorithm_min_bytes_to_activate 536870912000;然后执行OPTIMIZEOPTIMIZE TABLE emo_mdm_flowwork_pt1m_local PARTITION 20250925 FINAL;4.2 根本解决通过代码分析发现问题根源缺失列处理时未正确维护LowCardinality类型信息复合类型的Substream路径处理存在边界条件缺陷添加调试日志确认问题LOG_DEBUG(log, Missing column: {}, type: {}, column.name, column.type-getName());最终通过以下方式彻底解决升级到修复该问题的ClickHouse版本对历史part执行强制重写5. 经验总结与最佳实践5.1 问题处理流程优先恢复服务通过调整Merge策略快速止血保留现场在重启前收集堆栈、日志和指标根因分析通过代码走查和日志验证假设彻底修复升级或应用补丁5.2 ClickHouse运维建议Schema变更时避免在生产高峰执行提前评估影响范围考虑使用ALTER TABLE ... MODIFY SETTING调整Merge策略监控关键指标SELECT * FROM system.mutations WHERE is_done 0; SELECT * FROM system.replication_queue WHERE last_exception ! ;复杂类型使用谨慎使用多层嵌套的LowCardinality为复杂类型显式指定默认值5.3 深度技术理解通过这次事故我们深入理解了Merge算法选择逻辑if (sum_rows settings.vertical_merge_algorithm_min_rows sum_bytes settings.vertical_merge_algorithm_min_bytes) return MergeAlgorithm::Vertical;Substream处理流程通过ISerialization::enumerateStreams()收集子列每个子列对应独立的存储文件类型系统转换ColumnNothing作为占位符需要显式处理到目标类型的转换这次问题的解决过程再次证明在分布式系统中表面简单的操作如加列可能引发深层次的系统交互问题。关键在于建立系统的全局视角并掌握从现象到本质的分析方法。