☰
K8s 部署 Nacos 集群:YAML 字段拆解与启动死锁排查
2026/10/23 5:44:53 网站建设 项目流程

k8s 部署 nacos 集群这件事,我前前后后在不同环境里做过五六遍,第一次是照着官方给的示例 YAML 直接kubectl apply,结果三个 Pod 起来了、日志也刷得挺欢,但控制台上集群节点列表里始终只有一个节点,另外两个像人间蒸发了一样。后来才发现是 Headless Service 少了一个publishNotReadyAddresses字段,节点在变成 Ready 之前互相解析不到,于是谁也发现不了谁,形成死锁。这个坑官方文档里其实写了,但写得很隐蔽,藏在 Service 定义里的一行注释上。

这篇内容就是围绕"照官方那份示例走一遍"这件事展开的,但我不想写成一份翻译版的 YAML 注释。我更想把每个字段为什么这么写、不这么写会出什么问题讲透,把你真跑起来之后半小时内大概率会撞上的那几堵墙提前拆掉。适合已经能在 k8s 里跑起 Deployment 和 Service、但对 Nacos 集群的内部协同机制不太熟的读者,也适合那些"已经跑起来了但心里没底"的人对照着检查一遍。技术栈默认是 Nacos 2.x、Kubernetes 1.2x 时代的主流版本,MySQL 走 8.0,存储用集群已有的 StorageClass。

1. 先定形态:Nacos 在 k8s 里到底该怎么摆

动手写 YAML 之前,有个问题必须先回答清楚:你要的是"能跑起来的 Nacos",还是"生产可用的 Nacos 集群"。这两者的资源清单差别很大,而官方仓库里恰好两种都给了示例,很容易拿错文件。

1.1 单机、内嵌集群、外置数据库集群,三条路的分岔口

Nacos 的部署形态本质上是三种,区别只在数据落在哪。

第一种是单机模式,MODE=standalone,数据写进容器内嵌的 Derby 库,落在/home/nacos/data目录。它的优点是零依赖、启动快、YAML 短得可以贴在一张便利贴上。缺点是数据跟 Pod 生命周期绑死,Pod 重建数据就没了,而且 Derby 不支持多实例共享同一个库文件——这点很关键,很多人想着"我挂个 NFS 上去让三个 Pod 共享 data 目录不就行了",结果就是文件锁冲突、库文件损坏,服务直接起不来。

第二种是内嵌 Derby + 多节点集群,MODE=cluster但不接外置数据库。这条路在 Nacos 1.x 时代官方就不太推荐,2.x 更是明确要求集群模式必须外置存储。原因是集群模式下每个节点都要读写配置数据,Derby 的单文件特性撑不住这种并发写。

第三种是集群 + 外置 MySQL,这才是官方示例里nacos-mysql-cluster那一份干的事,也是唯一值得上生产的选择。它的结构是三个(或五个)Nacos Pod 各自无状态,所有配置数据、命名空间、历史版本统一落在 MySQL 里,节点之间只通过 Raft 和 Distro 协议同步服务实例这类临时数据。

我把三者的取舍整理成一张表,你对着自己的场景挑就行。

形态关键环境变量数据落点适用场景明显短板
单机MODE=standalone容器内 Derby本地开发、Demo 演示数据随 Pod 消失,无法多副本
内嵌集群MODE=cluster无数据库配置各自 Derby不建议数据不一致,节点间无法共享配置
外置库集群MODE=cluster+ 数据源外置 MySQL测试、预发、生产多一个 MySQL 依赖要维护

选型上我的建议很直接:只要这个环境里有人会依赖它做服务发现,就老老实实上第三种。省下来的那点 MySQL 运维成本,远小于某天配置全丢之后的重建成本。

1.2 节点数为什么必须是奇数,而且我建议就从 3 开始

Nacos 2.x 里持久化实例和集群元数据走的是 JRaft,Raft 的可用性建立在"多数派"上:3 个节点能容忍 1 个挂掉,5 个节点能容忍 2 个,2 个节点反而不如 1 个——因为任何一个挂掉,剩下那个凑不够多数派,整个集群的写能力直接归零。

