这一篇只做一件事:在一个已经具备 CNI、DNS 和动态存储的 Kubernetes 集群中,搭建三节点 Nacos。

系列导航:Kubernetes 重建迁移教程系列。完成本篇后,再做 Service DNS 治理和平滑升级。

文中的镜像版本、域名、密码和地址均使用占位符。正式部署前,要根据目标 Nacos 镜像核对环境变量和数据库 schema,不能直接使用 latest。

先理解最终结构

Nacos 只有三个 Pod,但需要两个 Service:

flowchart LR
  A["应用客户端"] --> B["nacos-client ClusterIP"]
  B --> C["nacos-0"]
  B --> D["nacos-1"]
  B --> E["nacos-2"]
  C <--> F["nacos-headless"]
  D <--> F
  E <--> F
  C --> G["外部 MySQL"]
  D --> G
  E --> G
  • nacos-headless 的 clusterIP: None,为 StatefulSet Pod 提供稳定 DNS,供三个 Nacos 节点互相发现。
  • nacos-client 是普通 ClusterIP,应用只连接它,不关心某个 Pod IP。
  • 配置与服务注册数据写入外部 MySQL,Pod 重建不会丢失。

Headless Service 不是多余的 Nacos 实例,业务客户端也无需直接使用它。

前置条件

先确认集群基础能力:

kubectl get nodes
kubectl get storageclass
kubectl -n kube-system get deployment coredns
kubectl run dns-smoke --image=<approved-busybox-image> --restart=Never \
  --command -- nslookup kubernetes.default.svc.cluster.local
kubectl logs dns-smoke
kubectl delete pod dns-smoke

要求:

  • 至少三个可调度工作节点。
  • 默认 StorageClass 能动态创建 PVC,或已经准备好静态 PV。
  • 外部 MySQL 已具备高可用和独立备份。
  • Nacos 镜像已经同步到内部镜像仓库并固定 digest。
  • 节点到 MySQL 的网络和 DNS 已验证。

第 1 步:准备命名空间与数据库

创建独立命名空间:

kubectl create namespace nacos-system

从目标 Nacos 发行包中获取对应数据库的 mysql-schema.sql,先在测试库执行,再由数据库管理员应用到正式库。不要从旧博客复制版本不明的 SQL。

执行后至少确认核心表存在:

SHOW TABLES;
SELECT COUNT(*) FROM config_info;
SELECT COUNT(*) FROM tenant_info;

数据库账号只授予 Nacos schema 所需的权限,不要在应用 Pod 中使用 MySQL root。

第 2 步:生成鉴权材料与 Secret

Nacos 鉴权 token 使用 32 字节以上的随机值再做 Base64。以下命令只把结果保存在当前 shell 变量中:

NACOS_RAW_TOKEN="$(openssl rand -hex 32)"
NACOS_AUTH_TOKEN="$(printf '%s' "$NACOS_RAW_TOKEN" | base64)"

kubectl -n nacos-system create secret generic nacos-secret \
  --from-literal=mysql-host='<mysql-host>' \
  --from-literal=mysql-port='<mysql-port>' \
  --from-literal=mysql-database='<mysql-database>' \
  --from-literal=mysql-username='<mysql-username>' \
  --from-literal=mysql-password='<mysql-password>' \
  --from-literal=auth-token="$NACOS_AUTH_TOKEN" \
  --from-literal=identity-key='serverIdentity' \
  --from-literal=identity-value='<random-identity-value>'

unset NACOS_RAW_TOKEN NACOS_AUTH_TOKEN

Secret 不应写进 Git。提交到仓库的应是 ExternalSecret、SealedSecret 模板,或一份不含值的创建说明。

第 3 步:创建两个 Service

保存为 00-services.yaml:

apiVersion: v1
kind: Service
metadata:
  name: nacos-headless
  namespace: nacos-system
spec:
  clusterIP: None
  publishNotReadyAddresses: true
  selector:
    app.kubernetes.io/name: nacos
  ports:
    - name: http
      port: 8848
    - name: grpc-client
      port: 9848
    - name: grpc-server
      port: 9849
    - name: raft
      port: 7848
---
apiVersion: v1
kind: Service
metadata:
  name: nacos-client
  namespace: nacos-system
spec:
  selector:
    app.kubernetes.io/name: nacos
  ports:
    - name: http
      port: 8848
      targetPort: http
    - name: grpc-client
      port: 9848
      targetPort: grpc-client
    - name: console
      port: 8080
      targetPort: console

应用的地址应写成:

nacos-client.nacos-system.svc.cluster.local:8848

创建并检查:

kubectl apply -f 00-services.yaml
kubectl -n nacos-system get service

nacos-headless 的 ClusterIP 应显示为 None,nacos-client 应获得普通 ClusterIP。

第 4 步:创建三节点 StatefulSet

保存为 01-statefulset.yaml。下面展示核心结构,环境变量名称必须对照你固定的目标镜像再次核对:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: nacos
  namespace: nacos-system
