内部揭秘 ClickHouse Cloud 极速扩容的机制:Make Before Break

图片

本文字数:8822;估计阅读时间:23 分钟

作者:Jayme Bird & Manish Gill

本文在公众号【ClickHouseInc】首发

图片

在打造 ClickHouse Cloud 的过程中,我们在技术方案上的首要考量是开发交付的速度。因此,我们决定让内部的 clickhouse-operator 使用 Kubernetes 的 StatefulSet 来管理服务器 pod。

这一选择在当时非常合理。StatefulSet 是运行像 ClickHouse 这样有状态服务的推荐方式,它为我们带来了不少好处:可以挂载持久化卷(PVC)、支持滚动重启,而且只需调整副本数量就能实现扩容。

不过,clickhouse-operator 做出了一项关键设计:所有 ClickHouse 实例都由同一个 StatefulSet 统一管理。这个方案本身并无不妥,它正是 Kubernetes 建议的管理有状态服务的方式。

但当我们开始支持手动与自动扩容时,这一方案逐渐暴露出了问题。在构建自动扩容能力时,我们意识到,ClickHouse 的大多数使用场景更适合纵向扩容(提升单个节点资源),而非横向扩容(增加节点数量)。因此,我们的扩容系统从一开始就是围绕纵向扩容设计的。

当某个服务需要更多的内存或 CPU,无论是自动扩容器的判断,还是用户手动触发,我们都不会调整副本数量,而是采用滚动重启的方式,一次重启一个副本。每次重启后,对应的服务器 pod 会以新的资源规格重新启动。

当前,ClickHouse Cloud 中 CPU 与内存的配比保持不变。

了解 Pod 驱逐机制

我们有必要深入理解 pod 是如何被调整资源的。下面这张架构图展示了 ClickHouse Cloud 在过去是如何执行 pod 资源重设的。

图片

图中两个关键组件是自动扩容器(autoscaler)和变更 webhook(mutating webhook)。pod 的资源变更只能在其终止或重启时发生。这是因为只有在 pod 被重新创建的那一刻,mutating webhook 才能拦截并修改其配置,从而以我们期望的资源规格启动新的 pod。图中可以看到,所谓的“合适资源规格”由两个因素决定:一是用户设定的纵向扩容的最大和最小限制,二是推荐系统根据历史使用数据做出的建议。

我们也不能同时重启所有副本。为了避免对服务造成较大干扰,我们引入了 Pod Disruption Budget(PDB),它会限制同一时间只能有一个 pod 被重启。结果就是,在一个拥有 3 个副本的集群中,我们只能逐个执行滚动重启。

还必须考虑的一点是,副本的终止过程往往并不迅速。服务器 pod 上可能正在运行一些任务,比如长时间查询或备份等。在进行 pod 驱逐时,我们需要充分考虑这些情况。例如,如果某个 pod 正在执行备份,我们绝不会对其进行驱逐。而对于长时间运行的查询(我们定义为运行超过 10 分钟的查询),我们会等待最多 60 分钟,等待其完成。如果超过时间限制仍未结束,并且扩容需求迫切,我们才会强制驱逐该 pod。

这背后带来的影响不容忽视——在最坏情况下,为了调整某个副本的资源配置,它可能需要长达 1 小时才能完成优雅终止并重新上线。而在 ClickHouse 副本重启后,还需要重新初始化并加载所有表和字典,加载时长则取决于表和字典的数量。因此,在一个 3 副本的集群中,完成一次完整的资源调整,可能耗时超过 3 小时。可见,纵向扩容本质上是一个较为缓慢的过程。

资源压力

图片

在扩容过程中使用滚动重启,还会带来额外的副作用。有时候,纵向扩容反而会让集群比平常承受更大的压力。这是因为在准备驱逐某个副本时,我们会先将其从负载均衡中摘除——我们不希望它在即将下线时还继续接收新的查询请求。这意味着,原本分布在 3 个副本上的流量,瞬间会集中到剩下的 2 个副本上。当然,这基于一个前提:客户端在连接断开后会自动重试,并重新连接到 ClickHouse Cloud。

如果当前集群负载较低,比如资源利用率正在下降、计划要缩容,那么影响可能不大。但如果资源使用正在上升,此时集群正需要通过纵向扩容来增强性能,那么将一个副本下线 1 小时,很可能会拖垮整个集群。在最坏的情况下,剩余的副本可能会因为压力过大而崩溃,甚至会被 OOM(内存溢出)机制杀死,或者 CPU 出现性能限制。