所以节点数只能是 1、3、5、7 这样的奇数。实际选型时我会按下表判断:

  • 3 节点:绝大多数业务的起点。资源占用可控,容忍单节点故障,滚动升级时不会中断写入。
  • 5 节点:写入压力大、或者对故障容忍度要求更高的场景。代价是节点间同步的网络往返变多,选举日志也更长。
  • 7 节点及以上:基本没必要。Raft 的写放大随节点数线性增长,收益却很小。

还有一个容易被忽略的点:Nacos 集群的节点数一旦定下来,扩缩容就不是kubectl scale一句话的事。因为NACOS_SERVERS这个环境变量里硬编码了所有节点的地址,加节点意味着所有已有节点的这个变量都要改,然后滚动重启。这个后面第 7 章我会详细讲。

注意:不要用 Deployment 跑 Nacos 集群。Pod 名字随机、没有稳定网络标识,节点之间根本没法互相寻址。集群模式必须用 StatefulSet。

2. 官方示例仓库里到底给了什么,哪些假设藏在细节里

很多人说"照官方文档部署",其实指的是那个维护在开源社区里的 k8s 部署示例仓库。它里面确实有几份能直接用的 YAML,但那些文件是"最小可运行示例",不是"生产就绪模板",两者的差距需要你自己补。

2.1 目录结构扫一遍,别拿错文件

那个仓库的deploy目录下大致是这么几类东西,我用表格对一下,你按需取用。

文件/目录模式依赖我什么时候会用它
nacos-quick-start.yamlstandalone无快速验证镜像能不能起、控制台长什么样
nacos-pvc-nfs.yamlstandaloneNFS StorageClass想验证持久化,但不想上 MySQL
mysql/nacos-mysql-standalone.yamlstandalone外置 MySQL验证数据库连接配置是否正确
mysql/nacos-mysql-cluster.yamlcluster外置 MySQL正式部署的起点,我通常拿它改
helm/下的 chart两者皆可视 values 而定集群已有 Helm 体系、需要参数化时

我踩过的一个低级坑:第一次直接 apply 了nacos-quick-start.yaml,然后纳闷为什么replicas改成 3 之后三个 Pod 各玩各的。因为那份文件本来就是单机模式的,MODE是standalone,改副本数只会得到三个互不相干的单机实例,它们各自写各自的 Derby,控制台上谁也看不见谁。

2.2 Headless Service 是集群寻址的地基,不是可选项

集群模式下 Nacos 节点之间怎么找到彼此?靠的就是NACOS_SERVERS里那串地址。而这串地址里的主机名,必须是每个 Pod 独立的、稳定的 DNS 名,形式是:

<nacos>-<序号>.<headless-service 名>.<命名空间>.svc.cluster.local

这就是 StatefulSet + Headless Service 组合的意义。普通 ClusterIP Service 只会给你一个虚拟 IP,请求打到哪个 Pod 是随机的,节点之间没法建立"我知道你是谁"的长连接。Headless Service(clusterIP: None)则会给每个 Pod 一条独立的 A 记录,nacos-0、nacos-1、nacos-2各有各的地址。

这里有个非常关键的字段:

spec: clusterIP: None publishNotReadyAddresses: true

publishNotReadyAddresses: true的意思是:即使 Pod 还没通过 readiness 探针,DNS 里也要把它的地址发布出来。为什么必须要?因为 Nacos 节点的"就绪"本身就依赖于它能看见集群里的其他节点——它得先完成集群握手,才会认为自己 ready。如果 DNS 只在 ready 之后才发布,那三个节点就永远等不到彼此,readiness 永远不通过,形成经典的启动死锁。

我第一次没加这个字段,现象就是:三个 Pod 都在Running,但READY列全是0/1,日志里反复刷failed to connect to server或者干脆卡在集群初始化。加上之后就顺了。

2.3 端口清单和版本差异,别只暴露 8848

Nacos 2.x 相比 1.x 最大的变化之一,是引入了一套基于 gRPC 的长连接通信,端口是对主端口做偏移:

端口名称用途能否省略
8848主端口HTTP 控制台、OpenAPI、健康检查不能
9848gRPC 客户端端口2.x 客户端与服务的长连接不能,客户端必连
9849gRPC 服务端端口节点之间的数据同步集群模式不能省
7848Raft 端口集群选举与元数据一致性集群模式不能省

偏移规则是 8848 加 1000 得到 9848,加 1001 得到 9849。这个设计的好处是客户端算得出来,坏处是你只对外暴露 8848 一定会失败——2.x 的客户端启动时会同时连 8848 和 9848,9848 连不上就报Client not connected, current status:STARTING,然后重试到超时。这个报错我在好几个群里见过,答案百分之百是端口没开。

