☰
K8s 部署 Nacos 集群:StatefulSet 与 MySQL 避坑
2026/10/28 12:24:06 网站建设 项目流程

简介:面向需要在 Kubernetes 环境中搭建 Nacos 集群的开发、运维与学习者,这份脚本包将数据库连接配置、无头服务、有状态副本、服务暴露以及 Ingress 外部入口等部署环节整理为可直接参考的 YAML 清单,着力降低配置分散、依赖顺序不清和初次上手门槛偏高的问题。压缩包共 6 个文件,主要由 5 份 YAML 清单和 1 份 TXT 说明组成,整体约 3KB,体量轻巧却覆盖了从数据库参数到集群访问入口的关键资源对象;随包说明可用于辅助理解执行次序与配置关系。已有 766 人学习,适合刚接触 Kubernetes 与 Nacos、希望快速搭建实验环境或梳理集群资源组织的读者。通过对照清单结构和字段设置,可进一步理解有状态服务在 K8s 中的编排方式、服务发现与外部访问的衔接逻辑,并据此沉淀可复用的部署模板和排错线索。

1. 三个 Pod 都在跑,Nacos 却不认识彼此

上周帮一个团队看他们的注册中心:三个 Nacos Pod 在 K8s 里Running得整整齐齐,可控制台打开只挂着一个节点,客户端每隔几分钟刷一次Client not connected。翻完他们那套 YAML 就明白了——StatefulSet 写成了随机名的 Deployment,成员列表里钉死的是旧 IP,gRPC 端口只暴露了 8848。这套 K8s + Nacos 集群部署脚本最容易翻车的地方从来不是镜像,而是网络标识、端口偏移和外部存储这三处。下面这份资源把集群模式的 StatefulSet、Headless Service、MySQL 外置连接、探针与灰度升级整理成一份可以改参数直接 apply 的 YAML 清单,适合正在自建注册配置中心、又不想从零啃官方示例的后端和运维同学。按「清单长什么样 → 数据落哪 → 端口怎么通 → 哪里会踩坑」的顺序拆一遍,照着改基本能落地。

2. StatefulSet 与 Headless Service:把 nacos-0/1/2 的身份钉死

2.1 为什么这套清单必须用 StatefulSet

Nacos 集群成员之间要靠地址互相发现,cluster.conf或NACOS_SERVERS里写的是固定成员列表。Deployment 的 Pod 名是随机后缀,重建一次 IP 换一次,成员列表要么写死 IP(重建即失联),要么每次手动改,生产里根本维护不动。StatefulSet 给每个副本一个稳定序号nacos-0/1/2,配合 Headless Service 拿到稳定 DNS 名,Pod 漂移、重建、扩缩容之后名字不变,成员列表就能写死在清单里,这是一切的前提。

volumeClaimTemplates也是我坚持用 StatefulSet 的理由:它给每个副本单独一块 PVC。Nacos 2.x 虽然配置和注册数据主要落 MySQL,但本地还有logs、data(protocol 的 raft 数据、缓存)要写,几个 Pod 共用一块盘或者干脆不落盘,重启后出脏数据的概率很高。副本数必须是奇数,3 个是性价比最高的起点,2 个在选主上有天然窗口,这条底线别突破。

2.2 Headless Service 与客户端 Service 要拆成两个

节点间通信和业务客户端接入的地址需求并不一样。给 StatefulSet 用的那个 Service 必须clusterIP: None,也就是 Headless,只有这样 DNS 才会返回每个 Pod 的 A 记录,nacos-0.nacos-headless才能解析到具体 Pod。还有一个细节很多人漏掉:publishNotReadyAddresses: true。默认 Pod 没 Ready 就不进 DNS,可集群启动时节点之间要先互相发现才能凑够成员,结果是三个 Pod 全卡在未就绪、谁也不认识谁,日志里一直刷UnknownHostException,这个死循环只有加上这个字段才解得开。

给业务客户端用的是另一个普通 ClusterIP Service,把 8848、9848、9849 三个端口都映射过去。Headless 不建议直接交给客户端——SDK 拿到的是多个 Pod IP,各版本连接池和重连逻辑对多 IP 的处理表现不一致,踩过的都知道有多玄学。内部走 ClusterIP,让 kube-proxy 去分发,客户端只认一个 DNS 名,简单可控。

2.3 清单主体