Make Before Break 扩容方案

以及 StatefulSet 的局限性

基于上述原因,我们意识到需要一种更高效的纵向扩容方式。于是我们提出了 Make-Before-Break(先构建、再替换)扩容方案。这个想法非常简单。

图片

我们不再进行滚动重启,而是直接为现有集群添加新的容量。这样一来,前述的两个问题都能迎刃而解:既避免了滚动重启带来的停机,也杜绝了扩容过程中的资源瓶颈。

  • 我们先执行 “Make” 操作,将新的 pod 加入集群,并通过 webhook 机制,以正确的资源规格启动。

  • 随后执行 “Break” 操作,标记旧 pod 为待淘汰,并最终将其终止。

这一方案在理论上很简单,但在实际落地过程中,原生 StatefulSet 的设计限制了它的实现。

为什么会这样?

因为 Kubernetes 中的 StatefulSet 是基于序号(Ordinals)机制设计的。每个 pod 都会被分配一个固定的编号(例如 server-0、server-1、server-2),这个编号不仅决定了它的身份,也决定了它的启动顺序。由于这种严格的编号机制,我们无法轻易地在不中断顺序的前提下添加或移除 pod。一旦我们想要执行 Make-Before-Break(先构建、再替换)的扩容策略——例如先创建 server-3、server-4,再移除 server-0、server-1——这种机制就成了核心障碍。

图片

举例来说,当我们通过 Make 操作添加 server-3、server-4 和 server-5 后,想要再移除 server-0、server-1 和 server-2,就不是一件容易的事。要让 Make-Before-Break(MBB)方案成立,我们必须具备“任意终止 pod” 的能力。

虽然 Kubernetes 提供了类似 StatefulSet Start Ordinals(启动序号)这样的绕过方式,但也有明显局限——例如更改起始编号依然只能按照固定顺序终止 pod,无法任意选择。我们还曾研究过 Advanced StatefulSet 和 CloneSets 等方案,但它们也存在一些不足,因此我们最终决定自研这项能力。

我们提出的方案在理念上并不复杂:我们对内部的 Kubernetes operator 进行了重构,引入了一个单独的执行路径,使其能够创建多个 StatefulSet,每个 StatefulSet 仅管理一个独立的 server pod。

图片

通过这种方式,我们既实现了任意终止任意 StatefulSet 的灵活性,又保留了 StatefulSet 控制器对每个 pod 的可靠管理能力。相比直接接管 pod,这种方式优势更大,因为即使我们的 operator 出现了 bug,只要 StatefulSet 对象还在,server pod 依然可以稳定运行。

在线迁移

自定义 Kubernetes 控制器与 Temporal

去年,我们在 ClickHouse Cloud 中通过特性开关灰度发布了 MultiSTS(多 StatefulSet)模式。从那时起,所有新服务都默认采用 MultiSTS 启动。不过,仍有大量存量服务运行在旧的 SingleSTS(单 StatefulSet)架构上。如何在不中断查询的前提下迁移这些服务,成为我们面临的又一项重要挑战。

在线迁移要求

迁移必须是“在线”的,也就是说,整个过程不能影响客户使用。虽然“停机-迁移-重启”会更容易实现,但我们决定采用无中断迁移的方式。原因很明确:迁移流程本身就是 Make-Before-Break(MBB)策略的典型应用——我们会先创建(Make)新的 MultiSTS pod,再逐步替换掉(Break)旧的 SingleSTS pod。这场迁移也成为验证 MBB 能力是否可靠的一次关键性实践。如果连一次性的迁移都做不好,那它就难以支持日常的扩容或升级操作。

因此,我们为在线迁移提出了以下技术要求:

  1. 所有客户查询(INSERT、SELECT、DDL)在迁移期间必须保持可用

  2. 需要将旧副本的 catalog 精准同步至新副本

  3. 同步元数据,确保不出现任何数据丢失

  4. 整个迁移过程必须可回滚,以应对潜在故障

  5. 在迁移过程中,要能够处理 SingleSTS 和 MultiSTS 两种形态同时存在的中间态