3. 地基要打牢:命名空间、MySQL 与存储类的前置准备

YAML apply 之前的那部分准备,看起来琐碎,但绝大多数"起不来"的问题都出在这一步。

3.1 建库建表:SQL 脚本从哪来、为什么必须手工导

Nacos 不会帮你自动建表。它启动时会去读config_info、config_info_gray、his_config_info、config_tags_relation、tenant_info、users、roles、permissions这一堆表,表不存在就直接抛异常退出,日志里通常是Table 'nacos.config_info' doesn't exist或者包装过的Nacos Server did not start because dumpservice bean construction failure。

建表脚本的位置:官方发布包的conf目录下有mysql-schema.sql;容器镜像里对应路径是/home/nacos/conf/mysql-schema.sql。你可以从镜像里拷出来:

docker run --rm --entrypoint cat nacos/nacos-server:v2.3.2 /home/nacos/conf/mysql-schema.sql > nacos-mysql.sql

然后按建库、导入两步走:

CREATE DATABASE nacos_config DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
mysql -h 10.0.0.10 -u root -p nacos_config < nacos-mysql.sql

字符集这里我要强调utf8mb4。用utf8(三字节那个)在存储带 emoji 或者部分生僻字的配置内容时会截断甚至报错,而配置文件里写备注带表情符号是很常见的事。这种问题上线之后才发现,排查起来极其费时。

3.2 连接参数里那几个"不写就报错"的选项

数据库连接串的拼装方式取决于你用哪种配置方式。官方示例里有两套写法:

  • 老写法:MYSQL_SERVICE_HOST、MYSQL_SERVICE_PORT、MYSQL_SERVICE_DB_NAME、MYSQL_SERVICE_USER、MYSQL_SERVICE_PASSWORD、MYSQL_SERVICE_DB_PARAM。
  • 新写法:直接给SPRING_DATASOURCE_PLATFORM=mysql,然后用MYSQL_SERVICE_*系列,或者用NACOS_PLUGIN_DATASOURCE_*这一套数据源插件参数(2.2 之后的版本支持)。

我更倾向用老写法,因为它在示例仓库里覆盖得更全,出问题时可搜到的资料多。参数串我会这么写:

characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true

逐个说一下为什么:

  • serverTimezone=Asia/Shanghai:容器默认 UTC,MySQL 8 默认也是 UTC 或系统时区,两边不一致时配置的创建时间会差 8 小时,做灰度发布按时间排序就会错乱。
  • useSSL=false:集群内网通信,不开 TLS 更省事;如果你们的 MySQL 强制 TLS,这里要改成一整套证书配置。
  • allowPublicKeyRetrieval=true:MySQL 8 默认认证插件是caching_sha2_password,在非 SSL 连接下不加这个参数会直接报Public Key Retrieval is not allowed。
  • autoReconnect=true:MySQL 连接被中间件或防火墙掐断后能自动重连,避免 Nacos 长时间空转。

3.3 StorageClass 选型的真实影响

虽然配置数据都在 MySQL 里,但 Nacos 每个节点本地还是要有目录落盘:/home/nacos/data(Raft 日志、协议状态)、/home/nacos/logs、/home/nacos/plugins(如果装了自定义插件)。

/home/nacos/data里放的是 Raft 的日志段文件,写频率不低,而且对顺序写比较敏感。我实测过在 NFS 上跑三个节点,QPS 一上来 Raft 提交延迟明显飙高,日志里能看到leader election timeout之类的告警。所以选存储类的时候:

  • 优先用块存储类型的 StorageClass(云上那种 ESSD、SSD 云盘),延迟低、顺序写好。
  • 如果没有块存储,本地盘 + nodeAffinity 也行,代价是 Pod 漂移会丢本地数据(好在能从 MySQL 和集群其他节点恢复)。
  • NFS 类共享存储只建议给logs目录用,data目录别放上面。

PVC 大小给 10Gi 起步通常够用,但如果你开了大量的服务实例注册、Raft 日志累积快,可以给到 20Gi 并配上日志清理。

4. 三个核心清单的逐字段拆解

真正要写的 YAML 就三样:Headless Service、一个给控制台/客户端用的普通 Service、StatefulSet。我按"这个字段不加会怎样"的思路来讲。

