首页 / 资讯中心 / 文章详情

K8s部署Nacos集群:StatefulSet实战指南

K8s部署Nacos集群:StatefulSet实战指南 ★ FEATURED ARTICLE
简介这份面向 Kubernetes 初学者与运维人员的 Nacos 集群部署配置包围绕在 k8s 环境中快速落地 Nacos 服务展开旨在用尽量精简的清单减少繁琐参数与手工配置降低集群部署门槛。压缩包共 6 个文件主体为 5 个 YAML 配置清单和 1 个 txt 说明文件整体约 3KB体积轻量却覆盖数据库连接配置、无头服务、有状态工作负载、服务暴露及 Ingress 入口等关键环节并按执行顺序组织便于读者依次应用、理解各组件之间的衔接关系。已有 766 人学习下载说明其作为入门实践参考具有一定认可度。读者可据此快速搭建可访问的 Nacos 集群雏形同时借助清单结构梳理 StatefulSet 与 Service 的配合方式掌握从配置到暴露的部署思路适合用于学习验证、实验环境搭建及后续二次调整。1. 从一份 Nacos 集群 YAML 说起手写 Deployment 的人最后都回到了 StatefulSet前段时间有位读者发来一段排查记录Nacos 三个 Pod 全是 Running 状态可控制台节点列表里只有自己一个客户端注册上来的实例还在节点之间来回漂移。他贴出的 YAML 用的是 Deployment 加一个普通 ClusterIP Service副本数写 3看上去毫无问题。这个组合放在无状态服务里几乎没有争议放到 Nacos 上就会出问题Pod 重启后 IP 变了cluster.conf 里写死的地址对不上节点之间互相认不出来注册中心就成了一个只进不出的摆设。这份 k8s nacos 集群部署脚本 yaml 要解决的就是这些事用 StatefulSet 保住稳定网络标识用 Headless Service 提供可解析的成员地址用 ConfigMap 收敛配置再把元数据外置到 MySQL。它面向的是准备在自建 K8s 上跑一套 nacos 软件、把它当作注册中心和配置中心用的后端与运维一套文件按依赖顺序执行 kubectl apply出来就是一个能承担 nacos 插件级别职责的集群组件。2. 拆开 YAML 之前Nacos 在 K8s 里的部署形态与选型理由2.1 三个端口与两条通信链路集群内部到底在聊什么Nacos 2.x 的端口设计比 1.x 复杂很多部署失败不是因为 YAML 写错而是端口没对上。默认情况下 8848 是 HTTP 主端口控制台、OpenAPI、健康检查都走这里9848 是客户端 gRPC 端口所有 2.x 的 Java 客户端和部分其他语言客户端会向服务端发起长连接9849 是服务端 gRPC 端口集群内节点之间的数据同步走这条链路。1.x 时代还有一个 7848 用于 Raft 选举2.x 虽然保留了但主要场景已经切换。这里有个必须刻在脑子里的偏移规则9848 8848 10009849 8848 1001。如果你在 application.properties 里把 server.port 改成 8858那两个 gRPC 端口不会自动跟着变你必须显式配置 nacos.server.grpc.port 或者接受它按新主端口重新推算。我见过最多的现场是主端口改了、Service 里只映射了新主端口客户端报 client not connected 或者 Server check fail日志里一行 gRPC 超时翻来覆去看不出问题其实就是 9858 没开。这也是为什么整套 YAML 里 Service 的端口列表比 StatefulSet 的 containerPort 更容易出错。containerPort 写错只影响声明Service 的 port 写错直接影响能不能连。我的习惯是把 8848、9848、9849 三个端口在三个地方保持一致ConfigMap 的服务端配置、StatefulSet 的 containerPort、Service 的 ports 数组。关于集群成员发现Nacos 提供了两条路一是挂载 cluster.conf 文件每行一个IP:PORT二是通过环境变量 NACOS_SERVERS 传入空格分隔的地址列表。在 K8s 里我更倾向后者因为 Pod 的 IP 会变而 hostname 不会。StatefulSet 的每个 Pod 有稳定域名nacos-0.nacos-headless.namespace.svc.cluster.local把这一串写进 NACOS_SERVERS节点扩缩容时只需改环境变量。2.2 StatefulSet 与 Deployment四个不能省的字段把 Nacos 塞进 Deployment 并不是完全跑不起来单副本模式下它工作得挺好问题出在副本数大于 1 的那一刻。Deployment 的 Pod 名字是随机后缀网络标识不稳定存储卷在 Pod 重建后不一定跟着走而这三点恰好是 Nacos 集群依赖的东西。下面这张表是我在选型时反复对照的差异点看懂了就知道为什么脚本里必须是 StatefulSet维度DeploymentStatefulSetNacos 集群的影响Pod 名称随机后缀固定序号 nacos-0/1/2成员地址可预测NACOS_SERVERS 能写死网络标识无稳定 DNS通过 headless Service 提供节点互相寻址不依赖 IP存储共享 PVC 或 emptyDirvolumeClaimTemplates 每个 Pod 独立每个节点日志、data 目录隔离启动顺序并行无序默认有序可改 Parallel影响首次建集群的速度四个关键字段里serviceName必须指向一个 headless ServiceclusterIP: None这是 Stable Network ID 的来源podManagementPolicy: Parallel建议显式写上因为 Nacos 集群节点之间是对等关系不需要像数据库主从那样严格按序启动默认的 OrderedReady 会让三个 Pod 一个个串行起来首次部署时间翻倍而且如果 nacos-0 因为探针没准备好一直卡着后面两个根本不会创建volumeClaimTemplates决定每个 Pod 独立的 PVC 命名规则删 Pod 不删卷replicas在集群模式下建议 3 起步奇数个方便后续做多数派判断。2.3 一套脚本的文件清单与依赖顺序拿到一个 yaml 资源包第一件事不是急着 apply而是先看清楚文件之间的依赖。这套部署脚本通常包含以下几类对象顺序错了会出现 ConfigMap 还没建、Pod 已经开始拉配置的竞态文件内容关键字段依赖00-namespace.yaml独立命名空间metadata.name无01-configmap.yamlapplication.properties、cluster.confnacos.core.auth、db.urlNamespace02-service-headless.yaml集群内互访clusterIP: None无03-service-client.yaml对外/对客户端暴露type、ports无04-statefulset.yaml工作负载主体serviceName、volumeClaimTemplates上述全部05-init-sql.sqlMySQL 建表脚本schema.sql 内容外部 MySQL执行顺序遵循 00 → 01 → 02/03 → 04。每逢升级版本我一般会先kubectl apply --dry-runserver -f ./把整个目录过一遍确认 API 版本在当前集群里都支持再真正提交。很多脚本跑不通的问题其实是集群版本比 YAML 里的 apiVersion 新或旧比如apps/v1beta1在 1.16 之后就被移除了。3. 逐段落地ConfigMap、Service、StatefulSet 的配置写法3.1 ConfigMapapplication.properties 里真正要改的参数Nacos 的镜像启动时会读取/home/nacos/conf/application.propertiesConfigMap 的作用就是把这个文件替换掉或者覆盖部分键值。下面这段是我常用的最小可用集合只保留集群模式必须动的那几项apiVersion: v1 kind: ConfigMap metadata: name: nacos-config namespace: nacos data: application.properties: | # 服务端主端口改了它就要同步改 9848/9849 的偏移 server.port8848 # 关闭内嵌 Derby走外部 MySQL spring.sql.init.platformmysql db.num1 db.url.0jdbc:mysql://mysql.nacos.svc.cluster.local:3306/nacos_config?characterEncodingutf8connectTimeout1000socketTimeout3000autoReconnecttrueuseUnicodetrueuseSSLfalseserverTimezoneAsia/Shanghai db.user.0nacos db.password.0数据库密码 # 开启鉴权生产环境必须 nacos.core.auth.enabledtrue nacos.core.auth.plugin.nacos.token.secret.key至少要 32 位 base64 字符串 nacos.core.auth.server.identity.key节点间通信标识 key nacos.core.auth.server.identity.value节点间通信标识 value # 会话与连接相关按 Pod 内存调整 nacos.naming.expireInstancetrue cluster.conf: | # 与 NACOS_SERVERS 二选一这里留空表示由环境变量注入spring.sql.init.platformmysql这一行在 2.2 之后替代了旧版的spring.datasource.platform两个键名写混是版本升级后最常见的翻车点表现是启动日志里 Derby 照常初始化你以为配了 MySQL 其实没生效。db.num1表示单数据源如果要做数据库高可用可以扩展到 db.url.1 并同步增加 num不过 Nacos 对多数据源的写入策略并不复杂一般企业内单实例 MySQL 加主从就够。nacos.core.auth.plugin.nacos.token.secret.key要求是 base64 编码后的字符串明文长度不足会直接启动失败日志里通常会提示 the length of secret key must great than or equal 32 bytes。这个值一旦上线就别随意改改完所有客户端的 token 全部失效等于强制全量重启一次。nacos.core.auth.server.identity.key/value是节点之间的身份标识它在集群模式下必须每个节点一致如果不一致节点列表里能看到对方但显示不健康。这个是很多人第一次搭鉴权集群时的黑匣子。3.2 cluster.conf 与 NACOS_SERVERS选一个别都写集群成员地址有两条注入路径写重了反而出问题。cluster.conf 是文件方式内容是每行一个地址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环境变量方式更贴合 K8senv: - 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: MODE value: cluster - name: PREFER_HOST_MODE value: hostnameMODEcluster是打开集群模式的开关单机模式下 Nacos 默认用 Derby 并且不认 cluster.conf这个环境变量漏了的话上面所有集群配置全部白搭。PREFER_HOST_MODEhostname告诉服务端在对外暴露地址时优先使用 hostname 而不是 IP配合 StatefulSet 的稳定域名客户端拿到的实例地址才是可解析的。注意一点环境变量里必须用完整的 FQDN 或至少pod名.headless服务名的形式。只写nacos-0在同一个 namespace 内偶尔能解析跨 namespace 或者 DNS 策略不是 ClusterFirst 时就断。我一般在脚本里统一写全 FQDN多打几十个字符换来的确定性很值。如果集群后续要扩到 5 个节点正确姿势是先改 NACOS_SERVERS 加入新地址再kubectl scale statefulset nacos --replicas5最后滚动重启老节点让它们重新读配置。顺序反了会出现新节点不知道老集群、老集群不认新节点的僵持状态。3.3 两个 Service 的分工Headless 给集群ClusterIP 给客户端apiVersion: v1 kind: Service metadata: name: nacos-headless namespace: nacos spec: clusterIP: None selector: app: nacos ports: - name: http port: 8848 targetPort: 8848 - name: grpc-client port: 9848 targetPort: 9848 - name: grpc-server port: 9849 targetPort: 9849 --- apiVersion: v1 kind: Service metadata: name: nacos-client namespace: nacos spec: type: ClusterIP selector: app: nacos ports: - name: http port: 8848 targetPort: 8848 - name: grpc-client port: 9848 targetPort: 9848Headless Service 的clusterIP: None是关键它让 K8s 不为这个 Service 分配虚拟 IP而是直接返回后端 Pod 的 A 记录nacos-0.nacos-headless才能解析到具体 Pod。StatefulSet 的serviceName必须填nacos-headless。第二个 Service 是给业务客户端用的。这里有个容易忽略的细节2.x 客户端连上 8848 之后服务端会返回一个 gRPC 地址让客户端去连 9848如果 ClusterIP Service 只暴露了 8848客户端第一阶段能连上第二阶段直接超时。所以 9848 必须一起暴露。9849 是服务端之间用的客户端 Service 不需要。另外如果集群外部比如 K8s 外面的虚拟机也要注册那就得再补一个 NodePort 或 LoadBalancer 类型的 Service并且注意 9848 同样要映射出去。只映射 8848 是最常见的本地能连、外部连不上的原因。3.4 StatefulSet 主体探针、资源与优雅停机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: terminationGracePeriodSeconds: 30 affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: topologyKey: kubernetes.io/hostname labelSelector: matchLabels: app: nacos containers: - name: nacos image: nacos/nacos-server:替换成你实际使用的版本 imagePullPolicy: IfNotPresent envFrom: - configMapRef: name: nacos-config-env resources: requests: cpu: 500m memory: 2Gi limits: cpu: 2 memory: 2Gi ports: - containerPort: 8848 - containerPort: 9848 - containerPort: 9849 readinessProbe: httpGet: path: /nacos/actuator/health port: 8848 initialDelaySeconds: 20 periodSeconds: 10 failureThreshold: 6 livenessProbe: httpGet: path: /nacos/actuator/health port: 8848 initialDelaySeconds: 60 periodSeconds: 20 failureThreshold: 3 lifecycle: preStop: exec: command: [/bin/sh, -c, sleep 10] volumeMounts: - name: conf mountPath: /home/nacos/conf/application.properties subPath: application.properties - name: data mountPath: /home/nacos/data - name: logs mountPath: /home/nacos/logs volumeClaimTemplates: - metadata: name: data spec: accessModes: [ReadWriteOnce] storageClassName: 你的 StorageClass 名 resources: requests: storage: 20Gi - metadata: name: logs spec: accessModes: [ReadWriteOnce] storageClassName: 你的 StorageClass 名 resources: requests: storage: 10Gi这段里有几个参数值得单独说明。terminationGracePeriodSeconds: 30配合preStop里的 sleep 10 秒是给服务端留出注销实例的窗口。Nacos 客户端有本地缓存强制 kill 之后客户端最长要等一次心跳超时才会摘除实例这段时间流量还可能打到已经死掉的 Pod 上。加了 preStop 之后Pod 会先从 Endpoints 摘除再等 10 秒让存量连接自然断开。readinessProbe用/nacos/actuator/health而不是/nacos/是因为后者在实例注册表未就绪时也可能返回 200探针会误判。initialDelaySeconds给到 20 秒是经验值Pod 冷启动时 JVM 加上 MySQL 连接池初始化通常在 15 到 25 秒之间给太短会导致第一次就绪检查失败Pod 被反复重启。limits和requests的内存建议写成相等JVM 在容器里读到的是 cgroup limit如果 limit 明显大于 request节点压力大时容易被驱逐而 Nacos 被驱逐一次意味着要重新加入集群代价不小。堆内存可以在环境变量里加JVM_XMS和JVM_XMX一般设成 limit 的 60% 到 70%留出堆外和元空间。podAntiAffinity用的是 preferred 而不是 required是因为很多测试集群只有两三个节点required 会导致第三个 Pod 一直 Pending。生产环境节点足够时改成 required 更稳妥避免三个副本落在同一台机器上一次故障全挂。4. 数据持久化与外部 MySQL把 Nacos 从单机玩具变成可用集群4.1 内嵌 Derby 的边界三副本一起来就打架Nacos 单机模式默认使用内嵌 Derby数据落在${nacos.home}/data/derby-data。它的定位是开箱即用官方文档写得很清楚不适合集群。原因不复杂Derby 是嵌入式数据库文件被进程独占三个 Pod 各自挂自己的 PVC等于三份互不相干的数据如果为了共享把同一个 PVC 挂到三个 Pod 上多个进程同时写同一个 Derby 文件损坏几乎是必然的。我见过有人用 NFS 共享卷跑三副本 Derby跑了两天开始出现配置丢失日志里是各种文件锁异常。所以集群模式的第一道分水岭就是外部 MySQL。它存的是配置内容、命名空间、用户权限、历史版本这些元数据。即使 MySQL 短暂不可用Nacos 服务端启动会失败但已经在运行的节点还能继续提供注册服务因为注册数据主要在内存和节点间同步配置读取有本地快照。理解这个分层才能判断什么样的故障是必须立刻修、什么可以等一等。要注意的是Derby 到 MySQL 没有官方一键迁移工具。如果之前已经用单机版跑了一段时间、配置数据有积累通常的做法是把单机版导出的配置通过 OpenAPI 批量拉下来再推到集群版。这个动作在 YAML 脚本层面不会体现但它是上线前必须补的一步否则会出现集群搭好了配置全是空的。4.2 建库建表与连接参数初始化 SQL 只跑一次MySQL 侧的准备工作和 K8s 无关但漏做一步整个部署就卡在 CrashLoopBackOff。典型操作如下-- 1. 创建独立库字符集必须显式指定 CREATE DATABASE nacos_config DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; -- 2. 创建专用账号不要用 root CREATE USER nacos% IDENTIFIED BY 数据库密码; GRANT SELECT, INSERT, UPDATE, DELETE ON nacos_config.* TO nacos%; FLUSH PRIVILEGES; -- 3. 切入目标库后导入官方 schema USE nacos_config; SOURCE /path/to/nacos-mysql.sql;schema.sql在 Nacos 发行包的conf目录下文件名是mysql-schema.sql或nacos-mysql.sql不同版本命名略有差异。它包含config_info、config_info_gray、his_config_info、users、roles、permissions等表缺一张表都会导致启动时的建表检查失败。导入之后执行SHOW TABLES;确认有十几张表少于这个数说明脚本没跑完。账号权限这里我一般只给 DML 权限而不给 DDL代价是版本升级时需要手工执行增量 SQL好处是应用账号没法误删表。如果团队规模小、升级频繁直接给 ALL 也常见但要接受一次误操作可能清空配置历史的风险。连接串里几个参数值得强调connectTimeout1000和socketTimeout3000是官方示例里的值网络稍差的跨可用区场景建议放大到 3000 和 10000否则启动时容易因为一次慢查询直接放弃useSSLfalse在内网可以接受跨网段建议打开并配置证书serverTimezone必须显式指定不写会在部分 MySQL 8.0 实例上报时区错误导致连接失败。这些参数写错表现都是 Nacos 启动日志里一行Communications link failure从 K8s 层面看不出任何异常只能进容器看日志。4.3 PVC、StorageClass 与扩容几个容易被忽略的字段存储项建议配置理由data 卷容量20Gi 起存快照和本地缓存配置量大的环境会涨logs 卷容量10Gi 起访问日志按天滚动未挂卷时写容器层会撑爆accessModesReadWriteOnce每个 Pod 独立卷不需要共享storageClassName显式指定不写会用集群默认 SC换集群时行为不一致reclaimPolicyRetain误删 StatefulSet 时数据还在能救回来volumeClaimTemplates生成的 PVC 名字是># 第一层Pod 与 Service 状态 kubectl -n nacos get statefulset,pod,svc -o wide kubectl -n nacos rollout status statefulset/nacos --timeout300s # 第二层集群节点列表看每个节点的 state 是否为 UP kubectl -n nacos exec nacos-0 -- \ curl -s http://127.0.0.1:8848/nacos/v1/ns/operator/servers | head -c 500 # 第三层真实注册一次确认读写链路都通 curl -X POST http://nacos-client.nacos.svc.cluster.local:8848/nacos/v1/ns/instance \ -d serviceNamedemo-serviceip10.0.0.1port8080namespaceIdpublic curl http://nacos-client.nacos.svc.cluster.local:8848/nacos/v1/ns/instance/list?serviceNamedemo-service第一层看的是 K8s 视角的就绪状态rollout status返回成功说明所有副本都通过了 readinessProbe。第二层是 Nacos 自己的视角接口返回的 JSON 里每个节点的state字段应为UPaddress应该是完整域名而不是 IP否则回去检查PREFER_HOST_MODE。第三层最关键注册和查询都通说明控制面、数据面、gRPC 链路都没问题。第三层验证完之后记得把测试实例删掉否则它会一直挂在服务列表里直到心跳超时。6.2 滚动升级的一次预演升级镜像版本前我会在测试环境先做一次预演重点看两件事配置是否兼容、节点能否重新加入。# 先只改一个副本的镜像观察它能否正常加入集群 kubectl -n nacos set image statefulset/nacos \ nacosnacos/nacos-server:新版本 # 盯住滚动过程任何 Pod 卡住就暂停 kubectl -n nacos rollout status statefulset/nacos --timeout600s kubectl -n nacos rollout pause statefulset/nacos # 出现异常时手动暂停 # 升级完成后复核集群节点 kubectl -n nacos exec nacos-0 -- \ curl -s http://127.0.0.1:8848/nacos/v1/ns/operator/servers滚动策略用默认的 RollingUpdate但 StatefulSet 是从序号大的往小滚这一点和 Deployment 相反。如果新版本启动异常rollout pause能帮你把已经升级的节点停住避免全量翻车。升级窗口内客户端会有短暂重连2.x 的客户端重连逻辑比较完善通常感知不到但配置监听可能会有一次延迟业务侧要能容忍。还有一个经常被忽视的验证点升级后确认config_info表的记录数没有变化。某些版本在表结构变更时会执行 DDL如果数据库账号没有 DDL 权限升级会在启动阶段静默失败Pod 反复重启但不报明显错误。提前给账号临时开 DDL 权限、升级完再收回是我固定走的流程。从那以后我每次动这套 YAML 都强制走一遍三段验证先 dry-run再单副本灰度最后全量滚动一次都不省。这套脚本本身不复杂真正花时间的是确认每一个参数在你自己的集群里成立这份谨慎比脚本本身更值钱。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站