为满足这些需求,我们开发了一个全新的控制器。为何不直接在原有控制器上改造?

  • 新控制器能更好地将主同步逻辑与迁移逻辑解耦,避免相互干扰

  • 将迁移流程的复杂性隔离开来,可以降低引入新 bug 的风险

  • 独立模块也让开发、维护和测试的流程更加清晰、高效

迁移实战:MBB(Make-Before-Break)风格

负责将服务从 SingleSTS 架构迁移到 MultiSTS 的自定义控制器,同样采用了 MBB 策略。控制器会在迁移期间临时暂停该服务的 operator 同步流程,先创建好 MultiSTS 副本,然后将对应的 SingleSTS 副本从负载均衡中移除,最后通过横向扩容的方式逐步缩容(即“Break”)旧副本。

迁移完成后,控制权会交还给 clickhouse-operator,继续执行正常的资源同步。此时,服务已经完全切换到 MultiSTS 模式。整个过程实现了全程无停机——不论是旧副本还是新副本,在迁移期间始终可用,保障了查询服务的持续运行。

图片

维护模式的构建

尽管迁移流程经过了精心设计,但实际运行中仍存在诸多风险。例如某些云端操作可能会与迁移过程冲突,包括用户主动发起的操作(如手动停止服务)以及平台自动任务(如定时备份、根据活跃度自动休眠)等,这些因素都可能引发复杂问题。

为了避免上述干扰,我们在 ClickHouse Cloud 中引入了“维护模式”(Maintenance Mode)机制,并设计了“部分维护”(Partial Maintenance)策略。在此模式下,用户通过共享代理访问副本的 SQL 查询仍然可以正常执行,但所有云端管理操作会被暂时禁用,从而为迁移提供一个安全、稳定的环境。

图片

迁移编排:基于 Temporal 的实现

在迁移过程中,还有一个关键环节是与其他系统的协同配合。我们需要将“启动迁移”的指令发送给 Kubernetes 控制器,同时还要能够接收迁移成功或失败的反馈。

为此,我们通过内部构建的自动化平台 Molen 来编排整个迁移流程,Molen 基于 Temporal 实现。Temporal 提供了丰富的功能特性,我们在工作流中广泛应用了其中的一些关键能力:

  • 持久化执行:这是迁移场景中最关键的一项功能。迁移完成后,我们希望能将集群还原为原始配置,因此需要一个可靠的配置存储方案。得益于 Temporal 对活动输入/输出的持久化支持,我们无需额外的数据库操作,就能自动完成状态记录和后续引用。
  • 失败检测:如果某次迁移失败,我们希望能第一时间收到通知并采取补救措施。Temporal 原生提供了这一能力,使用非常方便。
  • 重试与超时控制:在迁移过程中,我们需要调用内部管理 API 执行一系列操作,例如禁用空闲、暂停自动扩容、关闭备份等。这些操作可能会失败,因此我们借助 Temporal 实现了自动重试和失败管理机制。
  • 调度能力:由于需要完成成千上万次迁移,我们希望每天批量处理大约 10 至 20 个任务。Temporal 支持批次调度,让这一目标轻松实现。
  • 活动可组合性:我们非常喜欢 Temporal 的另一个优点——活动链式组合。一个活动的输出可以直接作为下一个活动的输入,这种模式大大提升了开发效率和代码可读性。
  • 良好的可扩展性:在整个过程中,Temporal 的 worker 扩展能力从未成为瓶颈。一旦某个迁移流程被启动,任务就会被调度到部署在内部 K8s 集群中的 Temporal worker 上,系统整体表现非常稳定可靠。

我们设计了两个核心工作流:

  • Watcher 父工作流:以固定周期运行,用于扫描哪些集群被安排迁移,并为每个目标集群并发启动一个迁移子流程。
  • Migration 子工作流:负责执行单个服务的迁移操作。该流程包含以下步骤:
    • 执行维护前的健康检查,检测配置是否存在漂移或异常

    • 捕捉实例状态(因为迁移过程中状态会动态变化)

    • 与迁移控制器建立通信

    • 等待控制器发出关键 Kubernetes 事件,如 Start、PodsReady 和 Finished

    • 迁移完成后,退出“部分维护模式”,并将集群恢复至原始运行状态

图片

迁移控制器设计复用 

operator 核心能力

