文中的 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 控制台操作:

  1. 进入正确 namespace。
  2. 搜索精确 Data ID 和 Group。
  3. 打开历史版本,确认能够回滚。
  4. 粘贴候选内容,查看控制台 diff。
  5. 只发布一个 Data ID。
  6. 立即回读并下载一次,比较校验值。

若使用自动化 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 回滚

如果业务验收失败:

  1. 停止继续发布其他 Data ID。
  2. 使用 Nacos 历史版本恢复刚才修改的内容。
  3. 回读确认恢复成功。
  4. 滚动刚才受影响的客户端。
  5. 保存故障日志和失败请求。

不要导入整个旧 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 配置引用,需要同时收集四类数据:

  1. 旧集群 Service 名称、namespace、ClusterIP 和端口。
  2. 新集群 Service 名称、namespace、ClusterIP 和端口。
  3. 线上或其他同构环境中的稳定配置。
  4. 当前 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 和配置导出不得进入仓库。

最终经验

这次治理最重要的结果不是替换了多少地址,而是建立了明确边界:

  1. Kubernetes 内部依赖优先使用 Service DNS。
  2. 已删除服务不能靠虚构 DNS 修复。
  3. Kafka、hostNetwork 和外部系统必须单独分类。
  4. 每次 Nacos 修改都要备份、逐项发布、回读和真实请求验证。

固定 IP 并不总是错误,但它必须是有负责人、有原因、有移除计划的显式兼容项,而不是来源不明的历史字符串。

地址迁移稳定后,可继续按 Spring Boot 3 Nacos 全局配置基线治理 Redis、Redisson、RabbitMQ、Hikari、Tomcat 与配置覆盖关系。