将关键 Kubernetes deployment(部署)从默认 namespace(命名空间)迁移且不造成任何停机

来源: Cloud Native Computing Foundation
作者: George Sims, Downtherabbithole.dev
发布时间: 2026年9月3日 19:00:00
原文链接: https://www.cncf.io/blog/2026/09/03/migrating-a-critical-kubernetes-deployment-from-the-default-namespace-without-any-downtime/


发布于 2026 年 9 月 3 日,作者:George Sims,Downtherabbithole.dev

本文重点介绍的 CNCF 项目

图片链接: https://www.cncf.io/projects/kubernetes

在集群的某个地方,可能有一个部署在 default 命名空间里,所有人都知道它不应该待在那儿。没有人恶意把它放进去,它只是很早就存在了,那时还没人对命名空间整洁有什么意见,而现在你一半的其他服务都悄悄依赖着它。移动它变成了一个棘手的问题。

我之前遇到的一个服务正是这种情况,我姑且称之为 auth-svc:这是一个认证服务,几十个其他服务不停地调用它,它在 default 里待了好几年,而一旦它需要命名空间级别的东西——自己的 Ingress 规则、自己的策略,而 default 在结构上无法提供这些——它就会变成一个真正的问题。移动它不能永远拖下去。但它也不能宕机,哪怕几秒钟都不行。这不是那种模糊的“其他服务可能会抱怨”的风险:auth-svc 负责整个区域集群的认证,如果它宕机了,那个区域里没人能登录。就此打住。

在深入探讨为什么这实际上很困难之前,有必要先明确“移动它”到底意味着什么。通向 auth-svc 有两条完全独立的路径,而且在迁移过程中两条路径都必须保持可用,否则修好一条只是给另一条制造故障。集群内的一切都以普通方式访问它:其他服务通过 Kubernetes 自己的内部 DNS 解析 auth-svc.default.svc.cluster.local,然后被路由到一个 Pod,这是标准的 Service 机制。集群外的一切则通过 Ingress 访问它,这是一个完全独立的机制,和那个 DNS 名称没有任何关系。无论最终采用什么修复方案,它都必须同时解决两条路径,而不只是那条更容易想清楚的路。

为什么“直接移动”行不通

最明显的方案——移动 Deployment、更新引用、完事——在你一看是谁在真正调用这个东西时,就立刻崩溃了。如前所述,几十个其他服务通过集群内部 DNS 名称引用 auth-svc,它们属于不同团队,处于不同的发布周期。不存在一个原子性的瞬间,让你拨一下开关,它们就能同时开始使用新名称。有些团队的服务已经好几个月没有重新部署了。你不应该去协调这种事。

工具链也碍事。我们的部署流水线只会把服务发布到一个命名空间。没有“同时部署到两个地方”的选项,而修改其他每个团队都依赖的共享流水线逻辑,恰恰是我们不想引入的那种爆炸半径。无论修复方案是什么,它都必须能装进单命名空间部署这个框架里,而不是要求重写共享基础设施。

除此之外,我们还有一条 OPA 策略,它强制规定完全相同的 Ingress 规则不能同时存在于两个命名空间中。这是一条合理的规则,专门用来阻止那种半途而废、让路由变得模糊不清的迁移。

关键洞察:一个转发地址

让这件事变得可解决的切入点在于:我并不需要迁移每个调用方对 auth-svc 所在位置的理解。我需要迁移这个服务,然后悄悄地把仍然在请求旧地址的人重定向走。

Kubernetes 正好有这个机制,而且你很容易忘记它的存在,因为你几乎从来用不到它:ExternalName Service。它不指向 Pod,而是指向另一个 DNS 名称,作用本质上类似于 CNAME(想了解更多原理的话可以看看我的 DNS 文章)。把真正的东西部署到它的新家,然后把旧的 Service 对象转换成一个转发地址。

 apiVersion: v1
 kind: Service
 metadata:
 name: auth-svc
 namespace: default
 spec:
 type: ExternalName
 externalName: auth-svc.authentication.svc.cluster.local

