文中的 namespace、服务名、域名、地址和配置数量均已脱敏或泛化。真实配置备份可能包含密码和 Token,绝不能进入博客或 Git 仓库。
系列导航:Kubernetes 重建迁移教程系列。本篇假设 Nacos 集群已经搭建并通过验收。
Kubernetes 集群迁移后,最隐蔽的一类故障不是 Pod 起不来,而是 Nacos 配置里还保存着旧集群的 Service ClusterIP。
典型现象包括:
Load balancer does not have available server
Unable to connect to Redis
No route to host
connect timed out
旧 IP 在新集群中可能不存在,也可能恰好被另一个 Service 占用。后者比直接超时更危险,因为请求会被转发到错误服务。
因此,这次治理的目标不是“把 IP 换成新 IP”,而是尽可能改为稳定的 Kubernetes Service DNS。
下面先完整走一遍操作流程,再解释为什么有些地址不能自动替换。
第 1 步:建立隔离工作目录
mkdir -p nacos-dns-migration/{backup,inventory,candidates,reports}
cd nacos-dns-migration
git init
printf 'backup/\n*.zip\n*.token\n.env\n' > .gitignore
原始 Nacos 导出文件可能包含密码、Token 和数据库地址,只能放在 backup/,并应使用加密存储。Git 中只保留脱敏后的规则、脚本和报告模板。
创建一份变更清单:
namespace,group,data_id,old_value,target_service,target_port,action,owner,status
app-test,DEFAULT_GROUP,global.properties,<old-address>,redis-service,6379,replace,team-a,pending
所有发布动作都要能追溯到这一行。
第 2 步:导出 Nacos 配置
最稳妥的方式是使用 Nacos 控制台按 namespace 导出配置压缩包,并记录导出时间、操作人和文件校验值:
sha256sum backup/<namespace>-configs.zip \
> backup/<namespace>-configs.zip.sha256
如果使用 OpenAPI 自动导出,API 路径、鉴权和兼容性必须按当前 Nacos Server 版本核对。从 Nacos 3.2 起移除了部分旧 HTTP API,不能直接用旧脚本操作新集群。
解压到临时目录后先只读扫描:
mkdir -p backup/unpacked
unzip -q backup/<namespace>-configs.zip -d backup/unpacked
rg -n --glob '*.{properties,yaml,yml,json,xml}' \
'([0-9]{1,3}\.){3}[0-9]{1,3}' backup/unpacked \
> reports/ip-references.txt
不要在扫描结果里打印整行 Secret。真实脚本应对 password、secret、token 等键的值进行掩码。
第 3 步:导出新旧集群 Service 清单
分别显式指定 kubeconfig 导出,不要依赖当前 context:
kubectl --kubeconfig <old-kubeconfig> get service -A -o json \
> inventory/old-services.json
kubectl --kubeconfig <new-kubeconfig> get service -A -o json \
> inventory/new-services.json
转换为便于审阅的 TSV:
jq -r '
.items[]
| select(.spec.clusterIP != "None")
| [
.metadata.namespace,
.metadata.name,
.spec.clusterIP,
(.spec.ports | map((.port|tostring) + ":" + (.targetPort|tostring)) | join(","))
]
| @tsv
' inventory/old-services.json > inventory/old-services.tsv
jq -r '
.items[]
| select(.spec.clusterIP != "None")
| [
.metadata.namespace,
.metadata.name,
.spec.clusterIP,
(.spec.ports | map((.port|tostring) + ":" + (.targetPort|tostring)) | join(","))
]
| @tsv
' inventory/new-services.json > inventory/new-services.tsv
现在可以按旧 IP 反查 Service:
rg -F '<old-service-ip>' inventory/old-services.tsv
预期得到:
app-test rbac-api <old-service-ip> 8082:8082
目标 DNS 就是:
rbac-api.app-test.svc.cluster.local
第 4 步:逐条确认目标 Service 和端口
先检查新集群是否存在同名 Service:
kubectl --kubeconfig <new-kubeconfig> -n app-test \
get service rbac-api -o yaml
kubectl --kubeconfig <new-kubeconfig> -n app-test \
get endpointslice \
-l kubernetes.io/service-name=rbac-api -o wide
只有同时满足下面条件,才能进入自动候选清单:
- 旧 IP 能唯一反查到一个 Service。
- 新集群存在语义相同的 Service。
- 配置端口对应新 Service 的
port,而不是误用targetPort。 - EndpointSlice 至少有一个 Ready 地址。
- 负责人确认该依赖仍然存在。
其余条目全部标成 manual-review,不能让脚本猜。
第 5 步:生成候选文件,不直接发布
复制一份原始配置:
cp backup/unpacked/global.properties \
candidates/global.properties
把:
rbac.api.url=http://<old-service-ip>:8082
改为:
rbac.api.url=http://rbac-api.app-test.svc.cluster.local:8082
然后审阅差异:
diff -u backup/unpacked/global.properties \
candidates/global.properties \
| tee reports/global.properties.diff
审阅时逐行确认:
- 只修改主机部分,协议、端口、路径和参数未丢失。
- 没有把外部依赖改成集群 DNS。
- 没有把 Kafka broker 地址合并成普通 Service。
- 没有新增不存在的 Service 名称。
第 6 步:在业务 Pod 中提前验证 DNS
正式发布前,从真实调用方 Pod 测试目标地址:
kubectl -n <caller-namespace> exec <caller-pod> -- \
getent hosts rbac-api.app-test.svc.cluster.local
kubectl -n <caller-namespace> exec <caller-pod> -- \
nc -vz rbac-api.app-test.svc.cluster.local 8082
这一步失败时,修改 Nacos 没有意义。先修复 Service、Endpoint、NetworkPolicy、CNI 或 kube-proxy。
第 7 步:逐个 Data ID 发布
首次治理时,建议通过 Nacos 控制台操作:
- 进入正确 namespace。
- 搜索精确 Data ID 和 Group。
- 打开历史版本,确认能够回滚。
- 粘贴候选内容,查看控制台 diff。
- 只发布一个 Data ID。
- 立即回读并下载一次,比较校验值。
若使用自动化 API,脚本必须实现:鉴权、超时、发布前备份、发布后回读、内容校验和单 Data ID 回滚。不要用一个循环无条件覆盖整个 namespace。
第 8 步:只滚动受影响的客户端
先确认配置是否支持动态刷新。不能动态刷新的应用再执行滚动重启:
kubectl -n <caller-namespace> rollout restart deployment/<caller-app>
kubectl -n <caller-namespace> rollout status deployment/<caller-app> --timeout=10m
查看最近日志:
kubectl -n <caller-namespace> logs deployment/<caller-app> --since=10m \
| grep -Ei 'nacos|refused|timed out|no route|available server'
不要因为修改了一份共享 global.properties 就同时重启所有命名空间。按依赖关系分批执行,前一批通过真实业务验收后再继续。
第 9 步:执行真实请求并搜索旧地址
除了健康检查,还应完成登录、查询、缓存读写或内部 RPC 等真实请求。然后在配置和运行日志中搜索旧地址:
rg -n -F '<old-service-ip>' candidates reports
kubectl -n <caller-namespace> logs deployment/<caller-app> --since=30m \
| grep -F '<old-service-ip>'
候选配置和新日志中都不应再出现旧 Service IP。
第 10 步:按 Data ID 回滚
如果业务验收失败:
- 停止继续发布其他 Data ID。
- 使用 Nacos 历史版本恢复刚才修改的内容。
- 回读确认恢复成功。
- 滚动刚才受影响的客户端。
- 保存故障日志和失败请求。
不要导入整个旧 namespace 压缩包,否则会覆盖迁移期间其他团队已经发布的正常配置。
为什么固定 ClusterIP 会成为迁移债务
ClusterIP 的生命周期属于 Service 对象。删除并重建 Service 后,如果没有显式指定 clusterIP,地址可能变化。
下面这种配置把应用与某一次集群状态绑定在一起:
rbac.api.url=http://10.x.y.z:8082
spring.data.redis.host=10.x.y.z
更稳定的写法是:
rbac.api.url=http://rbac-api.app-test.svc.cluster.local:8082
spring.data.redis.host=redis-service.app-test.svc.cluster.local
只要 Service 名和 namespace 保持不变,Pod、Endpoint 和 ClusterIP 的变化都不会影响调用方。
先建立来源,而不是直接替换
针对每个 Nacos 配置引用,需要同时收集四类数据:
- 旧集群 Service 名称、namespace、ClusterIP 和端口。
- 新集群 Service 名称、namespace、ClusterIP 和端口。
- 线上或其他同构环境中的稳定配置。
- 当前 Deployment、StatefulSet 和 Endpoint 的实际存在状态。
随后为每个地址建立分类:
| 类型 | 处理方式 |
|---|---|
| 当前存在的 Kubernetes Service | 改为完整 Service DNS,并核对端口 |
| 已删除或未迁移的 Service | 不自动替换,先确认恢复还是删除路由 |
| Pod IP | 禁止保留,找到对应 Service |
| Kafka broker 地址 | 单独设计 listener,不做普通 DNS 替换 |
| 节点上的 hostNetwork 服务 | 评估能否改为 Service;不能时登记例外 |
| 外部数据库、短信、专网系统 | 保持原有外部地址 |
这一步决定了治理是否安全。正则表达式只能找到地址,无法判断地址的所有权。
为什么不能把所有 IP 都替换掉
已删除服务
如果一个旧地址指向已经不存在的 Service,把它改成:
missing-service.app-test.svc.cluster.local
只会把连接超时变成 DNS 解析失败。正确选择只有两个:恢复目标 Service,或者删除不再使用的配置和路由。
Kafka advertised listener
Kafka 客户端不是只连接一个入口。Bootstrap Server 会返回 broker 的 advertised address,客户端随后直连具体 broker。
因此,原来为每个 broker 暴露的 NodePort 或节点地址,不能简单改成一个普通 ClusterIP。需要先设计:
- 内外部 listener。
- 每个 broker 独立 Service。
- advertised listener。
- DNS、NodePort 或负载均衡入口。
Kafka 地址治理应作为独立项目执行。
外部依赖
短信、云数据库、合作方接口和专网系统并不属于 Kubernetes。不能仅因为它们是 IP,就把它们改成 svc.cluster.local。
配置发布必须可回滚
每次修改前保存:
- namespace。
- Group。
- Data ID。
- 原始内容。
- MD5 或其他校验值。
发布流程:
flowchart LR
A["导出配置"] --> B["识别 IP 和 URL"]
B --> C["关联 Service 与端口"]
C --> D["生成候选改动"]
D --> E["人工审阅例外"]
E --> F["逐 dataId 发布"]
F --> G["回读并比对校验值"]
G --> H["滚动客户端并验证"]
不要一次覆盖整个 namespace,也不要把备份文件提交到 Git。回滚应精确到单个 Data ID,而不是用旧导出包覆盖之后产生的其他正常变更。
端口必须与 Service 一起校验
DNS 正确不代表配置正确。常见问题是服务名已改,端口仍然来自旧环境。
可以把引用解析成:
namespace / service / configured-port
再与 Kubernetes Service 的 port、targetPort 和实际 Endpoint 对比。最终要求:
- DNS 能解析。
- Service 存在。
- 配置端口与 Service port 一致。
- Endpoint 不为空。
- 目标 Pod 真实监听对应端口。
Redis 配置需要兼容不同 Spring Boot 版本
不同服务可能分别读取:
spring.data.redis.host
spring.redis.host
gateway.oauth2.redis.host
platform.redis.host
如果系统正在跨 Spring Boot 版本升级,可以先以新属性为源,并为旧服务保留临时别名:
spring.data.redis.host=redis-service.app-test.svc.cluster.local
spring.data.redis.port=6379
spring.redis.host=${spring.data.redis.host}
spring.redis.port=${spring.data.redis.port}
spring.redis.password=${spring.data.redis.password}
spring.redis.database=${spring.data.redis.database}
兼容别名需要注明移除条件,避免永久维护两套配置。
如果旧应用已经回退到 localhost:6379,或者补充 Lettuce pool 参数后出现 commons-pool2 类缺失,可继续阅读 Boot 2/3 Redis 配置回退故障复盘。
验证不能停在 Nacos 页面
配置发布后,按以下五层验证:
1. 配置回读
确认 namespace、Group、Data ID、内容和 MD5 与发布结果一致。
2. DNS 解析
从真实业务 Pod 中执行:
getent hosts redis-service.app-test.svc.cluster.local
3. Service 与 Endpoint
kubectl -n app-test get svc redis-service
kubectl -n app-test get endpointslice -l kubernetes.io/service-name=redis-service
4. 网络连接
从调用方 Pod 访问 Service DNS 和端口。若 Endpoint 可达而 Service 不可达,排查请求源节点 kube-proxy;若 Pod IP 跨节点不可达,排查 CNI 和 firewalld。
5. 真实业务请求
重启或刷新对应客户端后,执行登录、缓存、消息或内部 API 请求。日志中不应继续出现旧地址。
Nacos 服务发现与配置中心要分开看
Nacos 同时承担配置中心和服务发现,两个问题经常被混在一起:
- 配置中心决定 Redis、数据库、内部 API 等 URL。
- 服务发现决定某个服务是否有健康实例。
当网关提示“没有可用服务”时,应检查 Nacos instance list;当应用连接旧 ClusterIP 时,应检查配置内容。只重启 Pod 可能让故障现象暂时缓解,却不会修复错误的源配置。
自动化审计建议
持续治理可以包含这些检查:
1. 扫描所有 IPv4、URL 和主机名
2. 判断是否属于 Pod CIDR、Service CIDR 或节点网段
3. 将 Service IP 反查到 namespace/service
4. 校验配置端口和 Service port
5. 标记不存在的 Service DNS
6. 生成例外清单,而不是自动覆盖
可以在 CI 中为新增配置设置规则:
- 禁止写入 Pod CIDR。
- 默认禁止写入 Service CIDR。
- 允许完整 Service DNS。
- 固定 ClusterIP 必须引用白名单记录。
- Secret 和配置导出不得进入仓库。
最终经验
这次治理最重要的结果不是替换了多少地址,而是建立了明确边界:
- Kubernetes 内部依赖优先使用 Service DNS。
- 已删除服务不能靠虚构 DNS 修复。
- Kafka、hostNetwork 和外部系统必须单独分类。
- 每次 Nacos 修改都要备份、逐项发布、回读和真实请求验证。
固定 IP 并不总是错误,但它必须是有负责人、有原因、有移除计划的显式兼容项,而不是来源不明的历史字符串。
地址迁移稳定后,可继续按 Spring Boot 3 Nacos 全局配置基线治理 Redis、Redisson、RabbitMQ、Hikari、Tomcat 与配置覆盖关系。
DISCUSSION
讨论与反馈