简介:这是一份面向Kubernetes环境的Nacos集群极简部署脚本,针对需要在Kubernetes中快速搭建高可用Nacos的运维与开发人员,也适合刚接触K8s与Nacos的小白。资源将复杂配置拆分为按编号排列的YAML文件,依次执行即可完成数据库依赖、Headless Service、StatefulSet、对外Service与Ingress的创建,显著降低手工修改与排错成本。资源包共6个文件,其中5个YAML对应上述各环节,另有1个readme说明文档用于引导使用,整体压缩包仅3KB,十分轻量。整个过程无需额外插件,按编号顺序执行即可完成部署。目前已有764人学习下载。通过该脚本,读者不仅能获得一套可直接运行的Nacos集群配置,还能借助清晰的YAML划分理解组件间依赖关系,便于在测试环境快速验证或在此基础上做二次定制。适合已有基本K8s概念、希望快速落地中间件部署的团队或个人使用。
1. 为什么在 K8s 里部署 Nacos 集群总在第一步就翻车
k8s nacos集群部署脚本yaml——这个标题我太熟了,因为每次帮人排查Nacos集群问题,最后都能定位到三件事:网络、存储、版本。这三个词看着不复杂,但叠在 Kubernetes 上就变出很多具体烦恼:Pod 的 IP 是动态的,节点之间的域名得自己搭;节点重启后旧 IP 还在集群列表里赖着不走;新版本镜像默认多监听一个 9848 的 gRPC 端口,少暴露一个端口,客户端就反复报错。
这篇文章不打算讲 Nacos 原理,而是把"在 k8s 里部署 Nacos 集群"这件事从选型、YAML 编写到验证完整走一遍。你会看到完整的 Headless Service、StatefulSet、环境变量配置和可复用的部署脚本,也会看到我整理的五条血泪坑。适合已经在用 K8s、想把 Nacos 从单机搬到集群的读者,也适合正在被"Pod 起来了但集群没起来"折磨的人。
2. 先把三样底牌定死:存储、镜像版本、网络模型
2.1 有状态服务选型:为什么只用 StatefulSet + PVC
Nacos 集群是典型的有状态应用,对外提供服务、集群内部节点间通信,都需要稳定的身份标识。在 K8s 里跑有状态应用,最标准的答案就是 StatefulSet,这没什么好犹豫的。它和 Deployment 最大的区别在于:StatefulSet 创建的 Pod 名称是有序且固定的,比如 nacos-0、nacos-1、nacos-2,配合无头服务可以拿到稳定的 DNS 域名。
翻译成人话:每个 Nacos 节点都得能被"点名"找到。你如果图省事用 Deployment 加随机 Pod 名,节点 A 重启后变成了完全陌生的名字,集群里其他节点不认识它,整个集群就散了。StatefulSet 保证的是"nacos-0 重启以后还是 nacos-0,域名还是那个域名",节点在集群里的身份不会变。
存储也是同样的逻辑。Nacos 虽然配了 MySQL 做外部持久化,但节点本地还有 data 和 logs 目录需要落盘。data 目录存放 Raft 快照、临时文件,logs 目录存放运行日志。这两个目录不持久化,Pod 一重建,日志全没了,快照文件丢了虽然能从 MySQL 恢复,但对排查问题来说是灾难。
存储类这边,我一般建议先确认 K8s 集群的动态供给是否可用:
kubectl get storageclass如果输出里能看到一条带(default)标记的存储类,那 StatefulSet 里不写 storageClassName 也能自动创建 PVC。如果一条都没有,说明集群没有动态供给,这时候要么装一个存储插件,要么老老实实手动建 PV。我不建议在这地方偷懒,后续 PVC 卡在 Pending 的坑,根源基本都是这里。
还有一点:Nacos 集群模式下必须配外部数据库,官方内置的 Derby 只能单机玩。三个节点各带一个 Derby,数据互相看不见,和"集群"完全是两码事。数据库可以部署在 K8s 集群内,也可以部署在集群外的虚机上,关键是 Nacos 节点能通过网络访问到它。我在实际项目里更倾向于用集群外已有的 MySQL,这样备份、迁移、升级都不受 K8s 重建周期影响。
2.2 镜像与版本:nacos-server 1.x 和 2.x 部署逻辑差在哪
版本选择上,Nacos 1.x 和 2.x 在 K8s 里的部署差异非常大。最核心的变化是:2.x 引入了 gRPC 通信。客户端注册后不是直接走 HTTP 了,而是通过 gRPC 长连接和 Nacos 服务端通信;集群节点之间走 Raft 协议,默认在 8848 主端口上加 1000 得到 9848 的 gRPC 端口,再加 1000 得到 9849 的服务端 gRPC 端口。
这个差异直接决定了 Service 要开哪些端口。1.x 只用开 8848,2.x 至少还要开 9848,否则客户端注册时能连上,但后续心跳和推送全部超时。很多刚上手的人只映射了 8848,Pod 全是 Running,客户端却疯狂报错,折腾一整天最后发现是端口没开全。
镜像 tag 我建议锁稳定版本,比如 v2.3.x 这个系列。下载下来先别急着部署,用 docker 拉下来看一眼启动日志,确认版本号再配参数。不要图新用 latest,也不要用特别老的 1.4.x。如果是从 1.x 升上来的老集群,先搞清升级路径再动手,直接跨大版本升级的翻车现场我在第 5 章会细讲。
2.3 网络模型:Headless Service 到底在解决什么问题
Nacos 集群节点之间要互相通信,必须知道彼此的地址。物理机部署时直接填内网 IP 就行,但在 K8s 里 Pod 的 IP 是临时的,随时可能因为重建而变化。如果用 IP 做节点间通信,那每次 Pod 重启,集群里其他节点还在尝试连旧 IP,节点就丢了。
Headless Service 就是来解决这个问题的。它把 clusterIP 设为 None,不承担负载均衡职责,但会给每个 Pod 分配一条稳定的 DNS 记录。比如 serviceName 是 nacos-headless,那nacos-0.nacos-headless.nacos.svc.cluster.local就永远指向当前这个 Pod 的 IP,哪怕 Pod 漂了,域名也会自动更新到新 IP。
所以在 Nacos 的环境变量里,最稳的写法是设PREFER_HOST_MODE=hostname,配合 NACOS_SERVERS 里写三个稳定域名,让节点之间通过 hostname 互相发现,而不是直接记 IP。Nacos 2.x 也支持通过 fieldRef 拿 Pod IP 做注册,但 IP 方式在节点重建后会有残留问题,我一般不用。
同一个命名空间内可以简写域名,比如nacos-0.nacos-headless,但跨命名空间或生产环境,我建议直接写完整 FQDN:nacos-0.nacos-headless.<namespace>.svc.cluster.local,一劳永逸。部署完先在 Pod 里 ping 一下域名,确认解析正常再启动容器,能省掉后面大量排查时间。
3. 手写 Nacos 集群 YAML:从无头服务到 StatefulSet 全量拆解
3.1 命名空间与 Headless Service:先给每个副本一个可解析的域名
这一节开始进入动手阶段。先创建命名空间,我习惯用nacos这个名字,你按团队规范改也行。然后是无头服务,它负责给每个 Pod 发稳定域名:
apiVersion: v1 kind: Namespace metadata: name: nacos --- apiVersion: v1 kind: Service metadata: name: nacos-headless namespace: nacos labels: app: nacos spec: clusterIP: None ports: - name: http port: 8848 targetPort: 8848 - name: grpc port: 9848 targetPort: 9848 selector: app: nacos这段 YAML 里最关键的字段是clusterIP: None,它告诉 K8s 不要给这个 Service 分配集群内虚拟 IP。无头服务创建后,K8s 会为每个后端 Pod 生成一条 A 记录,格式就是podName.serviceName.namespace.svc.cluster.local。
端口这里我把 8848 和 9848 都提前声明了。8848 是 Nacos 的主端口,提供 HTTP 服务和客户端 API;9848 是 gRPC 端口,客户端发现集群地址后会自动连这个端口。StatefulSet 创建出来的 Pod 会自动挂到这个无头服务后面,前提是 selector 匹配上app: nacos这个标签。
3.2 StatefulSet 完整 YAML:三副本、反亲和、环境变量一次性写对
接下来是核心的 StatefulSet。我直接给出一个能用的完整定义,然后逐个字段解释:
apiVersion: apps/v1 kind: StatefulSet metadata: name: nacos namespace: nacos spec: serviceName: nacos-headless replicas: 3 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-server image: nacos/nacos-server:v2.3.2 ports: - containerPort: 8848 name: http - containerPort: 9848 name: grpc env: - name: MODE value: cluster - name: PREFER_HOST_MODE value: hostname - name: NACOS_REPLICAS value: "3" - 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.mysql.svc.cluster.local - name: MYSQL_SERVICE_DB_NAME value: nacos_config - name: MYSQL_SERVICE_PORT value: "3306" - name: MYSQL_SERVICE_USER value: nacos - name: MYSQL_SERVICE_PASSWORD valueFrom: secretKeyRef: name: nacos-db-secret key: password - name: JVM_XMS value: 1g - name: JVM_XMX value: 1g - name: JVM_XMN value: 512m volumeMounts: - name: nacos-data mountPath: /home/nacos/data - name: nacos-log mountPath: /home/nacos/logs volumeClaimTemplates: - metadata: name: nacos-data spec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 10Gi - metadata: name: nacos-log spec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 10GiserviceName字段必须和无头服务的名字一致,这是 StatefulSet 能拿到稳定域名的前提。replicas: 3是常规集群规模,小于 3 个节点时 Raft 选举容易脑裂,生产环境不建议 1 个或 2 个节点跑集群模式。
反亲和配置用preferredDuringSchedulingIgnoredDuringExecution,它不会硬性阻止调度,只是尽量把三个 Pod 分散到不同宿主机上。如果你们节点数很少,硬反亲和可能导致 Pod 一直在 Pending,所以这里用软策略更稳妥。
存储部分我用volumeClaimTemplates而不是固定 PVC,这是 StatefulSet 的标准用法。每个 Pod 会自动生成一块独立的 PVC,名字形如nacos-data-nacos-0、nacos-log-nacos-0,互不干扰。data 和 log 分开持久化,日志不会挤占数据盘空间。
3.3 环境变量逐个解析:NACOS_SERVERS、数据库连接、JVM 参数
环境变量是整套 YAML 里最容易写错也最值得逐条说明的地方。我按功能分组讲清楚。
MODE 和 PREFER_HOST_MODE 这组是集群模式的开关。MODE=cluster让 Nacos 以集群方式启动,而不是单机 standalone。PREFER_HOST_MODE=hostname告诉 Nacos 节点之间用主机名互相识别,而不是拿 IP。在 K8s 环境里,Pod 的 IP 不稳定,主机名却是固定的,这能省掉很多集群状态异常的问题。
NACOS_REPLICAS 和 NACOS_SERVERS 是节点发现的核心。官方镜像启动脚本会根据 NACOS_REPLICAS 计算集群节点数,结合 NACOS_SERVERS 里列出的地址完成节点初始化。这里有个细节:NACOS_SERVERS 里三个地址要写完整 FQDN,端口必须带 8848。顺序随意,但三个都要在,少一个节点就起不来集群。
数据库连接参数注意SPRING_DATASOURCE_PLATFORM=mysql,这个值告诉 Nacos 用 MySQL 而不是内置 Derby。MYSQL_SERVICE_HOST 指向的是 MySQL 服务地址,如果 MySQL 也在 K8s 里,就写它的 Service 域名。MYSQL_SERVICE_DB_NAME 要和你在 MySQL 里建的库名保持一致,并且建库的时候要先执行 Nacos 官方提供的初始化 SQL,否则启动时报表不存在。
密码这里用 secretKeyRef 从 Secret 读,不要在 YAML 里写明文。创建 Secret 的命令我放在第 4 章部署环节。如果你只是本地测试,先用明文密码也能跑,但生产环境务必走 Secret 方式。
JVM 参数 JVM_XMS / JVM_XMX / JVM_XMN 分别对应堆初始大小、堆最大值和新生代大小。镜像默认的 JVM 参数偏大,2G 起的都有,小内存节点直接 OOM。我习惯按节点规格给 1G 这个量级,配置中心这种重内存操作足够用。如果你们配置很多、服务注册量很大,可以放宽到 2G,但别超过 Pod 内存 limit。
4. 部署与验证:用 kubectl 把集群跑起来并确认健康
4.1 启动集群的最小命令集:apply、检查、日志
YAML 准备好之后,部署命令其实就三四条。先把数据库密码 Secret 建好,再 apply 整份 YAML:
kubectl create namespace nacos kubectl create secret generic nacos-db-secret -n nacos \ --from-literal=password='你的数据库密码' kubectl apply -f nacos.yaml注意 create secret 和 apply 的顺序不能反。StatefulSet 里的 Pod 启动时需要读取 Secret,如果 Secret 不存在,Pod 会一直处于 CreateContainerConfigError 状态,等半天也不见好。你可以用kubectl get events -n nacos --sort-by=.lastTimestamp看到具体的报错。
apply 完之后,先看 Pod 状态:
kubectl get pods -n nacos -o wide正常情况下,每个 Pod 会依次经历 Pending、ContainerCreating、Running 三个阶段,整个过程大概一到两分钟。如果某个 Pod 长时间停在 ContainerCreating,用kubectl describe pod nacos-0 -n nacos看事件,多半是 PVC 或镜像拉取的问题。
看 Nacos 启动日志,确认集群启动过程没有报错:
kubectl logs -f nacos-0 -n nacos日志里出现Nacos started successfully in cluster mode字样,说明这个节点启动成功。但我建议你再等十几秒,因为节点启动成功只是本机进程起来了,三个节点互相组成集群还需要一点时间。
4.2 验证集群状态:节点列表、配置写入、Leader 选举
Pod 都 Running 不代表集群真的组好了,需要验证节点之间的 Raft 选举和元数据同步。最直接的方式是 Nacos 控制台,但在集群里我不喜欢开 NodePort,而是用端口转发在本地看:
kubectl port-forward svc/nacos-server -n nacos 8848:8848先在 Service 里加一个普通的 ClusterIP 服务,名字比如 nacos-server,selector 同样指向app: nacos,端口映射 8848 和 9848。然后浏览器打开http://localhost:8848/nacos,输入默认账号密码 nacos/nacos 进入控制台,在"集群管理"里的节点列表应该能看到三个节点。
如果不想开浏览器,用 API 验证更直接:
curl -X GET "http://localhost:8848/nacos/v1/console/health/readiness"返回 200 说明就绪。接下来验证配置写入能力,往 Nacos 里写一条配置再读出来:
curl -X POST "http://localhost:8848/nacos/v1/cs/configs" \ -d "dataId=test.yaml&group=DEFAULT_GROUP&content=hello: world" curl -X GET "http://localhost:8848/nacos/v1/cs/configs?dataId=test.yaml&group=DEFAULT_GROUP"能写能读,说明数据库连接正常,配置存储没问题。最后验证 Raft 选举:把 nacos-0 杀掉,观察另外两个节点是否会自动选出新 Leader,配置读取是否不受影响:
kubectl delete pod nacos-0 -n nacosStatefulSet 会自动重建 nacos-0,新旧交替期间控制台里节点列表可能短暂显示节点数变少,但等新 Pod 启动完成加入集群后,节点数会恢复为 3。如果这时候节点数一直少一个,就说明集群节点发现有问题,去查第 5 章的坑。
4.3 客户端接入:集群地址到底该填什么
部署完集群,最后一步是让业务服务接进来。客户端填的地址不是某个节点的 IP,而是 K8s 里的 Service 域名。对于 Java 技术栈,通常配置这样一段:
spring.cloud.nacos.discovery.server-addr=nacos-server.nacos.svc.cluster.local:8848 spring.cloud.nacos.config.server-addr=nacos-server.nacos.svc.cluster.local:8848这个地址指向的 ClusterIP Service 会把请求负载均衡到三个节点上。客户端拿到响应后,会从 Nacos 返回的节点列表里解析出每个节点的真实地址,然后直接连对应节点的 9848 gRPC 端口。所以 9848 端口在 Service、NodePort 或负载均衡器里必须暴露,否则客户端能注册但后续心跳超时。
如果业务服务和 Nacos 不在同一个命名空间,域名就要写全,比如nacos-server.nacos.svc.cluster.local。在同一个命名空间里可以简写成nacos-server。我习惯在配置文件里始终写完整 FQDN,省得以后挪命名空间时还要改配置。
5. Nacos 集群部署避坑指南:五条血泪经验按顺序查
5.1 PVC Pending 导致 Pod 卡在 ContainerCreating
现象:三个 Pod 全部停在 ContainerCreating,kubectl get pvc -n nacos看到 NAME 以nacos-data-nacos-开头的 PVC 状态是 Pending,kubectl describe pod nacos-0里最后一行写着waiting for PVC ... to be created。
原因:集群里没有可用的 StorageClass,或者你在 volumeClaimTemplates 里写的 storageClassName 和集群里实际存在的对不上。还有一种情况是存储插件装好了,但没把它设为默认存储类,StatefulSet 又没显式指定存储类,动态供给就不知道该用哪个。
解决:先执行kubectl get storageclass看集群有哪些存储类,确认有(default)标记的那条。如果 StatefulSet 里写了 storageClassName,删掉让它走默认;如果集群根本没有存储类,需要先部署存储插件,或者手动创建 PV 再创建同名 PVC。注意 PVC 名字要和 StatefulSet 生成的完全一致,比如nacos-data-nacos-0,一个 Pod 对应一块 PVC。
5.2 节点之间互相注册不上:NACOS_SERVERS 和域名解析的坑
现象:每个节点都启动成功了,但控制台节点列表只有一个节点,日志里反复出现raft相关报错或者unable to connect to peer。
原因:NACOS_SERVERS 里域名拼写错误,或者无头服务的 selector 没匹配上 Pod,导致域名解析不到。另一个常见原因是把域名写成了nacos-0.nacos-headless这种简写,但命名空间不是默认的default,解析就失败了。
解决:先在 Pod 里测试域名解析:
kubectl exec -it nacos-0 -n nacos -- sh -c "ping nacos-1.nacos-headless.nacos.svc.cluster.local"能解析出 IP 再谈下一步。如果 ping 不通,检查无头服务是否存在、selector 是否命中。如果域名没问题,再看 Pod 之间 8848 端口通不通,可以用curl试试访问对端节点的/nacos/路径,确认网络没被 NetworkPolicy 拦。
5.3 内存 OOM 导致节点反复重启
现象:kubectl get pods -n nacos看到 Pod 好一阵又重启,kubectl describe pod里 Last State 是 OOMKilled。
原因:Nacos 镜像默认 JVM 堆设置偏大,常见的是-Xms2g -Xmx2g,而你的节点内存可能只有 4G,还跑着其他服务,K8s 的 Pod limit 一压就 OOM。另外 2.x 版本的元数据、健康检查在集群模式下会多占一些堆外内存。
解决:显式设置 JVM 参数。在环境变量里加 JVM_XMS=1g、JVM_XMX=1g、JVM_XMN=512m,然后重启 Pod。如果配置中心管理的配置量特别大,也可以把堆给到 2G,但要同步调大 Pod 的 resources.limits.memory,比如给 4Gi。改完再去kubectl logs确认启动日志里 JVM 参数生效。
5.4 只开了 8848 端口,客户端连不上 9848
现象:业务服务能注册成功,但一段时间后日志里出现gRPC request timeout、Client not connected之类的报错,注册列表里的实例频繁上下线。
原因:客户端通过 8848 拿到 Nacos 节点列表后,会自动连接每个节点的 9848 端口做 gRPC 长连接。你只暴露了 8848,9848 被防火墙或 Service 挡住了,长连接自然建不起来。
解决:检查 Service 里是否加了 9848 端口映射,NodePort 或负载均衡器是否也放行了 9848。如果是云环境,安全组规则里也要加这条。这里给一个自查命令:
kubectl exec -it nacos-0 -n nacos -- sh -c "curl -s http://localhost:9848"服务端对 gRPC 请求会返回一些响应或至少不是连接被拒。如果连接直接拒绝,说明配置没生效,回去改 Service。这个坑我踩过一次之后,已经养成了习惯:所有 Nacos 相关的 Service 定义里,端口永远是 8848 和 9848 成对出现。
5.5 版本跨度大升级后节点无法加入集群
现象:从 1.4.x 直接升到 2.3.x,旧节点还好,新节点日志里一直报元数据不一致或者节点无法加入集群。
原因:Nacos 1.x 和 2.x 的集群元数据格式、节点间通信协议都变了,跨大版本升级不是换镜像那么简单。如果数据库里的配置数据还带着 1.x 的格式,新版本节点启动时可能无法正确加载。
解决:有大版本升级需求时,先备份数据库和本地 data 目录,然后按官方升级路径一步步走:1.x 先升到 2.x 早期的稳定版本,确认数据正常后再升到最新的 2.3.x。升级前先在 staging 环境完整跑一遍部署和验证流程,尤其是第 4 章讲的配置写入和读出测试,没问题再动生产。
6. 最后:把 YAML 收进一个脚本,让集群可以反复重建
部署到这个程度,我通常会把整个流程收进一个脚本,方便测试环境反复重建集群,也方便以后扩容。脚本的核心思路是:把变量从 YAML 里抽出来,用环境变量注入,最后统一 apply。
#!/usr/bin/env bash # deploy-nacos.sh 用法: ./deploy-nacos.sh <namespace> <mysql-host> <mysql-password> set -euo pipefail NAMESPACE="${1:-nacos}" MYSQL_HOST="${2:?请传入 MySQL 地址}" MYSQL_PASSWORD="${3:?请传入 MySQL 密码}" IMAGE_TAG="${NACOS_IMAGE_TAG:-v2.3.2}" export NAMESPACE MYSQL_HOST MYSQL_PASSWORD IMAGE_TAG kubectl create namespace "${NAMESPACE}" --dry-run=client -o yaml | kubectl apply -f - kubectl create secret generic nacos-db-secret -n "${NAMESPACE}" \ --from-literal=password="${MYSQL_PASSWORD}" \ --dry-run=client -o yaml | kubectl apply -f - # 用 envsubst 渲染 YAML 模板,再 apply envsubst < nacos.yaml.tpl | kubectl apply -f - # 等待 StatefulSet 完成滚动 kubectl rollout status sts/nacos -n "${NAMESPACE}" --timeout=300s echo "Nacos 集群部署完成。"脚本里envsubst会把模板中的$NAMESPACE、$MYSQL_HOST等变量替换成实际值。你需要把第 3 章的 YAML 复制一份保存为nacos.yaml.tpl,把命名空间、数据库地址、镜像 tag 换成变量占位符。这样一套配置就能在 dev、staging、prod 任意环境复用,不会再出现"这个环境改了没同步到那个环境"的问题。
还有一件重要的事:删除集群时要注意顺序。我习惯先删 StatefulSet,再删 PVC,最后删 Service:
kubectl delete sts nacos -n nacos --cascade=orphan kubectl delete pvc -n nacos -l app=nacos kubectl delete svc -n nacos nacos-headless nacos-server--cascade=orphan会让 StatefulSet 删除时保留 PVC 数据,这样万一只是临时想停掉集群,数据还在,重新 apply 一份 YAML 就能原地复活。如果确定数据不要了,再单独删 PVC。这个习惯帮我避免过很多次误删数据的事故。
这套方案我前后在几个测试环境里重建过不下十次,最耗时的永远是数据库初始化那一步,其余环节从 apply 到三节点全部就绪基本控制在三分钟内。如果你部署时遇到控制台只有单节点、客户端连不上、Pod 反复重启,按第 5 章的顺序逐条排查,大部分问题都能定位到。希望帮到你。
本文还有配套的精品资源,点击获取