1. 项目概述与核心价值
最近在搞微服务架构的落地,服务注册与发现中心是绕不开的一环。Nacos 作为阿里开源的一站式动态服务发现、配置管理和服务管理平台,凭借其易用性和强大的功能,已经成了很多团队的首选。但在生产环境,单点部署的 Nacos 显然不够看,高可用集群是基本要求。而 Kubernetes 作为容器编排的事实标准,在 K8s 上部署和管理 Nacos 集群,就成了一个非常典型的、必须掌握的运维场景。
这个项目,说白了,就是要把 Nacos 的高可用集群塞进 K8s 里,让它能像其他 K8s 应用一样,享受声明式部署、弹性伸缩、自愈和便捷的运维管理。听起来好像就是写个 YAML 文件的事?实际操作起来,从存储选型、网络配置到集群发现机制,每一步都有不少门道。我把自己最近在生产环境折腾 Nacos on K8s 的完整过程,包括踩过的坑和验证过的稳定方案,梳理成这篇笔记。无论你是刚开始接触 K8s 的开发者,还是正在为生产环境寻求可靠部署方案的运维,这篇内容应该都能给你提供一条清晰的路径和可复现的实操细节。
2. 架构设计与核心组件解析
2.1 为什么选择 StatefulSet 而非 Deployment?
这是第一个关键决策点。Nacos 集群节点是有状态的:每个节点有自己唯一的 ID(比如nacos-0,nacos-1,nacos-2),并且它们需要持久化存储自己的数据(如 Derby 数据库文件,或者外接的 MySQL 数据)。Deployment 管理的 Pod 是无状态且可互换的,显然不符合要求。
StatefulSet 完美匹配了我们的需求:
- 稳定的、唯一的网络标识符:Pod 名称(
<statefulset-name>-<ordinal-index>)和对应的 Headless Service 域名是稳定的。即使 Pod 重启或重新调度,它的名称和域名不变。这对于 Nacos 集群节点间相互发现和通信至关重要。 - 按顺序的部署和扩缩容:默认情况下,Pod 按索引顺序(0, 1, 2...)创建和终止。这为集群初始化(比如选举 leader)提供了一定的可控性。
- 稳定的持久化存储:通过
volumeClaimTemplates,可以为每个 Pod 动态创建独立的 PersistentVolumeClaim (PVC),实现存储与 Pod 实例的绑定。Pod 重建后,仍然能挂载到原来的数据卷。
所以,我们的核心工作负载对象就是 StatefulSet。一个典型的命名会是nacos-statefulset,它管理的 Pod 将是nacos-0,nacos-1,nacos-2。
2.2 存储方案选型:嵌入式 Derby 还是外置 MySQL?
Nacos 支持两种存储模式:内嵌的 Derby 数据库,以及外部的集中式数据库(如 MySQL)。在 K8s 环境下,选择直接影响集群的数据一致性和运维复杂度。
嵌入式 Derby (不推荐用于生产集群):
- 原理:每个 Nacos 节点使用自己内嵌的 Derby 数据库。节点间通过 Raft 协议同步服务注册表等内存数据,但配置信息等持久化数据默认不同步。
- K8s 下的问题:每个 Pod 的持久化卷里存着自己的 Derby 数据文件。如果某个 Pod 故障,新调度的 Pod 挂载了新的空卷,或者挂载了其他节点的卷(数据不一致),都会导致该节点数据丢失或混乱。虽然 Nacos 支持通过某种方式同步 Derby 数据,但非常复杂且非主流。
- 结论:在 K8s StatefulSet 中,即使每个 Pod 有独立存储,使用嵌入式 Derby 也无法保证整个集群配置数据的一致性。生产环境强烈不建议使用。
外置 MySQL (推荐方案):
- 原理:所有 Nacos 节点连接同一个外部的 MySQL 数据库集群(主从或高可用架构)。所有持久化数据(命名空间、配置、用户权限等)都存储在中心化的 MySQL 里,天然保证一致性。
- K8s 下的优势:
- 数据一致性:所有节点读写同一数据源,无数据同步烦恼。
- 运维简单:数据库的备份、恢复、扩容由专业的 DBA 或云数据库服务负责,与 Nacos 应用本身解耦。
- 节点无状态化:Nacos 节点本身可以视为“无状态”,更符合云原生理念(虽然我们仍用 StatefulSet 是为了稳定的网络标识)。节点故障后新建的 Pod,只要连接串正确,就能立刻加入集群。
- 结论:对于生产级 K8s 部署,必须使用外置 MySQL。这通常意味着你需要先在 K8s 外或 K8s 内(通过另一个 StatefulSet 或 Operator)部署一个高可用的 MySQL 集群,并提前创建好 Nacos 所需的数据库和用户。
注意:即使使用外置 MySQL,Nacos 节点内存中维护的服务实例注册表,依然是通过节点间的 Raft 协议进行同步的,这部分数据不是存在 MySQL 里的。MySQL 主要负责存储那些需要持久化的元数据。
2.3 集群节点发现机制:K8s Service 的妙用
Nacos 集群节点需要知道彼此的存在才能组成集群。在传统虚拟机部署时,我们往往需要在一个配置文件中列出所有节点的 IP 和端口。在 K8s 中,由于 Pod IP 会变,我们不能写死 IP。
这里我们利用 K8s 的Headless Service和StatefulSet的特性来实现自动发现。
- 创建一个 Headless Service(
clusterIP: None),其选择器(selector)指向我们的 Nacos StatefulSet。 - 这个 Service 本身没有集群 IP,但它会为每个匹配的 Pod 创建一条 DNS A 记录,格式为:
<pod-name>.<headless-svc-name>.<namespace>.svc.cluster.local。 - 在我们的 Nacos StatefulSet 配置中,可以通过环境变量或配置文件,让每个 Pod 启动时,使用一个固定的模式来构建集群节点列表。例如,如果我们知道集群规模是 3,那么节点列表就可以构建为:
nacos-0.nacos-headless.default.svc.cluster.local:8848,nacos-1.nacos-headless.default.svc.cluster.local:8848,nacos-2.nacos-headless.default.svc.cluster.local:8848 - 这样,无论 Pod 如何调度,只要 Pod 名称和 Service 域名稳定,集群成员就能相互解析和通信。
3. 完整部署实操与配置详解
接下来,我们一步步拆解部署过程。假设我们的目标是在nacos命名空间部署一个 3 节点的 Nacos 集群,使用外置 MySQL。
3.1 前置条件与环境准备
- Kubernetes 集群:一个正常运行的 K8s 集群(1.16+),并配置好
kubectl命令行工具。 - 外置 MySQL 数据库:假设我们已经有一个高可用的 MySQL 8.0 集群,连接信息如下:
- 地址:
mysql-ha.example.com:3306 - 数据库名:
nacos_config - 用户名:
nacos - 密码:
YourStrongPassword123!
- 地址:
- 创建命名空间:
kubectl create namespace nacos
3.2 核心资源配置文件拆解
我们将创建几个关键的 YAML 文件。这里我会把核心部分贴出来并逐行解释。
1. 创建 ConfigMap (nacos-cm.yaml): 用于存储 Nacos 的公共配置文件,主要是cluster.conf和application.properties。注意,cluster.conf的内容我们通过一个巧妙的初始化容器来动态生成,而不是写死。
apiVersion: v1 kind: ConfigMap metadata: name: nacos-config namespace: nacos data: # 这里只放 application.properties 的基础部分,数据库连接信息通过环境变量或Secret注入更安全 application.properties: | # 启用数据源 spring.datasource.platform=mysql # 数据库数量,我们只有一个主库,但Nacos配置要求至少写一个 db.num=1 # 下面这些具体的连接信息,我们将通过环境变量来替换,避免硬编码在ConfigMap中 db.url.0=jdbc:mysql://${MYSQL_SERVICE_HOST}:${MYSQL_SERVICE_PORT}/${MYSQL_DATABASE}?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false db.user.0=${MYSQL_USER} db.password.0=${MYSQL_PASSWORD} # 其他重要配置 server.servlet.contextPath=/nacos # 开启认证(生产环境建议开启) nacos.core.auth.enabled=false # 先关闭,方便测试,生产环境务必改为true并配置密钥 nacos.core.auth.server.identity.key=serverIdentity nacos.core.auth.server.identity.value=security # 节点角色,默认为 both (同时负责读写和集群间同步) nacos.core.member.meta.roles=ROLE_BOTH2. 创建 Headless Service (nacos-headless-svc.yaml): 为 StatefulSet 的 Pod 提供稳定的网络标识。
apiVersion: v1 kind: Service metadata: name: nacos-headless namespace: nacos labels: app: nacos spec: clusterIP: None # Headless Service 的关键! ports: - port: 8848 name: server - port: 9848 name: raft-rpc # Nacos 2.0 新增的端口,用于节点间RPC通信 - port: 9849 name: grpc # Nacos 2.0 客户端gRPC通信端口 selector: app: nacos # 这个选择器必须和后面的StatefulSet匹配3. 创建对外访问的 Service (nacos-svc.yaml): 为了让集群外或其他命名空间的服务能访问 Nacos 控制台和 API,我们需要一个常规的 Service。这里使用 NodePort 类型方便演示,生产环境通常用 Ingress。
apiVersion: v1 kind: Service metadata: name: nacos namespace: nacos labels: app: nacos spec: type: NodePort # 生产环境建议使用 ClusterIP + Ingress ports: - name: server port: 8848 targetPort: 8848 nodePort: 30000 # 指定一个NodePort范围,或让K8s自动分配 - name: raft-rpc port: 9848 targetPort: 9848 - name: grpc port: 9849 targetPort: 9849 selector: app: nacos4. 创建 StatefulSet (nacos-statefulset.yaml): 这是最核心的部分,内容较长,我们分段解析。
apiVersion: apps/v1 kind: StatefulSet metadata: name: nacos namespace: nacos spec: serviceName: nacos-headless # 必须指向前面创建的Headless Service replicas: 3 # 集群节点数 selector: matchLabels: app: nacos template: metadata: labels: app: nacos spec: # 初始化容器:用于动态生成 cluster.conf initContainers: - name: init-cluster-conf image: busybox:1.35 command: ['sh', '-c'] args: - | set -ex # 获取当前Pod的序号,如 nacos-0 则 INDEX=0 INDEX=$(hostname | awk -F'-' '{print $NF}') # 根据StatefulSet名称和Headless Service名称,生成集群节点列表 # 假设我们知道集群规模是3 (REPLICAS=3) REPLICAS=3 DOMAIN="nacos-headless.nacos.svc.cluster.local" CLUSTER_CONF="" for i in $(seq 0 $(($REPLICAS-1))); do CLUSTER_CONF="${CLUSTER_CONF}$(printf \"nacos-%d.%s:8848\" $i $DOMAIN)\n" done # 将生成的列表写入到共享卷的指定位置 echo -e $CLUSTER_CONF > /home/nacos/conf/cluster.conf cat /home/nacos/conf/cluster.conf volumeMounts: - name: config-volume mountPath: /home/nacos/conf # 挂载到Nacos的配置目录 # 主容器:运行Nacos Server containers: - name: nacos image: nacos/nacos-server:v2.2.3 # 建议使用特定版本,而非latest imagePullPolicy: IfNotPresent ports: - containerPort: 8848 name: server - containerPort: 9848 name: raft-rpc - containerPort: 9849 name: grpc # 环境变量:用于传递数据库连接信息和JVM参数 env: - name: MODE value: "cluster" # 指定集群模式 - name: PREFER_HOST_MODE value: "hostname" # 使用hostname(即Pod名称)作为节点标识 - name: SPRING_DATASOURCE_PLATFORM value: "mysql" - name: MYSQL_SERVICE_HOST value: "mysql-ha.example.com" # 你的MySQL地址 - name: MYSQL_SERVICE_PORT value: "3306" - name: MYSQL_DATABASE value: "nacos_config" - name: MYSQL_USER valueFrom: secretKeyRef: name: nacos-mysql-secret # 建议使用Secret存储密码 key: username - name: MYSQL_PASSWORD valueFrom: secretKeyRef: name: nacos-mysql-secret key: password - name: JVM_XMS value: "512m" # 初始堆内存 - name: JVM_XMX value: "512m" # 最大堆内存 - name: JVM_XMN value: "256m" # 年轻代大小 # 资源请求与限制,根据实际负载调整 resources: requests: memory: "1Gi" cpu: "500m" limits: memory: "2Gi" cpu: "1000m" # 健康检查 livenessProbe: httpGet: path: /nacos/actuator/health port: 8848 initialDelaySeconds: 30 periodSeconds: 10 failureThreshold: 3 readinessProbe: httpGet: path: /nacos/actuator/health port: 8848 initialDelaySeconds: 30 periodSeconds: 5 volumeMounts: - name: config-volume mountPath: /home/nacos/conf - name: logs-volume mountPath: /home/nacos/logs - name:>apiVersion: v1 kind: Secret metadata: name: nacos-mysql-secret namespace: nacos type: Opaque data: username: bmFjb3M= # echo -n 'nacos' | base64 password: WW91clN0cm9uZ1Bhc3N3b3JkMTIzIQ== # echo -n 'YourStrongPassword123!' | base643.3 执行部署与验证
按顺序应用配置文件:
kubectl apply -f nacos-cm.yaml kubectl apply -f nacos-secret.yaml kubectl apply -f nacos-headless-svc.yaml kubectl apply -f nacos-svc.yaml kubectl apply -f nacos-statefulset.yaml观察部署状态:
# 查看StatefulSet和Pod创建情况 kubectl -n nacos get statefulsets kubectl -n nacos get pods -l app=nacos -w # 等待所有Pod状态变为 Running 且 Ready (2/2,如果有sidecar的话) # 查看初始化容器的日志,确认cluster.conf生成正确 kubectl -n nacos logs nacos-0 -c init-cluster-conf # 查看Nacos主容器日志 kubectl -n nacos logs nacos-0 -f验证集群状态:
- 通过 NodePort 访问 Nacos 控制台:
http://<任意NodeIP>:30000/nacos。默认账号密码是nacos/nacos。 - 在控制台集群管理 -> 节点列表中,应该能看到三个节点,它们的 IP 地址应该是各自的 Pod IP,状态应为UP。
- 可以尝试停掉一个 Pod (
kubectl -n nacos delete pod nacos-0),观察 StatefulSet 是否会自动重建一个新的nacos-0,并且新 Pod 是否能自动重新加入集群(在节点列表中恢复 UP 状态)。
- 通过 NodePort 访问 Nacos 控制台:
4. 生产环境进阶配置与调优
基础部署跑通只是第一步,要用于生产,还需要考虑更多。
4.1 配置持久化与高可用 MySQL
前面我们用了外置 MySQL,但“外置”具体怎么部署?对于严格要求,建议:
- 云托管服务:直接使用云厂商提供的 RDS(如 AWS RDS, Aliyun RDS, Tencent Cloud CDB),它们自带高可用、备份、监控,省心省力。只需确保 Nacos 所在的 K8s 集群网络能访问到 RDS 的内网地址。
- 自建 K8s 集群内 MySQL:如果必须在 K8s 内,可以使用成熟的 Operator,如
mysql-operator或presslabs/mysql-operator,来部署一个包含主从复制、自动故障转移的 MySQL 集群。切记,Nacos 的数据库和业务数据库最好物理隔离。
4.2 开启认证与使用 TLS
生产环境必须开启 Nacos 认证,并建议对控制台和 API 访问启用 TLS。
- 开启认证:修改 ConfigMap 中的
nacos.core.auth.enabled=true,并设置nacos.core.auth.plugin.nacos.token.secret.key为一个足够复杂且保密的密钥(同样建议放在 Secret 中)。所有客户端连接时都需要配置用户名和密码。 - 配置 TLS/HTTPS:这通常在 Ingress 层面解决。为 Nacos 的 Service 创建 Ingress 资源,并配置 TLS 证书。这样,外部访问通过 HTTPS 加密,内部 Pod 之间通信仍可用 HTTP。如果要求 Pod 间通信也加密,则需要在 Nacos 应用内配置 SSL,并管理证书,复杂度较高,需权衡必要性。
4.3 资源限制与监控告警
- 资源限制:前面 StatefulSet 中已经设置了
resources.requests/limits。需要根据实际监控数据调整。Nacos 的内存占用与注册的服务实例数和配置数量强相关。建议初期设置合理的 Limits 防止单个 Pod 吃光节点资源,同时根据监控观察值调整 Requests 以提高调度效率。 - 监控:
- Nacos 自身指标:Nacos 2.0 提供了基于 Micrometer 的指标端点 (
/nacos/actuator/prometheus),可以很方便地被 Prometheus 抓取。监控关键指标如:服务实例数、配置数量、HTTP/GRPC 请求 QPS、延迟、错误率、JVM 内存/GC 情况、集群节点状态和任期(term)等。 - K8s 层面监控:监控 Pod 的 CPU、内存使用率、重启次数、网络流量。
- Nacos 自身指标:Nacos 2.0 提供了基于 Micrometer 的指标端点 (
- 告警:基于上述监控指标设置告警规则,例如:集群节点 DOWN 的数量超过 1、JVM 内存使用率持续超过 80%、请求错误率突增等。
4.4 数据备份与灾难恢复
即使数据库高可用,定期备份仍是必须的。
- MySQL 数据库备份:使用你熟悉的 MySQL 备份工具(如
mysqldump、xtrabackup)或云 RDS 的自动备份功能,定期对nacos_config数据库进行全量和增量备份。 - Nacos 配置文件导出:对于非常重要的配置,可以考虑定期通过 Nacos Open API 将配置数据导出为文件,存档到对象存储(如 S3、OSS)中。
- 恢复演练:定期测试备份数据的恢复流程,确保在极端情况下(如误删命名空间、数据库逻辑错误)能快速恢复业务。
5. 常见问题排查与运维技巧
在实际运维中,你肯定会遇到各种问题。这里记录几个典型场景和排查思路。
5.1 集群节点无法形成集群,节点列表一直显示 DOWN
这是最常见的问题。
- 排查思路:
- 检查
cluster.conf文件:进入 Pod 查看/home/nacos/conf/cluster.conf内容是否正确。命令:kubectl -n nacos exec nacos-0 -- cat /home/nacos/conf/cluster.conf。确认里面的域名是否能解析到正确的 Pod IP。可以在 Pod 内用nslookup或ping测试。 - 检查网络连通性:确认 Pod 之间网络是否互通,特别是端口
8848(HTTP API)、9848(Raft RPC) 是否开放。可以使用telnet命令在 Pod 内测试:kubectl -n nacos exec nacos-0 -- telnet nacos-1.nacos-headless.nacos.svc.cluster.local 9848。 - 检查日志:查看 Nacos 节点的日志,重点关注
alipay-jraft.log和nacos-cluster.log。常见的错误有:连接被拒绝、认证失败、节点 ID 冲突等。 - 检查数据库连接:确认所有节点都能正常连接外置 MySQL。查看日志中是否有数据库连接异常。确认数据库用户有足够的权限。
- 检查防火墙或网络策略:如果 K8s 集群使用了网络插件(如 Calico, Cilium)并配置了 NetworkPolicy,需要确保
nacos命名空间内的 Pod 允许相互访问所需的端口。
- 检查
5.2 Pod 重启后数据“丢失”或服务列表为空
- 可能原因 1:使用了嵌入式 Derby。这是最可能的原因。Pod 重启后挂载了新的空卷,数据自然没了。解决方案:立刻切换到外置 MySQL。
- 可能原因 2:数据库连接失败。Pod 启动时无法连接 MySQL,导致从空数据库初始化。检查数据库服务状态和连接配置。
- 可能原因 3:客户端未正确配置集群地址。客户端只连接了某一个 Pod,该 Pod 重启期间,客户端无法上报心跳,导致服务实例被摘除。解决方案:客户端应配置所有 Nacos 节点的地址(通过前面提到的 Headless Service 域名列表),或配置一个负载均衡器(如 Nginx)地址。
5.3 客户端报错 “Connection refused” 或 “no server available”
- 排查思路:
- 检查客户端配置的地址:确认客户端配置的 Nacos 服务器地址是否正确,是否能从客户端网络环境解析和访问。如果是 K8s 内部的服务,应用
nacos.nacos.svc.cluster.local:8848。如果是外部,则是 NodePort 或 Ingress 地址。 - 检查 Nacos Service 和 Pod 状态:
kubectl -n nacos get svc,ep查看 Service 的 Endpoints 是否正常包含了所有健康的 Pod IP。 - 检查 Pod 的 readinessProbe:如果 readinessProbe 失败,Pod 不会加入到 Service 的 Endpoints 列表。检查 Pod 的
readiness状态和日志。 - 检查端口映射:确认 Service 的
targetPort与容器暴露的containerPort一致。
- 检查客户端配置的地址:确认客户端配置的 Nacos 服务器地址是否正确,是否能从客户端网络环境解析和访问。如果是 K8s 内部的服务,应用
5.4 内存占用过高或频繁 Full GC
- 可能原因:注册的服务实例或配置项数量极大(例如数十万)。
- 优化建议:
- 调整 JVM 参数:在 StatefulSet 的环境变量中调整
JVM_XMS,JVM_XMX,JVM_XMN。适当增加堆内存,并优化新生代与老年代比例。可以加入 GC 日志参数方便分析。 - 启用 Nacos 的数据分片和路由功能:对于超大规模场景,可以考虑部署多个 Nacos 集群,并通过域名或负载均衡进行分片,但这会引入额外的运维复杂度。
- 清理无用数据:建立定期清理机制,通过 Nacos API 清理长期离线的服务实例和不再使用的配置。
- 升级硬件:为运行 Nacos 的 K8s Node 节点分配更多内存。
- 调整 JVM 参数:在 StatefulSet 的环境变量中调整
5.5 运维技巧:优雅升降级与配置热更新
- 滚动更新:直接修改 StatefulSet 的镜像版本,K8s 会默认以滚动更新的方式逐个替换 Pod。由于我们使用外置数据库,每个新 Pod 启动后都能连接到中心数据库,因此滚动更新对服务影响很小。建议先更新一个 Pod,观察稳定后再更新其余。
- 配置热更新:修改 ConfigMap 后,需要让 Pod 内的 Nacos 重新加载配置。Nacos 本身不支持直接监听 ConfigMap 变化。通常有两种做法:
- 重启 Pod:这是最直接的方式。可以通过
kubectl rollout restart statefulset nacos -n nacos命令来滚动重启所有 Pod。对于高可用集群,短暂重启一个 Pod 通常不影响整体服务。 - 使用 Sidecar 同步:使用一个像
Reloader这样的工具,或者在 Pod 内增加一个 Sidecar 容器来监听 ConfigMap 变化,然后通过发送信号或调用 API 的方式通知 Nacos 重载配置。这种方法更复杂,但无需重启。对于生产环境,如果配置不常变,采用滚动重启的方式更简单可靠。
- 重启 Pod:这是最直接的方式。可以通过
部署和维护一个高可用的 Nacos 集群,是微服务稳定性的一块重要基石。在 K8s 上做这件事,虽然初期配置看起来有些繁琐,但一旦跑通,其带来的自动化运维、弹性伸缩和故障自愈能力,是传统部署方式难以比拟的。最关键的就是吃透 StatefulSet、Headless Service 和外置数据库这几个核心概念,剩下的就是根据实际业务负载,在监控数据的指导下进行细致的调优和加固。