spec:
  serviceName: nacos-headless
  replicas: 3
  podManagementPolicy: Parallel
  updateStrategy:
    type: RollingUpdate
  selector:
    matchLabels:
      app.kubernetes.io/name: nacos
  template:
    metadata:
      labels:
        app.kubernetes.io/name: nacos
    spec:
      terminationGracePeriodSeconds: 60
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchLabels:
                  app.kubernetes.io/name: nacos
              topologyKey: kubernetes.io/hostname
      containers:
        - name: nacos
          image: <image-registry>/nacos-server:<approved-version>@sha256:<digest>
          imagePullPolicy: IfNotPresent
          ports:
            - { name: http, containerPort: 8848 }
            - { name: grpc-client, containerPort: 9848 }
            - { name: grpc-server, containerPort: 9849 }
            - { name: raft, containerPort: 7848 }
            - { name: console, containerPort: 8080 }
          env:
            - { name: MODE, value: cluster }
            - { name: PREFER_HOST_MODE, value: hostname }
            - {
                name: NACOS_SERVERS,
                value: "nacos-0.nacos-headless.nacos-system.svc.cluster.local:8848 nacos-1.nacos-headless.nacos-system.svc.cluster.local:8848 nacos-2.nacos-headless.nacos-system.svc.cluster.local:8848",
              }
            - { name: SPRING_DATASOURCE_PLATFORM, value: mysql }
            - name: MYSQL_SERVICE_HOST
              valueFrom:
                { secretKeyRef: { name: nacos-secret, key: mysql-host } }
            - name: MYSQL_SERVICE_PORT
              valueFrom:
                { secretKeyRef: { name: nacos-secret, key: mysql-port } }
            - name: MYSQL_SERVICE_DB_NAME
              valueFrom:
                { secretKeyRef: { name: nacos-secret, key: mysql-database } }
            - name: MYSQL_SERVICE_USER
              valueFrom:
                { secretKeyRef: { name: nacos-secret, key: mysql-username } }
            - name: MYSQL_SERVICE_PASSWORD
              valueFrom:
                { secretKeyRef: { name: nacos-secret, key: mysql-password } }
            - { name: NACOS_AUTH_ENABLE, value: "true" }
            - name: NACOS_AUTH_TOKEN
              valueFrom:
                { secretKeyRef: { name: nacos-secret, key: auth-token } }
            - name: NACOS_AUTH_IDENTITY_KEY
              valueFrom:
                { secretKeyRef: { name: nacos-secret, key: identity-key } }
            - name: NACOS_AUTH_IDENTITY_VALUE
              valueFrom:
                { secretKeyRef: { name: nacos-secret, key: identity-value } }
          startupProbe:
            httpGet: { path: /nacos/v1/console/health/started, port: http }
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 30
          readinessProbe:
            httpGet: { path: /nacos/v1/console/health/readiness, port: http }
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 6
          livenessProbe:
            httpGet: { path: /nacos/v1/console/health/liveness, port: http }
            periodSeconds: 20
            timeoutSeconds: 5
            failureThreshold: 6
          resources:
            requests: { cpu: "1", memory: 2Gi }
            limits: { cpu: "2", memory: 4Gi }

探针路径可能随目标版本变化。先用目标镜像在测试环境 curl 验证,再写进生产清单。

创建 StatefulSet:

kubectl apply --server-side --dry-run=server -f 01-statefulset.yaml
kubectl apply -f 01-statefulset.yaml
kubectl -n nacos-system rollout status statefulset/nacos --timeout=15m
kubectl -n nacos-system get pods -o wide

三个 Pod 应分布在三台不同的节点。若有 Pod Pending,先看事件:

kubectl -n nacos-system describe pod nacos-2

如果集群只有两台合适的节点,required 反亲和会阻止第三个 Pod 调度。这是容量规划问题,不应通过删除反亲和来掩盖。

第 5 步:验证节点发现、数据库和 Service

逐个查看日志:

for pod in nacos-0 nacos-1 nacos-2; do
  echo "===== ${pod} ====="
  kubectl -n nacos-system logs "$pod" --tail=100
done

检查 DNS:

kubectl -n nacos-system exec nacos-0 -- \
  getent hosts nacos-1.nacos-headless.nacos-system.svc.cluster.local

检查 Service Endpoint:

kubectl -n nacos-system get endpointslice \
  -l kubernetes.io/service-name=nacos-client -o wide

EndpointSlice 中应该有三个 Ready 地址。最后从临时 Pod 访问客户端 Service:

kubectl -n nacos-system run nacos-smoke --rm -it --restart=Never \
  --image=<approved-curl-image> -- \
  curl -fsS http://nacos-client:8848/nacos/v1/console/health/readiness

第 6 步:接入第一个应用

先选一个非核心应用,不要一次性修改所有命名空间:

spring.cloud.nacos.server-addr=nacos-client.nacos-system.svc.cluster.local:8848
spring.cloud.nacos.username=${NACOS_USERNAME}
spring.cloud.nacos.password=${NACOS_PASSWORD}

发布后同时检查:

kubectl -n <app-namespace> rollout status deployment/<app> --timeout=10m
kubectl -n <app-namespace> logs deployment/<app> --since=10m \
  | grep -Ei 'nacos|register|error|exception'

再从 Nacos 控制台或 API 检查实例,而不是只看应用 Pod 是否为 Running。

完成定义

只有同时满足以下条件,Nacos 集群才算搭建完成:

  • 三个 Nacos Pod 位于三个不同节点。
  • 三个 Pod 都能解析另外两个 Pod 的 Headless DNS。
  • nacos-client 有三个 Ready Endpoint。
  • 配置发布、读取和历史版本正常。
  • 测试应用能注册、发现并调用另一个服务。
  • 删除任意一个 Nacos Pod 后,应用仍能读取配置和发现服务。
  • MySQL 与 Nacos 配置都具备独立备份和恢复步骤。

官方快速开始特别说明,其默认清单仅适合测试;生产环境应启用鉴权、使用可靠数据库和持久化方案,并部署在隔离内网。参考:Nacos Kubernetes Quick Start 与 Nacos 集群模式部署。