4.1 Headless Service:端口要全,发布策略要对

apiVersion: v1 kind: Service metadata: name: nacos-headless namespace: nacos labels: app: nacos spec: clusterIP: None publishNotReadyAddresses: true selector: app: nacos ports: - name: client port: 8848 targetPort: 8848 - name: raft port: 7848 targetPort: 7848 - name: grpc-client port: 9848 targetPort: 9848 - name: grpc-server port: 9849 targetPort: 9849

selector必须和 StatefulSet 的 Pod 标签完全对上,否则 DNS 里一个记录都不会有。publishNotReadyAddresses前面讲过了,是解开启动死锁的钥匙。端口给全是为了让节点之间的 9849 通信、Raft 的 7848 通信都能通过 Service 名字走到——虽然同命名空间内 Pod 之间直接用 Pod IP 也能通,但走域名更稳,Pod 重建后 IP 变了域名不变。

4.2 面向客户端的 Service:ClusterIP 还是 NodePort

集群内部的应用访问 Nacos,用一个普通的 ClusterIP Service 就够了:

apiVersion: v1 kind: Service metadata: name: nacos-client namespace: nacos spec: type: ClusterIP selector: app: nacos ports: - name: http port: 8848 targetPort: 8848 - name: grpc port: 9848 targetPort: 9848

这里有两点经验。第一,9848 一定要在这个 Service 里一起暴露,很多团队只配了 8848,然后 2.x 客户端在集群外怎么都连不上。第二,如果你打算用 Ingress 暴露控制台,别把客户端流量也走 Ingress——2.x 的 gRPC 长连接对七层代理很不友好,需要额外的backend-protocol: GRPC配置,而且连接会被代理层断掉重连。客户端流量走 ClusterIP 或 NodePort 更省心。

4.3 StatefulSet:环境变量与探针是重灾区

核心片段如下,我把关键项都标出来:

apiVersion: apps/v1 kind: StatefulSet metadata: name: nacos namespace: nacos spec: serviceName: nacos-headless replicas: 3 podManagementPolicy: Parallel selector: matchLabels: app: nacos template: metadata: labels: app: nacos spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchLabels: app: nacos topologyKey: kubernetes.io/hostname containers: - name: nacos image: nacos/nacos-server:v2.3.2 env: - name: MODE value: cluster - name: NACOS_SERVERS value: "nacos-0.nacos-headless.nacos.svc.cluster.local:8848 nacos-1.nacos-headless.nacos.svc.cluster.local:8848 nacos-2.nacos-headless.nacos.svc.cluster.local:8848" - name: SPRING_DATASOURCE_PLATFORM value: mysql - name: MYSQL_SERVICE_HOST value: "mysql.nacos.svc.cluster.local" - name: MYSQL_SERVICE_PORT value: "3306" - name: MYSQL_SERVICE_DB_NAME value: nacos_config - name: MYSQL_SERVICE_USER value: nacos - name: MYSQL_SERVICE_PASSWORD valueFrom: secretKeyRef: name: nacos-db key: password - name: MYSQL_SERVICE_DB_PARAM value: "characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true" - name: JVM_XMS value: "2g" - name: JVM_XMX value: "2g" - name: JVM_XMN value: "1g" - name: TZ value: Asia/Shanghai ports: - containerPort: 8848 - containerPort: 9848 - containerPort: 9849 - containerPort: 7848

几个点值得单独说。

podManagementPolicy: Parallel让三个 Pod 同时创建,而不是默认的OrderedReady一个个来。因为 Nacos 节点之间是平等协商,没有主从启动顺序的要求,并行启动能显著缩短整体就绪时间。不过要注意,并行启动时三个节点同时抢着初始化数据库表结构的情况是存在的,Nacos 自己做了幂等处理,一般没问题,但如果你的 MySQL 权限很严格(没有建表权限),务必先把表导进去。

NACOS_SERVERS里我用了完整的 FQDN,而不是简写的nacos-0.nacos-headless。短名字在同一个命名空间里能解析,跨命名空间就不行了,写全了永远没错。

