Kubernetes上部署高可用Nacos集群:生产级架构设计与实战
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_BOTH
2. 创建 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: nacos
4. 创建 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: data-volume
mountPath: /home/nacos/data
volumes:
- name: config-volume
configMap:
name: nacos-config # 引用前面创建的ConfigMap
# 持久化卷声明模板,为每个Pod创建独立的PVC
volumeClaimTemplates:
- metadata:
name: data-volume
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "standard" # 替换为你的实际StorageClass
resources:
requests:
storage: 5Gi
- metadata:
name: logs-volume
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "standard"
resources:
requests:
storage: 2Gi
5. 创建 Secret 存储数据库密码 (
nacos-secret.yaml
)
:
安全地管理敏感信息。
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!' | base64
3.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 和外置数据库这几个核心概念,剩下的就是根据实际业务负载,在监控数据的指导下进行细致的调优和加固。
更多推荐
所有评论(0)