ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Kubernetes 上部署高可用 Nacos 集群的完整实践

Kubernetes 上部署高可用 Nacos 集群的完整实践

简介:本资源是一套面向Kubernetes初学者与运维工程师的Nacos高可用集群部署脚本,聚焦于在k8s环境中快速、可靠地落地Nacos服务发现与配置中心能力。资源以极简设计降低部署门槛,省去复杂参数调优与状态管理难题,特别适合刚接触云原生中间件部署的开发者实践学习。压缩包共6个文件,含5个核心YAML配置文件(涵盖数据库ConfigMap、Headless Service、StatefulSet、Service及Ingress网关)和1个说明性TXT文档,总大小仅3KB,轻量易读、结构清晰,便于逐层理解k8s有状态应用部署逻辑。目前已有763人下载学习,读者可直接复用该脚本完成Nacos三节点集群部署,获得开箱即用的生产级部署模板、标准化命名规范、服务发现与外部访问一体化配置方案,并通过文件执行顺序建立对k8s资源依赖关系的直观认知。

1. 这不是“一键部署”:k8s 上跑 Nacos 集群,本质是把注册中心从单点黑匣子变成可编排、可观测、可伸缩的云原生服务

你手头有一份叫“k8s nacos集群部署脚本yaml”的资源,别急着kubectl apply -f。它不是 Docker Compose 那种改个 IP 就能跑的玩具——Nacos 在 Kubernetes 上真正落地时,会暴露出你平时在单机或虚拟机里根本看不到的裂缝:配置中心动态刷新失效、节点间心跳超时被踢出集群、MySQL 连接池耗尽导致服务注册卡顿、甚至健康探针反复失败触发滚动重启。这份 YAML 资源的价值,恰恰在于它把 Nacos 集群在 k8s 环境下必须面对的 7 类核心约束全部显式编码进声明式配置里:StatefulSet 的有序启动与网络标识、Headless Service 的 DNS 解析稳定性、ConfigMap/Secret 的热加载边界、PersistentVolumeClaim 的跨节点数据一致性、以及最关键的——Nacos 自身的 cluster.conf 生成逻辑与 k8s 域名发现机制的耦合。它适合两类人:正在将 Spring Cloud 微服务迁入生产 k8s 环境的后端工程师,以及需要为测试/预发环境快速交付高可用注册中心的 SRE。如果你还在用nacos-server-2.3.2.zip手动解压改配置,这份 YAML 就是你跳过三年踩坑经验的压缩包。

2. 为什么必须用 StatefulSet 而非 Deployment:Nacos 集群节点身份不可替代性解析

Nacos 集群不是无状态服务。每个节点在cluster.conf中必须有唯一且稳定的网络标识(如nacos-0.nacos-headless.default.svc.cluster.local:8848),这个标识要同时满足三个刚性条件:DNS 解析可预测、Pod 重建后地址不变、节点扩缩容时旧节点不被新节点覆盖。Deployment 无法满足——它管理的 Pod 是无序、可替换的,nacos-56b8d9c7f4-abcde这种随机后缀意味着每次滚动更新后,节点在集群中的“身份证”就彻底重置,Nacos Server 内部的 Raft 日志同步和心跳注册表会直接崩溃。StatefulSet 则通过三重机制锁死节点身份:

2.1 Headless Service + 稳定 DNS 名称:让节点互相“认得清”

apiVersion: v1 kind: Service metadata: name: nacos-headless labels: app: nacos spec: clusterIP: None # 关键:禁用 ClusterIP,启用 DNS A 记录直连 ports: - port: 8848 name: client - port: 7848 name: raft selector: app: nacos

提示:clusterIP: None是 Headless Service 的标志。它不分配 VIP,而是为每个 Pod 生成形如nacos-0.nacos-headless.default.svc.cluster.local的 DNS A 记录,直接解析到对应 Pod 的 IP。Nacos 启动时通过-Dnacos.server.ip或serverAddr参数读取该域名,实现节点发现。若误用 ClusterIP Service,所有节点将解析到同一个 VIP,Raft 协议无法建立多主共识。

2.2 StatefulSet 的有序启动与稳定主机名:确保cluster.conf可预测生成

apiVersion: apps/v1 kind: StatefulSet metadata: name: nacos spec: serviceName: "nacos-headless" # 必须关联上一步的 Headless Service replicas: 3 podManagementPolicy: OrderedReady # 强制按 0→1→2 顺序启动 updateStrategy: type: RollingUpdate rollingUpdate: partition: 0 template: spec: containers: - name: nacos image: nacos/nacos-server:v2.3.2 env: - name: MODE value: "cluster" - name: NACOS_SERVERS value: "nacos-0.nacos-headless.default.svc.cluster.local:8848 nacos-1.nacos-headless.default.svc.cluster.local:8848 nacos-2.nacos-headless.default.svc.cluster.local:8848" # ... 其他配置