apiVersion: v1 kind: Service metadata: name: nacos-headless namespace: middle labels: app: nacos spec: clusterIP: None # 关键:Pod 未 Ready 也要能解析,否则集群永远凑不齐成员 publishNotReadyAddresses: true selector: app: nacos ports: - name: client port: 8848 targetPort: 8848 - name: client-rpc # 2.x 客户端 gRPC port: 9848 targetPort: 9848 - name: raft-rpc # 2.x 服务端间 gRPC port: 9849 targetPort: 9849 --- apiVersion: v1 kind: Service metadata: name: nacos-client namespace: middle spec: type: ClusterIP selector: app: nacos ports: - name: client port: 8848 targetPort: 8848 - name: client-rpc port: 9848 targetPort: 9848 - name: raft-rpc port: 9849 targetPort: 9849 --- apiVersion: apps/v1 kind: StatefulSet metadata: name: nacos namespace: middle spec: serviceName: nacos-headless replicas: 3 # 默认 OrderedReady 会一个个起,而集群要互相发现,容易互相等 podManagementPolicy: Parallel selector: matchLabels: app: nacos template: metadata: labels: app: nacos spec: terminationGracePeriodSeconds: 60 affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: topologyKey: kubernetes.io/hostname labelSelector: matchLabels: app: nacos containers: - name: nacos # 示例 tag,换成你们灰度验证过的版本,别用 latest image: nacos/nacos-server:v2.3.2 imagePullPolicy: IfNotPresent env: - name: MODE value: cluster - name: PREFER_HOST_MODE value: hostname # 用域名而非 IP 注册,Pod 重建不影响 - name: NACOS_SERVERS value: >- nacos-0.nacos-headless.middle.svc.cluster.local:8848 nacos-1.nacos-headless.middle.svc.cluster.local:8848 nacos-2.nacos-headless.middle.svc.cluster.local:8848 - name: JVM_XMS value: 2g - name: JVM_XMX value: 2g - name: JVM_XMN value: 1g ports: - name: client containerPort: 8848 - name: client-rpc containerPort: 9848 - name: raft-rpc containerPort: 9849 resources: requests: cpu: "2" memory: 4Gi limits: cpu: "4" memory: 4Gi startupProbe: # 先给 JVM 足够起步时间,别急着上 liveness httpGet: path: /nacos/v1/console/health/liveness port: 8848 failureThreshold: 30 periodSeconds: 5 readinessProbe: httpGet: path: /nacos/v1/console/health/readiness port: 8848 initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 livenessProbe: httpGet: path: /nacos/v1/console/health/liveness port: 8848 initialDelaySeconds: 60 periodSeconds: 15 timeoutSeconds: 5 failureThreshold: 5 lifecycle: preStop: # 先从 Endpoints 摘流量,再让进程退出 exec: command: ["/bin/sh", "-c", "sleep 15"] volumeClaimTemplates: - metadata: name: data spec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 10Gi

2.4 几个必须按环境改的参数

参数常用值说明
replicas3必须是奇数,2 个节点存在选主窗口
podManagementPolicyParallel三个一起起,几十秒后成员收敛;OrderedReady 启动更慢
terminationGracePeriodSeconds60要大于 preStop 的 sleep,否则会被 SIGKILL 强杀
JVM_XMX / JVM_XMN2g / 1g新生代按堆的 1/3 或 1/2 给
limits.memory堆 × 2堆外内存、元空间、DirectBuffer 都要留余量

resources这一块我一般让 requests 与 limits 接近,避免节点内存紧张时被 Burstable 挤掉;容器内 JVM 默认按宿主机内存算堆,所以必须显式给JVM_XMX,靠 cgroup 限制去猜堆大小在 Nacos 镜像上表现并不稳定,翻车姿势就是反复OOMKilled退出码 137。

3. 外置 MySQL:初始化脚本、连接参数与密码的放法

3.1 内置 Derby 为什么撑不起集群

Nacos 默认走内嵌 Derby,每个 Pod 各存各的配置和注册数据,节点之间根本不同步,你以为搭了个集群,实际是三个单机互相看不见。SPRING_DATASOURCE_PLATFORM=mysql这一行是集群模式的分水岭,没有它,后面所有端口和探针调优都是白费。生产上我一般把 MySQL 放在 K8s 外面的独立实例,至少不要和 Nacos 抢同一个节点的资源,数据库一挂整个注册配置中心就全线不可用。

3.2 建库与导入,脚本必须从镜像里拿

# 1. 建库,字符集必须 utf8mb4,配置里有 emoji 时 utf8 直接报错 mysql -h 10.0.0.10 -uroot -p -e \ "CREATE DATABASE IF NOT EXISTS nacos_config DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" # 2. 从镜像里把初始化脚本掏出来,别去网上随便找一份 kubectl -n middle cp nacos-0:/home/nacos/conf/mysql-schema.sql ./mysql-schema.sql # 3. 导入 mysql -h 10.0.0.10 -uroot -p nacos_config < ./mysql-schema.sql # 4. 核对关键表是否存在 mysql -h 10.0.0.10 -uroot -p -e "USE nacos_config; SHOW TABLES;"

