K8s部署Nacos集群:StatefulSet与外置MySQL实践
简介:面向需要在Kubernetes环境中快速落地Nacos集群的开发与运维人员,这份脚本包以极简配置思路拆解数据库连接、无头服务、有状态副本、对外Service与Ingress等关键环节,降低复杂YAML的编写门槛,无需从零拼装大量参数,适合具备基础k8s操作能力、希望按顺序直接部署的读者。压缩包共6个文件,以5个YAML清单为主,另附1份txt说明,整体约3KB,体量轻巧,便于随项目仓库一并归档和版本管理。目前已有766人学习下载,说明该部署方式在同类场景中具备一定参考价值。内容按执行顺序组织,读者可据此依次完成配置与资源创建,快速搭建可访问的Nacos集群,并借助清单结构理解各对象职责,为后续调整副本数量、存储方案与访问入口提供清晰起点。
1. 三个 Pod 全是 Running,服务列表却各看各的
第一次把 Nacos 从单机容器挪进 K8s,很多人会撞上一个特别迷惑的现象:kubectl get pods三个副本都是 Running,控制台能登录,但三个节点各自看到的服务实例数量对不上,配置改完在 A 节点生效、B 节点读到的还是旧值。这不是镜像坏了,是集群模式没真正组起来——cluster.conf里填的是 Pod IP,Pod 一重建就全乱。
这套 k8s + Nacos 集群部署 YAML 要解决的正是这件事:用 StatefulSet 给每个节点稳定网络标识,用 headless Service 让节点间能互相解析,用外置 MySQL 把配置和元数据从 Pod 里剥离出去,再配上正确的 gRPC 端口和就绪探针。适合已经把业务容器化、想把注册中心和配置中心一起收进集群的后端和运维开发;如果你还在单机 docker run 阶段,也可以先拿它练手,理解 Nacos 2.x 的端口模型和集群寻址方式。
2. 清单结构拆解:一套 YAML 里的七类资源怎么分工
拿到这包 YAML,先别急着kubectl apply。把文件摊开看清楚每类资源在链路里的位置,比照着 README 一条条敲命令快得多——这套东西的坑八成不在镜像,而在资源之间的引用关系。
2.1 目录结构与文件职责
我一般把清单拆成两段:base/放不随环境变的东西,overlay/放环境差异。这套 YAML 属于前者,结构大致如下:
nacos-k8s/ ├── 00-namespace.yaml # 独立命名空间,便于整体回收 ├── 01-secret.yaml # MySQL 账号密码、鉴权 token ├── 02-configmap.yaml # application.properties 覆盖项 + cluster.conf ├── 03-mysql-init-job.yaml # 初始化 nacos_config 库(可选,库已存在就跳过) ├── 04-service-headless.yaml # clusterIP: None,给 StatefulSet 用 ├── 05-service-nodeport.yaml # 对外暴露控制台,内网用 ClusterIP + Ingress 也行 ├── 06-statefulset.yaml # 主体,三个副本 └── 07-ingress.yaml # 可选,控制台和 OpenAPI 走域名文件名前缀不是强迫症,是为了kubectl apply -f ./时按字典序执行。K8s 不保证同一批次 apply 的对象有依赖顺序,但按文件名排能让 Namespace 先于 StatefulSet 落地,少一次"namespace not found"重试。
2.2 七类资源在一条请求里的位置
客户端从注册服务到拉配置,实际会穿过好几个 K8s 对象。搞清楚谁是谁,排查时才不会在错误的层里打转:
| 资源 | 类型 | 在链路中的作用 | 出问题时的典型症状 |
|---|---|---|---|
| nacos | Namespace | 资源隔离与批量删除 | 删不干净,残留 PVC |
| nacos-secret | Secret | 注入 MySQL 密码、鉴权 token | 启动日志报 Access denied |
| nacos-config | ConfigMap | 覆盖 application.properties、放 cluster.conf | 节点互相看不见 |
| nacos-headless | Service(Headless) | 给 StatefulSet 提供稳定 DNS 名 | Pod 之间解析失败 |
| nacos | StatefulSet | 跑容器、挂 PVC、定义探针 | 反复重启、就绪慢 |
| nacos-svc | Service(ClusterIP/NodePort) | 业务侧统一入口 | 客户端连不上 9848 |
| nacos-mysql-init | Job | 建库建表 | 启动报 no such table |
最容易被忽略的是 headless Service。它不是给业务用的,业务走的是普通 Service;它只服务于 StatefulSet 的serviceName字段,作用是把nacos-0.nacos-headless.nacos.svc.cluster.local这种名字解析成 Pod IP。没有它,cluster.conf里就只能写 IP,而 IP 在 Pod 重建后必变。
2.3 为什么集群模式必须把 MySQL 拎出去
Nacos 单机模式默认用内嵌 Derby,数据落在容器内的data/目录。有人图省事,replicas: 3加共享 PVC,指望三个 Pod 读同一份 Derby 文件——这条路走不通,Derby 是嵌入式数据库,不支持多进程并发写,轻则锁表,重则数据文件损坏。
集群模式的正确姿势是外置 MySQL(8.0 或 5.7 都行),三个节点读写同一个库。这样带来的额外好处是:Pod 变成无状态的,重建、滚动升级、节点迁移都不影响配置数据;备份也只需要备份数据库,不用琢磨 PVC 快照。
代价是多了一个依赖。MySQL 要是挂了,Nacos 读不到配置,但已经拉过配置的客户端有本地缓存,短期内业务不受影响——这一点在故障预案里值得写清楚。
3. 核心 YAML 逐段拆解:StatefulSet 里的三处硬骨头
真正决定这套清单能不能跑起来的,是三个地方的写法:节点间怎么互相寻址、容器怎么启动才算"健康"、配置怎么注入而不覆盖镜像内置逻辑。下面按这三块拆。
3.1 headless Service 与 NACOS_SERVERS 的地址对齐
先看 headless Service,写法很短但每个字段都有用:
apiVersion: v1 kind: Service metadata: name: nacos-headless namespace: nacos labels: app: nacos spec: clusterIP: None # 关键:无头服务,DNS 直接返回 Pod IP publishNotReadyAddresses: true # 关键:未就绪的 Pod 也要能被解析 selector: app: nacos ports: - name: http port: 8848 targetPort: 8848 - name: client-grpc port: 9848 targetPort: 9848 - name: server-grpc port: 9849 targetPort: 9849 - name: raft port: 7848 targetPort: 7848publishNotReadyAddresses: true是这套清单里最容易被删掉又最要命的一行。StatefulSet 三个副本同时启动,谁也不比谁先就绪,如果 DNS 只解析 Ready 的 Pod,那么nacos-0在启动时去解析nacos-1、nacos-2会失败,集群永远组不起来。打开它,Pod 一被调度就进 DNS,节点间握手才能完成。
再看 StatefulSet 里对应的环境变量:
env: - name: MODE value: cluster # 三个节点的 FQDN,镜像启动脚本会把主机名解析成 IP 写入 cluster.conf - 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官方镜像的startup.sh会读NACOS_SERVERS,逐个解析主机名并拼成cluster.conf。所以这里必须写 FQDN 而不是短名,跨命名空间解析短名会失败;也必须写 8848 而不是 9848,cluster.conf 记录的是节点间 HTTP 通信端口。
如果镜像版本较老不认NACOS_SERVERS,就用 ConfigMap 挂一个cluster.conf到/home/nacos/conf/cluster.conf,内容三行host:8848。两种方式选一种,别同时用,后写的会覆盖前者。
3.2 JVM、探针与环境变量的配合
Nacos 是 Java 应用,启动慢是常态,三副本同时冷启动还要互相做 Raft 选主,探针参数配小了就是无限重启。这段是我改过最多次的部分:
containers: - name: nacos image: nacos/nacos-server:${NACOS_TAG} # 按实际版本替换,2.x 系列 env: - name: JVM_XMS value: "1g" - name: JVM_XMX value: "1g" - name: JVM_XMN value: "512m" - name: SPRING_DATASOURCE_PLATFORM value: mysql - name: MYSQL_SERVICE_HOST value: mysql.nacos.svc.cluster.local - name: MYSQL_SERVICE_DB_NAME value: nacos_config - name: MYSQL_SERVICE_PORT value: "3306" - name: MYSQL_SERVICE_USER valueFrom: secretKeyRef: { name: nacos-secret, key: mysql-user } - name: MYSQL_SERVICE_PASSWORD valueFrom: secretKeyRef: { name: nacos-secret, key: mysql-password } - name: MYSQL_SERVICE_DB_PARAM value: >- characterEncoding=utf8&connectTimeout=3000&socketTimeout=5000 &autoReconnect=true&useUnicode=true&useSSL=false &serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true readinessProbe: httpGet: path: /nacos/v1/console/health/readiness port: 8848 initialDelaySeconds: 30 # 冷启动 + 选主,给足时间 periodSeconds: 10 failureThreshold: 6 livenessProbe: httpGet: path: /nacos/v1/console/health/liveness port: 8848 initialDelaySeconds: 90 # 必须大于 readiness 的最长容忍时间 periodSeconds: 20 failureThreshold: 3 resources: requests: { cpu: "500m", memory: "2Gi" } limits: { cpu: "2", memory: "2Gi" }几个参数值得单独说。JVM_XMX设 1g 是给小集群的保守值,Nacos 内存吃得不凶,但limits.memory必须比 Xmx 大出 500M 以上,留给堆外内存和元空间,否则容器会被 OOMKilled,而日志里只有一句Killed,非常像"玄学崩溃"。
探针路径用的是 Nacos 2.x 自带的两个健康端点,不用自己写脚本。initialDelaySeconds的差值(90 对 30)是刻意留的:liveness 比 readiness 晚 60 秒介入,避免节点还在做数据同步就被判死。血泪经验是,livenessProbe一旦配成initialDelaySeconds: 10,三副本会陷进"启动→被杀→重启"的循环,日志刚打到Nacos started successfully就断了。
另外podManagementPolicy: Parallel建议显式写上。默认的OrderedReady会等nacos-0Ready 才起nacos-1,而nacos-0的就绪又依赖另外两个节点加入集群,直接死锁——这个坑比探针参数还隐蔽。
3.3 ConfigMap 注入 application.properties 与初始化 SQL
环境变量能覆盖大部分配置,但有些参数(比如鉴权白名单、自定义线程池)只能写 properties。这套清单用 ConfigMap 挂载覆盖文件:
apiVersion: v1 kind: ConfigMap metadata: name: nacos-config namespace: nacos data: application.properties: | # 关闭 metrics 采集,小集群用不上还占内存 management.endpoints.web.exposure.include=* # 鉴权开关,生产必须开 nacos.core.auth.enabled=true nacos.core.auth.system.type=nacos # 服务端间鉴权用,值来自 Secret nacos.core.auth.server.identity.key=${NACOS_AUTH_IDENTITY_KEY:serverIdentity} nacos.core.auth.server.identity.value=${NACOS_AUTH_IDENTITY_VALUE:security} # 配置变更推送超时,弱网环境可调大 nacos.config.push.timeout=5000挂载方式用subPath单独覆盖,不要整个conf目录挂进去——那样会把镜像里的cluster.conf模板、日志配置全盖掉:
volumeMounts: - name: nacos-config mountPath: /home/nacos/conf/application.properties subPath: application.properties - name: data mountPath: /home/nacos/data - name: logs mountPath: /home/nacos/logs数据库初始化这块,nacos-mysql.sql在镜像的/home/nacos/conf/下。可以手动kubectl exec进去跑,也可以用 Job 自动化:
apiVersion: batch/v1 kind: Job metadata: name: nacos-mysql-init namespace: nacos spec: backoffLimit: 3 template: spec: restartPolicy: OnFailure containers: - name: init image: mysql:8.0 command: ["/bin/sh", "-c"] args: - | mysql -h $DB_HOST -u$DB_USER -p$DB_PASS -e \ "CREATE DATABASE IF NOT EXISTS nacos_config DEFAULT CHARSET utf8mb4;" mysql -h $DB_HOST -u$DB_USER -p$DB_PASS nacos_config < /sql/nacos-mysql.sql envFrom: - secretRef: { name: nacos-secret } volumeMounts: - name: sql mountPath: /sql volumes: - name: sql configMap: { name: nacos-sql }IF NOT EXISTS保证 Job 可以重复执行,backoffLimit: 3应付 MySQL 还没就绪的情况。初始化脚本里建的表都有CREATE TABLE IF NOT EXISTS,重复跑不会报错,但config_info里的数据会被保留——所以千万别在升级时手滑删库重跑。
4. 从零跑通:apply 的执行顺序与验证点位
清单读懂了,落地还有几步绕不开:命名空间、Secret、存储、镜像拉取。这一节按实际执行顺序走一遍,每步给出验证点。
4.1 前置准备:Namespace、Secret 与 StorageClass
先把命名空间和密码建起来,后面的资源才有地方落:
# 1. 命名空间 kubectl apply -f 00-namespace.yaml # 2. Secret 用命令行建,避免密码进 Git kubectl -n nacos create secret generic nacos-secret \ --from-literal=mysql-user=nacos \ --from-literal=mysql-password='<改掉这个>' \ --from-literal=NACOS_AUTH_IDENTITY_KEY=serverIdentity \ --from-literal=NACOS_AUTH_IDENTITY_VALUE='<改掉这个>' \ --from-literal=NACOS_AUTH_TOKEN='<至少 32 位 base64 串>' # 3. 确认默认 StorageClass 存在 kubectl get scNACOS_AUTH_TOKEN不是随便填的字符串,Nacos 要求它是 Base64 编码且解码后不少于 32 字节,否则启动直接报the length of token should be greater than 32 byte。生成方式:
openssl rand -base64 32如果没有默认 StorageClass,volumeClaimTemplates会一直 Pending。临时环境可以用hostPath顶一下,但生产别这么干——Pod 一漂移,数据就找不着了。
4.2 逐层 apply 与首次启动观察
按依赖顺序推,一次推一层,出问题好定位:
# 配置类资源先上 kubectl apply -f 02-configmap.yaml kubectl apply -f 03-mysql-init-job.yaml # 等 Job 完成,确认表建出来了 kubectl -n nacos wait --for=condition=complete job/nacos-mysql-init --timeout=120s kubectl -n nacos logs job/nacos-mysql-init # 服务和主负载 kubectl apply -f 04-service-headless.yaml kubectl apply -f 06-statefulset.yaml # 盯启动过程,不要只看最后状态 kubectl -n nacos get pods -wkubectl get pods -w比反复get有用得多,StatefulSet 三个副本是并发起的,你能看到它们同时在ContainerCreating,然后几乎同时进Running,再过 20 到 40 秒变Ready。如果某个 Pod 卡在Running但Ready一直是0/1,八成是它在等别的节点,去看日志里的cluster.conf内容和解析结果。
单个节点的启动日志重点看三行:Nacos started successfully in cluster mode、server is starting with cluster里的节点列表、以及有没有failed to connect to mysql。前两行确认集群组起来了,第三行确认数据库通了。
4.3 三个验证点:注册列表、配置读写、节点重启
Pod Ready 不等于集群可用,至少做三组验证。
第一组,看集群节点列表:
kubectl -n nacos exec nacos-0 -- \ curl -s 'http://127.0.0.1:8848/nacos/v1/ns/operator/servers?accessToken=<token>'返回里的servers数组应该是三条,state为UP。如果只看到一条,说明cluster.conf解析失败或者 9849 端口不通。
第二组,验证配置在各节点一致——在nacos-0写一条配置,去nacos-2读:
# 写 kubectl -n nacos exec nacos-0 -- curl -s -X POST \ 'http://127.0.0.1:8848/nacos/v1/cs/configs' \ -d 'dataId=demo.yaml&group=DEFAULT_GROUP&content=foo: bar' # 从另一个节点读 kubectl -n nacos exec nacos-2 -- curl -s \ 'http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=demo.yaml&group=DEFAULT_GROUP'能读回foo: bar才说明数据真的落到了共用的 MySQL,而不是各节点本地。
第三组,删一个 Pod 让它重建,再看集群是否自动恢复:
kubectl -n nacos delete pod nacos-1 kubectl -n nacos get pods -w新 Pod 起来后,cluster.conf里的 FQDN 不变,其余两个节点会自动重连,服务列表恢复三节点。这一组验证的是 headless Service 那套寻址设计是否成立。
5. 避坑与排查:五个让我半夜爬起来的问题
5.1 现象:Pod 起得来但就绪慢,最后 CrashLoopBackOff
原因通常是livenessProbe的initialDelaySeconds小于实际启动耗时。Nacos 集群模式启动要建表检查、Raft 选主、同步配置快照,冷启动 60 到 90 秒很正常,探针 30 秒就来敲门,直接把进程干掉,K8s 再重启一次,周而复始。
解决办法是把 liveness 的initialDelaySeconds拉到 90 以上,并配上failureThreshold: 3。更稳的做法是用startupProbe接管启动阶段,设failureThreshold: 30、periodSeconds: 5,给足 150 秒,之后 liveness 才接管。
5.2 现象:控制台能看到服务,客户端拉配置超时
只看 8848 端口会被坑。Nacos 2.x 客户端走 gRPC,连的是 8848+1000 即 9848,服务端之间同步走 9849。Service 里只映射了 8848,控制台正常,业务客户端一直重连,日志刷client not connected, current status: STARTING。
排查方法是kubectl -n nacos exec进任意 Pod,curl -v telnet://nacos-headless:9848。不通就把 9848、9849 补进 Service 的ports,同时检查有没有 NetworkPolicy 拦着。集群内网里我一般直接把 7848、8848、9848、9849 四个端口全放行。
5.3 现象:三个 Pod 各自持有不同配置,改一个另一个不生效
大概率是MODE=cluster设了,但漏了SPRING_DATASOURCE_PLATFORM=mysql,或者数据库连接参数写错导致回退到内嵌 Derby。每个 Pod 各写各的本地库,看起来"集群起来了",实际数据完全隔离。
解决方式是进 Pod 看conf/cluster.conf和启动日志里的存储类型,日志里出现use embedded storage就是没连上 MySQL。再检查MYSQL_SERVICE_DB_PARAM里有没有useSSL=false——MySQL 8 默认要 TLS,参数不写全就会握手失败,被 Nacos 静默降级。
5.4 现象:PVC 挂上了,Pod 起来就报权限错误
NFS 或某些 CSI 存储返回的卷默认属主是 root,而 Nacos 镜像以非 root 用户运行,写data/、logs/时 permission denied,容器直接退出。
解决办法有两个:在securityContext里加fsGroup,让 kubelet 自动改卷的组权限;或者在启动前加一个 initContainer 跑chown -R 1000:1000 /home/nacos。前者更干净,但依赖 CSI 支持;后者万能,代价是多一个容器。我一般在自建 NFS 场景下用后者,云上托管存储用前者。
5.5 现象:扩到 4 个副本后,新节点一直不加入集群
NACOS_SERVERS是写死在 StatefulSet 里的字符串,replicas改了但环境变量没改,新起的nacos-3不知道自己在集群里,其他节点也不认识它。
这块没有后悔药,只能改完NACOS_SERVERS再滚动重启。所以我在模板里会把这个值提成变量,扩容时同步改,或者干脆用脚本按replicas生成。下一节讲具体怎么做。
6. 二次开发:把这套 YAML 改成能进 CI 的模板
清单能跑只是起点,真正要长期维护,得把它变成参数化的模板。硬编码的NACOS_SERVERS、散落的密码、写死的副本数,都是下一次扩容时的事故源。
最省事的改造是用 Kustomize 的configMapGenerator和 patch,把环境差异收进 overlay:
# overlay/prod/kustomization.yaml resources: - ../../base replicas: - name: nacos count: 3 patches: - target: { kind: StatefulSet, name: nacos } patch: |- - op: replace path: /spec/template/spec/containers/0/env/1/value 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" configMapGenerator: - name: nacos-config files: - application.propertiesreplicas和NACOS_SERVERS两处必须一起改,这是我在 CI 里加了个校验脚本的原因——用yq读出副本数,和NACOS_SERVERS里的地址个数比对,不一致就让流水线失败。听起来小题大做,但它拦下过两次差点上生产的错误配置。
版本升级的顺序也有讲究。Nacos 2.x 小版本之间兼容,但跨大版本要先升数据库 schema。我做灰度时按这个顺序:先备份 MySQL,再把nacos-2单独换镜像观察 10 分钟,确认它和旧版本节点能正常通信,再滚动其余两个。StatefulSet 的updateStrategy设成RollingUpdate配合partition: 2,就能实现"只升最后一个"。
验证方法我固定跑三件事:注册列表接口返回的节点数等于副本数;往nacos-0写配置、从nacos-$((N-1))读回来一致;删掉一个 Pod 观察 60 秒内自动重连。三件事都过,才允许合并。
从那以后我每次改 Nacos 清单,都强制先跑一遍这三项验证,再谈上线——不是不信任自己的改动,是集群类中间件的故障往往延迟十几分钟才暴露,而那时候你已经在做别的事了。希望这套拆解能帮你少熬几个夜。
本文还有配套的精品资源,点击获取