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 场景记得带证书参数。