JVM_XMS和JVM_XMX设成一样大,是为了避免 JVM 堆动态伸缩带来的抖动。堆大小怎么定?我的经验值是:3 节点、注册实例总数在 5000 以内、配置项在 2 万条以内,2g 堆足够;规模再往上加,先加堆到 4g,再考虑加节点。同时一定要给容器配resources.requests/limits,且limits.memory要比JVM_XMX大出至少 1Gi,留给堆外内存、元空间和线程栈,否则会被 OOMKilled 得莫名其妙。

探针配置:

readinessProbe: httpGet: path: /nacos/v1/console/health/readiness port: 8848 initialDelaySeconds: 30 periodSeconds: 10 failureThreshold: 3 livenessProbe: httpGet: path: /nacos/v1/console/health/liveness port: 8848 initialDelaySeconds: 60 periodSeconds: 15 failureThreshold: 5

initialDelaySeconds给宽一点。Nacos 2.x 启动要连数据库、建连池、初始化 Raft、拉集群成员,冷启动 40 到 60 秒很常见。liveness 设太紧会导致容器还在初始化就被判定死亡重启,进入无限重启循环——这个现象很有迷惑性,kubectl describe里看到的是一堆Liveness probe failed,你会以为是应用挂了,其实只是探针太急。

4.4 数据目录的挂载,以及为什么 logs 也要单独挂

volumeMounts: - name: data mountPath: /home/nacos/data - name: logs mountPath: /home/nacos/logs volumeClaimTemplates: - metadata: name: data spec: accessModes: ["ReadWriteOnce"] storageClassName: fast-ssd resources: requests: storage: 20Gi

logs目录我单独用一个emptyDir或者另一个 PVC 挂上。用 emptyDir 的话 Pod 重启日志就没了,排查偶发问题很痛苦;用 PVC 又要多一份存储开销。我的折中是:接一套日志采集,把/home/nacos/logs收走,本地只留 emptyDir。这样既不占存储,又能在日志平台里查历史——naming-server.log、config-server.log、nacos-cluster.log三个文件是排查问题的主战场。

5. 起来之后:怎么验证集群是真的成了

Pod 是Running且READY 1/1,只能说明容器活着,不能说明集群正常。我一般按下面的顺序逐层确认。

5.1 从控制台倒着查:节点列表、数据源、健康状态

登录控制台,进"集群管理"里的节点列表。三个节点都在,且"状态"是 UP,这才算第一步过了。如果只有一个节点,按这个顺序查:

  1. kubectl exec -it nacos-0 -- env | grep NACOS_SERVERS,确认环境变量真的注进去了,而且三个地址格式正确、没有多余空格和换行。
  2. kubectl exec -it nacos-0 -- nslookup nacos-1.nacos-headless.nacos.svc.cluster.local,确认 DNS 能解析。解析不到就是 Service 的selector对不上,或者publishNotReadyAddresses没开。
  3. kubectl exec -it nacos-0 -- curl -s http://nacos-1.nacos-headless.nacos.svc.cluster.local:8848/nacos/v1/console/health/readiness,确认能通。不通就是网络策略(NetworkPolicy)挡了,或者端口在 Service 里没映射。

这三步基本能定位 90% 的"节点不齐"问题。我自己遇到过最隐蔽的一次是:环境变量里手写地址时,中间用了换行符而不是空格分隔,NACOS_SERVERS被解析成了一个超长的无效主机名,日志里看不出所以然,只能靠打印环境变量发现。

5.2 数据库连接失败的几种面孔

数据库问题在日志里的表现形式五花八门,我总结了几种典型:

日志关键字真实原因处理方式
No DataSource set数据源参数没生效,SPRING_DATASOURCE_PLATFORM拼错或没设检查变量名,2.x 用mysql全小写
Access denied for user账号密码错,或账号没有该库的权限用同一个账号在容器里mysql -h手连一次
Public Key Retrieval is not allowedMySQL 8 认证插件 + 无 SSL连接串加allowPublicKeyRetrieval=true
Table 'nacos.xxx' doesn't exist建表脚本没导,或导到了别的库检查MYSQL_SERVICE_DB_NAME与导入的库是否一致
Communications link failure网络不通、Service 名错、端口错先在 Pod 里nc -zv <host> 3306

其中No DataSource set这个最有欺骗性,因为它看起来像是 Nacos 内部逻辑错误,实际上就是环境变量没配对。2.2 之后的版本数据源插件化改造过一次,有些版本对MYSQL_SERVICE_*系列变量的支持有细微差别,遇到死活不认的情况,可以退一步用SPRING_DATASOURCE_URL/SPRING_DATASOURCE_USERNAME/SPRING_DATASOURCE_PASSWORD这套标准 Spring 变量直接注入,成功率更高。

