Skip to content

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 反推资源模型,通常比查应用快。

最后更新:

内容来自一线生产环境,客户信息均已脱敏