所有仍在调用 auth-svc.default.svc.cluster.local 的消费方都会被静默重定向到新命名空间中的真实服务,它们的代码无需改动一行。这和邮政转寄指令是同一个套路:你不需要挨个拜访可能给你寄信的人、更新他们的通讯录,只需告诉邮局你现在的实际住址,所有信件就会自动转寄过去,直到人们最终按照自己的节奏自行更新地址——整个过程中你这边零协调成本。

这个转寄技巧只有在真正依赖它之前确认它确实有效,才值得发挥作用。代理上线后,下一步不是缩容任何东西,而是观察切换期间的指标:检查打到旧地址的流量是否真的落在了新部署上,而不是在某个地方静默失败或循环。只有当一切看起来正常时,旧 Pod 才会缩容到零,而不是直接删除。缩容到零不花任何成本,还能换来即时回滚能力——如果之后发现下游有问题,只需再扩容回来。而直接删除的话,一旦出了岔子就得从头重建,所以没有理由过早放弃这张安全网。我们可以把清理工作推迟到稍后的时间点。

先有鸡还是先有蛋的问题

剩下的麻烦在于 Ingress。外部流量访问 auth-svc 根本不走基于 DNS 的 Service 机制,而是通过 Ingress 进入。在安全移除旧命名空间中的 Ingress 之前,我需要在新命名空间中先有一个可用的 Ingress。但策略引擎不允许两个同时存在;两个命名空间中出现相同的 Ingress 规则,正是它要防止的歧义状态。

典型的鸡生蛋问题:没有策略例外就无法创建新的;先删旧的又会出现流量缺口。

解决办法是采用临时的、显式的例外,而不是与策略本身对抗:为新的命名空间添加注解(annotation),仅针对这次迁移绕过重复 Ingress 检查;让新 Ingress 与旧 Ingress 共同存在一小段重叠期,确认流量正确流向新部署;然后删除旧 Ingress,让该例外自然过期。这是一个短暂且有意的两者共存窗口,而不是两者皆无的真空期。

补丁大致如下:

 apiVersion: v1
 kind: Namespace
 metadata:
 name: authentication
 annotations:
 policy.example.com/allow-duplicate-ingress: "true"

实际发布过程

这些工作没有任何一项是直接进入生产环境的。它先在开发环境运行,然后进行预发布(staging)切换,在预发布成功与实际生产部署之间隔了几周时间,主要是为了先观察一段,看看在真实流量下是否会出现任何不易察觉的问题,然后再把关键路径押在它上面。

生产环境切换本身反倒成了最平淡无奇的部分。所有真正的难点都前置到了设计环节。一旦方案扎实了,执行起来就更像走个流程,而不是什么大事。

为什么这件事的意义不止于单个服务

关于默认命名空间(default namespace)的蔓延问题,关键在于:它很少是粗心大意造成的,而是因为根本没有任何压力去推动修复。有人制定了策略,限制新服务落在默认命名空间中——这是好的实践——但没有人给已经存在其中的服务设定截止日期或迁移计划。它们就那样一直搁置着,在这个案例中搁置了数年之久。直到某个团队需要命名空间级别的作用域,而默认命名空间在结构上无法提供时,这个问题才被迫浮出水面,也只有到那时,这笔技术债才到了偿还的时候。

如果你正面对一个类似卡住的服务,这个模式是可以推广的,不仅限于这一次迁移。ExternalName 代理(ExternalName proxy)为你提供了一条无需协调的路径,可以迁移任何通过 DNS 寻址的服务——前提是你愿意在一个短暂而审慎的时间窗口内,让两个版本小心翼翼地重叠共存,而不是试图一次性全部切换。下次有人告诉你某个服务无法迁移,因为依赖它的东西太多时,这通常意味着没有人去寻找可以沿着它切分的 DNS 接缝。

*

这次迁移之所以能干净利落地完成,是因为这里所有组件都是通过 DNS 名称与 auth-svc 通信,而不是硬编码的 IP 或 ClusterIP。如果你的消费者完全绕过了 DNS——一些遗留系统确实如此——那你面临的就是另一个更棘手的问题了。