K8s 部署 Nacos 2.x 集群:StatefulSet 与端口避坑 简介面向 Kubernetes 环境下需要快速部署 Nacos 集群的开发与运维人员这套脚本把数据库连接配置、Headless Service、StatefulSet、普通 Service 以及 Ingress 暴露等环节按执行顺序拆成独立清单降低多组件编排和参数关联的理解门槛适合初学者按序操作也便于有经验者作为极简模板复用。压缩包共 6 个文件以 5 个 YAML 清单和 1 个 txt 说明为主整体约 3KB目录结构简单清晰便于直接纳入 Git 仓库或实验目录管理。已有 766 人学习下载说明其在 K8s 部署 Nacos 场景中具有一定参考价值。读者可获得一套结构清晰的部署脚本借助分步清单理解 Nacos 集群在 K8s 中的服务发现、状态化部署、数据库连接与外部访问设置并通过说明文件核对执行顺序减少从零编写 YAML 时的试错成本适合用于学习验证或小型环境快速落地。1. 三个 Pod 全绿集群列表却只有一台Nacos 2.x 在 K8s 上最容易装成单机上个月帮一位同学排查一套 k8s nacos 集群部署脚本三个副本的 StatefulSet 全部 Runningreadiness 探针全绿进控制台一看「集群管理」只有他自己一个节点。YAML 语法没毛病问题出在 Service 只放通了 88489848 和 9849 两个 gRPC 端口没有暴露节点之间互相看不见等于花钱起了三台单机。这类部署问题并不少见Nacos 在 K8s 上并不是“一个 Deployment 配一个官方镜像”就完事外置数据库、Headless Service、端口偏移三样错任何一样装完就是坏的。这套 YAML 我按单文件拆开记录从预检到验收走一遍。2. 确定版本再动手Nacos 2.3 镜像、命名空间与存储类的装前预检先说结论这套清单我按 Nacos 2.x 的服务端写镜像固定在nacos/nacos-server:v2.3.2换成你们内网仓库的 tag 也行但大版本别乱跳。版本没选对后面所有配置都白搭因为 1.x 和 2.x 的端口模型完全不同。2.1 版本对照为什么不能拿 1.x 的 YAML 直接跑 2.xNacos 1.x 集群只认 8848节点之间靠 7848 做 raft 数据同步到了 2.x 引入 gRPC 长连接端口一下子变成三个8848 做主端口和 HTTP 接口9848 是客户端 gRPC 端口884810009849 是服务端之间 gRPC 端口88481001。这个偏移量是按主端口算出来的官方文档只提了一句很多人配置完成后就忘了结果就是 Pod 全部 Running客户端却报连不上。组件版本/镜像关键约束说明Nacos 服务端nacos/nacos-server:v2.3.2需 K8s 1.19StatefulSet PVC 三个端口Nacos 客户端2.x SDK必须能连 98481.x 客户端走 8848 长轮询仍兼容MySQLmysql:8.0.34utf8mb4 外置库集群模式不支持内嵌 derbyJDK镜像内置JVM_XMS/XMX 可调默认堆偏大小规格节点会 OOM判断标准很简单如果你的客户端是 2.x SDK那 9848 不通就是连不上没有任何降级空间如果是 1.x 老客户端走 8848 长轮询反而能连上这会让人误以为端口没问题等新客户端一上就暴雷。提示镜像 tag 千万别用latest。Nacos 2.x 内部迭代过快latest拉到的可能是 2.4 也可能是 2.2配置项spring.datasource.platform和spring.sql.init.platform就是在这中间改的名。2.2 命名空间、StorageClass 和资源余量三件预检装之前我一般先用三条命令确认基础环境能省掉后面一半的排查时间。# 1. 建独立命名空间别跟业务挤在 default kubectl create namespace nacos # 2. 确认有默认可用的 StorageClass否则 PVC 会一直 Pending kubectl get sc kubectl get sc standard -o jsonpath{.metadata.annotations.storageclass\.kubernetes\.io/is-default-class} # 3. 看节点剩余资源Nacos 三副本 MySQL 至少预留 6C8G kubectl describe nodes | grep -A 5 Allocated resources存储类这一条最容易出问题。有状态服务必须使用 PVC 挂载/home/nacos/data很多自建集群根本没有默认 StorageClassapply 完看着 YAML 全部创建成功kubectl get pvc全是 PendingPod 卡在 ContainerCreating 不动。没有默认存储类就显式指定比如storageClassName: nfs-client别指望它自己找。资源余量也要看。Nacos 默认堆大小在容器里是按宿主机内存算的节点内存大、容器 limit 小JVM 会按大的申请然后被 kubelet OOMKilled重启循环。后面第 4 章的JVM_XMS/JVM_XMX就是为这个准备的。2.3 清单文件怎么拆、按什么顺序 apply我不喜欢把几百行塞进一个 all-in-one 文件出问题没法单独回滚。这套 YAML 按依赖顺序拆成七份文件内容依赖顺序00-namespace.yaml命名空间101-mysql-secret.yaml数据库账号密码202-mysql-statefulset.yamlMySQL 实例 内部 Service303-nacos-configmap.yaml数据源与鉴权配置404-nacos-headless-svc.yaml无头 Service含 9848/9849505-nacos-svc.yaml对外 ClusterIP/NodePort606-nacos-statefulset.yamlNacos 三副本7顺序不能乱先命名空间再 Secret先 MySQL 再 Nacos先 Service 再 StatefulSet。Service 必须比 StatefulSet 先建因为 StatefulSet 启动时要用nacos-headless这个域名解析同伴地址域名解析不出来三个节点就各起各的。# 一次性 apply 整个目录kubectl 会按文件名排序 kubectl apply -f ./nacos-k8s/ -n nacos # 想单步来就这样 kubectl apply -f 00-namespace.yaml kubectl apply -f 01-mysql-secret.yaml # ...kubectl apply -f 目录是按文件名字典序执行的这就是我把文件统一加数字前缀的原因纯靠命名控制顺序比写 shell 脚本可靠。3. 先确保外置库可用MySQL 8 建库、导 mysql-schema.sql 与数据源参数落地Nacos 集群模式只认外置数据库内嵌 derby 是给单机 demo 用的。这一步没有捷径必须先把库跑起来、表建好、账号授权到位Nacos 才可能起来。3.1 起一个最小可用的 MySQL StatefulSet生产环境大概率用现成的 RDS跳过这段如果是自建集群或者本地验证环境一个单副本 StatefulSet 足够。apiVersion: v1 kind: Secret metadata: name: nacos-mysql-secret namespace: nacos type: Opaque stringData: MYSQL_ROOT_PASSWORD: Nacos2024 MYSQL_DATABASE: nacos_config MYSQL_USER: nacos MYSQL_PASSWORD: Nacos2024 --- apiVersion: apps/v1 kind: StatefulSet metadata: name: nacos-mysql namespace: nacos spec: serviceName: nacos-mysql replicas: 1 selector: matchLabels: app: nacos-mysql template: metadata: labels: app: nacos-mysql spec: containers: - name: mysql image: mysql:8.0.34 args: # 字符集必须是 utf8mb4Nacos 配置里可能存中文和 emoji - --character-set-serverutf8mb4 - --collation-serverutf8mb4_unicode_ci # 8.0 默认 caching_sha2老驱动连不上降级成 native - --default-authentication-pluginmysql_native_password envFrom: - secretRef: name: nacos-mysql-secret ports: - containerPort: 3306 name: mysql volumeMounts: - name: data mountPath: /var/lib/mysql volumeClaimTemplates: - metadata: name: data spec: accessModes: [ReadWriteOnce] storageClassName: standard resources: requests: storage: 20Gi这份清单有三个参数值得说。--default-authentication-pluginmysql_native_password是为了兼容MySQL 8 默认用caching_sha2_password部分 JDBC 驱动版本协商失败会报Public Key Retrieval is not allowed加了这个参数一劳永逸连接串里再补allowPublicKeyRetrievaltrue双保险。utf8mb4是硬要求配置中心存的内容五花八门utf8遇到四字节字符直接报错。volumeClaimTemplates而不是 hostPath是因为 MySQL 数据目录对 IO 有要求跨节点漂移后 hostPath 上的数据就丢了。配套的内部 Service 只有一行要注意clusterIP: None不需要普通 ClusterIP 就够Nacos 只通过nacos-mysql.nacos.svc.cluster.local:3306连它。注意本地验证用 Secret 明文写密码可以接受真上生产请接 External Secrets 或者直接上云托管库别把密码提交进 Git。3.2 从镜像里取出建表脚本并导入Nacos 的建表 SQL 就在镜像的conf目录里2.x 叫mysql-schema.sql1.x 叫nacos-mysql.sql名字不一样别拿错版本。标准做法是起一个临时 Pod 把文件拷出来# 起一个只用来拷文件的临时 Pod kubectl -n nacos run nacos-src --imagenacos/nacos-server:v2.3.2 \ --restartNever --command -- sleep 3600 kubectl -n nacos wait --forconditionReady pod/nacos-src --timeout180s # 把建表脚本抠到本地 kubectl -n nacos cp nacos-src:/home/nacos/conf/mysql-schema.sql ./mysql-schema.sql # 用完就删别留着占资源 kubectl -n nacos delete pod nacos-src拿到 SQL 之后确认一下 MySQL 是否已经就绪再导入# 等 MySQL Pod Ready kubectl -n nacos wait --forconditionReady pod/nacos-mysql-0 --timeout300s # 导入建表脚本注意是 exec -i少了 -i 标准输入进不去 kubectl -n nacos exec -i nacos-mysql-0 -- \ mysql -uroot -pNacos2024 nacos_config mysql-schema.sql # 校验2.x 应该能看到 config_info、his_config_info 等表 kubectl -n nacos exec -i nacos-mysql-0 -- \ mysql -uroot -pNacos2024 -e USE nacos_config; SHOW TABLES;exec -i里的-i是保 stdin很多人写成exec -it遇到重定向依然失败因为-t分配 TTY 会干扰管道。导入成功后再补一次授权把nacos用户对nacos_config库的权限落到%上跨 Pod 访问才不会被拒CREATE USER IF NOT EXISTS nacos% IDENTIFIED BY Nacos2024; GRANT ALL PRIVILEGES ON nacos_config.* TO nacos%; FLUSH PRIVILEGES;3.3 数据源参数怎么传给 Nacos环境变量还是 ConfigMap官方镜像支持两套方式我一般优先用环境变量因为改动小、易回滚ConfigMap 只在需要覆盖镜像不认的键时才动。环境变量这套很直白SPRING_DATASOURCE_PLATFORMmysql打开外置库MYSQL_SERVICE_HOST、MYSQL_SERVICE_PORT、MYSQL_SERVICE_DB_NAME、MYSQL_SERVICE_USER、MYSQL_SERVICE_PASSWORD拼出连接串MYSQL_SERVICE_DB_PARAM追加 JDBC 参数。好处是镜像启动脚本会自动拼application.properties你不用管格式。ConfigMap 这套适合需要写额外配置项的场景比如关掉控制台、改 token 有效期apiVersion: v1 kind: ConfigMap metadata: name: nacos-custom-config namespace: nacos data: application.properties: | server.servlet.contextPath/nacos server.port8848 # 2.2 之前叫 spring.datasource.platform之后改成 spring.sql.init.platform spring.sql.init.platformmysql db.num1 db.url.0jdbc:mysql://nacos-mysql.nacos.svc.cluster.local:3306/nacos_config?characterEncodingutf8connectTimeout1000socketTimeout3000autoReconnecttrueuseUnicodetrueuseSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue db.user.0nacos db.password.0Nacos2024 nacos.core.auth.enabledtrue这里有个关键取舍ConfigMap 一旦用 subPath 挂到/home/nacos/conf/application.properties就会完整覆盖镜像自带的那份文件镜像里其他默认键全部失效。所以走这条路就要把数据源、端口、上下文路径这些必需项全写进去不能只写一行。这也是我更倾向环境变量的原因不覆盖只追加。4. 再拼集群本体StatefulSet 三副本、Headless Service 与 8848/9848/9849 端口数据库通了接下来是真正决定这套 nacos 集群能不能成集群的部分。三副本的 StatefulSet 只是外壳能不能互相发现、端口通不通全靠 Service 和几个环境变量。4.1 Headless ServicepublishNotReadyAddresses 必须为 true这是整套 YAML 里最容易被忽略、后果又最严重的一个字段。apiVersion: v1 kind: Service metadata: name: nacos-headless namespace: nacos labels: app: nacos spec: clusterIP: None # 无头服务给 StatefulSet 提供稳定 DNS publishNotReadyAddresses: true # 未就绪也写 DNS集群自举的关键 selector: app: nacos ports: - name: http port: 8848 targetPort: 8848 - name: client-grpc port: 9848 targetPort: 9848 - name: server-grpc port: 9849 targetPort: 9849clusterIP: None让它不进 kube-proxy 的负载均衡直接返回 Pod IP 列表StatefulSet 的serviceName指向它就得到nacos-0.nacos-headless.nacos.svc.cluster.local这种稳定域名。真正要命的是publishNotReadyAddresses默认情况下 Pod 没通过 readiness 探针就不写 DNS可 Nacos 集群是先要互相解析到对方才能组集群、组完集群才会 Ready这就形成死锁三个节点永远各自为战。打开这个字段未就绪也写 DNS死锁就解开了。三个端口也必须在同一个 Service 里全列出来包括 9849。有人只写 8848 和 9848节点间数据同步直接断表现是控制台能看到三个节点但配置推送时好时坏。4.2 StatefulSet 清单逐段拆env、探针、生命周期apiVersion: apps/v1 kind: StatefulSet metadata: name: nacos namespace: nacos spec: serviceName: nacos-headless replicas: 3 podManagementPolicy: Parallel # 三副本同时起别排队等 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 image: nacos/nacos-server:v2.3.2 imagePullPolicy: IfNotPresent env: - name: MODE value: cluster # 只能用完整 FQDN短名在跨命名空间时会解析失败 - 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: nacos-mysql.nacos.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: Nacos2024 - name: JVM_XMS value: 1g - name: JVM_XMX value: 1g - name: JVM_XMN value: 512m - name: NACOS_AUTH_ENABLE value: true ports: - name: http containerPort: 8848 - name: client-grpc containerPort: 9848 - name: server-grpc containerPort: 9849 readinessProbe: httpGet: path: /nacos/v1/console/health/readiness port: 8848 initialDelaySeconds: 20 periodSeconds: 5 failureThreshold: 6 livenessProbe: httpGet: path: /nacos/v1/console/health/liveness port: 8848 initialDelaySeconds: 60 periodSeconds: 10 failureThreshold: 6 lifecycle: preStop: exec: # 给 gRPC 长连接留 10 秒下线时间直接杀会有一批客户端报错 command: [/bin/sh, -c, sleep 10] resources: requests: cpu: 1 memory: 2Gi limits: cpu: 2 memory: 2Gi volumeMounts: - name: data mountPath: /home/nacos/data - name: logs mountPath: /home/nacos/logs volumeClaimTemplates: - metadata: name: data spec: accessModes: [ReadWriteOnce] storageClassName: standard resources: requests: storage: 10Gi - metadata: name: logs spec: accessModes: [ReadWriteOnce] storageClassName: standard resources: requests: storage: 10Gi几个参数值得单独讲。NACOS_SERVERS里必须写完整 FQDN写成nacos-0.nacos-headless在同一个命名空间能解析跨命名空间就歇了我吃过这个亏现在一律写全。podManagementPolicy: Parallel让三个 Pod 同时创建比默认的 OrderedReady 快两三分钟代价是日志里会有一段“找不到同伴”的重试这是正常现象几十秒后自己就收敛了。JVM_XMX必须显式给且不能超过 limit1g 堆配 2Gi limit 是留了元空间和堆外内存的余量配成一样的必被 OOMKilled。探针路径这块/nacos/v1/console/health/readiness是内部就绪判断返回 200 才算真正可用liveness走/nacos/v1/console/health/liveness。别图省事用 TCP 探针端口通不代表服务起完了会出现流量打进还没初始化的节点。日志单独挂一个卷是我个人的习惯。Nacos 的logs目录增长很快尤其是开启 debug 之后混在 data 卷里清理起来麻烦分开挂载后直接删 PVC 就行。4.3 对外暴露的三个端口和三种方式的选择apiVersion: v1 kind: Service metadata: name: nacos namespace: nacos spec: type: NodePort selector: app: nacos ports: - name: http port: 8848 targetPort: 8848 nodePort: 30848 - name: client-grpc port: 9848 targetPort: 9848 nodePort: 30849 - name: server-grpc port: 9849 targetPort: 9849 nodePort: 30850三种暴露方式的取舍跟你的客户端在哪强相关。集群内应用走 ClusterIP 最省事什么都不用改直接用nacos.nacos.svc.cluster.local:8848。集群外客户端只能走 NodePort 或者四层转发而且三个端口必须一起放出去9848 少了客户端连不上9849 少了节点间半残。用七层 Ingress 暴露 8848 看着优雅但 gRPC 走到 Ingress 上要么不支持要么得单独配最后还是要为 9848 补一条 TCP 转发规则不如一开始就用 NodePort。还有个细节nodePort手动指定比让它随机分配好因为你还要在防火墙或者安全组里放行随机端口每次重建都变运维会疯。4.4 扩容与滚动更新的注意点横向扩到三副本以上时NACOS_SERVERS是写死在 env 里的加节点就必须改这个值然后滚动重启这会导致整个集群短暂不可用。副本数超过五个我一般不用 env改成挂 ConfigMap 里的cluster.conf把一个 ConfigMap 挂到/home/nacos/conf/cluster.conf改完kubectl rollout restart触发滚动至少配置和清单是分离开的。滚动更新默认是逆序从nacos-2开始配合preStop的 10 秒等待客户端感知到的错误会少很多。但请注意Nacos 的集群成员变更不是自动的滚动过程中控制台的节点列表会短暂少人属于正常只要最后回到三台就行。5. 避坑与排查五个把 Nacos 集群装成残废的真实现场下面这五条都是我或者身边人真实翻过的车现象看起来都不一样根因基本落在端口、数据库、资源这三块。5.1 现象三个 Pod 全 Running集群列表只有一台原因Headless Service 的publishNotReadyAddresses没开或者NACOS_SERVERS里的主机名写成了短名跨命名空间解析不到。两个问题表现完全一样都是「Pod 起了但互相不认识」。解决先kubectl -n nacos exec nacos-0 -- cat /home/nacos/conf/cluster.conf看生成出来的成员列表是不是三个。再从一个 Pod 里nslookup nacos-1.nacos-headless.nacos.svc.cluster.local解析不到就是 DNS 问题回头补publishNotReadyAddresses: true能解析但列表里没有就是NACOS_SERVERS拼错了逐字比对。5.2 现象2.x 客户端启动报server is DOWN或client not connected原因9848 没通。客户端和服务端建连是靠 9848 的 gRPC 长连接8848 只负责 HTTP 和 1.x 兼容所以服务端日志看着一切正常客户端就是连不上。解决先用kubectl -n nacos exec nacos-0 -- nc -zv 127.0.0.1 9848确认容器内监听正常再从客户端侧telnet或nc目标地址的 9848。如果走的是 NodePort 或 Ingress十有八九是那一层没放行把 9848、9849 一起补上。记住端口偏移规则主端口改了多少9848 和 9849 就跟着改多少探针和 Service 的targetPort都要同步。5.3 现象启动日志刷Table nacos.config_info doesnt exist原因建表脚本没导或者导进了别的库。常见于建库和导表两步分开做MYSQL_DATABASE环境变量建了库但脚本导到了默认库。解决kubectl -n nacos exec -i nacos-mysql-0 -- mysql -uroot -pxxx -e SHOW TABLES FROM nacos_config;确认表在不在。不在就重导一遍注意命令里要显式带库名nacos_config mysql-schema.sql别依赖默认库。还有一种情况是 1.x 的nacos-mysql.sql被用到了 2.x 上表结构少字段现象类似但日志会明确报某列不存在换对应版本的脚本重导。5.4 现象Pod 反复 OOMKilled或重启后配置丢一半原因前者是 JVM 堆没限容器内存 limit 又给得小JVM 按宿主机内存申请直接被 cgroup 干掉后者是/home/nacos/data没挂 PVC用的是容器临时层重启即清零。解决kubectl -n nacos describe pod nacos-0 | grep -A 3 Last State能看到OOMKilled和退出码 137。把JVM_XMS/JVM_XMX显式设成 limit 的一半左右1g 堆配 2Gi limit 是稳的。配置丢失就看volumeClaimTemplates里有没有data这一项以及volumeMounts的路径是不是/home/nacos/data——路径差一个字数据就写进容器层了。5.5 现象PVC 一直 PendingPod 卡在 ContainerCreating原因storageClassName写了集群里不存在的名字或者集群压根没有默认存储类。解决kubectl -n nacos describe pvc># 1. 三个副本都 Ready 且重启次数为 0 kubectl -n nacos get pods -l appnacos -o wide # 期望nacos-0/1/2 全部 1/1 RunningRESTARTS 都是 0 # 2. 就绪接口返回 ok kubectl -n nacos exec nacos-0 -- \ curl -s http://127.0.0.1:8848/nacos/v1/console/health/readiness # 期望{code:0,...} 或直接 ok # 3. 集群成员是三个不是一台 kubectl -n nacos exec nacos-0 -- \ curl -s http://127.0.0.1:8848/nacos/v1/ns/operator/servers?accessToken # 期望servers 数组长度 3state 均为 UP # 4. 写一条配置再读回来验证数据库链路 kubectl -n nacos exec nacos-0 -- curl -s -X POST \ http://127.0.0.1:8848/nacos/v1/cs/configs \ -d dataIdsmoke-test.yamlgroupDEFAULT_GROUPcontentok: true kubectl -n nacos exec nacos-0 -- curl -s \ http://127.0.0.1:8848/nacos/v1/cs/configs?dataIdsmoke-test.yamlgroupDEFAULT_GROUP # 期望返回 ok: true说明读写都过了 MySQL第三步是最关键的很多人止步于前两步觉得 Pod 起来了、健康检查过了就完事其实那只能证明单个节点活着。集群成员列表里有三台且都是 UP才说明 Headless Service、NACOS_SERVERS、9849 端口三样同时正确。第四步验证的是数据库链路写进去再读出来走的是完整的持久化路径比单纯看日志可靠得多。再加一条环境验证登录控制台看「配置管理」里刚才那条smoke-test.yaml在不在然后在「集群管理」里挨个确认三台节点的 IP 和状态。如果控制台打不开但 curl 通多半是 Ingress 或者反向代理把/nacos上下文路径吃掉了检查一下有没有做 rewrite。提示验收完记得把smoke-test.yaml删掉别让它跟着配置一起发布到生产环境。验收清单我一般会存成一个verify.sh丢在清单目录里每次部署完跑一遍。从那以后我每次装 Nacos 集群不管多急都强制走一遍第三步的集群成员检查——因为它能一次性揪出端口、DNS、publishNotReadyAddresses三类问题比翻日志快多了。希望这套 YAML 能帮到你少走一遍我踩过的路。本文还有配套的精品资源点击获取