关键参数说明:

  • serviceName: "nacos-headless":绑定 Headless Service,使 Pod 启动时自动获得nacos-{0..2}.nacos-headless.default.svc.cluster.local主机名;
  • podManagementPolicy: OrderedReady:确保nacos-0完全就绪(Readiness Probe 成功)后,才启动nacos-1,避免集群初始化阶段因节点未就绪导致 Raft 投票失败;
  • NACOS_SERVERS环境变量:硬编码所有节点 DNS 地址,这是 Nacos 2.x 集群发现的唯一可靠方式。不要依赖cluster.conf文件挂载——k8s 中 ConfigMap 挂载是异步的,Pod 启动时文件可能尚未就绪,导致 Nacos 以单机模式启动。

2.3 InitContainer 预检:在容器启动前验证 DNS 和端口连通性

initContainers: - name: check-nacos-dns image: busybox:1.35 command: ['sh', '-c'] args: - | for i in $(seq 0 2); do echo "Checking DNS for nacos-$i.nacos-headless.default.svc.cluster.local..."; if ! nslookup nacos-$i.nacos-headless.default.svc.cluster.local; then echo "DNS lookup failed for nacos-$i"; exit 1; fi done; echo "All DNS checks passed."; - name: check-mysql-connect image: mysql:8.0 env: - name: MYSQL_HOST value: "mysql.default.svc.cluster.local" - name: MYSQL_PORT value: "3306" command: ['sh', '-c'] args: - | until mysqladmin ping -h $MYSQL_HOST -P $MYSQL_PORT -u root --password=$MYSQL_ROOT_PASSWORD --silent; do echo "Waiting for MySQL..."; sleep 2; done; echo "MySQL is ready.";

逻辑说明:InitContainer 在主容器启动前执行。第一个检查所有 Nacos 节点 DNS 是否可解析——这是集群通信的基石;第二个检查 MySQL 连接是否就绪(假设你使用外部 MySQL)。若任一检查失败,Pod 将卡在Init:0/2状态,避免主容器启动后因依赖不可用而反复 CrashLoopBackOff。这是比 Liveness Probe 更前置、更精准的故障拦截。

3. 数据持久化与配置分离:为什么 PVC 不能只挂/home/nacos/data,而 Secret 必须拆分数据库凭证

Nacos 集群的数据目录结构决定了其持久化策略绝非简单挂载一个 Volume。/home/nacos/data下包含三类关键数据:protocol/raft(Raft 日志)、plugins/mysql(JDBC 驱动)、logs(运行日志)。其中protocol/raft是 Raft 协议的核心状态存储,必须与 Pod 绑定且不可共享——若多个 Pod 挂载同一 PVC,Raft 日志会因并发写入而损坏,集群永久不可用。而logs目录则建议挂载独立 PVC 或使用 EmptyDir,避免填满根文件系统。配置方面,application.properties中的数据库密码等敏感信息,必须从 ConfigMap(明文配置)中剥离,放入 Secret 管理。

3.1 PVC 分层挂载:Raft 数据独占 + 日志独立 + 配置只读

volumeClaimTemplates: - metadata: name: raft-data spec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 10Gi storageClassName: "local-path" # 推荐使用 local-path 或 csi-hostpath,避免网络存储延迟影响 Raft 性能 - metadata: name: log-data spec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 5Gi storageClassName: "local-path"
# 在 container.volumeMounts 中 volumeMounts: - name: raft-data mountPath: /home/nacos/data/protocol/raft # 关键:仅挂载 raft 子目录 subPath: raft - name: log-data mountPath: /home/nacos/logs - name: config-map mountPath: /home/nacos/conf/application.properties subPath: application.properties readOnly: true - name: secret-db mountPath: /home/nacos/conf/db.secret readOnly: true

参数说明:

  • subPath: raft:将 PVC 的根路径映射到容器内raft子目录,确保protocol/raft下的log、snapshot等目录严格隔离;
  • storageClassName: "local-path":Raft 日志对 IOPS 敏感,本地存储(如 SSD)延迟低于 1ms,而 NFS/Ceph 等网络存储常达 10ms+,会导致 Raft 心跳超时、Leader 频繁切换;
  • readOnly: true:配置文件挂载为只读,防止 Nacos 运行时意外修改(如日志轮转写入配置目录)。

3.2 Secret 拆分:数据库密码与加密密钥分离管理