5.3 启动成功但客户端连不上:把 9848 这个坎跨过去

控制台能进、节点也齐了,但业务服务启动报Client not connected, current status:STARTING或者一直server is DOWN,八成是 gRPC 端口的问题。

排查链路是这样的:先看客户端配置的serverAddr是什么。如果写的是某个 NodePort 或者 Ingress 域名,那么客户端会自动用8848 + 1000 = 9848去连 gRPC,而这个端口你可能根本没映射出去。解决办法有两个:把 9848 也映射出来;或者在客户端显式指定nacos.remote.client.grpc.port之类的参数(不同版本参数名有差异,以你用的客户端版本为准)。

还有一种情况是客户端和服务端版本跨了大版本。2.x 服务端对 1.x 客户端是兼容的(走 HTTP 长轮询),但 1.x 服务端对 2.x 客户端就不行了。混部环境里最容易出这种事:服务端升到了 2.3,某个老服务还在用 1.4 的客户端,那个服务就会一直报连接异常。

6. 暴露与接入:控制台给人用,客户端给机器用

这两种流量的特性完全不同,混在一起设计方案,后面一定会返工。

6.1 控制台走 Ingress,但要注意路径前缀

控制台的 Web 路径默认带/nacos前缀。用 Ingress 转发时有两种做法:一是保留前缀,path: /nacos,pathType: Prefix;二是用 rewrite 把前缀剥掉。我推荐保留前缀,因为前端资源里的绝对路径是按/nacos生成的,剥掉之后静态资源会 404。

apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: nacos-console namespace: nacos spec: ingressClassName: nginx rules: - host: nacos.internal.example http: paths: - path: /nacos pathType: Prefix backend: service: name: nacos-client port: number: 8848

顺手提一句鉴权。2.2 之后的版本默认要求配置nacos.core.auth.plugin.nacos.token.secret.key,长度至少 32 字节且要 Base64 编码,否则启动会直接失败并提示密钥不合法。这个值建议用 Secret 注入,不要明文写在 ConfigMap 里,也不要全公司所有环境共用同一个。

6.2 客户端侧的三行配置与两个常见误配

Spring Cloud 体系里的典型配置:

nacos: discovery: server-addr: nacos-client.nacos.svc.cluster.local:8848 config: server-addr: nacos-client.nacos.svc.cluster.local:8848 file-extension: yaml

我见过两个高频误配。第一个是server-addr里写了http://前缀,某些版本的客户端解析会出问题,一律不带协议头更稳妥。第二个是命名空间:服务端建了dev、test、prod三个命名空间,客户端只配了server-addr没配namespace,结果永远读的是 public 下的配置,改了半天"配置不生效",其实是读错了地方。命名空间要用ID(一串 UUID)而不是名字,这个坑我踩过,填名字不报错但就是不生效。

7. 运营期:扩容、升级、备份与监控

部署完成只是起点。真正消耗精力的是后面这些日常动作。

7.1 扩容为什么不能直接改 replicas

前面提过,NACOS_SERVERS是硬编码的。你把replicas从 3 改成 5,新起来的nacos-3、nacos-4会带着旧的NACOS_SERVERS(只有三个地址),而已有的三个节点根本不知道有新成员存在。结果是控制台上看着有 5 个 Pod,节点列表还是 3 个,新节点成了孤岛。

正确顺序是:

  1. 把NACOS_SERVERS环境变量改成 5 个地址。
  2. 先把replicas调到 5,让新 Pod 起来(此时它们能连上老节点,但老节点不认它们,属于过渡状态)。
  3. 对 StatefulSet 做一次滚动重启(可以用kubectl rollout restart statefulset/nacos),让所有节点都读到新的NACOS_SERVERS。
  4. 在控制台确认 5 个节点全部 UP。

缩容反过来:先改NACOS_SERVERS去掉要删的节点,滚动重启,最后再降replicas。顺序错了会出现一边已经下线、另一边还在往它发请求的情况,短暂的数据不一致。

7.2 升级 2.x 小版本时的滚动策略