mysql-schema.sql就在镜像的/home/nacos/conf下,跟版本严格对齐。直接拿旧版本脚本导新版本,启动时会报字段或表不存在;反过来用新脚本导老版本,多出来的表不影响启动,但升级 SQL 的顺序还是得按版本逐级来。核心表里config_info存配置正文,his_config_info存历史版本,users/roles/permissions是三张权限表,tenant_info对应命名空间——排查“控制台能登录但看不到配置”时,先查tenant_info和config_info的tenant_id对不对得上。

3.3 连接参数与密码,别写死在 YAML 里

apiVersion: v1 kind: Secret metadata: name: nacos-db namespace: middle type: Opaque stringData: username: nacos password: "换成你们的密码" --- # StatefulSet 中容器的 env 片段 env: - name: SPRING_DATASOURCE_PLATFORM value: mysql - name: MYSQL_SERVICE_HOST value: mysql.middle.svc.cluster.local - name: MYSQL_SERVICE_PORT value: "3306" - name: MYSQL_SERVICE_DB_NAME value: nacos_config - name: MYSQL_SERVICE_USER valueFrom: secretKeyRef: name: nacos-db key: username - name: MYSQL_SERVICE_PASSWORD valueFrom: secretKeyRef: name: nacos-db key: password - name: MYSQL_SERVICE_DB_PARAM value: >- characterEncoding=utf8&connectTimeout=3000&socketTimeout=5000 &autoReconnect=true&useUnicode=true&useSSL=false &serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true

MYSQL_SERVICE_DB_PARAM这几个参数是按踩坑顺序加的:serverTimezone不写,his_config_info里的时间会差 8 小时;MySQL 8 默认caching_sha2_password,老驱动连不上,加allowPublicKeyRetrieval=true&useSSL=false最省事;connectTimeout给 3000 毫秒是权衡——太大启动时卡住很久,太小网络抖一下就起不来。连接池想再调,db.pool.config.maxWait、maximumPoolSize、connectionTimeout这些键没有对应环境变量,得靠挂application.properties,但那样会整份覆盖镜像里的配置,必须把数据源相关项一并写全,漏一项就是启动即失败。

3.4 升级前先比一遍表结构

版本升级最容易忽略的一步是数据库脚本。新版本往往新增表或字段,跳过升级直接换镜像,Pod 会反复重启,日志里是Table xxx doesn't exist或Unknown column。我的习惯是升级前把新镜像里的mysql-schema.sql抽出来,跟线上的表结构做一次 diff,确认差异再执行对应的 upgrade 脚本,最后才动 StatefulSet 的镜像 tag。

4. 8848 之外的 9848/9849:端口偏移与探针时间预算

4.1 端口偏移规则先记住

Nacos 2.x 起客户端走 gRPC,端口不是只有 8848。规则很简单:客户端 gRPC 端口 = 主端口 + 1000,服务端间 gRPC 端口 = 主端口 + 1001,默认就是 8848/9848/9849。如果你把server.port改成 8849,那另外两个也跟着变成 9849 和 9850,Service、NetworkPolicy、防火墙、负载均衡监听器全都要同步改,漏一个就是“控制台能打开、服务能注册、配置监听不推送”。

这类故障的表现特别有迷惑性:服务列表看着是正常的,客户端日志却在刷Client not connected或server is DOWN,隔一会儿又自己好了。原因就是 HTTP 走 8848 通了,长连接走 9848 被别人拦了,客户端不停重连。内部 Service 把三个端口都开出来只是第一步,如果集群外还有客户端接入,NodePort 或 LoadBalancer 也要把三个端口一起映射,且客户端配置的地址要能让三个端口都通。

4.2 探针端点与时间预算

探针路径初始延迟周期失败阈值
startupProbe/nacos/v1/console/health/liveness05s30
readinessProbe/nacos/v1/console/health/readiness10s10s3
livenessProbe/nacos/v1/console/health/liveness60s15s5

JVM 应用冷启动慢是常识,liveness 的initialDelaySeconds给不够,Pod 会在启动途中被判定死亡然后杀掉,形成“启动—被杀—再启动”的循环,看起来像镜像坏了,实际是探针太急。用 startupProbe 兜住启动阶段、把 liveness 的初始延迟放到 60 秒之后,是这套清单里最省心的一个改动。readiness 可以相对激进,它只影响流量摘除,不影响进程存活。

4.3 优雅停止要留够时间

preStop里睡 15 秒的意义在于:kubelet 先把它从 Endpoints 摘掉,客户端收到连接断开、开始重连到其他节点,这段时间里进程还活着,能把正在处理的请求收尾。如果 preStop 和terminationGracePeriodSeconds配得比客户端重连间隔还短,每次滚动更新都会有一批客户端报错,业务方会来找你。我一般让宽限期至少是 preStop 的两倍以上,滚动升级时观察客户端错误率,有毛刺就再加。