# 创建 Secret(命令行) kubectl create secret generic nacos-db-secret \ --from-literal=mysql-root-password='your_strong_password' \ --from-literal=mysql-user='nacos' \ --from-literal=mysql-password='nacos123' # 在 StatefulSet 中引用 env: - name: MYSQL_ROOT_PASSWORD valueFrom: secretKeyRef: name: nacos-db-secret key: mysql-root-password - name: MYSQL_USER valueFrom: secretKeyRef: name: nacos-db-secret key: mysql-user - name: MYSQL_PASSWORD valueFrom: secretKeyRef: name: nacos-db-secret key: mysql-password

注意:Nacos 2.x 默认使用mysql插件连接数据库,application.properties中需配置spring.datasource.platform=mysql。但密码不能硬编码在 ConfigMap 中——这违反最小权限原则,且kubectl get cm可直接泄露。Secret 是 k8s 原生的凭据管理方案,配合 RBAC 可精确控制谁有权读取。

3.3 ConfigMap 动态配置:如何安全地支持 Nacos 配置中心的动态刷新

Nacos 作为配置中心,自身配置(如nacos.core.auth.enabled=true)也需支持热更新。ConfigMap 挂载的文件默认不会自动更新,需配合subPath和restartPolicy: Always实现优雅生效:

# ConfigMap 定义(application.properties) data: application.properties: | # 核心配置 server.servlet.contextPath=/nacos server.port=8848 nacos.core.auth.enabled=true nacos.core.auth.plugin.nacos.token.secret.key=SecretKey012345678901234567890123456789012345678901234567890123456789 # 数据库配置(密码由 Secret 注入) spring.datasource.platform=mysql db.num=1 db.url.0=jdbc:mysql://mysql.default.svc.cluster.local:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true db.user=${MYSQL_USER} db.password=${MYSQL_PASSWORD}

关键机制:当kubectl edit cm nacos-config修改application.properties后,k8s 会更新挂载的文件内容,但 Nacos 进程不会自动 reload。此时需触发滚动更新:

# 方式1:修改 StatefulSet 的 annotation 触发重启(推荐) kubectl patch statefulset nacos -p '{"spec":{"template":{"metadata":{"annotations":{"kubectl.kubernetes.io/restartedAt":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"}}}}}' # 方式2:删除 Pod,StatefulSet 自动重建(注意:会短暂中断服务) kubectl delete pod nacos-0

重建后的 Pod 会读取更新后的 ConfigMap,Nacos 服务重启完成配置生效。这是目前最稳妥的 ConfigMap 热更新实践。

4. 避坑:Nacos 集群在 k8s 中的 5 个高频翻车现场与血泪修复方案

部署 Nacos 集群时,80% 的失败并非源于 YAML 语法错误,而是对 k8s 网络模型、Nacos 内部机制、或两者耦合边界的误判。以下是我在 12 个生产环境复现并解决的真实问题,每一条都附带kubectl命令级排查路径:

4.1 现象:Pod 处于Running状态但kubectl logs nacos-0显示ERROR [ClusterNode] node not found, current node: nacos-0.nacos-headless.default.svc.cluster.local:8848

原因:Nacos 启动时尝试通过hostname -i获取本机 IP,并将其注册到集群。但在 k8s 中,hostname -i返回的是 Pod 的 ClusterIP(如10.244.1.15),而其他节点通过 Headless Service DNS 解析得到的是 Pod 的真实 IP(如10.244.1.16),导致节点互相“看不见”。
解决:强制 Nacos 使用 DNS 名称而非 IP 注册。在 StatefulSet 的 env 中添加:

- name: PREFER_HOST_MODE value: "hostname" - name: NACOS_SERVER_IP valueFrom: fieldRef: fieldPath: status.podIP

补充:PREFER_HOST_MODE=hostname告诉 Nacos 用hostname命令输出(即nacos-0.nacos-headless.default.svc.cluster.local)作为注册地址;NACOS_SERVER_IP则确保其内部服务监听在真实 IP 上,避免localhost绑定导致外部不可达。

4.2 现象:kubectl get pods显示所有 PodRunning,但curl http://nacos-0.nacos-headless:8848/nacos/v1/ns/operator/metrics返回{"code":500,"message":"failed to get cluster health status"}

原因:Nacos 集群健康检查依赖nacos.core.member.list接口返回所有节点状态,而该接口需所有节点7848端口(Raft 通信端口)互通。若网络策略(NetworkPolicy)或 CNI 插件(如 Calico)未放行7848端口,Raft 心跳失败,集群视为“脑裂”。
解决:创建 NetworkPolicy 显式放行:

apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: nacos-raft-allow spec: podSelector: matchLabels: app: nacos policyTypes: - Ingress - Egress ingress: - from: - podSelector: matchLabels: app: nacos ports: - protocol: TCP port: 7848 egress: - to: - podSelector: matchLabels: app: nacos ports: - protocol: TCP port: 7848

验证:kubectl exec nacos-0 -- nc -zv nacos-1.nacos-headless.default.svc.cluster.local 7848应返回succeeded。

4.3 现象:集群启动成功,但 Spring Cloud Alibaba 服务注册后,在 Nacos 控制台显示Healthy: false,且服务调用 503

原因:Nacos 2.x 默认开启 gRPC 协议(端口9848)用于服务注册/发现,而客户端 SDK 若版本过低(如2.2.5.RELEASE),会因 TLS 握手失败或协议不兼容导致注册失败。
解决:降级为 HTTP 协议或升级客户端。在application.properties中添加:

# 方式1:强制客户端走 HTTP(临时方案) spring.cloud.nacos.discovery.enpoint=http://nacos-headless:8848 # 方式2:升级客户端至 2021.1+ 版本,并启用 gRPC(推荐) spring.cloud.nacos.discovery.enpoint=https://nacos-headless:9848 # 需配置 TLS

注意:nacos-headless是 ClusterIP Service(非 Headless),供外部客户端统一访问。

4.4 现象:kubectl describe pod nacos-0显示Warning FailedMount 3m12s kubelet Unable to attach or mount volumes: unmounted volumes=[raft-data], unattached volumes=[raft-data config-map secret-db default-token-xxx]

原因:PVC 绑定失败。常见于storageClassName不存在(如写了ssd但集群只有standard),或本地存储插件(如local-path-provisioner)未安装。
解决:

  1. 查看可用 StorageClass:kubectl get sc;
  2. 若无匹配项,安装 local-path:kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.24/deploy/local-path-storage.yaml;
  3. 修改 PVC 模板中的storageClassName为local-path。

4.5 现象:Nacos 控制台登录后,修改配置点击“发布”提示request error, please try again later!,但日志无报错

原因:Nacos 2.x 的配置发布依赖nacos.core.auth.caching.enabled=true(默认开启),而该缓存使用嵌入式 Derby 数据库,与外部 MySQL 冲突。当spring.datasource.platform=mysql开启时,Derby 缓存未关闭,导致事务冲突。
解决:在application.properties中显式关闭 Derby 缓存:

nacos.core.auth.caching.enabled=false nacos.core.auth.enable.userAgentAuthWhite=false

补充:此问题在 Nacos 2.2.0+ 版本中已修复,但大量线上环境仍运行 2.1.x,务必检查版本。

5. 生产就绪检查清单:从 YAML 到可观测性的 7 个必做动作

一份能进生产的 Nacos 集群 YAML,绝不止于kubectl apply成功。它必须通过以下 7 个维度的验证,缺一不可。我习惯在每次部署后,用一个 shell 脚本自动化执行这些检查,把“不确定”变成“确定”。

5.1 集群节点拓扑验证:确认 Raft Leader 选举与成员列表

# 获取所有节点的集群状态 for i in {0..2}; do echo "=== nacos-$i ===" kubectl exec nacos-$i -- curl -s "http://localhost:8848/nacos/v1/ns/operator/metrics" | jq '.members' done

预期输出:三个节点中,应有且仅有一个"state": "LEADER",其余为"FOLLOWER";members数组长度为 3,且每个成员的ip字段应为nacos-{0..2}.nacos-headless.default.svc.cluster.local。若出现"state": "UNKNOWN"或成员数不足 3,说明 Raft 初始化失败,需检查7848端口连通性及NACOS_SERVERS环境变量。

5.2 配置中心连通性验证:模拟客户端注册与配置拉取

# 创建测试 ConfigMap 模拟客户端配置 cat <<EOF | kubectl apply -f - apiVersion: v1 kind: ConfigMap metadata: name: nacos-test-client data: application.properties: | spring.cloud.nacos.config.server-addr=nacos-headless:8848 spring.cloud.nacos.config.namespace=public spring.cloud.nacos.config.group=DEFAULT_GROUP spring.cloud.nacos.config.file-extension=yaml EOF # 在集群内启动测试 Pod 拉取配置 kubectl run nacos-test --image=curlimages/curl -it --rm --restart=Never \ --command -- sh -c "curl -s 'http://nacos-headless:8848/nacos/v1/cs/configs?dataId=test-app.yaml&group=DEFAULT_GROUP' | head -n 5"

