1. 为什么Nacos上K8S要用StatefulSet而不是Deployment1.1 从一次集群脑裂说起早些年我在一个内部项目里第一次把Nacos搬到K8S上当时图省事直接用了Deployment副本数设了3前面挂了个Service做负载均衡。跑测试环境的时候一切正常服务注册、配置拉取都没问题结果一上预发环境问题就来了三个Pod各自为政每个Pod都认为自己是独立节点配置数据互相覆盖服务列表也各存各的。排查了半天才反应过来——Nacos集群节点之间需要稳定的网络标识来互相发现和通信而Deployment生成的Pod名字是随机后缀重启一次IP和主机名全变节点之间根本认不出彼此。这就是为什么Nacos集群部署必须用StatefulSet。StatefulSet给每个Pod分配固定的序号和稳定的DNS名称比如nacos-0、nacos-1、nacos-2配合Headless Service每个节点都能通过固定的域名找到其他节点。这个特性对于需要集群成员互相感知的中间件来说是刚需。1.2 StatefulSet给Nacos带来的三个核心能力稳定的网络标识是第一个关键点。StatefulSet创建的Pod会按照statefulset-name-ordinal的规则命名配合Headless Service每个Pod获得一个固定的DNS记录格式是pod-name.service-name.namespace.svc.cluster.local。Nacos集群配置文件里需要写明每个节点的地址如果地址老变配置就没法固定下来。有序的启停控制是第二个关键点。StatefulSet默认按照0到N-1的顺序依次创建Pod删除时则逆序执行。Nacos集群在启动时节点需要知道其他节点的存在才能完成集群初始化有序启动能避免所有节点同时抢着做Leader选举导致的混乱。稳定的持久化存储是第三个关键点。Nacos 2.0版本默认使用内嵌Derby数据库但在集群模式下必须切换到外部MySQL存储。即便如此每个节点仍然需要持久化一些本地数据比如日志、协议文件等。StatefulSet通过volumeClaimTemplates为每个Pod自动创建独立的PVCPod重建后PVC依然保留数据不会丢失。1.3 版本选择为什么是Nacos 2.0Nacos 2.0相比1.x最大的变化是通信协议从纯HTTP改成了gRPC长连接。这个改动带来的直接影响是客户端与服务端之间的连接数大幅减少配置推送的实时性显著提升服务端能更精确地感知客户端状态。但同时也带来了新的部署要求——2.0版本需要额外暴露两个gRPC端口默认是9848和9849K8S的Service和网络策略都要相应调整。另外2.0版本对集群节点间的数据同步机制也做了优化Raft协议在集群一致性上的表现更稳定。如果你的Nacos集群规模在3到7个节点之间2.0版本是更合适的选择。2. 部署前的环境准备与关键决策2.1 集群规模怎么定Nacos官方推荐集群至少3个节点这是Raft协议能正常工作的最低要求。但3个节点意味着你只能容忍1个节点故障如果业务对可用性要求极高建议上5个节点。不过节点数也不是越多越好Raft协议在节点数增加时写操作的延迟会上升因为需要多数节点确认才能提交。我个人的经验是中小规模业务3个节点足够核心业务5个节点超过7个节点的场景非常少见。另外要注意Nacos集群节点数最好是奇数这样在选举时不容易出现平票。2.2 存储方案选型Nacos 2.0集群模式下配置数据必须存在外部MySQL中。MySQL本身的高可用方案不在本文讨论范围内但你需要确保Nacos能稳定连接到MySQL。我建议MySQL也部署在K8S集群内通过Service访问这样可以减少网络延迟。每个Nacos Pod的本地存储主要用于存放日志和协议数据。PVC的大小不需要太大10Gi到20Gi通常够用具体取决于你的日志保留策略和配置数据量。存储类选择上如果K8S集群有SSD存储类优先使用因为Nacos在启动和配置推送时对磁盘IO有一定要求。2.3 网络端口规划Nacos 2.0需要暴露的端口比1.x多规划清楚可以避免后续踩坑端口用途是否必须暴露到集群外8848HTTP主端口控制台和OpenAPI是9848gRPC客户端连接端口是9849gRPC服务端间通信端口否仅集群内部7848Raft通信端口否仅集群内部9848端口是客户端gRPC连接的入口必须暴露给业务服务。9849是Nacos节点之间gRPC通信用的只在集群内部使用。7848是Raft选举和数据同步的端口同样只在集群内部使用。3. 完整部署实操从零搭建高可用Nacos集群3.1 创建命名空间与MySQL数据库先把Nacos放到独立的命名空间里方便管理apiVersion: v1 kind: Namespace metadata: name: nacos-cluster然后在MySQL中创建Nacos所需的数据库和表。Nacos 2.0的建表SQL在官方发布包的conf目录下可以找到文件名叫mysql-schema.sql。执行这个SQL会创建一系列表包括config_info、config_info_beta、his_config_info等。注意MySQL字符集建议使用utf8mb4排序规则用utf8mb4_general_ci。如果字符集不对配置内容中有中文时会出现乱码。3.2 编写ConfigMap存放Nacos配置Nacos集群的配置文件主要是application.properties和cluster.conf。在K8S中我习惯用ConfigMap来管理这些配置方便修改和版本控制。apiVersion: v1 kind: ConfigMap metadata: name: nacos-config namespace: nacos-cluster data: application.properties: | server.servlet.contextPath/nacos server.port8848 spring.datasource.platformmysql db.num1 db.url.0jdbc:mysql://mysql-service.nacos-cluster.svc.cluster.local:3306/nacos_config?characterEncodingutf8connectTimeout1000socketTimeout3000autoReconnecttrueuseUnicodetrueuseSSLfalseserverTimezoneUTC db.user.0nacos db.password.0nacos_password nacos.core.auth.enabledtrue nacos.core.auth.server.identity.keyserverIdentity nacos.core.auth.server.identity.valuesecurity nacos.core.auth.plugin.nacos.token.secret.keySecretKey012345678901234567890123456789012345678901234567890123456789 cluster.conf: | 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这里有几个关键点需要说明。db.url.0中的主机名用的是MySQL的Service名称这样Nacos Pod可以通过K8S内部DNS解析到MySQL。nacos.core.auth.enabled开启鉴权生产环境必须开。nacos.core.auth.plugin.nacos.token.secret.key是2.0版本新增的配置用于生成JWT令牌长度必须足够官方建议至少32位。cluster.conf中每个节点的地址格式是pod-name.headless-service-name.namespace.svc.cluster.local:port。这个域名在Pod内部是可以直接解析的因为Headless Service会为每个Pod创建对应的DNS记录。3.3 创建Headless ServiceHeadless Service是StatefulSet实现稳定网络标识的前提它的clusterIP必须设为NoneapiVersion: v1 kind: Service metadata: name: nacos-headless namespace: nacos-cluster labels: app: 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 - name: raft port: 7848 targetPort: 7848这个Service不分配ClusterIPK8S的DNS会为它下面的每个Pod返回各自的IP地址。Nacos节点之间通过这个Service的DNS名称互相发现。3.4 创建对外访问的Service业务服务需要访问Nacos所以还需要一个普通的ClusterIP Service或者NodePort ServiceapiVersion: v1 kind: Service metadata: name: nacos-client namespace: nacos-cluster labels: app: nacos spec: type: ClusterIP selector: app: nacos ports: - name: http port: 8848 targetPort: 8848 - name: grpc-client port: 9848 targetPort: 9848这个Service的selector和Headless Service一样但它是给业务服务用的。业务服务通过nacos-client.nacos-cluster.svc.cluster.local:8848访问Nacos。提示如果你的业务服务不在同一个K8S集群内需要把Service类型改成NodePort或者配合Ingress使用。但要注意gRPC端口9848用Ingress暴露比较麻烦通常建议用NodePort或者LoadBalancer。3.5 编写StatefulSet核心配置这是整个部署的核心部分我拆开来讲apiVersion: apps/v1 kind: StatefulSet metadata: name: nacos namespace: nacos-cluster spec: serviceName: nacos-headless replicas: 3 podManagementPolicy: OrderedReady selector: matchLabels: app: nacos template: metadata: labels: app: nacos spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - nacos topologyKey: kubernetes.io/hostname containers: - name: nacos image: nacos/nacos-server:v2.0.4 imagePullPolicy: IfNotPresent ports: - containerPort: 8848 name: http - containerPort: 9848 name: grpc-client - containerPort: 9849 name: grpc-server - containerPort: 7848 name: raft env: - name: MODE value: cluster - 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: SPRING_DATASOURCE_PLATFORM value: mysql - name: MYSQL_SERVICE_HOST value: mysql-service.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_password - name: JVM_XMS value: 2g - name: JVM_XMX value: 2g - name: JVM_XMN value: 1g volumeMounts: - name: nacos-data mountPath: /home/nacos/data - name: nacos-logs mountPath: /home/nacos/logs - name: nacos-config mountPath: /home/nacos/conf/application.properties subPath: application.properties - name: nacos-config mountPath: /home/nacos/conf/cluster.conf subPath: cluster.conf readinessProbe: httpGet: path: /nacos/actuator/health port: 8848 initialDelaySeconds: 30 periodSeconds: 10 failureThreshold: 3 livenessProbe: httpGet: path: /nacos/actuator/health port: 8848 initialDelaySeconds: 60 periodSeconds: 20 failureThreshold: 5 volumes: - name: nacos-config configMap: name: nacos-config volumeClaimTemplates: - metadata: name: nacos-data spec: accessModes: - ReadWriteOnce storageClassName: standard resources: requests: storage: 10Gi - metadata: name: nacos-logs spec: accessModes: - ReadWriteOnce storageClassName: standard resources: requests: storage: 20Gi3.6 关键参数逐个拆解podAntiAffinity这个配置让Nacos的Pod尽量分散到不同的K8S节点上。用的是preferredDuringSchedulingIgnoredDuringExecution意思是尽量满足但不强制。如果你的K8S节点数不够3个Pod还是能调度成功只是会挤在同一个节点上。生产环境建议用requiredDuringSchedulingIgnoredDuringExecution强制分散但前提是你至少有3个可调度节点。JVM参数Nacos默认的JVM堆内存是2G对于中小规模集群够用。但如果你的配置项特别多或者服务注册量很大建议调到4G。JVM_XMN是新生代大小一般设为堆内存的一半。readinessProbe和livenessProbeNacos 2.0提供了/nacos/actuator/health端点返回UP表示节点健康。readinessProbe的initialDelaySeconds设为30秒是因为Nacos启动需要一定时间做集群初始化和数据同步。livenessProbe的延迟设得更长避免节点在启动过程中被误杀。volumeClaimTemplates这里定义了两个PVC模板一个用于数据一个用于日志。StatefulSet会为每个Pod分别创建PVC命名规则是template-name-statefulset-name-ordinal比如nacos-data-nacos-0、nacos-data-nacos-1。注意PVC的storageClassName要根据你K8S集群的实际情况填写。如果集群没有默认存储类需要先创建一个或者手动指定已有的存储类。4. 部署后的验证与常见问题排查4.1 验证集群状态部署完成后先看Pod状态kubectl get pods -n nacos-cluster -l appnacos正常的话应该看到三个Pod都是Running状态且READY为1/1。如果某个Pod一直处于Pending多半是PVC没创建成功或者资源不足。然后检查集群节点列表kubectl exec -n nacos-cluster nacos-0 -- curl -s http://localhost:8848/nacos/v1/ns/operator/servers返回的JSON中应该包含三个节点的信息每个节点的state字段应该是UP。如果只有部分节点UP说明集群间通信有问题。4.2 常见问题速查表问题现象可能原因排查方向解决方案Pod启动后立即退出数据库连接失败查看Pod日志中的SQLException检查MySQL Service是否可达账号密码是否正确集群节点列表只有一个节点cluster.conf配置错误检查ConfigMap中的域名格式确保域名格式为pod.svc.ns.svc.cluster.local客户端连接报gRPC错误9848端口未暴露检查Service的端口配置确保客户端Service包含9848端口映射配置推送延迟高Raft选举频繁查看日志中的Raft相关记录检查网络延迟确保节点间通信稳定Pod反复重启健康检查失败查看livenessProbe的失败日志适当增加initialDelaySeconds和failureThreshold控制台无法登录鉴权配置问题检查token.secret.key长度确保密钥长度至少32位且所有节点一致4.3 我踩过的几个坑第一个坑是cluster.conf的域名解析。最开始我图省事直接在cluster.conf里写了Pod的IP地址结果Pod重启后IP变了集群就散了。后来改成用Headless Service的域名才彻底解决。这里要注意域名中的namespace必须和Nacos部署的namespace一致否则解析不到。第二个坑是MySQL连接超时。Nacos默认的数据库连接超时是1秒如果MySQL负载高或者网络抖动很容易超时。我在JDBC URL里加了connectTimeout1000socketTimeout3000同时把MySQL的最大连接数调大了一些。另外autoReconnecttrue这个参数建议加上避免连接断开后Nacos无法自动重连。第三个坑是JVM内存溢出。有一次配置数据量突然增大Nacos节点频繁Full GC最后OOM了。后来把堆内存从2G调到4G同时优化了配置的存储结构问题才解决。如果你的配置项超过1万条建议直接上4G堆内存。第四个坑是PVC回收策略。StatefulSet删除后PVC默认是不会自动删除的。如果你要彻底清理环境需要手动删除PVC否则下次部署时可能会因为PVC已存在而报错。当然这也意味着数据不会丢失算是有利有弊。4.4 扩容与缩容的注意事项Nacos集群扩容不是简单改replicas就完事。StatefulSet扩容时会创建新的Pod但新Pod的地址需要加到所有已有节点的cluster.conf中。也就是说你需要先更新ConfigMap然后重启所有Pod让新配置生效。这个过程会导致集群短暂不可用建议在业务低峰期操作。缩容则更麻烦因为Raft协议需要保证多数节点存活。如果你从3节点缩到2节点Raft就无法正常工作了。所以缩容前一定要确认剩余节点数仍然是奇数且不少于3个。如果确实要缩到1个节点那就不叫集群了直接改成单机模式更合适。5. 生产环境加固建议5.1 资源限制与调度策略生产环境一定要给Nacos Pod设置resource requests和limitsresources: requests: memory: 4Gi cpu: 2 limits: memory: 4Gi cpu: 4requests和limits的内存设为一样可以避免Pod被OOM Killer杀掉。CPU的limits可以比requests大一些允许突发流量时占用更多CPU。调度策略上除了前面提到的podAntiAffinity还可以给Nacos Pod设置nodeSelector或者tolerations让它们调度到特定类型的节点上。比如把Nacos调度到SSD节点的池子里提升IO性能。5.2 监控与告警Nacos 2.0暴露了Prometheus格式的指标可以通过/nacos/actuator/prometheus端点获取。建议在K8S集群里部署Prometheus Operator创建ServiceMonitor来采集Nacos的指标。关键监控指标包括JVM堆内存使用率、GC次数和耗时、配置推送延迟、服务注册数量、Raft选举次数。其中Raft选举次数如果频繁上升说明集群不稳定需要重点排查。告警规则可以设堆内存使用率超过80%持续5分钟、配置推送延迟超过1秒、集群节点数少于预期值。这些告警能帮你提前发现潜在问题。5.3 备份策略Nacos的配置数据存在MySQL中所以备份MySQL就是备份Nacos的配置。建议每天做一次全量备份同时开启binlog做增量备份。如果MySQL也部署在K8S里可以用CronJob定期执行mysqldump。另外Nacos的日志PVC里也保存了一些运行数据虽然不像配置那么关键但在排查问题时很有用。建议给日志PVC也做定期快照保留最近7天的数据。5.4 安全加固生产环境必须开启鉴权前面ConfigMap里已经配了。但还有几个点需要注意一是nacos.core.auth.server.identity.key和value要改成自定义的复杂字符串不要用默认值二是token密钥要足够长且定期轮换三是如果Nacos控制台需要对外暴露一定要加HTTPS和访问白名单。K8S层面的NetworkPolicy也建议配上只允许业务命名空间的Pod访问Nacos的8848和9848端口其他流量一律拒绝。这样即使Nacos有未修复的漏洞攻击面也会小很多。5.5 版本升级路径Nacos 2.0的小版本升级相对平滑但跨大版本升级比如1.x到2.0需要特别注意。2.0版本的数据存储结构有变化升级前一定要备份MySQL数据。升级步骤通常是先升级一个节点观察集群状态确认没问题后再滚动升级其他节点。StatefulSet的滚动升级默认是从序号最大的Pod开始逐个替换。你可以通过kubectl rollout status查看升级进度。如果升级过程中发现问题可以用kubectl rollout undo回滚到上一个版本。我在实际升级中遇到过一次问题新版本Pod启动后无法加入集群原因是cluster.conf中的域名格式在新版本中要求更严格。后来把域名中的namespace补全后才解决。所以升级前一定要仔细阅读官方的升级说明不要想当然。