相信不少同学在做ClickHouse交付或者日常运维时,都有过这么一段糟心经历:业务侧报了一个慢查询,你连上实例,改了max_threads、调了max_memory_usage,SQL直接跑起来了,你以为完事大吉。结果第二天一上班,发现配置全被还原了,业务那边又是一轮“昨晚还好好的”轰炸。
今天要聊的这个实例ck-ec818311,名字看起来就是一长串环境自动分配的ID,大概率是某套容器平台或云管理平台上托管的ClickHouse节点。这类实例有个共同特点:表面上看能用SQL改配置,但很多人没搞懂这些改动到底写在哪儿、什么时候真正落入磁盘、重启之后还在不在。这篇文章就把配置持久化这件事掰开揉碎讲清楚,包括背后的配置体系、修改语句的执行逻辑、文件层面的落盘路径,以及我踩过的一些坑。不管你是刚上手ClickHouse,还是已经维护了一堆集群,这部分内容都能让你少走弯路。
1. ClickHouse的配置体系与持久化机制拆解
搞清楚“配置持久化”之前,得先理解ClickHouse的配置是怎么组织的。它不是一个配置文件打天下,而是分了三层逻辑:静态XML配置、运行时动态配置、以及系统表里存的那部分元数据。
1.1 静态配置与动态配置的分界线在哪里
ClickHouse的静态配置主要集中在/etc/clickhouse-server/config.xml以及config.d/、users.d/这类目录下的补丁文件。服务启动时,主配置会把这些目录下所有*.xml文件按文件名顺序合并加载,后面的定义可以覆盖前面的默认值。这层配置改完之后,常规做法是重启服务或者用SYSTEM RELOAD CONFIG来生效,它描述的是“这个ClickHouse进程应该以什么参数运行”。
动态配置走的则是另一套逻辑。像SET语法、ALTER USER ... SETTINGS这类语句,直接作用在会话层或者用户层,改完不需要重启,当前会话、新建会话就能感知到。问题就出在这个“感知”上:很多同学以为执行了SET max_memory_usage = 10000000000,这个值就全局写死了,其实只对当前会话生效。一旦断开连接,尘归尘土归土。
ck-ec818311这种托管实例,我遇到过最典型的情况是:用户执行SET以后没报错,就以为改的是全局配置,结果慢查询依旧,凌晨一重启全部归零。这个实例的具体表现,其实就是典型的动态配置没走持久化通道。
1.2 配置持久化涉及哪些核心目录与文件
先说结论:ClickHouse真正需要持久化的,不只是config.xml,还包括用户配置目录和内置元数据库。
/etc/clickhouse-server/config.xml:主配置。承载端口、路径、日志级别、查询复杂性限制等全局参数。/etc/clickhouse-server/users.d/:用户级配置。default用户的配额、profile、约束都在这个地方定义。/etc/clickhouse-server/config.d/:主配置覆盖项。通常容器平台会把自定义环境变量渲染成这个目录下的XML片段。/var/lib/clickhouse/:数据目录。内含metadata/、data/、access/等子目录,其中access/存的是SQL管理的用户、角色、行策略这类对象。/var/lib/clickhouse/metadata/:记录库表结构的关键元数据。
结合ck-ec818311这个实例,我看到过一种常见错误:在config.d/下手工扔了一个zzz_my_settings.xml,里面写了某个profile,但没仔细核对标签是否覆盖到了目标配置项,结果服务加载时直接忽略。要知道,config.d下的文件不是“全部生效”,而是“和主配置合并后按优先级生效”。如果文件里只写了<profiles>却没写<default>引用,很可能你自定义的profile压根没被任何用户启用。
1.3 为什么重启后配置丢失最常见
重启后配置丢失,优先级最高、最常见的原因是:你改的是运行期内存态配置,而不是文件态配置。
比如SET命令,只影响当前会话;ALTER USER ... SETTINGS虽然会话级和用户级都动到了,但落盘的是/var/lib/clickhouse/access/下的users.xml(实际上是SQL管理用户时才会生成)。如果你用的是users.d/目录里的静态XML定义用户,那ALTER USER改完后未必会回写你的XML文件,而是写到access存储里。看起来都叫“持久化”,但存储通道完全不是一回事。
ck-ec818311这类实例如果带有编排平台,还得注意容器重启之后文件系统会不会被重置。很多平台把/etc/clickhouse-server挂成了临时层,容器重建后全部回到镜像默认状态。这时候就算你在容器里改了config.xml,只要没有挂载持久卷,照样丢得干干净净。
注意:排查配置丢失,先确认你用的是
SET、ALTER USER还是直接改XML,三者持久化的目标路径完全不同。
1.4 核心要点:配置优先级与生效顺序
ClickHouse配置优先级有个非常重要的原则:越细粒度、越后加载的越优先。细化下来大致是:命令行参数 >config.d/>config.xml默认值。用户级配置里则是:users.d/>users.xml> 默认模板。
这里有个很容易被忽略的细节:同样的配置项,如果在config.xml里写在了<profiles>外部,又在users.d/里写进了某个profile,最终生效的是user级profile。而SET语句设置的会话变量,优先级又高于profile内的定义。这样一层层套下来,你看到的最终生效值,很可能是多个来源层层覆盖后的结果。
对于ck-ec818311这个实例,配置持久化分析的核心就是要把这层覆盖关系理清楚,否则你根本不知道改动到底写给了谁。
2. 实例ck-ec818311的配置持久化路径拆解
既然标题点名了ck-ec818311这个实例,那我们就针对这类实例,把持久化路径一条一条拆出来看。
2.1 实例级目录与默认可视化路径确认
先做一件事:SSH到实例上,或者如果平台给了控制台终端,直接执行:
ls -la /etc/clickhouse-server/ cat /etc/clickhouse-server/config.xml | grep -E "<path>|<tmp_path>|<user_files_path>|<format_schema_path>"拿到结果后,你需要确认四样东西:
<path>,通常默认是/var/lib/clickhouse/<tmp_path>,一般默认是/var/lib/clickhouse/tmp/<user_files_path>,一般默认是/var/lib/clickhouse/user_files/<format_schema_path>,一般默认是/var/lib/clickhouse/format_schemas/
在ck-ec818311上,我建议你确认一下<path>是不是被平台重定向到了另一个数据盘。如果配置成了/data/clickhouse而你又只备份了/var/lib/clickhouse,那么配置持久化的数据层备份就是不完整的,恢复的时候连库表元数据都对不上。
2.2 访问控制与SQL管理对象的落盘位置
ClickHouse从20.x版本开始力推SQL管理模式来管理用户、角色、行策略。和静态XML不同,SQL管理对象存储在/var/lib/clickhouse/access/目录下。
具体表现是:
system.users表里能看到SQL管理的用户system.roles、system.row_policies等系统表可以直接查到对象定义- 对应磁盘文件是
/var/lib/clickhouse/access/{user|role|row_policy|quota|settings_profile}/*.sql
这个目录是ClickHouse内部管理的,不能手工编辑,改完以后重启依然保留。这也是为什么你执行CREATE USER、CREATE SETTINGS PROFILE之后,配置不会丢的原因——它们走的是access存储通道。
但注意:如果你在/etc/clickhouse-server/users.d/里手动加了一个用户XML,那么此时这个用户管理方式是“静态文件型”,system.users能看到它,但执行ALTER USER修改它时,ClickHouse会报错或者说行为不符合预期,因为它不属于access存储管理。
ck-ec818311实例上我见过一个很典型的误操作:同学想要加大某个用户的max_memory_usage限制,在users.d/里加了一个同名用户定义,然后执行ALTER USER xxx SETTINGS max_memory_usage = ...。结果查询生效但过一段时间发现不生效,其实就是因为静态XML的优先级下,ALTER USER的变更虽然写入了access存储,但每次服务重启加载时,静态XML又重新覆盖了一次。这个问题排查起来相当费劲,最终我是通过比对system.settings和system.merge_tree_settings两个表才定位到的。
2.3 大名单:哪些配置写内存,哪些配置落文件
为了把持久化这个事彻底说明白,我整理了一个清单。日常改配置时,按这个表对照即可。
| 配置类型 | 修改方式示例 | 持久化位置 | 重启后是否保留 |
|---|---|---|---|
| 会话级设置 | SET max_threads = 8 | 仅当前会话内存 | 不保留 |
| 全局级设置 | SET GLOBAL max_threads = 8 | 不推荐,语义不明确 | 不保留 |
| 用户级设置 | ALTER USER u1 SETTINGS max_threads = 8 | /var/lib/clickhouse/access/ | 保留 |
| 设置画像 | CREATE SETTINGS PROFILE p1 SETTINGS max_threads = 8 | /var/lib/clickhouse/access/ | 保留 |
| 静态XML | 编辑users.d/xx.xml | /etc/clickhouse-server/users.d/ | 保留 |
| 主配置 | 编辑config.xml | /etc/clickhouse-server/config.xml | 保留 |
| 库表元数据 | CREATE TABLE/ALTER TABLE | /var/lib/clickhouse/metadata/ | 保留 |
| MergeTree引擎配置 | ALTER TABLE ... MODIFY SETTING | /var/lib/clickhouse/metadata/<db>/<table>.sql | 保留 |
这张表建议截图存一下,实际工作里90%的配置丢失,都是因为把第一行的修改方式当成了第三、四行的效果来用。
2.4 MergeTree引擎级配置持久化的判断要点
ClickHouse里MergeTree系列表有很多表级设置,比如index_granularity、storage_policy、ttl_only_drop_parts等。修改这类设置,通常用ALTER TABLE table_name MODIFY SETTING setting_name = value。
这类修改的持久化位置在/var/lib/clickhouse/metadata/<database_name>/<table_name>.sql文件里。这个文件存的是表的完整CREATE TABLE语句快照,ALTER TABLE执行成功后会同步更新。
但这里有个前提:这个修改只有在表是Replicated或者非Replicated但服务器正常关闭时才会稳定落盘。如果服务器直接kill -9,理论上新快照还停留在内存和WAL里,可能丢。好在ClickHouse处理metadata写入比较保守,大多数时候都能恢复,不过这依然是ck-ec818311实例如果做实验性配置修改时必须考虑的风险。
3. 配置修改的完整操作流程与验证方法
前面的章节把原理讲清了,现在进入实操。以下操作流程,我是基于一个常规的Linux环境ClickHouse实例来跑,你在ck-ec818311这类托管实例上也能对照执行。
3.1 流程设计:改前备份、改中验证、改后巡检
先给出一套稳妥的流程,这个流程我在多个实例上验证过,可以避免绝大多数问题。
第一步,检查当前生效配置:
SELECT name, value, changed FROM system.settings WHERE name = 'max_threads';这里能看出值是从哪个层级来的。changed字段等于1说明这个值不是默认值,而是被某个profile或SET改写过了。
第二步,确认配置文件落盘情况:
grep -r "max_threads" /etc/clickhouse-server/users.d/ /etc/clickhouse-server/config.d/如果这里有大段的profile配置,大概率你之后改的SET不会覆盖它,因为用户会话初始化的settings聚合时,profile的优先级比默认值高、比SET低。
第三步,有需要的话先备份整个配置目录:
cp -a /etc/clickhouse-server /etc/clickhouse-server.bak.$(date +%F) cp -a /var/lib/clickhouse/access /var/lib/clickhouse/access.bak.$(date +%F)这个动作成本极低,但能让你在排查“到底是不是我自己改错了”的时候有据可查。
3.2 通过SQL管理用户级配置(真正的持久化方式)
假设我们要把default用户的max_memory_usage从默认值调整到32GB,并持久化。
推荐做法:
ALTER USER default SETTINGS max_memory_usage = 34359738368;执行后,到磁盘上确认:
find /var/lib/clickhouse/access -name "*user*" -o -name "*default*" | xargs grep -l "max_memory_usage" cat /var/lib/clickhouse/access/xxx.sql如果能看到对应的CREATE USER语句或者ALTER USER语句的内容,就说明用户设置层已经持久化。之后重启服务,这个配置依然在。
这里补充一个细节:如果你希望这个用户只在这个值生效,不继承其他profile里的同名配置,还需要检查配置画像里有没有定义同样的项。有继承关系时,优先级是:显式用户设置 > 画像设置 > 默认值。所以ALTER USER里的设置是最能打的,这一点在生产上非常有用。
3.3 通过配置文件片段覆盖(静态文件型)
有些时候你不想走SQL管理通道,比如你想统一管理一批实例的配置,希望配置文件随发布系统走。那就需要修改users.d/下的文件,或者新建一个新的XML文件。
建议新建一个/etc/clickhouse-server/users.d/10_default_profile.xml,内容类似:
<clickhouse> <profiles> <default> <max_memory_usage>34359738368</max_memory_usage> <max_threads>16</max_threads> </default> </profiles> </clickhouse>写完之后:
sudo clickhouse-server --config /etc/clickhouse-server/config.xml --pid-file=/var/run/clickhouse-server/clickhouse-server.pid --daemon或者如果你用的是systemd管理,直接:
sudo systemctl restart clickhouse-server但注意,静态文件型配置在托管实例上有个隐患:很多容器平台会挂载只读的/etc/clickhouse-server,或者启动时用init容器覆盖该目录。你在容器里改了,一旦重新调度,就回到原来的镜像内容。针对这种情况,一定要确认平台是否开放了config.d或者users.d的持久卷挂载。
3.4 验证持久化是否生效的完整命令串
改完配置、执行完SQL、也确认文件存在了,还需要做一次“重启验证”,逻辑很简单:
- 记录修改前的值
- 做修改
- 重启服务(或
SYSTEM RELOAD CONFIG) - 查询修改后的值是否保持
完整命令串大概是:
# 修改前 clickhouse-client --query "SELECT value FROM system.settings WHERE name='max_threads'" # 修改(SQL管理通道) clickhouse-client --query "ALTER USER default SETTINGS max_threads = 24" # 重启服务 sudo systemctl restart clickhouse-server # 修改后 clickhouse-client --query "SELECT value, changed FROM system.settings WHERE name='max_threads'"如果返回的不是24,说明你的修改被某个静态配置覆盖了。这时候下一步就是检查profile继承链。
检查命令:
SELECT * FROM system.settings WHERE name = 'max_threads'\G SELECT user_name, profile, settings FROM system.session_log WHERE event_time > now() - 60system.session_log是近年来版本里特别好用的排查表,能够看到用户会话初始化时加载了哪些profile和settings。如果这个表没数据,大概率表没开,需要先开启session_log。
3.5 实操现场:一次“改完重启丢失”的完整排障
有一次我在某台实例上做了这么个操作:直接用了clickhouse-client --query "SET max_threads=16",然后查看了system.settings,值是16,看起来一切正常。但是重启之后,值变回了默认的8。
排查过程:
- 先确认有没有profile覆盖:
SHOW PROFILES;发现
default画像里面只定义的参数很少,根本没有max_threads,排除profile覆盖。查
/var/lib/clickhouse/access/下有没有对应的user设置,发现空无一物。最终确认:
SET是会话级,退出客户端就没了,压根没写任何文件。
这个坑其实太经典了。SET可比大部分新手想象的脆弱,它只对当前连接生效。CLI执行完退出,配置立刻蒸发。要在可交互的会话里保持,要么建一个配置画像并绑定,要么用ALTER USER。
4. 常见问题与排查技巧实录
配置持久化这块,问题永远比文档多。我把实际排障中高频出现的问题整理成了一张速查表,另外也把一些难以定位的诡异问题单独拎出来讲。
4.1 高频配置丢失问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
重启后SET改的值丢失 | 会话级配置不落盘 | 改用ALTER USER或profile |
| 服务重启后访问地址变了 | 平台重建容器,config.xml丢了 | 检查持久卷挂载,配置放到config.d/ |
ALTER USER报语法错误或权限不足 | 用户为静态XML管理 | 改用编辑users.d/,或重建为SQL管理用户 |
修改config.xml后重启不生效 | 端口被config.d/另一文件覆盖 | grep -r全目录排查同名配置项 |
SYSTEM RELOAD CONFIG之后部分配置没更新 | 配置项属于动态加载不支持的范畴 | 检查错误日志,通常需要重启进程 |
| 配置文件改了但系统表值不变 | 缓存或者优先级被profile覆盖 | 查system.merge_tree_settings和system.settings对比 |
| 修改MergeTree设置后表文件还是旧值 | ALTER TABLE MODIFY SETTING没加persistent语义 | 查metadata下对应SQL文件时间戳 |
这张表最核心的价值在于:不要相信“看起来生效了”,一切以系统表和磁盘文件为准。
4.2 “改users.d为何重启无效”的独家排查心得
users.d/下改配置不生效,大部分原因不是文件本身有问题,而是“加载顺序”和“覆盖优先级”理解有偏差。
比如:
<clickhouse> <profiles> <default> <max_memory_usage>17179869184</max_memory_usage> </default> <readonly_profile> <max_memory_usage>8589934592</max_memory_usage> <readonly>1</readonly> </readonly_profile> </profiles> </clickhouse>如果同名profile既在users.xml中出现,又在users.d/的某个文件里出现,后者的定义会合并覆盖前者。但很多人会忽略合并是“逐项”覆盖,不是“整体替换”。也就是说,你在users.d/里只写了max_memory_usage,觉得这个profile就只限制内存了,但users.xml里的readonly=1依然会被继承下来。你以为是覆盖,实际上是叠加。
排查这种问题时,我习惯用一句话总结:检查同名配置项而不是同名profile。所以在ck-ec818311这类实例上,如果你要“重置”某个配置项,一定要显式写出来,不要指望覆盖掉整个画像。
4.3 进程级配置与机器重启的边界条件
还有一种极其恼火的情况:ClickHouse进程没重启,配置看起来也挺对,但是某天整个机器重启了,之后ClickHouse起不来。
这种问题和配置持久化强相关。典型场景是config.xml里改了<path>,把数据目录指到了新盘,但新盘的/etc/fstab挂载是自动挂载顺序晚于ClickHouse启动,服务起在挂载前,数据目录一看就是空的,于是用了初始化逻辑重建空实例。配置看似在,其实库表全“丢了”(实际是文件挂在另一个路径下)。
ck-ec818311这种托管实例更容易遇到这种问题,因为平台管理生命周期的编排工具会在实例启动后进行健康检查,健康检查通过后才会挂载业务。如果ClickHouse启动太快,挂载点还没就绪,进程直接认为数据目录不存在并执行初始化,这就是灾难性的。
遇到这种情况,正确方式是检查平台挂载顺序,并确保ClickHouse服务启动前绑定目录已就绪。可以写一个启动前检查的守护脚本,比如:
while ! mountpoint -q /var/lib/clickhouse; do sleep 1 done但说实话,托管实例很难让你改init脚本,所以尽量选择平台侧保证的挂载能力。
4.4 配置持久化排障的时间线法
如果配置变化很神秘,建议用时间线法来排查。具体操作是:
- 记录发现异常的时间点T0
- 查
system.events和system.asynchronous_metrics里有没有统计信息跳变 - 查
/var/log/clickhouse-server/clickhouse-server.log中T0附近有没有ConfigReloader或Applying user settings之类的记录 - 查
/var/log/clickhouse-server/clickhouse-server.err.log看有没有报错
ClickHouse的日志实际上质量很高,ConfigReloader会打印加载了哪些文件、哪些配置被覆盖。比如我见过一条日志:
2025-06-XX 10:22:33.12345 [ 12345 ] {} <Information> ConfigReloader: Loaded new config from '/etc/clickhouse-server/config.xml', updated 23 settings这一下就能定位到配置文件被重新加载了。配合文件修改时间:
stat /etc/clickhouse-server/config.xml /etc/clickhouse-server/users.d/*.xml基本就能锁定是谁改了、什么时候改的。
4.5 关于ZooKeeper和配置持久化的边界澄清
这里必须多提一句:很多人以为ClickHouse的副本表配置会持久化到ZooKeeper,配置就不会因为节点重启丢失。这种理解是错误的。
ZooKeeper保存的是表结构与副本协调元数据,配置层面的settings也会有一部分同步到ZooKeeper路径下,但ClickHouse进程本地配置文件仍然需要独立持久化。你可以把ZooKeeper理解成“集群的公共记事本”,但它不是“本机的配置仓库”。你的节点本地文件丢了,重启后它依然会尝试从ZK拉取副本元数据,但本体配置如果缺失,服务可能直接起不来或者行为异常。
因此,不论ck-ec818311是不是集群中的一员,本机配置文件的持久卷挂载仍然是最基本的前置条件。
5. 配置持久化的进阶思考与个人实践体会
前面说了很多操作层面的事,现在聊点进阶判断。
5.1 配置应该走SQL管理还是静态XML?
我的个人倾向是:如果实例是平台化、容器化部署,优先走SQL管理,也就是ALTER USER、CREATE SETTINGS PROFILE这条路。原因很简单:静态XML文件容易随容器重建丢失,而SQL管理的对象存放在数据目录里,只要数据卷挂载没丢,它就在。
但SQL管理也不是银弹。万一数据目录坏了需要从备份恢复,或者你要在完全新的空集群上复现配置,SQL管理对象的恢复方式是执行system.users里的SQL语句重建对象,而不是简单复制文件。这要求你额外维护配置导出的脚本。
5.2 让我印象最深的一次配置事故
有一次,我在某套ClickHouse实例上看到:某个业务用户反映“查询一会儿快一会儿慢”。查了system.settings发现max_threads的changed字段忽0忽1。一开始以为是并发修改引起的,后来查system.session_log才发现,每来一个新连接,都会执行一次初始化SQL,而这个初始化SQL来自该应用连接池里的“设置语句”。也就是说,连接池配置了每次建连都执行SET max_threads=...,导致这个值时有时无,看起来就像配置持久化出了问题。
这种问题,报告上写的是“配置持久化异常”,实际上完全不是。这就是配置优先级理解不够的衍生问题。也正因为如此,我在讲配置持久化时一定会带一句:先查会话级别的SET,再查profile,再看静态XML,最后才是怀疑持久化通道坏了。
5.3 配置持久化的最佳实践清单
针对ck-ec818311这类实例,我最后给出一份可以直接拿走的实操清单:
- 确认
config.xml、users.d/、config.d/、access/、metadata/都在持久卷上 - 用户管理优先使用
CREATE USER/ALTER USER - 画像管理优先使用
CREATE SETTINGS PROFILE - 出问题先查
system.settings的changed字段 - 有条件的实例开启
session_log - 做配置变更前,先把
access/和config.d/打包备份一份 - 每季度做一次配置恢复演练——在临时实例上重放配置,验证是否能还原
这份清单看着简单,但每一条都对应着真实踩过的坑。尤其是最后一条,很多团队平时不看,真正出事的时候才后悔没有恢复演练。
配置持久化不是一个高深的机制,但它踩中了运维中最容易被忽视的角落:不重启不觉得,一重启全剧终。希望这篇基于ck-ec818311实例的分析,能帮你把这些坑都填上。