5. 避坑与排查:五个真实翻车现场

5.1 现象:Pod 全 Running,控制台登录 403 或直接空白

原因基本只有一个——数据库没初始化或者表不全。容器日志里搜Caused by,大概率是Table 'nacos_config.users' doesn't exist。有人图快只建了库没导脚本,有人导了旧版本的脚本,还有人把库名写成nacos而环境变量里是nacos_config。解决顺序是:确认MYSQL_SERVICE_DB_NAME与实际库名一致,进容器ls /home/nacos/conf找到mysql-schema.sql重新导入,再kubectl rollout restart。导入前先备份,别在已有数据的库上直接覆盖。

5.2 现象:三个节点都起来了,控制台节点列表只有一个

这是成员列表写错最典型的样子。要么NACOS_SERVERS里写的是 IP,Pod 重建后 IP 变了;要么域名拼错,比如 namespace 写成了default而实际是middle;要么PREFER_HOST_MODE没设成hostname,节点用 IP 注册自己。进容器用getent hosts nacos-1.nacos-headless.middle.svc.cluster.local验证域名能否解析,解析不了就是 Headless Service 或publishNotReadyAddresses的问题。改完 YAML 之后要三个 Pod 一起重启,只重启一个是凑不齐的。

5.3 现象:注册正常,配置监听不生效,客户端反复重连

前面提过的端口问题。先用kubectl exec在客户端 Pod 里nc -zv nacos-client 9848,不通就是 Service 没开这个端口;通了但外网客户端不行,就是 NodePort 或负载均衡少了映射。还有一种情况是 NetworkPolicy 只放行了 8848,9848/9849 被拦,这种在开了网络策略的集群里很常见,排查时别只盯 Service。

5.4 现象:Pod 周期性重启,退出码 137

137 是 SIGKILL,两种可能:liveness 探针超时被 kubelet 杀,或者内存超 limit 被 OOMKilled。先kubectl describe pod看Last State里的 Reason,是Error还是OOMKilled。探针问题就调初始延迟和失败阈值;内存问题就把 limit 提到堆的两倍,同时确认JVM_XMX真的生效了——进容器ps -ef | grep java看启动参数里有没有-Xmx2g,没有的话说明环境变量名写错了。

5.5 现象:升级镜像后启动失败,报字段或表不存在

跳过数据库升级脚本的典型后果。新版本的表结构变更写在发布包的conf目录里,顺序是从当前版本逐级升到目标版本,不能一步跨两个大版本。回滚也要注意:镜像回退了,数据库结构不会自动回退,所以升级前必须做全量备份,回滚预案里写清楚是恢复备份还是执行反向脚本。

6. 灰度升级:用 partition 把三个节点拆开验证

StatefulSet 的partition是这套清单里最实用的一个进阶开关。它让滚动更新从序号最大的 Pod 开始,并且只更新序号大于等于 partition 的副本。比如partition: 2,就只会动nacos-2,另外两个保持旧版本继续提供服务。三个节点的集群,先升一个观察十几分钟,没问题再往下推,出问题影响面只有三分之一。

# 1. 只允许更新 nacos-2 kubectl -n middle patch statefulset nacos --type='json' \ -p='[{"op":"replace","path":"/spec/updateStrategy/rollingUpdate/partition","value":2}]' # 2. 换镜像,触发单副本更新 kubectl -n middle set image statefulset/nacos nacos=nacos/nacos-server:<目标tag> kubectl -n middle rollout status statefulset/nacos --timeout=300s # 3. 冒烟:健康端点 + 集群节点列表 kubectl -n middle exec nacos-2 -- curl -s localhost:8848/nacos/v1/console/health/readiness kubectl -n middle exec nacos-0 -- curl -s localhost:8848/nacos/v1/ns/operator/servers # 4. 观察无异常后逐步放开 kubectl -n middle patch statefulset nacos --type='json' \ -p='[{"op":"replace","path":"/spec/updateStrategy/rollingUpdate/partition","value":1}]'

升级前我会把下面几项过一遍,缺一项就不动手:

检查项通过标准
数据库升级脚本已在测试库执行并验证
镜像 tag固定版本或 digest,不用 latest
旧 YAML 备份已kubectl get -o yaml存档
客户端兼容性客户端 SDK 版本不低于服务端要求的最低版本
回滚路径镜像回退命令与数据库恢复方案都写在变更单里

最后说个习惯。早几年我在一个测试环境上手改过生产 YAML,改完直接apply,结果漏了publishNotReadyAddresses,三个 Pod 互等了半小时,最后靠回滚才恢复。从那以后每次动这组清单,我都强制先跑一遍kubectl diff -f nacos.yaml,看完差异再决定是否应用,镜像我固定用 digest 而不是浮动 tag,回滚时不用猜当时跑的是哪个版本。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询