简介:在Kubernetes中部署Nacos集群,常因StatefulSet、Headless Service、Ingress等配置繁多而增加部署门槛,尤其对微服务架构中的服务发现与配置管理场景,更需快速落地。这份脚本包将流程整理为按执行顺序编排的5个YAML文件:先初始化数据库,再创建Headless Service与StatefulSet,随后暴露Service,最后通过Ingress提供外部访问,同时附带readme说明关键步骤,把复杂配置拆解成可直接套用的极简命令,即使新手也能逐条操作。资源整体仅3KB,共6个文件,全部为YAML与TXT文本格式,便于阅读、修改和纳入Git管理,适合需要快速在K8s环境落地Nacos的运维、开发或架构师参考。目前已有764人学习下载,对于希望规避踩坑、快速搭建高可用注册中心的团队来说,这份轻量级部署脚本能显著缩短排障时间,是一份低门槛、高性价比的实用工具。
1. 在 K8s 上用 YAML 脚本部署 Nacos 集群:为什么很多人的集群活不过三天
先把话说在前面:K8s 上部署 Nacos 集群,真正的难点不是把 Pod 拉起来,而是让三个节点的身份在整个生命周期里保持稳定。我见过太多次这样的现场——用 Deployment 把 Nacos 跑起来,三个副本看着都 READY,第二天某个节点重启,Pod 名字变了,IP 也变了,三个节点互相找不到对方,控制台上每个节点都只显示自己,服务注册心跳开始报错,最后只能人肉重建整个集群。这个问题的根子在于 Nacos 2.x 的集群是基于 Raft 协议的,节点之间要靠稳定的网络标识互相通信和选举,而 Deployment 恰恰不保证这一点。
这篇文章要解决的就是这个问题:用 StatefulSet + 无头 Service + 初始化脚本这套组合,把 Nacos 集群的部署固化成一个个 YAML 文件和一段可重复执行的部署脚本。你照着做,三节点集群能一次性拉起来,节点重启、Pod 迁移之后集群依然健康。适合谁?适合正在搭建测试环境、准备上生产,或者被 K8s 上 Nacos 半死不活的状态折磨过的运维和开发。读完你能拿走一套完整可改的 YAML 模板,以及部署、验证、排错和升级的完整操作路径。
2. 选型定架构:StatefulSet、无头 Service 与 Nacos 端口规划
2.1 为什么是 StatefulSet,而不是 Deployment
很多人第一次在 K8s 上部署 Nacos 时,顺手就写了 Deployment,因为大家习惯了用 Deployment 管理无状态服务。但 Nacos 集群是有状态的,状态不在数据文件里,而在节点之间的成员关系和 Raft 选举结果里。Deployment 管理的 Pod 名称是随机后缀,比如 nacos-abc12,Pod 重建之后名称会变;虽然 Deployment 也支持固定 IP 的机制,但常规用法下 Pod IP 也是新的。Nacos 节点启动后会把自己注册到集群成员表里,如果其他节点记录的地址已经失效,这个节点就永远进不了集群。
StatefulSet 解决的正是这个问题。它为每个 Pod 分配一个稳定的序号标识,比如 nacos-0、nacos-1、nacos-2,Pod 重建之后名称不变,对应的 DNS 记录也能保持稳定。Nacos 节点之间只需要通过nacos-0.nacos-headless.namespace.svc.cluster.local这种域名互相通信,无论底层 IP 怎么变,域名总能解析到当前 Pod 的真实地址。这就是 StatefulSet 在 Nacos 场景下不可替代的原因。我一般会跟团队说:注册中心这种组件,Pod 身份漂移是致命的,能用 StatefulSet 就别用 Deployment。
2.2 端口规划:8848 之外的三个隐藏端口
Nacos 2.x 的端口规划比 1.x 复杂。主端口 8848 负责 HTTP API 和控制台,这是大家都知道的。但 Nacos 2.x 同时引入了 gRPC 通信,客户端连接服务端、服务端节点之间通信都要走额外的端口。如果只暴露 8848,你会发现服务能注册,但客户端拉取订阅内容时疯狂报错。
| 端口 | 作用 | 说明 |
|---|---|---|
| 8848 | HTTP 主端口 | 控制台、配置管理、服务注册的 HTTP 接口 |
| 9848 | gRPC 客户端端口 | 客户端与服务端之间的长连接,= 8848 + 1000 |
| 9849 | gRPC 服务端端口 | 服务端节点之间同步数据,= 8848 + 1001 |
| 7848 | Raft 通信端口 | 集群节点选举与成员同步,2.x 自动启用 |
这四个端口在写 K8s Service 时都要考虑。客户端访问走 8848 和 9848,集群内部节点间通信走 9849 和 7848。无头服务需要把四个端口全部暴露,否则就算 Nacos 进程起来了,节点间握手也会失败。我在第一次部署时就只开了 8848,结果三个节点日志里反复出现 gRPC 连接失败,折腾了半个下午。
2.3 选型结论与文件清单
整体方案确定为:三节点 Nacos StatefulSet,前端配一个无头 Service 提供稳定 DNS,后端配一个单实例 MySQL 作为配置中心数据库。数据目录用 PVC 持久化。部署入口是一个 bash 脚本,按顺序 apply 所有 YAML,并做健康检查。
选 MySQL 而不是 Nacos 内置的嵌入式存储,是因为生产环境里配置数据不能随着 Pod 销毁而丢失。内置 Derby 虽然部署简单,但三节点模式下数据一致性弱,备份也不方便。MySQL 单独跑,Nacos 只把它当数据源,后续扩缩容、迁移都从容。下面的文件清单就是整套部署的骨架:
| 文件名 | 作用 |
|---|---|
| 01-namespace.yaml | 创建独立的命名空间 |
| 02-mysql.yaml | MySQL 单实例 + PVC + Service |
| 03-nacos.yaml | Nacos 三节点 StatefulSet 主体 |
| 04-service.yaml | Nacos 无头 Service |
| deploy.sh | 一键部署与健康检查脚本 |
3. 核心 YAML 拆解:Nacos 三节点与内置 MySQL 的完整配置
3.1 命名空间与 MySQL:先把数据底座立住
我先从最小的文件开始。命名空间隔离是必须的,Nacos 的配置、服务数据都在这个命名空间里,后面清理环境时kubectl delete namespace一刀切,不会误伤其他服务。
apiVersion: v1 kind: Namespace metadata: name: nacos-clusterapiVersion: apps/v1 kind: StatefulSet metadata: name: mysql-nacos namespace: nacos-cluster spec: serviceName: mysql-nacos-headless replicas: 1 selector: matchLabels: app: mysql-nacos template: metadata: labels: app: mysql-nacos spec: containers: - name: mysql image: mysql:5.7 env: - name: MYSQL_ROOT_PASSWORD value: "Nacos@123456" - name: MYSQL_DATABASE value: "nacos_config" - name: MYSQL_USER value: "nacos" - name: MYSQL_PASSWORD value: "Nacos@123456" ports: - containerPort: 3306 name: mysql volumeMounts: - name: data mountPath: /var/lib/mysql resources: requests: memory: 512Mi cpu: "0.5" limits: memory: 2Gi cpu: "2" volumeClaimTemplates: - metadata: name: data spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 20Gi --- apiVersion: v1 kind: Service metadata: name: mysql-nacos-headless namespace: nacos-cluster spec: clusterIP: None selector: app: mysql-nacos ports: - port: 3306 targetPort: 3306这段 YAML 里最关键的是volumeClaimTemplates,它为每个 Pod 动态创建一份独立的 PVC。MySQL 虽然只有一个副本,但数据必须持久化,Pod 被调度到别的节点或者重建之后,数据还能找回来。MYSQL_DATABASE指定了实例启动后自动创建名为nacos_config的库,MYSQL_USER和MYSQL_PASSWORD创建了 Nacos 专用的账号,避免直接用 root。Nacos 首次连接空库时会自动建表,所以这里不需要额外的初始化 SQL 文件。
内存和 CPU 的 requests 别写得太大。MySQL 5.7 在 512Mi 内存下能正常启动,但 Nacos 初始化建表时稍有点吃紧,所以 limits 给到 2Gi。如果你手头是 4G 内存的小集群,这个配置能跑起来且不会 OOM。
3.2 Nacos StatefulSet:集群节点与数据库的核心连接
Nacos 的 StatefulSet 是整个部署的核心。这里有几个环境变量决定了集群能不能正常组起来,逐个拆开说。
apiVersion: apps/v1 kind: StatefulSet metadata: name: nacos namespace: nacos-cluster spec: serviceName: nacos-headless replicas: 3 selector: matchLabels: app: nacos template: metadata: labels: app: nacos spec: terminationGracePeriodSeconds: 30 affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchLabels: app: nacos topologyKey: kubernetes.io/hostname containers: - name: nacos image: nacos/nacos-server:v2.2.3 ports: - containerPort: 8848 name: client - containerPort: 9848 name: grpc-client - containerPort: 9849 name: grpc-server - containerPort: 7848 name: raft env: - name: NACOS_SERVERS value: >- nacos-0.nacos-headless.nacos-cluster.svc.cluster.local:8848, nacos-1.nacos-headless.nacos-cluster.svc.cluster.local:8848, nacos-2.nacos-headless.nacos-cluster.svc.cluster.local:8848 - name: NACOS_APPLICATION_PORT value: "8848" - name: NACOS_SERVER_PORT value: "8848" - name: NACOS_SERVER_IP valueFrom: fieldRef: fieldPath: status.podIP - name: SPRING_DATASOURCE_PLATFORM value: mysql - name: MYSQL_SERVICE_HOST value: mysql-nacos-headless.nacos-cluster.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 value: "Nacos@123456" - name: MYSQL_SERVICE_DB_PARAM value: >- characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true - name: JVM_XMS value: "512m" - name: JVM_XMX value: "512m" - name: JVM_XMN value: "256m" - name: NACOS_AUTH_ENABLE value: "true" volumeMounts: - name: logs mountPath: /home/nacos/logs volumeClaimTemplates: - metadata: name: logs spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 5GiNACOS_SERVERS是集群组网的关键,格式是主机名:8848的逗号列表。这里不能用 Pod IP,原因前面说过:IP 会变。三个节点的 DNS 域名由后面的无头 Service 提供,域名里的序号nacos-0、nacos-1、nacos-2就是 StatefulSet 的 Pod 名称,稳定不变。
NACOS_SERVER_IP我是用fieldRef从 Pod 状态里取的。这个变量在 Pod 启动时写入当前 IP,Nacos 会用它做节点间的通信绑定。有些部署方案会省略这个变量,但我在多节点环境里发现不写它会导致节点间地址判断异常,所以显式注入更稳。
数据库连接串里的参数connectTimeout=1000&socketTimeout=3000是我调过的值。默认超时太长,MySQL 短暂不可用时 Nacos 会卡住很久,调短之后故障恢复快很多。allowPublicKeyRetrieval=true是 MySQL 8.0 客户端连 5.7 时的兼容参数,如果镜像换成 8.0 也要保留。
podAntiAffinity这一段是为了让三个 Nacos 节点尽量分散到不同的宿主机。注册中心这种关键组件,如果三个节点都挤在同一台机器上,宿主机一挂整个集群全灭。preferred 是软性调度,集群节点不够时还是会调度到一起,但至少调度器会优先分散。
JVM 参数我压到了 512m,这是踩过坑之后改的。Nacos 官方默认 JVM 堆很大,在 2G 内存的容器里直接 OOM。配置中心场景下 512m 堆跑三节点完全够用,控制台和 API 的响应速度不受影响。
3.3 无头 Service 与参数速查:给每个 Pod 一个稳定门牌号
apiVersion: v1 kind: Service metadata: name: nacos-headless namespace: nacos-cluster spec: clusterIP: None selector: app: nacos ports: - name: client port: 8848 targetPort: 8848 - name: grpc-client port: 9848 targetPort: 9848 - name: grpc-server port: 9849 targetPort: 9849 - name: raft port: 7848 targetPort: 7848无头 Service 的clusterIP: None让 Service 不再提供虚拟 IP 负载均衡,而是为每个 Pod 生成独立的 DNS A 记录。K8s 的 DNS 解析nacos-0.nacos-headless.nacos-cluster.svc.cluster.local时,返回的是对应 Pod 的 IP,这是 StatefulSet 与无头 Service 配合的标准模式。四个端口都声明出来,这样集群内部的 gRPC 和 Raft 通信才能走通。
| 环境变量 | 必填 | 说明 |
|---|---|---|
| NACOS_SERVERS | 是 | 集群节点列表,格式为域名:8848 |
| NACOS_APPLICATION_PORT | 是 | HTTP 主端口,默认 8848 |
| NACOS_SERVER_PORT | 是 | 服务端端口,gRPC 端口由此推算 |
| SPRING_DATASOURCE_PLATFORM | 是 | 数据源类型,mysql 或 embedded |
| MYSQL_SERVICE_HOST | 是 | MySQL 地址,用 Service 域名 |
| MYSQL_SERVICE_DB_PARAM | 否 | 连接参数,建议按需调超时 |
| JVM_XMS / JVM_XMX | 否 | 堆内存上下限,小内存机器必改 |
写到这里,YAML 部分就齐了。下一步把它们串成一个可重复执行的脚本,这也是标题里“脚本”二字的落点。
4. 一键部署脚本:apply、等待就绪与集群健康检查
4.1 从 apply 到跑起来:deploy.sh 怎么写
YAML 文件准备好之后,手动一条条 apply 也能部署成功,但重复操作容易漏文件。我把整个流程固化成脚本,每次部署都是确定性行为。脚本里加了set -e,任何一步失败立即退出,避免半截部署还以为是成功的。
#!/usr/bin/env bash set -e NAMESPACE="nacos-cluster" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" kubectl apply -f "${SCRIPT_DIR}/01-namespace.yaml" kubectl apply -f "${SCRIPT_DIR}/02-mysql.yaml" kubectl apply -f "${SCRIPT_DIR}/04-service.yaml" kubectl apply -f "${SCRIPT_DIR}/03-nacos.yaml" echo "等待 Nacos 集群就绪..." for i in $(seq 1 60); do READY=$(kubectl get statefulset nacos -n "${NAMESPACE}" \ -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "0") if [ "${READY}" = "3" ]; then echo "Nacos 三个节点均已就绪,耗时 ${i}0 秒左右" break fi if [ "${i}" -eq 60 ]; then echo "等待超时,请检查:kubectl describe pod -n ${NAMESPACE}" exit 1 fi sleep 10 done kubectl get pods -n "${NAMESPACE}" -o wide脚本的逻辑很简单:先 apply 四个 YAML,然后轮询 StatefulSet 的readyReplicas字段。这里有个关键点,readyReplicas不等于replicas,只有 Pod 真正通过就绪检查才会计入。轮询间隔设 10 秒,最多等 10 分钟,超时输出排查提示。
为什么04-service.yaml要在03-nacos.yaml之前 apply?因为 Nacos Pod 启动时就要解析nacos-0.nacos-headless这个域名来做集群初始化,Service 不存在时 DNS 解析会失败,虽然之后 Service 创建了也能恢复,但日志里会多出一堆连接错误,干扰判断。先建 Service,让 DNS 记录先于 Pod 存在,启动日志干净很多。
4.2 怎么确认集群是真的健康:检查节点列表与日志特征
Pod 全部 Running 不代表 Nacos 集群组起来了。最常见的情况是三个进程各自活着,但互相不认账。这个时候要进 Pod 看集群状态。
# 查看所有节点视角的集群成员列表 for pod in nacos-0 nacos-1 nacos-2; do echo "========== ${pod} ==========" kubectl exec -n nacos-cluster "${pod}" -- \ curl -s "http://127.0.0.1:8848/nacos/v1/ns/operator/servers" echo done这个请求是 Nacos 2.x 的运维接口,会返回当前节点视角下的集群成员列表。如果三个节点返回的内容都是三行地址,说明 Raft 组网正常;如果每个节点只返回自己,说明集群没组起来,按第 5 章的排查路径去查。
日志特征也很明显。集群组网成功时,启动日志里会出现 Raft 相关的选举记录,最后一行通常是Nacos started successfully。单节点模式不会有选举日志。我第一次部署时看到三份started successfully日志就以为成功了,结果控制台里集群节点列表只有自己,后来才学会用上面的接口做校验。这个习惯一直留到现在。
4.3 脚本的收尾工作:连接验证与控制台访问
部署完成之后,还需要验证客户端视角的可用性。我会在脚本最后加一段端口转发和健康检查,确认从外部能访问到 Nacos 的控制台和 API。
# 验证 8848 端口健康检查接口 kubectl exec -n nacos-cluster nacos-0 -- \ curl -s "http://127.0.0.1:8848/nacos/v1/console/health/readiness" # 查看 MySQL 中自动建的表,确认数据源连接正常 kubectl exec -n nacos-cluster mysql-nacos-0 -- \ mysql -unacos -p"Nacos@123456" -h127.0.0.1 nacos_config \ -e "show tables;" 2>/dev/null | head -20健康检查接口返回OK说明 Nacos 主进程正常,但还要确认数据源。如果show tables能列出config_info、service_info这些表,说明 Nacos 已经成功连上 MySQL 并完成了初始化建表。这一步能提前暴露数据库连接问题,否则等客户端开始读写时才报错,排查面就大了。
想从本地访问控制台,可以用kubectl port-forward -n nacos-cluster svc/nacos-headless 8848:8848,然后浏览器打开http://localhost:8848/nacos。默认账号密码是nacos/nacos,如果开启了NACOS_AUTH_ENABLE,首次登录会要求改密码。
5. Nacos 部署避坑记录:四个最容易搞崩集群的低级错误
5.1 现象:Pod 反复重启,日志刷屏 jdbc connect error
我在一次模拟环境部署时,Nacos 容器启动后几秒钟就退出,kubectl logs里全是jdbc connect error。原因有两个层次:一是 MySQL 还没就绪 Nacos 就启动了,二是数据库连接串里的参数写错了导致鉴权失败。MySQL 首次启动要初始化数据目录,通常需要 30 到 60 秒,而 Nacos 启动只需要十几秒,两个进程同时拉起时必然撞车。
解决方法是调整 apply 顺序或者加等待。我一般会让 MySQL 的就绪检查先通过再启 Nacos,但 YAML 里不直接支持依赖关系,所以靠脚本控制:先单独 apply MySQL,确认mysql-nacos-0的READY变成 1 后再 apply Nacos。另外确认连接串里的账号密码和02-mysql.yaml里MYSQL_USER、MYSQL_PASSWORD完全一致,大小写敏感。
5.2 现象:三个节点各自显示自己是唯一的集群成员
这是最迷惑人的坑。三个 Pod 都 Running,控制台都能打开,但每个节点看到的集群成员都只有自己。检查NACOS_SERVERS环境变量,值看起来也对。最后排查发现是NACOS_SERVERS里写的是 Pod IP,比如192.168.1.10:8848,Pod 重启之后 IP 变了,其他节点拿着旧 IP 找不到人。
解决方法是全部改用 DNS 域名,也就是第 3 章里那种nacos-0.nacos-headless.nacos-cluster.svc.cluster.local格式。顺便强调一下:不要用status.podIP去拼NACOS_SERVERS,这个变量拿来绑定本机通信可以,拿来组集群就是给自己埋雷。我在部署脚本里把这个环境变量写死成域名列表,杜绝人为手误。
5.3 现象:客户端报错 9848 端口连接失败,服务注册不上
服务端明明在运行,客户端却报connection to server 9848 failed。原因是我在 Service 里只暴露了 8848 端口,Nacos 2.x 的客户端会自动尝试连接主端口加 1000 的 gRPC 端口,也就是 9848。Service 没暴露这个端口时,K8s 网络层直接拒绝连接。
解决方式就是第 3 章无头 Service 那样,把 9848、9849、7848 全部声明在 Service 里。还有一个隐蔽点:如果客户端在不同命名空间,还要确认网络策略允许跨命名空间访问。我排查这类问题时习惯先在 Pod 里telnet <nacos-address> 9848,连通性一目了然。
5.4 现象:小内存机器上三节点全部 OOMKilled
我用一台 4G 内存的测试机跑三节点,结果三个 Pod 相继进入 CrashLoopBackOff,kubectl describe pod显示OOMKilled。原因很直接:Nacos 官方镜像默认堆内存超过 1G,三个节点加 MySQL 直接把机器内存打爆。
解决方法是显式设置JVM_XMS、JVM_XMX和JVM_XMN。我把堆压到 512m,新生代 256m,三节点总内存占用控制在 2G 左右,加 MySQL 的 1G,整机还剩 1G 余量。配置中心场景不比注册中心的高并发写入,512m 堆足够。如果你那边配置量大,可以按 1G 堆重新评估,但一定要同步调大内存 limits,否则容器还是会 OOM。
6. 升级与参数调优:滚动更新怎么做才不打断注册中心
Nacos 集群一旦跑起来,后续的升级不能像无状态服务那样直接kubectl rolling restart。Nacos 的节点重启意味着 Raft 成员短暂离线,如果同时重启多个节点,可能导致选举失败。我的升级流程是严格逐个操作:先确认集群健康,再改镜像版本号,观察第一个节点恢复并重新加入集群,再继续下一个。
# 确认当前集群状态 kubectl get pods -n nacos-cluster -l app=nacos # 逐个节点升级,先升第 0 个节点 kubectl set image statefulset/nacos \ nacos=nacos/nacos-server:v2.2.3 \ -n nacos-cluster # 等待第 0 个节点重新 Ready kubectl rollout status statefulset/nacos -n nacos-cluster # 检查该节点视角的集群成员 kubectl exec -n nacos-cluster nacos-0 -- \ curl -s "http://127.0.0.1:8848/nacos/v1/ns/operator/servers"kubectl set image会触发 StatefulSet 从序号最大的 Pod 开始逐个重建,默认策略下不会像 Deployment 那样同时滚动所有副本。但 Nacos 对节点同时离线的容忍度很低,所以升级前最好手动把partition设到 3,手动控制升级节奏:
spec: updateStrategy: type: RollingUpdate rollingUpdate: partition: 3partition: 3表示只有序号大于等于 3 的 Pod 才会被更新,当前三个 Pod 全部不动。要升级时把 partition 改成 2,只重建 nacos-2;它恢复后改成 1,重建 nacos-1;最后改成 0,重建 nacos-0。这样每个时刻最多只有一个节点在重启,Raft 集群始终保持多数派在线,不会触发重新选举导致短暂不可用。
升级完还有一些参数值得回头调一调。第一个是NACOS_AUTH_ENABLE,测试环境可以不开启,生产必须开,而且要用强 token,否则控制台裸奔在内网也很危险。第二个是配置中心的容量,MySQL 的 PVC 建议按月监控使用率,配置数量增长比想象中快。第三个是日志量,Nacos 的日志文件增长很快,我在volumeClaimTemplates里给 logs 单独申请了 5Gi,如果日志盘满了节点会异常退出,建议在部署时就把日志轮转的配置一起挂进去。
最后说一个我的个人习惯:每次部署或升级完,都保留一份当时的 YAML 快照,标注日期和镜像版本。Nacos 集群出问题时,我从来不先看控制台,而是先对比当前运行的 YAML 和上一个稳定版本的 diff。很多“玄学”问题,其实都是某次手改环境变量留下的后遗症。一套能重复部署的脚本和一份干净的 YAML 版本记录,比任何排错技巧都管用。希望这篇笔记里的 YAML 和脚本能帮你少走一段弯路。
本文还有配套的精品资源,点击获取