Field Notes
生产里踩出来的经验法则和判断方法。每条结论在前,原因在后。
和文章的区别:这里是条目,不是叙事。一句话能说清的就不展开。
判断方法
改了不生效,先别问「为什么不生效」,先问「它到底生效了没有」。 前者是个开放问题,后者是个能查的问题。ClickHouse 的 settings 散在 system.settings、system.server_settings、system.merge_tree_settings 几张表里,不确定就都查一遍。
「改了不生效」有三种形态,排查要按顺序过: ① 参数还活着吗 —— WHERE changed AND is_obsolete 一条 SQL 就能捞出你配置里已经废弃的那些; ② 改对地方了吗 —— 设置会跨版本换表、换作用域,改在旧位置不报错,只是不生效; ③ 有没有被别的开关架空 —— 新开关可能覆盖旧开关,旧开关的显示值已经不代表实际行为。 三种的排查方法完全不同,混在一起想就会卡住。
任何把同步变异步的改动,都在制造一个新的中间状态。 不配套可观测性,就是用「明确的失败」换「模糊的成功」。merge 是这样,表加载是这样,副本同步也是这样。
当一个现象在旧环境不成立时,别急着归因于业务差异 —— 先去查默认值变更。 近年 ClickHouse 的 breaking change 有相当一部分藏在默认值里,而不是 API 里。26.3 的集群 Keeper 涨到 126 万 znode,而 23.8 上 4000 张表相安无事 —— 这个反例才是追到根因的钥匙。
跨大版本升级要回答两个独立的问题:升上去会不会变慢,以及升上去还退不退得回来。 前者可以边跑边调,后者必须在新版本第一次写数据之前决定 —— 格式类开关控制的是「新数据用什么格式写」,等发现要回滚时再关,已经写出去的 part 救不回来。22.8→23.8 和 23.8→26.3 这两跳都是这个结构。
「上游支持这个能力」和「我能拿到能上生产的包」是两回事。 ClickHouse 一直在构建 aarch64v80compat,编译选项也一直都在 —— 但它只存活于流水线产物,拉到的永远是 master。生产要的是 LTS 版本号和支持周期,而那份没有任何渠道提供。评估一个能力可不可用,要一路追到发布渠道,不能停在「代码里有」。
别用 grep 判断一个二进制需要什么指令集。 现代 GCC / Clang 的 aarch64 二进制里必然含有 ldadd / cas / swp —— 它们在 libgcc 的 outline-atomics helper 里,被 __aarch64_have_lse_atomics 运行时标志保护着,带 LL/SC 回退。这些 helper 存在的目的恰恰是让二进制能跑在 ARMv8.0 上。有意义的问题是「它有没有出现在保护之外」,这要对符号表,不是 grep。
当一个系统在某种失败模式下失去了解释自己的能力,这个失败模式就会永远得不到修复。 ClickHouse 写了指令集自检,但它只覆盖 x86,而且 init_priority(101) 晚于 priority-100 的静态初始化器 —— 崩溃发生在检查之前。于是所有 ARM SIGILL 报告最后都只剩一句 Illegal instruction,被关成 st-need-info。不是报告者不配合,是系统没给他们可报告的东西。
当两个组件各自都「符合设计」却仍然出错时,别再往任何一层里挖 —— 去看它们的接触面。 连接池复用连接是对的,服务端按连接缓存 prepare 也是对的,合起来就串表了。修复往往也不在任何一层里,而在于切断或改变接触方式。同理:26.2 的默认去重没错、26.3 的默认异步也没错,撞一起就是百万 znode。
一个解法会长出它自己的问题。 按 SQL 隔离连接池解决了串表,但「专用池」这个新概念带来了生命周期管理的负担 —— 池的存活时间和连接的存活时间不是一回事。改完之后要问一句:我刚引入的这个东西,它自己需要被管理吗?
手上有多个生产环境时,最快的定位方式不是往下挖,是横着比。 找到那个「按理说也该出问题、但没出」的环境,两边的差集就是根因的候选集。26.3 的 Keeper 涨到 126 万而 23.8 的 4000 张表没事 → 差集是默认值;ARM 上 HTTP 泄漏而同样是 ARM 的另一个客户走 TCP 没事 → 差集是协议。两次都是靠对照组收敛的。
猜大户不如做差分。find_super_nodes 找到的是存量最大的目录,未必是正在增长的目录。「谁占得多」和「谁在涨」是两个问题,要用两种方法回答。
想知道某个函数历史上改过什么行为,去读它构造函数里读了哪些 compat setting。FunctionFormatDateTime 一口气读了五个 formatdatetime_* 设置 —— 有开关,说明每一个都对应过一次行为变更。官方不会为从没改过的行为加兼容开关。
验证治理效果要同时看总量和目标项,别把两者混为一谈。 清理刚结束的瞬时低点也不是稳态水位,拿它当告警基线会一直误报。
写入与 merge
OPTIMIZE TABLE 不解决 too many parts,而且通常会加剧。 too many parts 判定的是 part 的数量,OPTIMIZE 优化的是数据组织 —— 它会挑最大的那些 part 去合并,长任务占满线程池槽位,真正造成问题的小 part 反而排不上队。
线程池满、但 CPU 很闲 —— 这是并发度问题,不是资源问题。 扩池子之前先看这两个指标的组合,能省掉大部分瞎调参数的时间。
副本与 Keeper
副本不是备份。 ReplicatedMergeTree 防的是节点故障,防不了误删和逻辑错误。这是从 Oracle 迁过来的团队最常见的认知盲区。
存储与冷热
资源与容器
超分集群叠加 cgroup CPU 限额是个反模式。 实际核 1 × 物理核 5 ÷ 超分 32 vcore = 15.625% —— 和 cgroup 实测值完全吻合。遇到 CPU 莫名被压在某个固定百分比,从 cgroup 实际生效的 quota/period 反推资源模型,通常比查应用快。