我们将迁移控制器设计为主 operator 的封装组件,专门用于独立编排迁移相关任务,与日常运维逻辑解耦。在这个控制器内部,我们复用了主 operator 中用于集群配置、数据库与数据表同步等常规任务的已有逻辑;而涉及迁移步骤协调与顺序控制的专有逻辑,则被完整地封装在迁移控制器内部。通过这种职责划分,我们将迁移逻辑与日常运维流程有效隔离,降低了操作风险。与此同时,这一设计也让我们可以在需要时快速回滚迁移,恢复到由原始 operator 管理的状态。

图片

与 Kubernetes 控制器的协调

引入包装控制器的最大挑战在于,它和原始 operator 会同时对同一个 Custom Resource(CR)进行控制。这样一来,相当于有两个控制循环在同时作用于同一资源,极易引发混乱,并导致同步行为不可预测。

为了解决这个问题,我们实现了一个互斥锁机制,确保任意时刻只有一个控制器进程在运行。这样可以实现严格的串行化操作,避免由于并发导致的状态不一致或资源冲突,从而保证系统稳定性。

图片

迁移挑战

尽管在纸面上看起来流程标准,但实际操作中却远非如此。每位 ClickHouse Cloud 客户运行的工作负载各不相同,这让迁移过程充满挑战。这也是我们第一次真正测试 ClickHouse 在弹性环境下动态添加和移除副本的能力,同时也是对我们自研的 MultiSTS 架构的首次大规模验证。在当时,ClickHouse Server 从未在这种副本频繁增删的集群架构中运行过,我们也预期会遇到各种新颖的技术难题。

以下是我们在迁移过程中遇到的一些典型问题:

问题一:未完成的 ON CLUSTER DDL 查询导致删除副本失败
在某个副本尚有 DDL 操作待执行时将其删除,可能导致其主机名失效,从而引发 DNS 查询失败。

解决方案:我们对 ClickHouse 提交了修复,使 DDL 查询在处理已被移除的副本时更加健壮,避免整个集群阻塞。

问题二:外部表引擎连接丢失
部分集群依赖 PostgreSQL、RabbitMQ 或 NATS 等外部表引擎。在传统重启场景中,失效连接可能被忽略,但在 MBB 迁移中,新副本上线时会尝试连接这些外部系统,从而导致迁移失败。

解决方案:我们通过调试模式定位失效连接,部分问题由我们直接处理,部分则与客户协作恢复外部依赖。

问题三:外部字典引用失效
ClickHouse 中的外部字典可以引用 S3 等外部表或存储。若这些引用配置错误或不存在,会在新副本创建过程中导致失败。

解决方案:我们与 ClickHouse 核心团队合作,在引擎层修复了部分问题,为未来的迁移提供更强健的基础。

问题四:物化视图缺失基础表
如果某个物化视图的基础表已被删除,新副本将无法正确创建依赖关系,导致视图构建失败。

解决方案:核心团队再次修复了该逻辑,确保在迁移过程中能够自动恢复缺失的基础表。

图片

问题五:ReplicatedMergeTree 的 Part 元数据同步异常
在 MBB 缩容流程中,我们使用 SYSTEM SYNC REPLICA LIGHTWEIGHT 命令在存活副本上同步元数据。但该命令原本会尝试从所有副本同步,包括正在写入的新副本,容易陷入僵局。

解决方案:我们增加了 FROM 修饰符,仅从“Break” 状态的旧副本中同步,从而实现了无阻塞的数据同步。

图片

问题六:MultiSTS 拓扑分布异常
MultiSTS 旨在将副本分散在多个可用区中。但某些情况下会导致 maxSkew(最大倾斜)约束被打破。例如从 [a, b] 扩容到 [a, b, c, c],再移除 [a, b],最终变为 [c, c],违反了可用区分布要求。

解决方案:我们优化了拓扑逻辑,确保在扩缩容过程中,所有副本始终满足可用区分布的策略约束。

图片

迁移过程中,我们经常通过将集群切换至调试模式,从我们这边直接修复问题。但也有不少情况需要联系客户,协作解决特定的异常。在这个过程中,我们还发现了若干 ClickHouse 核心层面的 Bug,并由核心开发团队完成了修复。这一系列改进,不仅提升了 ClickHouse 在弹性场景下的稳定性与健壮性,也让整套系统对动态操作的适应性更强。更重要的是,“Make Before Break” 原则确保了整个迁移过程中,从未对客户的生产业务造成中断或影响。