Raft 集群的升级有个原则:先升 follower,最后升 leader。StatefulSet 默认从序号大的开始删(nacos-2、nacos-1、nacos-0),而 leader 通常是先启动的那个(序号 0 的概率最大),所以默认顺序恰好是友好的,不用特别调整。

升级过程中集群会短暂失去多数派吗?3 节点滚动升级时,任何时刻只有一个节点不可用,剩余两个仍构成多数派,写入不会中断。但如果升级的版本跨了较大范围(比如从 2.0 直接跳到 2.3),中间的数据结构可能有变更,务必先在测试环境完整演练一遍,并做好 MySQL 全量备份。

7.3 备份的三层:数据库、配置导出、PVC 快照

很多人只做数据库备份,这在多数场景下够用,因为配置数据全在 MySQL 里。备份策略我一般这么定:

  • MySQL 每日全量 + binlog:恢复粒度可以到秒。这是主力手段。
  • 控制台配置导出:Nacos 支持按命名空间导出配置压缩包,定期导一份存到对象存储,作为跨版本的兜底。它的好处是可读、可人工比对。
  • PVC 快照:只针对 Raft 数据,万一整个集群状态错乱可以用它快速回滚,但要三个节点一起回滚,单独回滚一个节点的快照可能引入不一致。

7.4 监控指标怎么抓

Nacos 暴露了 Prometheus 格式的指标,但默认可能没开全。需要在配置里打开端点暴露:

management.endpoints.web.exposure.include=*

然后curl http://<pod>:8848/nacos/metrics就能拿到。我关注的几个指标:

指标含义告警阈值参考
nacos_monitor{name="configCount"}配置总数突增突降都值得看
nacos_monitor{name="serviceCount"}服务数与预期注册量对不上就查
nacos_timer_seconds系列各类请求耗时P99 明显升高时查数据库
JVM 堆使用率内存压力持续高于 80% 考虑扩堆
Raft 提交延迟集群一致性健康度持续偏高多半是存储慢

配置中心最典型的故障是"配置推送延迟",表现是改了配置但应用迟迟收不到。这种时候先看 Nacos 的推空比例和长轮询连接数,再看下游应用的监听是否正常注册了。

8. 我踩过的几个不那么常见的坑

前面讲的都是主流问题,最后补几个偏门但确实会让人卡半天的。

容器时区。即便加了TZ=Asia/Shanghai,某些基础镜像里的 JVM 仍然读的是/etc/localtime。如果日志时间还是差 8 小时,可以在启动参数里加-Duser.timezone=Asia/Shanghai,或者直接把宿主机的时区文件挂进容器。日志时间和实际时间对不上,排查时序问题时会非常痛苦,两个事件的先后顺序都可能判反。

ConfigMap 热更新不生效。如果你把application.properties通过 ConfigMap 挂载进去,改 ConfigMap 之后文件确实会更新(有几分钟延迟),但 Nacos 不会自动重新加载,必须重启 Pod。所以能用环境变量配的尽量用环境变量,Nacos 官方镜像对MYSQL_SERVICE_*、JVM_*这些变量都有专门处理,比挂配置文件省心。

多个命名空间共用一套 MySQL。测试环境和生产环境共用一个 MySQL 实例、不同的库,看起来隔离了,但 Nacos 的连接池、慢查询、磁盘 IO 是全实例共享的。测试环境跑个压测,生产环境的配置推送就可能超时。有条件的话,实例级别隔离更稳。

health 接口被安全策略拦掉。有些团队会在 Service 前面加一层网络策略,只放行 8848 和特定来源。这时候 kubelet 的探针请求(来自节点 IP)可能被挡,表现为 Pod 反复重启,但手动curl是通的。遇到这种"手动通、探针不通",第一时间怀疑 NetworkPolicy。

StatefulSet 的 PVC 不会自动清理。删掉 StatefulSet 之后再重建,同名 PVC 还在,旧的 Raft 数据会被新集群读到,可能出现集群 ID 冲突、节点状态异常。彻底重来的时候记得把 PVC 一起删干净,或者干脆换个命名空间重建。

这套东西跑顺之后其实很省心,一年到头出不了几次问题。真正花时间的从来都是前面那几次"起来了但不对"的排查。我个人的习惯是:每套新环境部署完,都按第 5 章那个顺序过一遍检查清单,把每一步的验证结果记在部署文档里。下次出问题时,至少知道自己是从哪个"正常状态"偏出去的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询