10-29-Kubernetes动态卷供应StatefulSet与ETCD

Kubernetes 小白教程:动态卷供应 · StatefulSet · etcd

日期:2026-10-29

面向:零基础小白,每条命令可直接复制执行(行尾 ------ 是注释,别复制进去)


写在前面

这篇教程由课堂笔记改写,一共三大块,先记住每块是干啥的:

  • 动态卷供应:让 K8s 自动帮你造存储卷,不用找管理员手动建。
  • StatefulSet:专门管"有状态"应用(数据库、集群中间件),让每个 Pod 有固定名字、固定网络身份、不丢数据。
  • etcd:K8s 背后的"数据库",集群所有状态都存在它里面。

每节都按"这是干啥 → 命令 → 预期结果 → 清理"来写,命令可直接复制(提示符 root@xxx:~# 表示在哪台机器上执行)。


一、动态卷供应(StorageClass 自动造卷)

1.1 这是干啥

以前要建存储卷(PV),都得管理员手动操作,这叫静态卷------很麻烦。

动态卷供应就是:用户写一张"申请单"(PVC),K8s 收到后自动调用"造卷工具"造出一个 PV 并绑给申请单,全程不用管理员插手。

1.2 三个角色一句话

角色 类比 作用
StorageClass 存储套餐模板 定义用什么工具造卷、卷怎么回收
PVC 存储申请单 用户写:我要多大、用哪个套餐
PV 实际造出的卷 系统自动造,用户不直接管

流程:管理员建 StorageClass(套餐)→ 用户建 PVC(申请单,指定套餐名)→ 系统自动造 PV 并绑定。

注意两点:用户选套餐靠 StorageClass 的名字(所以命名很重要);StorageClass 建好后名字和参数不能改,要改只能删了重建。

1.3 StorageClass 核心配置速查

  • provisioner:造卷工具,必填。名字以 kubernetes.io/ 开头的是内置的(AWS EBS、Azure Disk 等);NFS、Local 这类没有内置工人,要自己装外置 provisioner。
存储类型 是否内置 说明
AWSElasticBlockStore 内置 亚马逊云硬盘
AzureFile / AzureDisk 内置 微软云文件/硬盘
Ceph RBD / CephFS 需自装 Ceph 块存储/文件系统
iSCSI / Glusterfs 需自装 分布式存储
NFS 需自装 网络文件系统
Local 需自装 本地文件系统
  • reclaimPolicy(卷回收策略):
    • Delete(默认):PVC 删了,PV 和后端存储(云盘/NFS目录)一起删。
    • Retain:PVC 删了 PV 保留,后端数据也不删,管理员手动处理。
  • volumeBindingMode(绑定时机):
    • Immediate(立即):一建 PVC 就造卷,适合所有节点都能访问的存储(如云盘、NFS)。
    • WaitForFirstConsumer(等使用者):等第一个 Pod 要用时,在 Pod 所在节点造卷,适合本地存储。
  • mountOptions:挂载附加参数(权限、调试模式等),填错会导致挂载失败。
  • parameters:给造卷工具的参数,如存储类型 SSD/HDD、副本数,不同工具不一样。

StorageClass 最小示例:

bash 复制代码
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: standard            # 套餐名,用户 PVC 里要填这个名字
provisioner: kubernetes.io/aws-ebs   # 用 AWS EBS 的造卷工具
parameters:
  type: gp2                 # 存储类型 gp2(AWS 通用型 SSD)
reclaimPolicy: Retain       # PVC 删了卷保留
allowVolumeExpansion: true  # 允许扩容
volumeBindingMode: Immediate

1.4 实验 A:Local Path 本地动态卷

适用:要用节点本地磁盘的场景(速度快,但卷只能被所在节点用)。

步骤 0:下载并部署制备器(造卷工具)

在 master30 上执行:

bash 复制代码
wget https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml
kubectl apply -f local-path-storage.yaml
kubectl get sc local-path

预期:

bash 复制代码
NAME         PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE
local-path   rancher.io/local-path   Delete          WaitForFirstConsumer

这个模板文件里一共 5 类资源,各干各的:

资源 名称 作用
Namespace local-path-storage 隔离这套资源
ConfigMap local-path-config 定义本地存储路径规则(默认 /opt/local-path-provisioner)
ServiceAccount + RBAC local-path-provisioner 给造卷工具操作 PV/PVC 的权限
Deployment local-path-provisioner 跑造卷工具程序本体
StorageClass local-path 提供给 PVC 的套餐(WaitForFirstConsumer)

注意看 StorageClass 是 WaitForFirstConsumer:PVC 建出来后先卡在 Pending,等 Pod 真正要用、调度到某节点后才在节点上造卷。

步骤 1:创建 PVC(申请单)

bash 复制代码
cat > local-path-pvc.yaml <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: local-path-pvc
  namespace: default
spec:
  accessModes: ["ReadWriteOnce"]
  resources:
    requests:
      storage: 5Gi
  storageClassName: local-path   # 指定套餐名
EOF

kubectl apply -f local-path-pvc.yaml
kubectl get pvc

预期:local-path-pvc 状态是 Pending(正常,等 Pod 关联才造卷)。

步骤 2:创建 Deployment

bash 复制代码
cat > local-path-deployment.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: webapp
  name: webapp
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: webapp
  template:
    metadata:
      labels:
        app: webapp
    spec:
      containers:
      - image: hub.laoma.cloud/library/nginx
        name: nginx
        volumeMounts:
        - name: webapp-data
          mountPath: /usr/share/nginx/html   # nginx 网页目录
      volumes:
      - name: webapp-data
        persistentVolumeClaim:
          claimName: local-path-pvc          # 挂这个 PVC
EOF

kubectl apply -f local-path-deployment.yaml
kubectl get pods -o wide
kubectl get pvc

预期:2 个 pod 都 Running,而且全部调度到 worker31(因为卷只能在 worker31 上造);PVC 从 Pending 变成 Bound。

步骤 3:验证(写入文件,看本地目录)

bash 复制代码
kubectl exec webapp-6d46b54487-6nqmh -- bash -c 'echo Hello World From Nginx > /usr/share/nginx/html/index.html'
ls /opt/local-path-provisioner/pvc-e992833c-af6a-4d4d-aff5-fc544394db6d_default_local-path-pvc/   ------ 在 worker31 上执行
cat /opt/local-path-provisioner/pvc-e992833c-af6a-4d4d-aff5-fc544394db6d_default_local-path-pvc/index.html   ------ 在 worker31 上执行

预期:worker31 的 /opt/local-path-provisioner/ 下面出现和 PVC 对应的目录,里面有 index.html,内容是 Hello World From Nginx。这说明 Pod 里写的文件真的落到了节点本地磁盘。

步骤 4:清理(验证 Delete 回收)

bash 复制代码
kubectl delete deployment webapp
kubectl delete pvc local-path-pvc
ls /opt/local-path-provisioner/pvc-e992833c-af6a-4d4d-aff5-fc544394db6d_default_local-path-pvc/   ------ 在 worker31 上执行

预期:删掉 PVC 后,worker31 上对应的本地目录也没了(Delete 策略生效,后端存储被清掉)。

1.5 实验 B:NFS 网络动态卷

适用:网络共享存储,所有节点都能访问(后面 StatefulSet 的多副本场景必须用它)。

步骤 0:部署 NFS 服务端和客户端

bash 复制代码
apt install -y nfs-kernel-server
mkdir -m 777 /shares/
cat << EOF > /etc/exports
/shares *(rw,sync,no_root_squash,no_all_squash,insecure)
EOF

systemctl restart nfs-server.service

apt install -y nfs-common
apt install -y nfs-common

这是干啥:master30 当 NFS 服务器,把 /shares 目录共享出去;worker31/32 装客户端。

步骤 1:创建命名空间 + RBAC 权限

bash 复制代码
kubectl create ns kube-storage
kubectl config set-context --current --namespace kube-storage

cat > nfs-rbac.yaml <<'EOF'
apiVersion: v1
kind: ServiceAccount
metadata:
  name: nfs-client-provisioner
  namespace: kube-storage
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: nfs-client-provisioner-runner
rules:
  - apiGroups: [""]
    resources: ["persistentvolumes"]
    verbs: ["get", "list", "watch", "create", "delete"]
  - apiGroups: [""]
    resources: ["persistentvolumeclaims"]
    verbs: ["get", "list", "watch", "update"]
  - apiGroups: ["storage.k8s.io"]
    resources: ["storageclasses"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["events"]
    verbs: ["create", "update", "patch"]
  - apiGroups: [""]
    resources: ["endpoints"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: run-nfs-client-provisioner
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    namespace: kube-storage
roleRef:
  kind: ClusterRole
  name: nfs-client-provisioner-runner
  apiGroup: rbac.authorization.k8s.io
EOF

kubectl apply -f nfs-rbac.yaml

这是干啥:造卷工具要代替你操作 PV/PVC,必须先给它发"权限证"(ServiceAccount + 角色绑定)。

步骤 2:部署 NFS Provisioner 应用

bash 复制代码
cat > nfs-provisioner.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nfs-client-provisioner
  namespace: kube-storage
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: nfs-client-provisioner
  template:
    metadata:
      labels:
        app: nfs-client-provisioner
    spec:
      serviceAccountName: nfs-client-provisioner
      volumes:
      - name: nfs-client-root
        nfs:
          server: 10.1.8.30      # NFS 服务器 IP
          path: /shares          # NFS 共享路径
      containers:
      - name: nfs-client-provisioner
        image: hub.laoma.cloud/sig-storage/nfs-subdir-external-provisioner:v4.0.2
        volumeMounts:
        - name: nfs-client-root
          mountPath: /persistentvolumes
        env:
        - name: PROVISIONER_NAME
          value: fuseim.pri/ifs   # 造卷工具的名字,后面 StorageClass 要引用
        - name: NFS_SERVER
          value: 10.1.8.30
        - name: NFS_PATH
          value: /shares
EOF

kubectl apply -f nfs-provisioner.yaml
kubectl get deployments.apps -n kube-storage

预期:nfs-client-provisioner 1/1 Running。它是"造卷工人",连上 NFS 服务器,谁申请卷它就在 /shares 下建目录。

步骤 3:创建 NFS StorageClass

bash 复制代码
cat > nfs-storageclass.yaml <<'EOF'
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-storage
  namespace: kube-storage
provisioner: fuseim.pri/ifs      # 必须和上面 PROVISIONER_NAME 一致
parameters:
  onDelete: retain               # 删 PVC 时保留 NFS 里的数据
  archiveOnDelete: "false"
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: Immediate
EOF

kubectl apply -f nfs-storageclass.yaml
kubectl get sc

预期:nfs-storage 的 PROVISIONER 是 fuseim.pri/ifs,VOLUMEBINDINGMODE 是 Immediate(一建 PVC 就造卷,所以后面 PVC 秒变 Bound)。

步骤 4:创建 PVC + Deployment

bash 复制代码
kubens default

cat > nfs-pvc.yaml <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: webclaim
  namespace: default
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: "nfs-storage"
EOF

kubectl apply -f nfs-pvc.yaml
kubectl get pvc
kubectl get pv
ls /shares/

预期:webclaim 秒变 Bound;自动出现一个 PV;NFS 的 /shares 下自动多了目录 default-webclaim-pvc-xxxx。

在 NFS 里先写好网页内容:

bash 复制代码
echo Hello World From Nginx > /shares/default-webclaim-pvc-6a020ff4-6b7f-4ef4-90fc-3dc94d3e8ddc/index.html

创建 Deployment 并暴露服务:

bash 复制代码
cat > nfs-deploy.yaml << EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: webapp
  name: webapp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: webapp
  template:
    metadata:
      labels:
        app: webapp
    spec:
      containers:
      - image: hub.laoma.cloud/library/nginx
        name: nginx
        volumeMounts:
        - name: webapp-data
          mountPath: /usr/share/nginx/html
      volumes:
      - name: webapp-data
        persistentVolumeClaim:
          claimName: webclaim
EOF

kubectl apply -f nfs-deploy.yaml
kubectl expose deployment webapp --target-port 80 --port 80 --protocol TCP
kubectl get svc webapp

步骤 5:验证(curl 访问)

bash 复制代码
curl 10.109.140.205

预期:输出 Hello World From Nginx。因为两个 Pod 挂的是同一个 NFS 目录,页面内容一致。

步骤 6:清理(对比:NFS 数据保留)

bash 复制代码
kubectl delete deployments.apps webapp
kubectl delete pvc webclaim
kubectl get pv
ls /shares/default-webclaim-pvc-6a020ff4-6b7f-4ef4-90fc-3dc94d3e8ddc/

预期:PVC 删了,PV 也没了;但 /shares 下那个目录还在(onDelete: retain 生效),数据没丢。

保留 NFS provisioner 别删,下一章 StatefulSet 实验还要用它。

1.6 动态卷小结

  • Local Path:本地盘造卷,快,但卷只能被所在节点用,所以 Pod 全挤到一台节点。
  • NFS:网络共享造卷,所有节点都能访问,多副本、有状态场景首选。
  • Delete 策略:PVC 删了后端数据跟着删;Retain / onDelete: retain:数据保留。

二、StatefulSet(有状态应用)

2.1 这是干啥

Deployment 管的是"无状态"应用:所有实例一模一样,删了重建毫无影响。但数据库、Redis、etcd 这类"有状态"应用不行------每个实例有身份、有数据,随便替换会出事。

StatefulSet 就是专门管有状态应用的,核心三件事:给每个 Pod 固定名字、固定网络身份、绑定固定的存储卷。

无状态 vs 有状态:

维度 无状态(Deployment) 有状态(StatefulSet)
数据 不落地,删了无所谓 必须持久化,依赖固定存储
实例 完全一样,随便替换 有身份(编号),启动/缩容有顺序
网络 共享 IP,无固定标识 固定名字 + 固定 DNS

最佳实践:把应用拆成"无状态业务层 + 有状态数据层"。

2.2 StatefulSet 四大特性

  • 固定身份:每个 Pod 名字是「StatefulSet名-序号」,从 0 开始(nginx-0、nginx-1...);hostname 固定,通过 Headless Service 提供固定 DNS(如 nginx-0.nginx)。
  • 有序操作:扩容按 0→1→2 逐个建,前一个 Ready 才建下一个;缩容从大到小删;更新从最后一个开始滚。
  • 稳定存储:用 volumeClaimTemplates(PVC 模板)给每个 Pod 自动建独立 PVC,名字格式「模板名-StatefulSet名-序号」(如 nginx-data-nginx-0)。Pod 删了 PVC/PV 还在,重建自动复用,数据不丢。
  • 必须配 Headless Service:ClusterIP 设 None,它不做负载均衡,只负责给每个 Pod 提供固定 DNS。

2.3 StatefulSet vs Deployment

控制器 StatefulSet Deployment
适用 数据库、分布式集群、消息队列 Web 服务、API、静态服务
网络 Headless Service(固定 DNS) ClusterIP(共享 IP)
存储 必须 PVC + PV 可选 PVC
扩缩容 按编号顺序(0→1→2) 无顺序,瞬间完成
重启 恢复原数据/身份 无影响

2.4 准备环境

用上一章保留的 NFS provisioner,再建一个命名空间:

bash 复制代码
kubectl create ns statefulset
kubectl config set-context --current --namespace statefulset

后面所有实验命令都在 statefulset 命名空间执行。

2.5 实验 1:Nginx 3 节点集群(核心理解)

需求:3 节点 Nginx,数据持久化、固定网络标识、有序部署。

步骤 1:创建 Headless Service

bash 复制代码
cat > nginx-service.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
  name: nginx
  namespace: statefulset
spec:
  selector:
    app: nginx
  clusterIP: None        # 关键:Headless,不给虚拟 IP,只给 DNS
  ports:
  - port: 80
    targetPort: 80
    name: nginx-port
EOF

kubectl apply -f nginx-service.yaml
kubectl get svc

预期:nginx 的 CLUSTER-IP 是 None。

步骤 2:创建 StatefulSet

bash 复制代码
cat > nginx-statefulset.yaml <<'EOF'
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: nginx
spec:
  selector:
    matchLabels:
      app: nginx
  serviceName: "nginx"        # 关联上面的 Headless Service
  replicas: 3
  minReadySeconds: 10
  template:
    metadata:
      labels:
        app: nginx
    spec:
      terminationGracePeriodSeconds: 10
      containers:
      - name: nginx
        image: hub.laoma.cloud/library/nginx:latest
        ports:
        - containerPort: 80
          name: nginx
        volumeMounts:
        - name: nginx-data
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:        # PVC 模板:每个 Pod 自动一个独立 PVC
  - metadata:
      name: nginx-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "nfs-storage"   # 用上一章 NFS 套餐
      resources:
        requests:
          storage: 10Gi
EOF

kubectl apply -f nginx-statefulset.yaml
watch kubectl get pods

预期:Pod 按顺序出现------先 nginx-0 Running,约 20 秒后 nginx-1,再 nginx-2。这就是有序部署。

bash 复制代码
kubectl get sts
watch "kubectl get pvc | awk '/nginx-data-nginx/ {print $1,$3}'"

预期:自动出现 3 个独立 PVC:nginx-data-nginx-0 / -1 / -2,各绑一个 PV。

步骤 3:给每个 Pod 写不同数据

bash 复制代码
kubectl get pvc|awk '{print $1,$3}'
echo nginx-data-nginx-0 > /shares/statefulset-nginx-data-nginx-0-pvc-8eb06bf1-a5de-4e94-bf83-514941f73c9f/index.html
echo nginx-data-nginx-1 > /shares/statefulset-nginx-data-nginx-1-pvc-e853eccd-8060-4326-8c45-0b88c34a3d97/index.html
echo nginx-data-nginx-2 > /shares/statefulset-nginx-data-nginx-2-pvc-27d944f7-1e62-4352-9023-3874e6867ef9/index.html

NFS 目录名规则:命名空间-StatefulSet名-PVC名-PV名。也可以写个脚本批量做:

bash 复制代码
cat > prepare_nginx_content.sh <<'EOF'
nfs_share_path=/shares
namespace=statefulset
kubectl get pvc | awk '/nginx-data-nginx/ {print $1,$3}' > pvc.list
cat pvc.list | while read pvc
do
  pvc_name=$(echo -n $pvc | awk '{print $1}')
  pv_name=$(echo -n $pvc | awk '{print $2}')
  echo ${pvc_name} > ${nfs_share_path}/${namespace}-${pvc_name}-${pv_name}/index.html
done
rm pvc.list
EOF

bash prepare_nginx_content.sh

步骤 4:验证

验证固定 DNS(每个 Pod 一个域名):

bash 复制代码
kubectl run test --image nginx -it -- bash
curl nginx-0.nginx
curl nginx-1.nginx
curl nginx-2.nginx

预期:依次返回 nginx-data-nginx-0 / -1 / -2。域名规则:Pod名.Service名。

验证持久化(删 Pod 数据不丢):

bash 复制代码
kubectl delete pod nginx-2
kubectl get pods
kubectl exec test -it -- bash
curl nginx-2.nginx

预期:nginx-2 重建后(名字不变、IP 可能变),数据还是 nginx-data-nginx-2------因为重建时自动复用了原来的 PVC/PV。

思考题:怎么给 StatefulSet 一个统一的 IP 入口?

答案:再建一个普通 ClusterIP 的 Service 就行。

bash 复制代码
cat > nginx-service-www.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
  labels:
    app: www
  name: www
spec:
  ports:
  - name: 80-80
    port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx
  type: ClusterIP
EOF

kubectl apply -f nginx-service-www.yaml
kubectl get svc www
curl 10.100.163.95
curl 10.100.163.95
curl 10.100.163.95

预期:同一个 IP,多次访问轮询到不同 Pod(nginx-data-nginx-0 / -1 / -2)。

步骤 5:清理

bash 复制代码
kubectl delete sts nginx
kubectl delete svc nginx www
kubectl delete pvc nginx-data-nginx-{0..2}
rm -fr /shares/statefulset-nginx-data-nginx-{0..2}-*
kubectl delete pod test

2.6 实验 2:Etcd 3 节点集群

需求:3 节点 etcd,数据持久化、固定网络标识、集群自动组网。

步骤 1:创建 Headless Service

bash 复制代码
cat > etcd-service.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
  name: etcd
  namespace: statefulset
  labels:
    app: etcd
spec:
  ports:
  - port: 2379
    name: client   # 客户端端口
  - port: 2380
    name: peer     # 节点间通信端口
  clusterIP: None
  selector:
    app: etcd
EOF

kubectl apply -f etcd-service.yaml
kubectl get svc

步骤 2:创建 StatefulSet

bash 复制代码
cat > etcd-statefulset.yaml <<'EOF'
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: etcd
  namespace: statefulset
spec:
  serviceName: etcd
  replicas: 3
  selector:
    matchLabels:
      app: etcd
  template:
    metadata:
      labels:
        app: etcd
    spec:
      containers:
      - name: etcd
        image: hub.laoma.cloud/kubernetes/etcd:3.5.12-0
        command:
        - /usr/local/bin/etcd
        ports:
        - containerPort: 2379
          name: client
        - containerPort: 2380
          name: peer
        env:
        - name: ETCD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name   # 节点名 = Pod 名(etcd-0/1/2)
        - name: ETCD_DATA_DIR
          value: /var/lib/etcd
        - name: ETCD_INITIAL_ADVERTISE_PEER_URLS
          value: "http://$(ETCD_NAME).etcd.statefulset.svc.cluster.local:2380"
        - name: ETCD_LISTEN_PEER_URLS
          value: "http://0.0.0.0:2380"
        - name: ETCD_LISTEN_CLIENT_URLS
          value: "http://0.0.0.0:2379"
        - name: ETCD_ADVERTISE_CLIENT_URLS
          value: "http://$(ETCD_NAME).etcd.statefulset.svc.cluster.local:2379"
        - name: ETCD_INITIAL_CLUSTER
          value: "etcd-0=http://etcd-0.etcd.statefulset.svc.cluster.local:2380,etcd-1=http://etcd-1.etcd.statefulset.svc.cluster.local:2380,etcd-2=http://etcd-2.etcd.statefulset.svc.cluster.local:2380"
        - name: ETCD_INITIAL_CLUSTER_TOKEN
          value: "etcd-token"
        - name: ETCD_INITIAL_CLUSTER_STATE
          value: "new"
        volumeMounts:
        - name: etcd-data
          mountPath: /var/lib/etcd
        resources:
          requests:
            cpu: 100m
            memory: 256Mi
          limits:
            cpu: 500m
            memory: 512Mi
  volumeClaimTemplates:
  - metadata:
      name: etcd-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "nfs-storage"
      resources:
        requests:
          storage: 10Gi
EOF

kubectl apply -f etcd-statefulset.yaml
kubectl get pods
kubectl get sts

预期:etcd-0 → etcd-1 → etcd-2 顺序创建,最终 3/3 Running。集群怎么互相发现?靠 ETCD_INITIAL_CLUSTER 里写死的 3 个域名,而域名正好由 StatefulSet 的固定名字 + Headless Service 提供。

步骤 3:验证

验证集群成员:

bash 复制代码
kubectl exec etcd-0 -- etcdctl member list

预期:3 个成员 etcd-0/1/2,状态 started。

验证身份固定(删 Pod 重建,成员 ID 不变):

bash 复制代码
kubectl delete pod etcd-2
kubectl get pods
kubectl exec etcd-0 -- etcdctl member list

预期:etcd-2 重建后,member list 里还是同一个成员 ID(身份没变,集群没乱)。

验证数据同步:

bash 复制代码
kubectl exec etcd-0 -- etcdctl put /users/user1/name laowang
kubectl exec etcd-2 -- etcdctl get /users/user1/name

预期:在 etcd-0 写入,在 etcd-2 能读到(Raft 数据同步)。

步骤 4:清理

bash 复制代码
kubectl delete sts etcd
kubectl delete svc etcd
kubectl delete pvc etcd-data-etcd-{0..2}
rm -fr /shares/statefulset-etcd-data*

2.7 实验 3:Redis 1 主 2 从 + Sentinel 哨兵

需求:3 节点 Redis(1 主 2 从),自动组网、主从同步、哨兵自动故障切换。

步骤 1:创建 Headless Service

bash 复制代码
cat > redis-service.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
  name: redis
  namespace: statefulset
  labels:
    app: redis
spec:
  ports:
  - port: 6379
    name: redis      # redis 服务端口
  - port: 26379
    name: sentinel   # 哨兵端口
  clusterIP: None
  selector:
    app: redis
EOF

kubectl apply -f redis-service.yaml
kubectl get svc

步骤 2:创建 StatefulSet(启动脚本自动组主从 + 哨兵)

bash 复制代码
cat > redis-statefulset.yaml <<'EOF'
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis
  namespace: statefulset
spec:
  serviceName: redis
  replicas: 3
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
      - name: redis
        image: redis:7.0-alpine
        ports:
        - containerPort: 6379
          name: redis
        - containerPort: 26379
          name: sentinel
        command: ["/bin/sh", "-c"]
        args:
        - |
          NODE_ID=$(hostname | awk -F'-' '{print $2}')
          cat > /data/redis.conf << EOF
          bind 0.0.0.0
          protected-mode no
          port 6379
          dir /data
          appendonly yes
          requirepass "redis123"
          masterauth "redis123"
          EOF
          if [ "$NODE_ID" != "0" ]; then
            echo "replicaof redis-0.redis.statefulset.svc.cluster.local 6379" >> /data/redis.conf
          fi
          redis-server /data/redis.conf &

          # 等待主节点域名解析成功
          until nslookup redis-0.redis.statefulset.svc.cluster.local; do
            echo "Waiting for DNS resolution..."
            sleep 2
          done

          cat > /data/sentinel.conf << EOF
          bind 0.0.0.0
          protected-mode no
          port 26379
          sentinel resolve-hostnames yes
          sentinel announce-hostnames yes
          sentinel monitor mymaster redis-0.redis.statefulset.svc.cluster.local 6379 2
          sentinel auth-pass mymaster redis123
          sentinel down-after-milliseconds mymaster 5000
          sentinel failover-timeout mymaster 10000
          EOF
          redis-sentinel /data/sentinel.conf
        volumeMounts:
        - name: redis-data
          mountPath: /data
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 512Mi
  volumeClaimTemplates:
  - metadata:
      name: redis-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "nfs-storage"
      resources:
        requests:
          storage: 10Gi
EOF

kubectl apply -f redis-statefulset.yaml
kubectl get pods
kubectl get sts

启动脚本逻辑(这是自动组网的关键):

  • redis-0:只写主配置(无 replicaof),当主节点。
  • redis-1/2:配置文件里追加 replicaof redis-0...,当从节点。
  • 所有节点再启动哨兵,监视主节点 redis-0。

步骤 3:验证

验证主从:

bash 复制代码
kubectl exec -it redis-0 -- redis-cli -a redis123 info replication

预期关键行:

bash 复制代码
role:master
connected_slaves:2

验证哨兵能识别主节点:

bash 复制代码
kubectl exec -it redis-0 -n statefulset -- redis-cli -p 26379 sentinel master mymaster

预期:ip 是 redis-0.redis.statefulset.svc.cluster.local,num-slaves 2。

验证主从数据同步:

bash 复制代码
kubectl exec -it redis-0 -- redis-cli -a redis123 set test-key "redis-cluster-test"
kubectl exec -it redis-1 -- redis-cli -a redis123 get test-key

预期:redis-1 读到 redis-cluster-test。

验证故障自动切换(核心亮点):

bash 复制代码
kubectl delete pod redis-0
kubectl exec -it redis-0 -- redis-cli -a redis123 info replication

预期:redis-0 重建后,role 变成了 slave(master_host 指向别的节点)------说明哨兵已经自动把主节点切到了其他节点,集群没挂。

步骤 4:清理

bash 复制代码
kubectl delete svc redis
kubectl delete sts redis
kubectl delete pvc redis-data-redis-{0..2}
kubectl delete pv redis-pv-{0..2}
rm -fr /shares/redis/data-*/*

2.8 实验 4:Cassandra 集群(简版)

官方参考:https://kubernetes.io/zh-cn/docs/tutorials/stateful-application/cassandra/

套路和前面完全一样:Headless Service(9042 客户端 + 7000 节点间通信)+ StatefulSet,靠 CASSANDRA_SEEDS 指向 cassandra-0 完成自动组网。

bash 复制代码
cat > cassandra-service.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
  name: cassandra
  namespace: statefulset
  labels:
    app: cassandra
spec:
  ports:
  - port: 9042
    name: cql
  - port: 7000
    name: intra-node
  clusterIP: None
  selector:
    app: cassandra
EOF

kubectl apply -f cassandra-service.yaml

cat > cassandra-statefulset.yaml <<'EOF'
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: cassandra
  namespace: statefulset
spec:
  serviceName: cassandra
  replicas: 3
  selector:
    matchLabels:
      app: cassandra
  template:
    metadata:
      labels:
        app: cassandra
    spec:
      containers:
      - name: cassandra
        image: bitnami/cassandra:4.1.3
        ports:
        - containerPort: 9042
          name: cql
        - containerPort: 7000
          name: intra-node
        env:
        - name: CASSANDRA_CLUSTER_NAME
          value: "my-cassandra"
        - name: CASSANDRA_SEEDS
          value: "cassandra-0.cassandra.statefulset.svc.cluster.local"
        - name: CASSANDRA_LISTEN_ADDRESS
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: CASSANDRA_BROADCAST_ADDRESS
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: CASSANDRA_BROADCAST_RPC_ADDRESS
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: CASSANDRA_RPC_ADDRESS
          value: "0.0.0.0"
        - name: CASSANDRA_NUM_TOKENS
          value: "256"
        - name: CASSANDRA_REPLICATION_FACTOR
          value: "3"
        volumeMounts:
        - name: cassandra-data
          mountPath: /bitnami/cassandra
        resources:
          requests:
            cpu: 500m
            memory: 2Gi
          limits:
            cpu: 1000m
            memory: 4Gi
        livenessProbe:
          exec:
            command: ["nodetool", "status"]
          initialDelaySeconds: 60
          periodSeconds: 10
        readinessProbe:
          exec:
            command: ["cqlsh", "-e", "DESCRIBE KEYSPACES;"]
          initialDelaySeconds: 30
          periodSeconds: 5
  volumeClaimTemplates:
  - metadata:
      name: cassandra-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "nfs-storage"
      resources:
        requests:
          storage: 10Gi
EOF

kubectl apply -f cassandra-statefulset.yaml
kubectl get pods
kubectl get sts

验证集群状态:

bash 复制代码
kubectl exec -it cassandra-0 -- nodetool status

预期:3 个节点都 UN(Up/Normal),形成集群。

清理:

bash 复制代码
kubectl delete sts cassandra
kubectl delete svc cassandra
kubectl delete pvc cassandra-data-cassandra-{0..2}

2.9 实验 5:MySQL 集群(简版,主从待完善)

官方参考:https://kubernetes.io/zh-cn/docs/tasks/run-application/run-replicated-stateful-application/

步骤 1:创建 Headless Service

bash 复制代码
cat > mysql-service.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
  name: mysql
  namespace: statefulset
spec:
  selector:
    app: mysql
  clusterIP: None
  ports:
  - port: 3306
    targetPort: 3306
    name: mysql-port
EOF

kubectl apply -f mysql-service.yaml
kubectl get svc

步骤 2:创建 StatefulSet

bash 复制代码
cat > mysql-statefulset.yaml <<'EOF'
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
  namespace: statefulset
spec:
  serviceName: mysql
  replicas: 2
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:5.7
        ports:
        - containerPort: 3306
          name: mysql
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: "123456"
        volumeMounts:
        - name: mysql-data
          mountPath: /var/lib/mysql
  volumeClaimTemplates:
  - metadata:
      name: mysql-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "nfs-storage"   # 注意:用本教程的 NFS 就填 nfs-storage
      resources:
        requests:
          storage: 10Gi
EOF

kubectl apply -f mysql-statefulset.yaml
kubectl get pods
kubectl get sts

预期:mysql-0 先 Running,再 mysql-1,最终 2/2。

步骤 3:验证

验证域名解析:

bash 复制代码
kubectl run test-pod --image busybox -- sleep 3600
kubectl exec test-pod -- ping -c2 mysql-1.mysql

预期:能 ping 通 mysql-1.mysql(固定 DNS 生效)。

验证数据持久化:

bash 复制代码
kubectl exec -it mysql-1 -- mysql -u root -p123456 -e 'create database wordpress;'
kubectl delete pod mysql-1
kubectl get pods mysql-1
kubectl exec -it mysql-1 -- mysql -u root -p123456 -e 'show databases;'

预期:mysql-1 重建后(IP 可能变了,域名不变),wordpress 库还在------数据没丢。

步骤 4 配置主从:待完善(本笔记尚未完成该部分)。

步骤 5:清理

bash 复制代码
kubectl delete svc mysql
kubectl delete sts mysql
kubectl delete pvc mysql-data-mysql-{0..1}
kubectl delete pv mysql-pv-{0..1}
rm -fr /shares/mysql/data-*/*

2.10 StatefulSet 避坑总结

  • 密码不要明文写 YAML:生产环境敏感信息(MySQL 根密码、Redis 密码)必须用 Secret 管理。
  • 缩容严格从大到小删 Pod;要保留数据先备份 PV 内容。
  • 改 volumeClaimTemplates(容量、存储类)后直接 kubectl apply 不生效:要删掉现有 PVC 和 Pod 重建(数据在,因为 PV 是 Retain)。
  • serviceName、PVC 模板这类核心配置不能动态改:要改只能删 StatefulSet 重建(Pod 和 PVC 可保留,数据不丢)。
  • 存储选型:测试环境用 NFS 可以;生产环境建议高可用 NFS 集群或 Ceph,避免 NFS 单点故障。

2.11 环境清理

bash 复制代码
kubectl delete ns statefulset

三、etcd 存储系统

3.1 这是干啥

etcd 是一个开源的分布式键值(key-value)数据库,Go 语言写的,节点之间用 Raft 共识算法保证数据一致。

它就是 K8s 的"数据库":K8s 里所有状态(Service、Pod、ConfigMap...)都存到 etcd 里,路径以 /registry 开头。

特点:读写性能强(2 核 1.8G 内存 SSD 下单节点写可达 16K QPS)、安装简单、支持 HTTPS、有 HTTP API、跨平台、二进制文件小、社区活跃。

应用场景:服务注册与发现、消息发布订阅、分布式配置中心、选主、分布式锁。

3.2 单节点部署

环境约定:主机名 etcd.laoma.cloud,IP 10.1.8.10。

步骤 1:配置 hosts 并安装

bash 复制代码
vim /etc/hosts

加一行:

bash 复制代码
10.1.8.10 etcd.laoma.cloud etcd

apt install -y etcd-server etcd-client

安装完成后 etcd 默认启用并自动启动。

步骤 2:配置 /etc/default/etcd

bash 复制代码
cp /etc/default/etcd /etc/default/etcd.ori
vim /etc/default/etcd

改成:

bash 复制代码
# 节点名称,默认 default
ETCD_NAME="default"
# 数据存储目录,默认 /var/lib/etcd/default
ETCD_DATA_DIR="/var/lib/etcd/default"
# 对外监听地址(服务端自己监听)
ETCD_LISTEN_CLIENT_URLS="http://localhost:2379,http://10.1.8.10:2379"
# 对外宣告地址(告诉客户端连哪个)
ETCD_ADVERTISE_CLIENT_URLS="http://localhost:2379,http://10.1.8.10:2379"

这两个监听配置怎么选,取决于谁要访问:

  • 只要本机能访问:两个都写 http://localhost:2379。
  • 只要内网 IP 能访问:LISTEN 写 localhost+IP,ADVERTISE 写 localhost。
  • 客户端和服务器都能访问(推荐):两个都写 localhost+IP。

步骤 3:重启并验证

bash 复制代码
systemctl restart etcd
etcdctl member list

预期:

bash 复制代码
8e9e05c52164694d, started, default, http://localhost:2380, http://10.1.8.10:2379,http://localhost:2379, false

3.3 键值对基础操作

单行文本:

bash 复制代码
etcdctl put /etc/hosts '10.1.8.10 etcd.laoma.cloud etcd'
etcdctl get /etc/hosts --print-value-only

多行文本:

bash 复制代码
etcdctl put /etc/hosts '10.1.8.11 etcd1.laoma.cloud etcd1

10.1.8.12 etcd2.laoma.cloud etcd2'

bash 复制代码
etcdctl get /etc/hosts --print-value-only

从文件导入:

bash 复制代码
cat /etc/hosts | etcdctl put /etc/hosts
etcdctl get /etc/hosts --print-value-only

彻底清除单节点数据:

bash 复制代码
systemctl stop etcd.service
apt remove -y etcd-server etcd-client
rm -fr /var/lib/etcd/default

3.4 三节点集群部署

环境约定:etcd1=10.1.8.11、etcd2=10.1.8.12、etcd3=10.1.8.13。

步骤 1:所有节点配置 hosts + 安装 + 清数据

每台机器都执行:

bash 复制代码
vim /etc/hosts

加内容:

bash 复制代码
10.1.8.11 etcd1.laoma.cloud etcd1
10.1.8.12 etcd2.laoma.cloud etcd2
10.1.8.13 etcd3.laoma.cloud etcd3

apt update && apt install -y etcd-server etcd-client
systemctl stop etcd.service
rm -fr /var/lib/etcd/default

为什么清数据目录:如果节点之前是独立集群,里面有旧数据,不清理会加入集群失败。

步骤 2:etcd1 配置

bash 复制代码
cp /etc/default/etcd /etc/default/etcd.ori
vim /etc/default/etcd

ETCD_NAME="etcd1"
ETCD_DATA_DIR="/var/lib/etcd/default"
ETCD_LISTEN_PEER_URLS="http://localhost:2380,http://10.1.8.11:2380"
ETCD_LISTEN_CLIENT_URLS="http://localhost:2379,http://10.1.8.11:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://10.1.8.11:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://localhost:2379,http://10.1.8.11:2379"
ETCD_INITIAL_CLUSTER="etcd1=http://10.1.8.11:2380,etcd2=http://10.1.8.12:2380"
ETCD_INITIAL_CLUSTER_TOKEN="etcdcluster"
ETCD_INITIAL_CLUSTER_STATE="new"

参数都是干啥的:

bash 复制代码
# ETCD_NAME                  本节点名字
# ETCD_DATA_DIR              数据存储目录
# ETCD_LISTEN_PEER_URLS      监听 2380,和其他节点心跳/同步数据用
# ETCD_LISTEN_CLIENT_URLS    监听 2379,给客户端连(服务端自己用)
# ETCD_INITIAL_ADVERTISE_PEER_URLS  告诉其他节点:连我走这个 2380 地址
# ETCD_ADVERTISE_CLIENT_URLS        告诉客户端:连我走这些 2379 地址
# ETCD_INITIAL_CLUSTER       集群里所有节点的名字和 2380 地址(每台都要一样)
# ETCD_INITIAL_CLUSTER_TOKEN 集群令牌,所有节点必须统一,用于通信认证
# ETCD_INITIAL_CLUSTER_STATE 新建集群写 new;加入已有集群写 existing

步骤 3:etcd2 配置

bash 复制代码
cp /etc/default/etcd /etc/default/etcd.ori
vim /etc/default/etcd

ETCD_NAME="etcd2"
ETCD_DATA_DIR="/var/lib/etcd/default"
ETCD_LISTEN_PEER_URLS="http://localhost:2380,http://10.1.8.12:2380"
ETCD_LISTEN_CLIENT_URLS="http://localhost:2379,http://10.1.8.12:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://10.1.8.12:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://localhost:2379,http://10.1.8.12:2379"
ETCD_INITIAL_CLUSTER="etcd1=http://10.1.8.11:2380,etcd2=http://10.1.8.12:2380"
ETCD_INITIAL_CLUSTER_TOKEN="etcdcluster"
ETCD_INITIAL_CLUSTER_STATE="new"

注意:INITIAL_CLUSTER 两台机器写的一样(整个集群的成员表)。

步骤 4:启动集群并验证

bash 复制代码
systemctl start etcd
systemctl start etcd
etcdctl member list

预期:2 个成员,都是 started。

验证数据同步:

bash 复制代码
etcdctl put /users/user1/name laoma
etcdctl get /users/user1/name

预期:etcd1 写入,etcd2 能读到(多节点数据一致)。

3.5 给集群添加节点(加 etcd3)

三步:改老节点配置 → member add 注册 → 新节点部署启动。

步骤 1:更新 etcd1 配置(加 etcd3,状态改 existing)

bash 复制代码
vim /etc/default/etcd

ETCD_NAME="etcd1"
ETCD_DATA_DIR="/var/lib/etcd/default"
ETCD_LISTEN_PEER_URLS="http://localhost:2380,http://10.1.8.11:2380"
ETCD_LISTEN_CLIENT_URLS="http://localhost:2379,http://10.1.8.11:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://10.1.8.11:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://localhost:2379,http://10.1.8.11:2379"
ETCD_INITIAL_CLUSTER="etcd1=http://10.1.8.11:2380,etcd2=http://10.1.8.12:2380,etcd3=http://10.1.8.13:2380"
ETCD_INITIAL_CLUSTER_TOKEN="etcdcluster"
ETCD_INITIAL_CLUSTER_STATE="existing"

systemctl restart etcd

步骤 2:更新 etcd2 配置

bash 复制代码
vim /etc/default/etcd

ETCD_NAME="etcd2"
ETCD_DATA_DIR="/var/lib/etcd/default"
ETCD_LISTEN_PEER_URLS="http://localhost:2380,http://10.1.8.12:2380"
ETCD_LISTEN_CLIENT_URLS="http://localhost:2379,http://10.1.8.12:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://10.1.8.12:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://localhost:2379,http://10.1.8.12:2379"
ETCD_INITIAL_CLUSTER="etcd1=http://10.1.8.11:2380,etcd2=http://10.1.8.12:2380,etcd3=http://10.1.8.13:2380"
ETCD_INITIAL_CLUSTER_TOKEN="etcdcluster"
ETCD_INITIAL_CLUSTER_STATE="existing"

systemctl restart etcd

步骤 3:在集群里注册 etcd3 成员

在任意节点执行:

bash 复制代码
etcdctl member add etcd3 --peer-urls=http://10.1.8.13:2380

会返回一段提示,里面有 etcd3 需要的配置信息,直接照抄即可。

步骤 4:部署 etcd3

bash 复制代码
apt install -y etcd-server etcd-client
systemctl stop etcd.service
rm -fr /var/lib/etcd/default
scp /etc/default/etcd root@etcd3:/etc/default/etcd
vim /etc/default/etcd

改成:

bash 复制代码
ETCD_NAME="etcd3"
ETCD_DATA_DIR="/var/lib/etcd/default"
ETCD_LISTEN_PEER_URLS="http://localhost:2380,http://10.1.8.13:2380"
ETCD_LISTEN_CLIENT_URLS="http://localhost:2379,http://10.1.8.13:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://10.1.8.13:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://localhost:2379,http://10.1.8.13:2379"
ETCD_INITIAL_CLUSTER="etcd1=http://10.1.8.11:2380,etcd2=http://10.1.8.12:2380,etcd3=http://10.1.8.13:2380"
ETCD_INITIAL_CLUSTER_TOKEN="etcdcluster"
ETCD_INITIAL_CLUSTER_STATE="existing"

systemctl start etcd
etcdctl member list

预期:3 个成员,etcd3 也在里面。

重要:节点加入集群前,etcd 数据目录必须没有数据。

3.6 从集群删除节点(删 etcd3)

bash 复制代码
etcdctl member remove 2038744cfb6bf628
etcdctl member list

然后 etcd1、etcd2 的配置改回 2 节点,STATE 保持 existing,重启:

bash 复制代码
vim /etc/default/etcd

ETCD_NAME="etcd1"
ETCD_DATA_DIR="/var/lib/etcd/default"
ETCD_LISTEN_PEER_URLS="http://localhost:2380,http://10.1.8.11:2380"
ETCD_LISTEN_CLIENT_URLS="http://localhost:2379,http://10.1.8.11:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://10.1.8.11:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://localhost:2379,http://10.1.8.11:2379"
ETCD_INITIAL_CLUSTER="etcd1=http://10.1.8.11:2380,etcd2=http://10.1.8.12:2380"
ETCD_INITIAL_CLUSTER_TOKEN="etcdcluster"
ETCD_INITIAL_CLUSTER_STATE="existing"

vim /etc/default/etcd

ETCD_NAME="etcd2"
ETCD_DATA_DIR="/var/lib/etcd/default"
ETCD_LISTEN_PEER_URLS="http://localhost:2380,http://10.1.8.12:2380"
ETCD_LISTEN_CLIENT_URLS="http://localhost:2379,http://10.1.8.12:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://10.1.8.12:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://localhost:2379,http://10.1.8.12:2379"
ETCD_INITIAL_CLUSTER="etcd1=http://10.1.8.11:2380,etcd2=http://10.1.8.12:2380"
ETCD_INITIAL_CLUSTER_TOKEN="etcdcluster"
ETCD_INITIAL_CLUSTER_STATE="existing"

systemctl restart etcd
systemctl restart etcd

清理被删节点 etcd3 上的残留:

bash 复制代码
systemctl stop etcd.service
apt remove -y etcd-server etcd-client
rm -fr /var/lib/etcd/default

3.7 etcdctl 命令速查

etcd API 分 v2 和 v3,用环境变量 ETCDCTL_API 设置(默认 v3)。注意所有节点保持一致。

配置命令补全(可选):

bash 复制代码
mkdir /etc/bash_completion.d
etcdctl completion bash > /etc/bash_completion.d/etcdctl
source /etc/bash_completion.d/etcdctl

key-value 操作:

bash 复制代码
# 写入
etcdctl put /users/user1/name laoma

# 查询(显示 key 和 value)
etcdctl get /users/user1/name

# 只显示 value
etcdctl get /users/user1/name --print-value-only

# 只显示 key
etcdctl get /users/user1/name --keys-only

# 查某个前缀下的所有 key(--prefix)
etcdctl get /users --keys-only --prefix |grep '^/users'

# 更新(同一个 put 覆盖)
etcdctl put /users/user1/name laohu

# 删除
etcdctl del /users/user2/name

# 实时监控某个 key 的变化
etcdctl watch /users/user1/name

K8s 数据在 etcd 里的命名规律:

/registry/services/endpoints/命名空间/服务名

查"目录"小脚本 etcdls(/usr/local/bin/etcdls):

bash 复制代码
#!/bin/bash
PREFIX=${1:-/}
count=$(etcdctl get $PREFIX --keys-only --prefix |grep -v '^$'|egrep -o "$PREFIX[^/]*/" |sort |uniq|wc -l)
if [ $count -eq 1 ];then
  etcdctl get $PREFIX --keys-only --prefix |grep -v '^$'|egrep -o "^$PREFIX[^/]*/[^/]*" | sort | uniq
else
  etcdctl get $PREFIX --keys-only --prefix |grep -v '^$'|egrep -o "^$PREFIX[^/]*" | sort | uniq
fi

chmod +x /usr/local/bin/etcdls
etcdls
etcdls /user

user / role / member 管理:

etcdctl user add 用户名

etcdctl role add 角色名

etcdctl member add 节点名 --peer-urls=http://IP:2380

bash 复制代码
etcdctl member list

etcdctl member remove 成员ID

3.8 查询 K8s 里的数据

K8s 从 1.13 起默认用 etcd v3 API,数据都在 /registry 下。master 上要先装客户端:

bash 复制代码
apt install etcd-client -y

在 master 建两个脚本(带证书参数,因为 K8s 的 etcd 开了 TLS):

bash 复制代码
/usr/local/bin/etcdls:

#!/bin/bash
export ETCDCTL_API=3
key_file=/etc/kubernetes/pki/etcd/server.key
cert_file=/etc/kubernetes/pki/etcd/server.crt
cacert_file=/etc/kubernetes/pki/etcd/ca.crt
endpoints=https://127.0.0.1:2379
PREFIX=${1:-/}
tmpfile=$(mktemp)
etcdctl --key=${key_file} --cert=${cert_file} --cacert=${cacert_file} --endpoints=${endpoints} get $PREFIX --keys-only --prefix |grep -v '^$'|egrep "$PREFIX[^/]*/" > $tmpfile
count=$(cat $tmpfile | egrep -o "$PREFIX[^/]*/" | sort | uniq | wc -l )
if [ $count -eq 1 ];then
  cat $tmpfile | egrep -o "^$PREFIX[^/]*/[^/]*" | sort | uniq
else
  cat $tmpfile | egrep -o "^$PREFIX[^/]*" | sort | uniq
fi
rm -f $tmpfile

/usr/local/bin/etcdget:

#!/bin/bash
export ETCDCTL_API=3
key_file=/etc/kubernetes/pki/etcd/server.key
cert_file=/etc/kubernetes/pki/etcd/server.crt
cacert_file=/etc/kubernetes/pki/etcd/ca.crt
endpoints=https://127.0.0.1:2379
etcdctl --key=${key_file} --cert=${cert_file} --cacert=${cacert_file} --endpoints=${endpoints} get $*

使用示例(顺着目录往下钻,查 kube-dns 的 endpoints):

bash 复制代码
etcdls /registry/ser
etcdls /registry/services
etcdls /registry/services/endpoints
etcdls /registry/services/endpoints/kube-system
etcdls /registry/services/endpoints/kube-system/kube-dns
etcdget /registry/services/endpoints/kube-system/kube-dns --print-value-only

注意:K8s 存的数据除了 /registry/apiregistration.k8s.io 是 JSON,其他都是 protobuf 二进制格式(为了性能),直接看是乱码。用 strings 解码:

bash 复制代码
etcdget /registry/services/endpoints/kube-system/kube-dns --print-value-only | strings

strings 工具由 binutils 包提供。更强大的工具是 openshift 的 etcdhelper。

3.9 备份与恢复

先记住结论:etcd v3 能备份/恢复 v2 和 v3 的数据;etcd v2 不行(所以一定要用 v3 API)。

物理备份与恢复(直接备份数据目录)

单节点:

bash 复制代码
# 准备测试数据
etcdctl put /users/user1/name laoma
etcdctl put /users/user1/age 18
etcdctl put /users/user2/name laowang

# 备份:停服务,整个目录拷走
systemctl stop etcd.service
cp -a /var/lib/etcd/default /var/lib/etcd/default.bak

# 模拟数据被删
systemctl start etcd.service
etcdctl del /users/user2/name

# 恢复:停服务,换回备份目录
systemctl stop etcd.service
mv /var/lib/etcd/default /var/lib/etcd/default.bad
cp -a /var/lib/etcd/default.bak /var/lib/etcd/default
chown -R etcd:etcd /var/lib/etcd/default/
systemctl start etcd.service

# 验证
etcdctl get /users/user2/name

多节点:所有节点停服务 → 所有节点各自备份目录 → 恢复时所有节点一起换回(步骤一样,在每台机器执行)。

逻辑备份与恢复(snapshot 快照)

单节点:

bash 复制代码
export ETCDCTL_API=3
etcdctl snapshot save snapshot.db

systemctl stop etcd
mv /var/lib/etcd/default /var/lib/etcd/default.old
etcdctl snapshot restore snapshot.db --data-dir=/var/lib/etcd/default/
chown -R etcd:etcd /var/lib/etcd/default
systemctl start etcd.service

多节点:因为集群数据一致,只在一个节点备份就行,再把快照文件同步到所有节点:

bash 复制代码
export ETCDCTL_API=3
etcdctl snapshot save /root/snapshot1.db

# 停止所有节点 etcd 并清空数据目录
systemctl stop etcd
rm -fr /var/lib/etcd/default/

# 每个节点分别恢复快照(命令一行写完,方便复制)
etcdctl snapshot restore /root/snapshot1.db --data-dir=/var/lib/etcd/default
chown -R etcd:etcd /var/lib/etcd/default
systemctl start etcd

etcdctl snapshot restore /root/snapshot1.db --data-dir=/var/lib/etcd/default
chown -R etcd:etcd /var/lib/etcd/default
systemctl start etcd

etcdctl snapshot restore /root/snapshot1.db --data-dir=/var/lib/etcd/default
chown -R etcd:etcd /var/lib/etcd/default
systemctl start etcd

K8s 的备份与恢复(带证书参数)

master 上,etcd 开了 TLS,etcdctl 要带证书和 endpoints:

bash 复制代码
# 准备工作(设成变量,后面命令直接引用)
export ETCDCTL_API=3
key_file=/etc/kubernetes/pki/etcd/server.key
cert_file=/etc/kubernetes/pki/etcd/server.crt
cacert_file=/etc/kubernetes/pki/etcd/ca.crt
endpoints=https://127.0.0.1:2379

# 创建一个应用当测试数据
kubectl create deployment web --image=hub.laoma.cloud/library/httpd --replicas=2

# 备份
etcdctl --key=${key_file} --cert=${cert_file} --cacert=${cacert_file} --endpoints=${endpoints} snapshot save snapshot.db

# 删掉应用,模拟故障
kubectl delete deployments.apps web

# 恢复(清空数据目录,从快照重建;要带上本节点主机名和 IP)
rm -fr /var/lib/etcd/
etcdctl --key=${key_file} --cert=${cert_file} --cacert=${cacert_file} --endpoints=${endpoints} snapshot restore snapshot.db --data-dir=/var/lib/etcd --name=master30.laoma.cloud --initial-cluster=master30.laoma.cloud=https://10.1.8.30:2380 --initial-advertise-peer-urls=https://10.1.8.30:2380

说明:master30.laoma.cloud 是 master 节点主机名,10.1.8.30 是它的 IP,按你实际环境替换。

bash 复制代码
# 验证:被删的 web 应用回来了
kubectl get deployments.apps

收尾自查

  • 动态卷供应三件套:StorageClass(套餐)→ PVC(申请单)→ PV(自动造的卷)。
  • Local Path 适合单节点本地盘;NFS 适合多节点共享,StatefulSet 实验必备。
  • StatefulSet 三句话:名字固定、顺序操作、存储不丢;必须配 Headless Service。
  • etcd 是 K8s 的数据库,Raft 保证一致;加节点先 member add 再改配置,删节点先 member remove。
  • 备份恢复:物理备份拷目录、逻辑备份 snapshot,K8s 场景记得带证书参数。
相关推荐
小哈里1 小时前
【推理】NVIDIA DLI培训:大规模部署与优化 AI 推理(推理原理,K8S,网关层,pd分离,MoE,Dynamo)
人工智能·容器·kubernetes·nvidia·推理
Blockbuater_drug20 小时前
Dify 1.18 本地部署实战:Windows + WSL2 + Docker,从零跑到本地可用
windows·docker·容器·dify·大模型应用平台
shangjian00721 小时前
K8S学习-基础组件
学习·容器·kubernetes
徐小黑ACG21 小时前
docker 基础教程
运维·docker·容器
时间售卖铺1 天前
Docker镜像构建全解析:从原理到实战优化
运维·docker·云原生·容器
wzq11_6661 天前
Kubernetes集群——工作负载
云原生·容器·kubernetes
lwx9148522 天前
Docker-安装SSH服务
docker·容器·ssh
艾伦_耶格宇2 天前
【kubernetes - 1.28版本】-11 K8S核心组件
java·容器·kubernetes
月落汀兰2 天前
拒绝配置写死镜像!K8s 存储卷与配置管理,PV/PVC、ConfigMap、Secret 实战避坑指南
运维·kubernetes