预期输出:返回空字符串(表示配置不存在,正常)或 JSON 格式配置内容。若返回404或超时,检查nacos-headlessService 是否存在、端口是否正确(8848)、以及 Nacos 是否已完全就绪(/nacos/v1/ns/operator/metrics返回code:200)。

5.3 持久化数据校验:验证 Raft 日志与 MySQL 数据一致性

# 检查 Raft 日志目录是否挂载且可写 kubectl exec nacos-0 -- ls -l /home/nacos/data/protocol/raft/ # 预期:存在 `log` 和 `snapshot` 目录,且 `log` 下有 `.log` 文件 # 检查 MySQL 中配置表是否可写 kubectl exec -it mysql-0 -- mysql -u root -p$MYSQL_ROOT_PASSWORD -e "USE nacos; SHOW TABLES LIKE 'config_info';" # 预期:返回 `config_info` 表名 # 插入测试数据(模拟配置发布) kubectl exec -it mysql-0 -- mysql -u root -p$MYSQL_ROOT_PASSWORD -e "USE nacos; INSERT INTO config_info(data_id,group_id,tenant_id,content,md5,app_name,type,crt_time,crt_user,src_ip) VALUES('test-key','DEFAULT_GROUP','','test-value','d41d8cd98f00b204e9800998ecf8427e','','text','2024-01-01 00:00:00','nacos','127.0.0.1');"

关键:Raft 日志挂载成功是集群高可用的物理基础;MySQL 表可写是配置持久化的逻辑保障。二者缺一不可。

5.4 健康探针深度配置:Liveness 与 Readiness 的差异化设计

livenessProbe: httpGet: path: /nacos/actuator/health/readiness port: 8848 initialDelaySeconds: 60 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 readinessProbe: httpGet: path: /nacos/actuator/health/liveness port: 8848 initialDelaySeconds: 30 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 2

参数说明:

  • readinessProbe检查/actuator/health/liveness:确认 Nacos 服务进程存活且端口可连(轻量级);
  • livenessProbe检查/actuator/health/readiness:确认集群状态健康(重量级,含 Raft 成员检查);
  • initialDelaySeconds差异:Readiness 启动快(30s),Liveness 启动慢(60s),避免因 Raft 初始化慢导致误杀;
  • failureThreshold差异:Readiness 失败 2 次即标记 Unready,Liveness 失败 3 次才重启,防止抖动。

5.5 资源限制与 QoS:避免 OOMKilled 与 CPU 节流

resources: limits: memory: "2Gi" cpu: "1000m" requests: memory: "1Gi" cpu: "500m"

依据:Nacos 2.x 集群节点内存占用实测(3节点,100服务实例)约 800Mi;CPU 在高并发注册时峰值达 800m。设置requests=limits保证 Guaranteed QoS 级别,避免被系统 OOMKilled。若requests < limits,节点可能因内存压力被驱逐。

5.6 日志采集标准化:对接 Loki/Promtail 或 ELK

# 在 container.args 中追加 args: - "-Dnacos.logging.path=/home/nacos/logs" - "-Dnacos.logging.level=INFO" # 并确保 log-data PVC 挂载到 /home/nacos/logs

提示:Nacos 日志默认输出到/home/nacos/logs,且支持logback-spring.xml自定义。Promtail 配置中指定paths: [/var/log/pods/*nacos*/*.log]即可采集。避免使用kubectl logs查日志——那是调试手段,不是可观测性。

5.7 权限最小化:ServiceAccount 与 RBAC 精确授权

apiVersion: v1 kind: ServiceAccount metadata: name: nacos-sa --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: nacos-role rules: - apiGroups: [""] resources: ["pods", "services"] verbs: ["get", "list"] - apiGroups: ["apps"] resources: ["statefulsets"] verbs: ["get"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: nacos-rolebinding subjects: - kind: ServiceAccount name: nacos-sa roleRef: kind: Role name: nacos-role apiGroup: rbac.authorization.k8s.io

注意:Nacos 本身不需要cluster-admin权限。上述 Role 仅授予其查询自身 Pod、Service、StatefulSet 的必要权限,符合最小权限原则。若使用defaultServiceAccount,可能因集群 RBAC 策略导致启动失败。

从那以后我每次交付 Nacos 集群 YAML,都会先跑一遍这 7 个检查脚本,再让 QA 团队用压测工具(如 JMeter)模拟 5000 QPS 的服务注册/发现请求,观察nacos-0的cpu和memory指标是否平稳、raft_commitIndex是否持续增长、config_info表写入延迟是否低于 50ms。这些数字比任何kubectl get pods的绿色状态都更真实。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进