整个迁移过程花费了大量时间,也充满了挑战。但随着经验积累,我们逐渐建立起一套问题识别与快速响应机制。大多数问题都有规律可循,修复过程虽不轻松,但并不复杂。当然,也有一些需要特别处理的例外情况。

当 MultiSTS 迁移全部完成后,ClickHouse Cloud 的客户理论上可以全面受益于 Make Before Break 架构了。

不过,真的是这样吗?

系统表的迁移挑战

对于熟悉 ClickHouse 的用户来说,system 表一定不陌生。它们是 ClickHouse 系统用来存储运行状态的核心数据,常用于排查查询异常、备份失败或集群运行故障。工程师在调试问题时,第一步通常就是查这些表。

很多 system 表是纯内存表,不具备持久化能力。而即使是支持持久化的那些表(例如使用 MergeTree 引擎、以 _log 结尾的 system.query_log 和 system.text_log),也只会存储在当前副本本地。这就意味着,它们并不使用 ClickHouse Cloud 中常见的 SharedMergeTree 引擎。这么做的原因很直接:如果 SharedMergeTree 本身出了问题,我们仍需要通过系统表排查异常,不能让两者共命运。

但与此同时,我们也尤其希望在 Make Before Break 过程中能保留这些关键的系统表数据。为了解决这个问题,ClickHouse 核心开发团队(特别是 Julia Kartseva)引入了一种全新的 S3 磁盘类型:s3_plain_rewritable。借助这种“可重写”的对象存储支持,我们可以将系统表从原有的本地磁盘迁移到 S3 上。再结合根据表名、UUID 和对象存储路径执行 ATTACH 命令的能力,我们现在已经能够在 MBB 操作中完整保留系统表数据。

图片

系统表挂载机制

虽然从表面看,系统表的挂载只是一个简单操作,但实际上它需要对每张系统表进行非常细致的追踪管理。我们对 clickhouse-operator 做了修改,使其可以记录每张系统表以及所属的副本名称。因为每个副本都有自己本地的 system.query_log 等系统日志表,在旧的 3 个副本被移除后,我们希望能将这些表数据挂载到新副本上,并尽量保持原有的数据分布不变。

此外,这也和日志数据的保留策略密切相关。目前我们对系统表设定了 30 天的硬性保留期限。随着云平台中越来越多的 MBB 操作(包括升级与扩容),系统日志表的数量也随之激增,这直接影响到了副本的启动时间。我们正在积极探索更优的管理方式。

总结

从构建 MultiSTS 架构,到实现 Make Before Break,再到全面支持在线迁移,这一系列工作前后持续了近两年。面对众多客户的差异化场景,我们经历了无数挑战。尤其在迁移阶段,不乏一些“迁移尾部客户”,他们的问题特殊,迁移推进缓慢。我们的典型模式就是:迁移一批客户 → 遇到问题 → 修复 → 继续前进。同时,我们也非常注重客户沟通。每一次迁移前,我们都会通过控制台界面或邮件提前向关键客户进行提醒,确保他们了解即将发生的变更。

对于那些有特殊要求的客户,我们的工程师也会“亲自上阵”,全程保障迁移顺利推进。更值得庆幸的是,得益于 Make Before Break 策略,整个迁移过程中从未影响客户的生产业务。

最终,我们用了超过一年的时间,成功完成了最后一位客户的迁移。现在,整个 ClickHouse Cloud 集群都已切换至 MultiSTS 架构,并全面启用了 Make Before Break 功能。该方案已经在数千个集群上稳定运行,支撑着扩容与版本升级操作。

随着架构的全面升级,我们的扩容效率显著提升,系统也不再受到中断与长时扩容等早期问题困扰,资源分配更加快速与稳定。

如果你对云基础设施与数据库融合带来的工程挑战充满兴趣,欢迎加入我们,一起构建下一代云原生数据库平台。我们正在招聘!

征稿启示

面向社区长期正文,文章内容包括但不限于关于 ClickHouse 的技术研究、项目实践和创新做法等。建议行文风格干货输出&图文并茂。质量合格的文章将会发布在本公众号,优秀者也有机会推荐到 ClickHouse 官网。请将文章稿件的 WORD 版本发邮件至:Tracy.Wang@clickhouse.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值