Kubernetes Dashboard,Kubernetes 动态卷供应,Kubernetes StatefulSet详细讲解
内容:
-
Kubernetes Dashboard
-
Kubernetes 动态卷供应
-
Kubernetes StatefulSet
Kubernetes 集群 Dashboard
kuboard(自学)
kuboard 介绍
Kuboard 是一款专为 Kubernetes 设计的免费管理界面,兼容 Kubernetes 版本 1.13 及以上。Kuboard 每周发布一个 beta 版本,最长每月发布一个正式版本,经过两年的不断迭代和优化,已经具备多集群管理、权限管理、监控套件、日志套件等丰富的功能,并且有 1000+ 的企业将 Kuboard 应用于其生产环境。Kuboard 自 2019年8月发布第一个版本以来,得到了众多用户的认可,目前已经获得了 10000+ GitHub Star。
Kuboard 的定位和 Dashboard 是相似的,主要的区别 在于:
-
Kuboard 关注微服务参考架构的视角对界面进行组织,参考 Kuboard 简介。
-
Kuboard 中,不需要手工编写 YAML 文件,进一步降低 K8S 使用难度,提高便捷性。
-
Kuboard 可以导出整个微服务架构的部署信息,并在新的名称空间/集群导入配置信息。
-
Kuboard 的一个发展方向是,提供内建的 监控套件(目前的全局监控套件成熟度比较高)。
kuboard 特点
相较于 Kubernetes Dashboard 等其他 Kubernetes 管理界面,Kuboard 的主要特点有:
-
多种认证方式:Kuboard 可以使用内建用户库、gitlab / github 单点登录或者 LDAP 用户库进行认证,避免管理员将 ServiceAccount 的 Token 分发给普通用户而造成的麻烦。使用内建用户库时,管理员可以配置用户的密码策略、密码过期时间等安全设置。
-

-
多集群管理:管理员可以将多个 Kubernetes 集群导入到 Kuboard 中,并且通过权限控制,将不同集群/名称空间的权限分配给指定的用户或用户组。

-
微服务分层展示:在 Kuboard 的名称空间概要页中,以经典的微服务分层方式将工作负载划分到不同的分层,更加直观地展示微服务架构的结构,并且可以为每一个名称空间自定义名称空间布局。

-
工作负载的直观展示:Kuboard 中将 Deployment 的历史版本、所属的 Pod 列表、Pod 的关联事件、容器信息合理地组织在同一个页面中,可以帮助用户最快速的诊断问题和执行各种相关操作。

-
工作负载编辑:Kuboard 提供了图形化的工作负载编辑界面,用户无需陷入繁琐的 YAML 文件细节中,即可轻松完成对容器的编排任务。支持的 Kubernetes 对象类型包括:Node、Namespace、Deployment、StatefulSet、DaemonSet、Secret、ConfigMap、Service、Ingress、StorageClass、PersistentVolumeClaim、LimitRange、ResourceQuota、ServiceAccount、Role、RoleBinding、ClusterRole、ClusterRoleBinding、CustomResourceDefinition、CustomResource 等各类常用 Kubernetes 对象,

-
存储类型支持:在 Kuboard 中,可以方便地对接 NFS、CephFS 等常用存储类型,并且支持对 CephFS 类型的存储卷声明执行扩容和快照操作。

-
丰富的互操作性:可以提供许多通常只在
kubectl命令行界面中才提供的互操作手段,例如:-
Top Nodes / Top Pods
-
-
容器的日志、终端
-
容器的文件浏览器(支持从容器中下载文件、上传文件到容器)
-
KuboardProxy(在浏览器中就可以提供
kubectl proxy的功能)

-
-
套件扩展:Kuboard 提供了必要的套件库,使得用户可以根据自己的需要扩展集群的管理能力。当前提供的套件有:
-
资源层监控套件,基于 Prometheus / Grafana 提供 K8S 集群的监控能力,可以监控集群、节点、工作负载、容器组等各个级别对象的 CPU、内存、网络、磁盘等资源的使用情况;
-
-
日志聚合套件,基于 Grafana / Loki / Promtail 实现日志聚合;
-
存储卷浏览器,查看和操作存储卷中的内容;

-
-
告警配置:可以通过界面直接配置资源层监控套件发送告警消息:
-
支持邮件、微信发送告警消息;
-
-
支持告警路由配置;
-
支持告警规则配置等;

-
-
操作审计:Kuboard 支持操作审计的功能:
-
审计用户通过 Kuboard 界面和 Kuboard API 执行的操作;
-
自定义审计规则;

-
在线演示
在线演示环境中,您具备 只读 权限,只能体验 Kuboard 的一部分功能。
地址:https://demo.kuboard.cn 用 户:demo 密 码:demo123
kuboard 版本
Kuboard v2.x 版本说明
Kuboard v2.x 支持 Kubernetes 单集群管理。
版本支持:

Kuboard v3.x 版本说明
Kuboard v3.x 支持 Kubernetes 多集群管理。如果您从 Kuboard v1.0.x 或者 Kuboard v2.0.x 升级到 Kuboard,请注意:
-
您可以同时使用 Kuboard v3.x 和 Kuboard v2.0.x;
-
Kuboard v3.x 支持 amd64 (x86) 架构和 arm68 (armv8) 架构的 CPU;
版本支持:

kuboard 安装和配置
本次实验环境kubernetes版本是1.32.0,使用kuboard v3.x版本。
安装方式
基于如下原因,建议您以 独立容器 方式 Kuboard:
-
结构更清晰(Kuboard 作为多个集群的管理界面应该独立于任何集群之外);
-
登录 Kuboard 时使用不同的认证方式;
-
问题排查更简单;
以 独立容器 方式运行
安装计划
在正式安装 kuboard v3 之前,需做好一个简单的部署计划的设计,在本例中,各组件之间的连接方式,如下图所示:
-
假设用户通过 http://外网IP:80 访问 Kuboard v3;
-
安装在 Kubernetes 中的 Kuboard Agent 通过
内网IP访问 Kuboard 的 Web 服务端口 80 和 Kuboard Agent Server 端口 10081。

安装过程
# 创建目录 root@master30:~# mkdir /kuboard-data # 创建容器 root@master30:~# nerdctl run -d --name=kuboard \ -p 80:80/tcp \ -p 10081:10081/tcp \ -e KUBOARD_ENDPOINT="http://10.1.8.30:80" \ -e KUBOARD_AGENT_SERVER_TCP_PORT="10081" \ -v /kuboard-data:/data \ eipwork/kuboard:v3 # 10.1.8.30是物理机的ip
卸载
root@master30:~# nerdctl container rm kuboard --force root@master30:~# rm -fr /kuboard-data
以 static pod 方式运行(自学)
安装
# 在master节点上执行 root@master30:~# curl -fsSL https://addons.kuboard.cn/kuboard/kuboard-static-pod.sh -o kuboard.sh root@master30:~# sh kuboard.sh current ip address is 10.1.8.30 create file /root/kuboard-sa.yaml kubectl apply -f /root/kuboard-sa.yaml namespace/kuboard created serviceaccount/kuboard-admin created clusterrolebinding.rbac.authorization.k8s.io/kuboard-admin-crb created serviceaccount/kuboard-viewer created clusterrolebinding.rbac.authorization.k8s.io/kuboard-viewer-crb created create file /etc/kubernetes/manifests/kuboard.yaml restart kubelet 检查状态 待 kuboard-v3-master30.laoma.cloud 的容器组变为 Running 状态后,则安装成功,可以通过 http://10.1.8.30 访问 kuboard 界面 No resources found in kuboard namespace. # 验证 root@master30:~# kubectl get all -n kuboard NAME READY STATUS RESTARTS AGE pod/kuboard-v3-laoma30.redhat.fun 1/1 Running 0 45s
卸载
# 删除静态文件 root@master30:~# rm -f /etc/kubernetes/manifests/kuboard.yaml # 删除数据目录 root@master30:~# rm -fr /usr/share/kuboard # 删除命名空间 root@master30:~# kubectl delete ns kuboard
访问过程
在浏览器输入 http://your-host-ip:80 ,访问 Kuboard v3.x 的界面。
登录信息:
-
用户名:
admin -
密 码:
Kuboard123

添加集群

这里选择:.kubeconfig

导入凭据文件(复制~/.kube/config内容到红色方框中)

导入后效果:


选择访问集群时所使用的身份:使用 ServiceAccount kuboard-admin

参考资料
安装手册:安装 Kubernetes 多集群管理工具 - Kuboard v3 | Kuboard
Kubernetes Dashboard
1. 定位
Kubernetes Dashboard 是 K8s 官方原生 Web 可视化面板,项目开源地址:https://github.com/kubernetes/dashboard/
2. 核心功能通俗总结
-
可视化部署容器应用(不用敲 kubectl 命令)
-
故障排查:查看 Pod 日志、容器崩溃、资源报错
-
全集群资源管理:Deployment、Pod、Service、Job、DaemonSet、ConfigMap 等增删改查
-
集群监控:节点、Pod 资源指标(CPU / 内存)
-
运维操作:扩缩容 Deployment、滚动更新、重启 Pod、灰度发布
-
统一展示集群所有报错事件
3. 组件构成(部署后两个核心 Pod)
-
kubernetes-dashboard:主面板 Web 服务,提供 HTTPS 登录界面 -
dashboard-metrics-scraper:指标采集器,给面板提供 CPU、内存监控数据
# 下载资源yaml文件
[root@master30 ~ 19:55:12]# wget https://raw.githubusercontent.com/kubernetes/dashboard/v2.6.1/aio/deploy/recommended.yaml
# 查看 yaml 内镜像版本
[root@master30 ~ 20:50:06]# grep image: recommended.yaml
image: kubernetesui/dashboard:v2.6.1
image: kubernetesui/metrics-scraper:v1.0.8
#- 作用:从 github 拉取官方一键部署清单recommended.yaml
#- 清单内置:命名空间、ServiceAccount、ClusterRole、ClusterRoleBinding、Deployment、Service、Metrics 采集器全套资源,开箱即用
#- 版本:v2.6.1,aio=all in one 一体化部署文件
# 执行部署:执行后自动创建独立命名空间kubernetes-dashboard,隔离面板相关资源
[root@master30 ~ 20:50:38]# kubectl apply -f recommended.yaml
namespace/kubernetes-dashboard created
serviceaccount/kubernetes-dashboard created
service/kubernetes-dashboard created
secret/kubernetes-dashboard-certs created
secret/kubernetes-dashboard-csrf created
secret/kubernetes-dashboard-key-holder created
configmap/kubernetes-dashboard-settings created
role.rbac.authorization.k8s.io/kubernetes-dashboard created
clusterrole.rbac.authorization.k8s.io/kubernetes-dashboard created
rolebinding.rbac.authorization.k8s.io/kubernetes-dashboard created
clusterrolebinding.rbac.authorization.k8s.io/kubernetes-dashboard created
deployment.apps/kubernetes-dashboard created
service/dashboard-metrics-scraper created
deployment.apps/dashboard-metrics-scraper created
验证
#验证命名空间,查看是否创建了命名空间kubenetes-dashboard
[root@master30 ~ 20:51:31]# kubectl get ns
NAME STATUS AGE
default Active 6d23h
kube-node-lease Active 6d23h
kube-public Active 6d23h
kube-system Active 6d23h
kubernetes-dashboard Active 47s
zy Active 6d23h
#切换到 dashboard 命名空间
[root@master30 ~ 20:52:18]# kubectl config set-context --current --namespace=kubernetes-dashboard
Context "kubernetes-admin@kubernetes" modified.
[root@master30 ~ 20:54:07]# kubectl get deployments.apps
NAME READY UP-TO-DATE AVAILABLE AGE
dashboard-metrics-scraper 1/1 1 1 3m10s
kubernetes-dashboard 1/1 1 1 3m10s
#输出两个 Deployment:
- kubernetes-dashboard:面板主程序
- dashboard-metrics-scraper:指标采集服务
字段说明:
- READY 1/1:每个 Deployment 正常运行 1 个 Pod,就绪
- UP-TO-DATE:副本版本同步完成
- AVAILABLE:服务正常可用
#查看内部 Service
[root@master30 ~ 20:54:43]# kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
dashboard-metrics-scraper ClusterIP 10.102.191.211 <none> 8000/TCP 4m12s
kubernetes-dashboard ClusterIP 10.96.43.158 <none> 443/TCP 4m12s
#两个 ClusterIP 类型 Service:
1. kubernetes-dashboard-web:面板主服务,内部端口 8000(底层 HTTPS)
2. kubernetes-dashboard-metrics-scraper:指标服务,端口 8000
- 默认ClusterIP:仅集群内部 Pod 能访问,外部机器无法打开页面
#查看服务账号 ServiceAccount
[root@master30 ~ 20:55:23]# kubectl get serviceaccounts
NAME SECRETS AGE
default 0 4m45s
kubernetes-dashboard 0 4m45s
# ServiceAccount(SA):K8s 内部 Pod 身份凭证,Pod 运行时用 SA 申请 API Server 权限
# kubernetes-dashboard:面板专用账号,后续用来绑定权限
#查看 dashboard SA 详情
[root@master30 ~ 20:55:56]# kubectl describe serviceaccounts kubernetes-dashboard
Name: kubernetes-dashboard
Namespace: kubernetes-dashboard
Labels: k8s-app=kubernetes-dashboard
Annotations: <none>
Image pull secrets: <none>
Mountable secrets: <none>
Tokens: <none> #- Tokens: <none>:默认没有生成登录 token,这就是登录面板需要手动创建管理员 token的原因
Events: <none>
# 创建了clusterrolebindings/kubernetes-dashboard
[root@master30 ~ 20:58:02]# kubectl get clusterrolebindings.rbac.authorization.k8s.io |grep dashboard
kubernetes-dashboard ClusterRole/kubernetes-dashboard
# sa/kubernetes-dashboard具有ClusterRole/kubernetes-dashboard
[root@master30 ~ 20:58:04]# kubectl describe clusterrolebindings.rbac.authorization.k8s.io kubernetes-dashboard
Name: kubernetes-dashboard
Labels: <none>
Annotations: <none>
Role:
Kind: ClusterRole
Name: kubernetes-dashboard
Subjects:
Kind Name Namespace
---- ---- ---------
ServiceAccount kubernetes-dashboard kubernetes-dashboard
#核心两段:
#1. Role 绑定:ClusterRole/kubernetes-dashboard(权限规则集合)
#2. Subjects 主体:绑定对象是ServiceAccount: kubernetes-dashboard
#含义:给 dashboard 这个服务账号分配 ClusterRole 的权限
# 查看 dashboard 默认权限
[root@master30 ~ 20:58:47]# kubectl describe clusterroles kubernetes-dashboard
Name: kubernetes-dashboard
Labels: k8s-app=kubernetes-dashboard
Annotations: <none>
PolicyRule:
Resources Non-Resource URLs Resource Names Verbs
--------- ----------------- -------------- -----
nodes.metrics.k8s.io [] [] [get list watch]
pods.metrics.k8s.io [] [] [get list watch]
#关键重点(必懂)
#默认权限只能查看节点、Pod 监控指标,没有管理集群资源的权限:不能创建 Deployment、查看 Pod、删除资源,只能看监控。所以生产 / 实验环境必须新建管理员 ClusterRoleBinding,绑定 dashboard SA 才能完整操作集群。
#查看 Deployment 使用的服务账号
[root@master30 ~ 21:01:06]# kubectl get deployments.apps kubernetes-dashboard -o yaml | grep ' serviceAccount'
serviceAccount: kubernetes-dashboard
serviceAccountName: kubernetes-dashboard
#含义:dashboard Pod 运行时,身份是kubernetes-dashboard这个 SA,权限完全继承上面 ClusterRole 的规则。
修改 service/kubernetes-dashboard 类型为 NodePort(节点端口暴露),从外部访问。
三种 Service 类型区别:
-
ClusterIP:仅集群内部访问(默认)
-
NodePort:每个节点开放一个宿主机端口,外部机器通过
节点IP:端口访问 -
LoadBalancer:云厂商负载均衡
[root@master30 ~ 21:01:13]# kubectl edit svc kubernetes-dashboard service/kubernetes-dashboard edited spec: ...... type: NodePort # 原本默认ClusterIP,改成NodePort #查看修改后的 Service [root@master30 ~ 21:03:37]# kubectl get svc kubernetes-dashboard NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes-dashboard NodePort 10.96.43.158 <none> 443:31042/TCP 12m #端口解读: #- 443:集群内部 Service 端口(HTTPS) #- 30518:宿主机 NodePort 端口(范围 30000-32767 随机分配) #访问地址:https://节点IP:30518,示例:https://10.1.8.30:31042 #注意:Dashboard 强制 HTTPS,不能用 http 访问,浏览器会提示证书不安全,直接忽略继续即可。
外部访问方案 2:Ingress 域名方式(生产推荐)
使用ingress配置访问,参考:
apiVersion: networking.k8s.io/v1 # Ingress稳定版API kind: Ingress # 资源类型:Ingress ingress网关 metadata: name: ingress-dashboard # ingress名称 namespace: kubernetes-dashboard # 和dashboard同命名空间 annotations: # 1. SSL透传:Ingress不终止HTTPS,原始加密流量直接转发给dashboard nginx.ingress.kubernetes.io/ssl-passthrough: "true" # 2. 后端协议是HTTPS,不是http nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" spec: ingressClassName: nginx # 使用nginx-ingress控制器 rules: - host: dashboard.laoma.cloud # 自定义访问域名 http: paths: - path: / # 匹配域名下所有路径 pathType: Prefix # 前缀匹配 backend: service: name: kubernetes-dashboard # 后端service名称 port: number: 443 # service内部HTTPS端口客户端配置dashboard.laoma.cloud域名到ingress服务地址,实验环境是10.1.8.40
访问 Dashboard
访问 https://10.1.8.30:31042(注意一定要手动加上https://)
不加的后果:(原因:你浏览器地址栏只输了 http://10.1.8.30:31042(明文 HTTP 访问),但 Dashboard 后端强制只支持 HTTPS 加密访问,拒绝明文 HTTP 请求,直接抛出这个提示页面。)



新版本
-
新版 Dashboard 不再支持 kubeconfig 证书登录,只能 Bearer Token 令牌登录
-
Dashboard 安装自带的默认账号权限极低,只能看自身命名空间资源,没法操作集群所有节点、Pod、Deployment、存储、权限等,日常运维不够用。
-
解决思路:
-
新建一个专用服务账号 ServiceAccount(集群内置身份账号)
-
绑定集群最高管理员权限
cluster-admin -
用这个账号生成临时 Token,拿 Token 登录 Dashboard 获得全集群操作权限
-
#文件分两块,用 --- 分割,一次创建两种资源:ServiceAccount + ClusterRoleBinding [root@master30 ~ 21:03:58]# cat > kubernetes-dashboard-admin.yaml <<'EOF' # 第一段:创建服务账号 apiVersion: v1 kind: ServiceAccount metadata: name: kubernetes-dashboard-admin # 账号名称 namespace: kubernetes-dashboard # 必须放在dashboard所在命名空间 ------------------------------------------------------------------ # 第二段:全局权限绑定 --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding #集群级绑定,作用整个集群所有命名空间;如果是 RoleBinding 只能限制单个命名空间。 metadata: name: kubernetes-dashboard-admin roleRef: #roleRef:绑定哪个权限角色 apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin # 集群内置最高权限角色,拥有所有增删改查权限 subjects: #subjects:授权给谁:类型:ServiceAccount;账号名 + 所在命名空间,对应下面创建的账号 - kind: ServiceAccount name: kubernetes-dashboard-admin namespace: kubernetes-dashboard EOF
[root@master30 ~ 21:19:05]# kubectl apply -f kubernetes-dashboard-admin.yaml serviceaccount/kubernetes-dashboard-admin created clusterrolebinding.rbac.authorization.k8s.io/kubernetes-dashboard-admin created #输出结果解析: #- serviceaccount/xxx created 账号创建成功 #- clusterrolebinding/xxx created 权限绑定成功 #查看是否创建了 ServiceAccount(账号本体) [root@master30 ~ 09:17:48]# kubectl get sa -n kubernetes-dashboard NAME SECRETS AGE default 0 12h #每个命名空间天生自带 kubernetes-dashboard 0 12h #安装 dashboard 自动生成,权限极低,只能访问本命名空间资源,无法查看集群节点、其他命名空间业务资源,生产 / 日常运维基本不用它登录。 kubernetes-dashboard-admin 0 12h #你手动创建的,绑定了 cluster-admin 超级权限,用来生成 token 登录 Dashboard 管理全集群。 #解释: #get sa:sa 是 ServiceAccount 简写,查询服务账号列表 #-n kubernetes-dashboard:指定 dashboard 命名空间 #输出里出现 kubernetes-dashboard-admin 代表账号创建成功。 #kubectl describe 资源类型 资源名 [-n 命名空间](clusterrolebinding 是集群级资源,不需要加 -n) [root@master30 ~ 09:47:42]# kubectl describe clusterrolebinding kubernetes-dashboard-admin Name: kubernetes-dashboard-admin Labels: <none> Annotations: <none> Role: Kind: ClusterRole Name: cluster-admin Subjects: Kind Name Namespace ---- ---- --------- ServiceAccount kubernetes-dashboard-admin kubernetes-dashboard #解释: #describe:查看资源详细信息 #clusterrolebinding:集群级权限绑定资源 #重点看两块内容: #1.RoleRef 部分 Name: cluster-admin(确认绑的是超级管理员) #2.Subjects 部分名称、命名空间和我们创建的 SA 一致。 # 创建 Bearer Token:整条命令作用:给指定命名空间下的指定服务账号,生成一段临时 Bearer 登录令牌,用来登录 Kubernetes Dashboard。 [root@master30 ~ 21:19:52]# kubectl -n kubernetes-dashboard create token kubernetes-dashboard-admin eyJhbGciOiJSUzI1NiIsImtpZCI6Ik1oT2h1X3JrbkJxMjJHcmFmUXVIUk4zUl9xNTlBZnJqbThvMzhSV3FyVlEifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzgzODY2MDI1LCJpYXQiOjE3ODM4NjI0MjUsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiNDA2M2UxM2MtYjFkOS00MTk4LTkyNDctZDkzMmE3NjA0YmYwIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsInNlcnZpY2VhY2NvdW50Ijp7Im5hbWUiOiJrdWJlcm5ldGVzLWRhc2hib2FyZC1hZG1pbiIsInVpZCI6IjhmY2NmMGE0LTgxZTUtNGY0OC1iNTM5LTA3NDc0OTA5YzI5ZCJ9fSwibmJmIjoxNzgzODYyNDI1LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZXJuZXRlcy1kYXNoYm9hcmQ6a3ViZXJuZXRlcy1kYXNoYm9hcmQtYWRtaW4ifQ.ZtbvyTfw3KdSJMsIr5e0kkMp3z5oau0I58OeGWNyYDx0vvQuzoehTkVncv3wZ6lGYJ48KS9YAZWOLN-GVwnYhz0eGnuBPsIYaOcgWzS9V3U7o_4CLlhR_QokMfUsKasODHQzeHettA_IpfNL9kKYJyrLUIBu-m0mRVeMMXvUNta7q7m2k-82R55_9j0ZsraxqzAfisov-RcUUrMVfprH5WOPcm2rzgrY0PMdg0kmP_WshJY5AFH4y5x5XHgQ0KUnCR7i9QhMWcyyywuM-VznX4eHdQip7IHnt9PaWKxtewPe_iC6R8-ue8fM-VDYS52M3J5MxZFpIRU8xFSj3ERnDA #重点说明: #1. -n kubernetes-dashboard 指定账号所在命名空间 #2. create token:k8s 1.24+ 新版命令,专门生成 ServiceAccount 临时令牌 #3. 输出一长串字符串就是 Bearer Token,复制粘贴到 Dashboard 登录框即可进入页面 #4. 有效期默认 24 小时,到期令牌直接失效,需要重新执行这条命令生成新令牌登录 #5. kubernetes-dashboard-admin:目标资源名称:我们前面 YAML 创建的 ServiceAccount(服务账号)名字。命令会找到这个账号,基于账号身份签发认证令牌。 1. 集群自带的 Dashboard 默认账号是什么? 你安装 Dashboard 时,集群会自动创建一个默认的 dashboard serviceAccount。 但是!官方为了安全,给这个默认账号设置了【极低只读权限】 只能看 kubernetes-dashboard 这一个命名空间 看不了业务 Pod、看不了节点、改不了任何资源、删不了、部署不了 99% 的运维操作都会报错 forbidden 2. 为什么要特意加 kubernetes-dashboard-admin 这个账号? (1)不冲突 不改动系统默认账号,新建一个独立账号,安全、不破坏集群原生配置。 (2)专门提权 新账号是空权限的,我们手动给它绑定了 cluster-admin 超级权限 让 Dashboard 拥有整个集群所有命名空间的读写删改全部权限。 (3)适配新版登录规则 新版 Dashboard 只能 Token 登录,自定义管理员账号,才能生成有权限的 Token。 如果你用默认账号生成 Token,登录进去也是残废权限界面。 3. 一句话总结 默认账号:官方锁死最低权限 → 没法干活 kubernetes-dashboard-admin:我们自建的超级管理员账号 → 全盘可控,正常运维 4. 额外关键点(你疑惑的重点) 你 yaml 里的名字可以随便改,不一定要叫 kubernetes-dashboard-admin 你叫 dashboard-root、k8s-admin 都行,只是一个自定义账号名 关键不是名字,是后面那行 ClusterRoleBinding 绑定了 cluster-admin 权限
输入token登录进来:

旧版本
之前的一些版本,默认已经为sa账户kubernetes-dashboard 创建了token。
获取token过程如下:
# sa/kubernetes-dashboard使用的token是kubernetes-dashboard-token-hm26j
root@master30:~# kubectl describe sa kubernetes-dashboard
Name: kubernetes-dashboard
Namespace: kubernetes-dashboard
Labels: k8s-app=kubernetes-dashboard
Annotations: <none>
Image pull secrets: <none>
Mountable secrets: kubernetes-dashboard-token-hm26j
Tokens: kubernetes-dashboard-token-hm26j
Events: <none>
# 获取token内容
root@master30:~# kubectl describe secrets kubernetes-dashboard-token-hm26j
Name: kubernetes-dashboard-token-hm26j
Namespace: kubernetes-dashboard
Labels: <none>
Annotations: kubernetes.io/service-account.name: kubernetes-dashboard
kubernetes.io/service-account.uid: d4b7d7ec-a76b-46c5-9dc5-629217cd07ba
Type: kubernetes.io/service-account-token
Data
====
token: eyJhbGciOiJSUzI1NiIsImtpZCI6IkNVVmdmMGNUckVGZFFPUVJ0dTkxZGtEc2hoOTlGZ2ktWjI2RmtnRWI5dFEifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJrdWJlcm5ldGVzLWRhc2hib2FyZC10b2tlbi1obTI2aiIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6ImQ0YjdkN2VjLWE3NmItNDZjNS05ZGM1LTYyOTIxN2NkMDdiYSIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDprdWJlcm5ldGVzLWRhc2hib2FyZDprdWJlcm5ldGVzLWRhc2hib2FyZCJ9.nBy6Hpv3a5jM-LWzqi95i2jkdLT8bx-jnTOuM5eroxGjpUq8OFOIFgky-0LUlJ-4dlFLdh3FZ2RV8SwQwQoctaay2p-xGfsB1IWNJkySYlGDy9-iMnH6Lc3J_Kb085rgqvY1nLPbSIhBlkueHUFsG9ii52gnGZf2RqroNBtsl_t2ofWfNDJRE7dm51B-WM_rSnNgTChFygRSoMXpzP7jG64dTrvQyxqHKRwr3zMNeYUlleHECUx0Z525hkdFv5B-gPV6ev_2cRS0BR2TM0V2qr1TKqM6G56E7CDkHiu6S2B1mrLdz5E9IKQCPFvxGVM4O4-bia4MHdmJo_INFD57pA
ca.crt: 1066 bytes
namespace: 20 bytes
登录页面
使用上面的 token 登录

选择Pods

分析 SA 使用案例
整体实验背景:
官方recommended.yaml部署 Dashboard 时,会自动创建专属命名空间级 ServiceAccount(SA):kubernetes-dashboard,配套同命名空间内的Role、RoleBinding,仅授予面板自身运行必需的最小权限,无任何集群业务资源查看 / 管理权限。 设计目的:遵循最小权限安全原则,防止 Dashboard 被入侵后,攻击者拿到集群最高权限。
Kubernetes Dashboard 默认部署时,只配置了最低权限的 RBAC。接下来分析默认的权限配置。
# Kubernetes Dashboard 以deployment形式运行
[root@master30 ~ 21:20:25]# kubectl get deployments.apps
NAME READY UP-TO-DATE AVAILABLE AGE
dashboard-metrics-scraper 1/1 1 1 75m
kubernetes-dashboard 1/1 1 1 75m
#- 两个 Deployment 分工:
#1. kubernetes-dashboard:Web 面板主程序 Pod
#2. dashboard-metrics-scraper:指标采集 Pod,给面板提供 CPU / 内存监控数据
# 查看 Deployment 绑定的 ServiceAccount
[root@master30 ~ 22:06:53]# kubectl get deployments.apps kubernetes-dashboard -o yaml | grep serviceAc
---------------
serviceAccount: kubernetes-dashboard
serviceAccountName: kubernetes-dashboard
#核心含义
#kubernetes-dashboard创建的所有 Pod,运行身份为命名空间内的 ServiceAccount:kubernetes-dashboard。Pod 内部会自动挂载该 SA 的认证 Token,所有调用集群 API 的请求,都会携带这个 Token 鉴权。
# 查看权限绑定关系(RoleBinding)
[root@master30 ~ 22:06:56]# kubectl get rolebindings.rbac.authorization.k8s.io
NAME ROLE AGE
kubernetes-dashboard Role/kubernetes-dashboard 83m
#逻辑关系
#RoleBinding = 权限绑定桥梁:
#- 绑定主体:ServiceAccount kubernetes-dashboard
#- 绑定权限模板:同命名空间内Role/kubernetes-dashboard
#- 生效范围:仅 kubernetes-dashboard 这一个命名空间,跨命名空间资源无任何权限
#重点区分:这里是Role(命名空间权限),不是ClusterRole(全局集群权限),天然无法访问集群其他 ns 资源。
# 查看默认 Role 的完整权限(核心安全分析)
[root@master30 ~ 22:15:08]# kubectl describe role kubernetes-dashboard
Name: kubernetes-dashboard
Labels: k8s-app=kubernetes-dashboard
Annotations: <none>
PolicyRule:
Resources Non-Resource URLs Resource Names Verbs
--------- ----------------- -------------- -----
secrets [] [kubernetes-dashboard-certs] [get update delete]
secrets [] [kubernetes-dashboard-csrf] [get update delete]
secrets [] [kubernetes-dashboard-key-holder] [get update delete]
configmaps [] [kubernetes-dashboard-settings] [get update]
services/proxy [] [dashboard-metrics-scraper] [get]
services/proxy [] [heapster] [get]
services/proxy [] [http:dashboard-metrics-scraper] [get]
services/proxy [] [http:heapster:] [get]
services/proxy [] [https:heapster:] [get]
services [] [dashboard-metrics-scraper] [proxy]
services [] [heapster] [proxy]
#关键结论(企业安全重点)
#1. 该 Role完全没有Pod、Deployment、Service、PVC、Node、Namespace 等业务资源的读写 / 查看权限;
#2. 权限全部限制在kubernetes-dashboard命名空间内,且仅能操作面板专属资源;
#3. 用这个默认 SA 登录 Dashboard 后,看不到集群任何业务 Pod、服务、数据库,只能打开空白面板。
# 生成 SA 登录 Token
#注意和上面的 kubectl -n kubernetes-dashboard create token kubernetes-dashboard-admin 这条命令区分,一个是使用自己创建的kubernetes-dashboard-admin服务账号,绑定了cluster-admin超级管理员权限,登录面板能操作集群所有命名空间、节点、资源,无权限拦截;和Dashboard 安装时系统自带的默认服务账号kubernetes-dashboard,仅拥有kubernetes-dashboard命名空间内少量只读权限,查看节点、其他业务命名空间资源会报forbidden权限不足。
[root@master30 ~ 22:15:26]# kubectl -n kubernetes-dashboard create token kubernetes-dashboard
eyJhbGciOiJSUzI1NiIsImtpZCI6Ik1oT2h1X3JrbkJxMjJHcmFmUXVIUk4zUl9xNTlBZnJqbThvMzhSV3FyVlEifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzgzODY5MzU5LCJpYXQiOjE3ODM4NjU3NTksImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiMWJhZDFjZWEtNzdiMC00YmYyLTg4NGUtNWE0MjBjMzQ3N2UzIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsInNlcnZpY2VhY2NvdW50Ijp7Im5hbWUiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsInVpZCI6IjExM2E4Y2I0LTYzZWYtNDFmMC04NWNmLThiMGUzM2VjNjI5MyJ9fSwibmJmIjoxNzgzODY1NzU5LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZXJuZXRlcy1kYXNoYm9hcmQ6a3ViZXJuZXRlcy1kYXNoYm9hcmQifQ.CmKoswvcQm5JrO9z2Xpf8mWhWwZk3LRIbUcPuV32tigimseovOVBdoMufjJTpURXnc7RHoLsHjteiBCV9UGxkIQsCOCgnNDKd0oNeDMMuWExOUB3uiOZyFvF_ZIsDWWnO07F0nz9-ZwoS4fTbdxCRwyUgNwQCEaXFYOp2jAukNBzmBquA6zl20CuamPOa5gyGovO5irbigXCjgyu0ufz0VS1FSzirfGX23Lvel7hoIbIaoV4Hf3ZjidsghXoDE28pyfZCCM-mlhXTN1_blGWt_Pp11OIoSEmhLM3Q1PCn3P_tA-NBEkWC2ozmPJoDU0cn1RDdvTr-sSx-Q2FVt-RaA
使用 serviceAccount: kubernetes-dashboard Token 登录

无法看到Pods。

为什么: Kubernetes Dashboard RBAC权限对比实验解析 分两部分讲:1、为什么之前能看到资源,现在报错;2、这个实验核心想证明什么 一、先解释「前后两种登录结果差异」 情况 1:你之前能正常查看集群所有资源 你之前操作时,手动执行过授权操作:创建了 ClusterRoleBinding,把 kubernetes-dashboard 这个 SA 绑定了集群管理员权限 cluster-admin。 # 之前你加过的管理员绑定 apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: dashboard-full-admin subjects: - kind: ServiceAccount name: kubernetes-dashboard namespace: kubernetes-dashboard roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin 此时 SA 拥有全集群所有命名空间的增删改查权限,登录面板自然能看到 Pod、Service、SA、节点等全部资源。 情况 2:现在页面提示权限禁止、空白无资源 本次实验使用官方原生推荐 yaml 的默认权限配置,没有额外添加管理员 ClusterRoleBinding: 1. 权限载体是 Role(命名空间级权限,不是 ClusterRole); 2. Role 仅允许操作面板自身专属证书、配置、监控代理,没有 list ServiceAccounts、list Namespaces、list Pod 等任何资源查看权限; 3. 你用默认 SA 生成的 Token 登录,API Server 校验权限后直接拒绝你的查询请求,弹出红色forbidden禁止报错,页面显示无资源。 简单区分: - 加了 cluster-admin 绑定 → 全权限,什么都能看 - 纯默认 yaml 不做任何授权 → 最低权限,绝大多数资源都禁止访问 ------------------------------------------------------------------------------- 二、这个实验想要说明的 4 个核心知识点(企业运维 / 面试重点) 1. 遵循「最小权限安全原则」是官方默认设计 K8s 官方部署 Dashboard 时,不会给任何集群管理权限,只分配面板自身运行必须的最小权限。 企业价值: 如果 Dashboard 对外开放,一旦页面存在漏洞被攻击者拿到登录 Token,攻击者也只能操作面板自身的证书配置,无法入侵业务 Pod、数据库、节点,极大缩小安全攻击面。 2. 区分两种权限资源:Role(命名空间内) vs ClusterRole(全局集群) - 默认配置用 Role + RoleBinding:权限锁死在kubernetes-dashboard命名空间,仅能操作面板内部专属资源; - 想要查看全集群资源,必须手动创建 ClusterRoleBinding 绑定全局 ClusterRole,权限跨所有命名空间生效。 实验直观对比出两者权限生效范围的巨大差异。 3. ServiceAccount 的权限完全由绑定的 RBAC 规则决定 Pod 本身没有权限,一切权限来源于 SA 绑定的 Role/ClusterRole: (1). 同一个 SA,不加集群绑定 = 只能维持面板基础运行; (2). 同一个 SA,追加 cluster-admin 绑定 = 拥有集群全部操作权限; 证明:Token 的权限 = 对应 ServiceAccount 绑定的 RBAC 权限,Token 只是身份凭证,权限由 RBAC 管控。 4. 生产环境必须手动精细化授权,不能依赖默认配置 (1). 测试环境:临时绑定 cluster-admin 方便运维调试; (2). 生产环境:禁止直接绑定最高权限 cluster-admin,需要自定义只读 / 运维专用 ClusterRole,按需开放查看、修改权限; (3). 默认配置仅适合验证面板能否启动,完全无法用于日常集群管理。 --- 三、一句话总结实验目的 通过有无管理员 RBAC 绑定两种登录效果对比,直观验证 K8s RBAC 权限控制逻辑,理解官方默认最小权限的安全设计,区分命名空间级 Role 与集群级 ClusterRole 的权限边界。
核心知识点总结:
-
ServiceAccount 是 Pod 的内置身份,Pod 访问集群 API 必须依赖 SA 鉴权;
-
默认 Dashboard 仅绑定命名空间内窄权限 Role,遵循最小权限原则,安全隔离;
-
kubectl create token生成 SA 临时登录凭证,用于网页登录鉴权; -
默认 SA 无业务资源权限,必须新增 ClusterRoleBinding 授予全局权限才能管理集群。
Kubernetes 动态卷供应
动态卷介绍
之前创建存储卷(PV)都需要管理员手动操作,这类卷被称为静态卷。那么 Kubernetes 能不能 “智能” 管理卷呢?比如需要用卷时自动创建,不用时自动清理?
答案是可以的 —— 这就是动态卷供应的能力。它彻底省去了集群管理员提前手动配置存储的工作,用户只要提出存储需求,系统就会自动创建对应的存储卷。
动态卷供应流程
-
管理员先创建 “存储模板”(StorageClass),指定用哪个 “造卷工具”(Provisioner,制备器)来创建卷;
-
用户创建 “存储申请单”(PVC)时,指定要用哪个 “存储模板”;
-
系统收到申请后,会让模板绑定的 “造卷工具” 自动创建一个 PV,并且把 PV 和用户的 PVC 绑定,用户直接用 PVC 就行。
StorageClass
StorageClass 就像管理员定义的 “存储套餐”—— 不同套餐对应不同的存储类型、服务质量(比如读写速度)、备份策略等。Kubernetes 不关心套餐叫什么,只负责按套餐规则造卷。
-
命名很重要:用户创建 PVC 时,要通过这个名字选择对应的存储套餐;
-
创建后不能改:StorageClass 一旦创建,名字和参数就没法修改了,要改只能重新建。
核心配置
每个 “存储套餐” 都包含以下核心信息,用来指导系统创建 PV:
-
provisioner:造卷工具(制备器),指定用哪个工具创建 PV,必须设置。
Kubernetes 自带一些 Provisioner,名字以 “kubernetes.io” 开头(比如 AWS EBS、Azure Disk);也可以使用第三方提供的(比如 NFS 没有内置工人,需要自己装)Provisioner,按 Kubernetes 规则运行即可。
存储类型 是否内置 说明 AWSElasticBlockStore ✅ 是 亚马逊云硬盘 AzureFile/AzureDisk ✅ 是 微软云文件 / 硬盘 Ceph RBD ✅ 是 Ceph 块存储 Glusterfs ✅ 是 分布式文件系统 NFS ❌ 否 网络文件系统,需装外部工人 本地存储(Local) ❌ 否 节点本地硬盘,需装外部工人 iSCSI/CephFS ❌ 否 需外部工人 -
reclaimPolicy:PV 回收策略,决定当绑定它的 PVC 被删除后,这块磁盘存储怎么处理。(PVC:Pod 申请存储的申请单;PV:集群真实的存储盘。流程:PVC 绑定 PV → Pod 挂载 PV 使用 → 删除 PVC → 触发 reclaimPolicy 执行对应操作。)
-
Delete(默认):PVC 删除后,PV 也自动删,对应的后端存储(比如云硬盘、NFS 目录)也会清掉;
-
Retain:PVC 删除后,PV 保留下来,后端存储的数据也不删,管理员可以手动处理。
-
-
volumeBindingMode:PVC 创建出来后,指定 PVC 与P V 绑定时机。
-
Immediate(立即绑定):创建 PVC 就造卷,不管 Pod 会不会用;适合存储能被所有节点访问的场景(比如云硬盘);
-
WaitForFirstConsumer(等使用者):先不造卷,等第一个用这个 PVC 的 Pod 创建后,根据 Pod 要跑的节点造卷;适合本地存储、只能被特定节点访问的存储。
-
-
mountOptions:指定 PV 挂载时附加的 “额外设置”,比如权限、调试模式。
注意:如果存储不支持这些选项,造卷会失败;如果选项填错,PV 挂载会失败。
-
parameters:比如存储类型(SSD/HDD)、副本数等,不同工具参数不一样。
基础示例
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 # 允许扩容 mountOptions: - debug # 挂载时开启调试模式 volumeBindingMode: Immediate # 立即创建并绑定PV
部署 Local Path provisioner
一:是什么、解决什么问题:
Local Path Provisioner(简称 LPP)是 Rancher 开源、K8s 集群常用的本地磁盘动态存储插件。
痛点(不用它的麻烦)
原生 hostPath / local PV 必须手动预先创建 PV,还要手动写节点亲和,PVC 不能自动生成 PV,运维繁琐。
LPP 核心能力
-
动态供给 PV:只需要创建 PVC,插件自动在对应节点生成本地目录 + 自动创建 PV,不用手动写 PV 清单;
-
自动调度约束:
WaitForFirstConsumer模式,先等 Pod 调度到某个节点,再在该节点创建存储目录,不会出现 Pod 和存储跨节点; -
统一管理节点本地磁盘目录,自动创建、自动清理目录;
-
性能高:走本地硬盘,无网络存储延迟,适合测试、轻量单机 / 小规模集群。
缺点(必须清楚)
存储绑定节点:Pod 只能跑在创建 PV 的节点;节点宕机 / 下线,本地数据直接无法访问,不适合生产核心数据库。
二:部署本地路径制备器
整套实验完整工作流程总结 1. 部署 Local Path 插件:创建 ns、权限账号、控制器、存储模板、磁盘操作脚本; 2. 创建 PVC 申请本地存储,此时 PVC 处于等待状态; 3. 启动 Nginx Pod 挂载 PVC,Pod 调度到某 worker 节点; 4. 插件监听到 Pod 绑定 PVC,在对应节点/opt/local-path-provisioner生成存储目录,自动创建 PV,PVC 绑定成功; 5. 容器读写挂载目录,数据持久化保存在宿主机本地硬盘; 6. 删除 PVC 后,自动执行清理脚本,本地目录全部删除,数据清空。
# 下载官方模板(下不了,自己复制地址去浏览器下载完传上去) [root@master30 ~ 09:52:52]# wget https://github.com/rancher/local-path-provisioner/blob/v0.0.35/deploy/local-path-storage.yaml # 查看资源配置 [root@master30 ~ 10:31:31]# cat local-path-storage.yaml
apiVersion: v1
kind: Namespace
metadata:
name: local-path-storage
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: local-path-provisioner-service-account
namespace: local-path-storage
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: local-path-provisioner-role
namespace: local-path-storage
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch", "create", "patch", "update", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: local-path-provisioner-role
rules:
- apiGroups: [""]
resources: ["nodes", "persistentvolumeclaims", "configmaps", "pods", "pods/log"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["persistentvolumes"]
verbs: ["get", "list", "watch", "create", "patch", "update", "delete"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "patch"]
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: local-path-provisioner-bind
namespace: local-path-storage
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: local-path-provisioner-role
subjects:
- kind: ServiceAccount
name: local-path-provisioner-service-account
namespace: local-path-storage
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: local-path-provisioner-bind
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: local-path-provisioner-role
subjects:
- kind: ServiceAccount
name: local-path-provisioner-service-account
namespace: local-path-storage
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: local-path-provisioner
namespace: local-path-storage
spec:
replicas: 1
selector:
matchLabels:
app: local-path-provisioner
template:
metadata:
labels:
app: local-path-provisioner
spec:
serviceAccountName: local-path-provisioner-service-account
containers:
- name: local-path-provisioner
image: rancher/local-path-provisioner:v0.0.35
imagePullPolicy: IfNotPresent
command:
- local-path-provisioner
- --debug
- start
- --config
- /etc/config/config.json
volumeMounts:
- name: config-volume
mountPath: /etc/config/
env:
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: CONFIG_MOUNT_PATH
value: /etc/config/
volumes:
- name: config-volume
configMap:
name: local-path-config
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-path
provisioner: rancher.io/local-path
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Delete
---
kind: ConfigMap
apiVersion: v1
metadata:
name: local-path-config
namespace: local-path-storage
data:
config.json: |-
{
"nodePathMap":[
{
"node":"DEFAULT_PATH_FOR_NON_LISTED_NODES",
"paths":["/opt/local-path-provisioner"]
}
]
}
setup: |-
#!/bin/sh
set -eu
mkdir -m 0777 -p "$VOL_DIR"
teardown: |-
#!/bin/sh
set -eu
rm -rf "$VOL_DIR"
helperPod.yaml: |-
apiVersion: v1
kind: Pod
metadata:
name: helper-pod
spec:
priorityClassName: system-node-critical
tolerations:
- key: node.kubernetes.io/disk-pressure
operator: Exists
effect: NoSchedule
containers:
- name: helper-pod
image: busybox
imagePullPolicy: IfNotPresent
local-path-storage.yaml 核心由 5 类资源 组成,每部分各司其职:
| 资源类型 | 名称 | 核心作用 |
|---|---|---|
| Namespace | local-path-storage | 隔离 Local Path Provisioner 相关资源 |
| ConfigMap | local-path-config | 定义本地存储路径规则(如默认 /opt/local-path-provisioner) |
| ServiceAccount + RBAC | local-path-provisioner | 赋予 Provisioner 操作 PV/PVC 的权限 |
| Deployment | local-path-provisioner | 运行 Local Path Provisioner 核心程序(动态创建本地 PV) |
| StorageClass | local-path | 提供给 PVC 调用的「本地存储模板」 |
# 资源默认部署在命名空间:local-path-storage;执行kubectl apply -f local-path-storage.yaml一次性创建全部9种资源:ns、sa、role、clusterrole、rolebinding、clusterrolebinding、deployment、sc、configmap,代表所有依赖一次性就绪。 [root@master30 ~ 10:39:58]# kubectl apply -f local-path-storage.yaml namespace/local-path-storage created serviceaccount/local-path-provisioner-service-account created role.rbac.authorization.k8s.io/local-path-provisioner-role created clusterrole.rbac.authorization.k8s.io/local-path-provisioner-role created rolebinding.rbac.authorization.k8s.io/local-path-provisioner-bind created clusterrolebinding.rbac.authorization.k8s.io/local-path-provisioner-bind created deployment.apps/local-path-provisioner created storageclass.storage.k8s.io/local-path created configmap/local-path-config created # 确认资源状态 [root@master30 ~ 10:40:30]# kubectl get sc local-path NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE local-path rancher.io/local-path Delete WaitForFirstConsumer false 28s #1.PROVISIONER: rancher.io/local-path:存储供给者标识,PVC 靠这个匹配插件; #2.RECLAIMPOLICY: Delete:删除 PVC 自动删除 PV + 本地磁盘目录; #3.VOLUMEBINDINGMODE: WaitForFirstConsumer:核心特性,不会提前绑定 PV,必须等 Pod 调度到节点后,才在对应节点创建存储目录,杜绝 Pod 和存储跨节点。 #Pod 状态Running代表存储插件控制器正常运行,持续监听集群存储事件。 [root@master30 ~ 10:40:58]# kubectl get all -n local-path-storage NAME READY STATUS RESTARTS AGE pod/local-path-provisioner-74b5c4bf98-p6zkv 1/1 Running 0 52s NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/local-path-provisioner 1/1 1 1 52s NAME DESIRED CURRENT READY AGE replicaset.apps/local-path-provisioner-74b5c4bf98 1 1 1 52s
三:验证部署
# 设置默认命名空间 [root@master30 ~ 10:41:37]# kubectl config set-context --current --namespace=default Context "kubernetes-admin@kubernetes" modified.
1. 创建 pvc(存储申请单)
[root@master30 ~ 10:42:58]# 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 # 指定 Local Path 的 StorageClass EOF [root@master30 ~ 10:45:26]# kubectl apply -f local-path-pvc.yaml persistentvolumeclaim/local-path-pvc created #查看pvc状态 [root@master30 ~ 10:45:29]# kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE local-path-pvc Pending local-path <unset> 16s #执行后 PVC 状态为Pending #现象原理:开启了WaitForFirstConsumer模式,没有 Pod 使用这个 PVC 时,插件不会创建 PV,PVC 一直处于待绑定状态,避免提前在随机节点生成磁盘。
2. 创建 Deployment 挂载 PVC(触发 PV 自动创建)
[root@master30 ~ 10:45:45]# cat > local-path-deployment.yaml <<'EOF' apiVersion: apps/v1 kind: Deployment metadata: labels: app: webapp name: webapp namespace: default spec: replicas: 2 #副本数 2 selector: matchLabels: app: webapp template: metadata: labels: app: webapp spec: containers: - image: nginx #镜像 nginx name: nginx volumeMounts: - name: webapp-data mountPath: /usr/share/nginx/html #挂载 PVC 到容器/usr/share/nginx/html; volumes: - name: webapp-data persistentVolumeClaim: claimName: local-path-pvc EOF [root@master30 ~ 10:46:04]# kubectl apply -f local-path-deployment.yaml deployment.apps/webapp created # 查看 pod 状态 [root@master30 ~ 10:46:25]# kubectl get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES webapp-6d46b54487-dv8d2 1/1 Running 0 8s 10.224.215.138 worker31.zy.cloud <none> <none> webapp-6d46b54487-kjqbs 1/1 Running 0 8s 10.224.215.139 worker31.zy.cloud <none> <none> #apply 后生成 2 个 Pod,全部调度到 worker31 节点; #底层逻辑:本地存储绑定节点,同一个 PVC 只能对应一个节点本地目录,所以所有副本只能调度到存储所在节点; #缺点:节点故障后存储无法迁移,不适合高可用业务。 # 查看 pvc 状态 [root@master30 ~ 10:46:33]# kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE local-path-pvc Bound pvc-77c5234c-7280-4c74-90e7-ec5d29215199 5Gi RWO local-path <unset> 3m7s #Pod 正常运行后,PVC 状态自动变为Bound: #插件感知到 Pod 调度到 worker31,自动在该节点创建本地目录、生成对应 PV,完成 PVC 与 PV 绑定。 # 写入测试文件(数据读写验证,确认本地磁盘持久化) [root@master30 ~ 10:48:36]# kubectl exec webapp-6d46b54487-dv8d2 -- bash -c 'echo Hello world from nginx > /usr/share/nginx/html/index.html' # 到宿主机 worker31 查看目录: #路径格式固定:/opt/local-path-provisioner/【PV名称】_【命名空间】_【PVC名称】/ #文件真实存在,证明容器写入的数据保存在节点本地物理磁盘,实现持久化。 [root@worker31 ~ 09:15:05]# ls /opt/local-path-provisioner/pvc-77c5234c-7280-4c74-90e7-ec5d29215199_default_local-path-pvc/ index.html [root@worker31 ~ 10:51:02]# cat /opt/local-path-provisioner/pvc-77c5234c-7280-4c74-90e7-ec5d29215199_default_local-path-pvc/index.html Hello world from nginx
3. 清理资源
# 删除 deployment [root@master30 ~ 11:27:59]# kubectl delete deployments.apps webapp deployment.apps "webapp" deleted # 删除 pvc [root@master30 ~ 11:27:20]# kubectl delete pvc local-path-pvc # 关键现象:宿主机本地目录直接消失 [root@worker31 ~ 11:28:37]# ls /opt/local-path-provisioner/pvc-77c5234c-7280-4c74-90e7-ec5d29215199_default_local-path-pvc/ ls: cannot access '/opt/local-path-provisioner/pvc-77c5234c-7280-4c74-90e7-ec5d29215199_default_local-path-pvc/': No such file or directory #原理;删除 PVC 后插件自动启动 helper 临时 Pod,执行rm -rf $VOL_DIR清空整个存储目录,数据全部销毁,对应 Delete 回收策略特性。
4:实验核心知识点总结
-
WaitForFirstConsumer 绑定模式 无 Pod 使用 PVC 时不创建 PV,Pod 调度完成后才在对应节点生成本地存储,不会出现存储和 Pod 跨节点无法挂载的问题。
-
本地存储强绑定节点 同一个 PVC 生成的 PV 固定在某一台 worker 节点,应用所有副本只能跑在该节点,节点宕机数据不可访问。
-
reclaimPolicy: Delete 风险点 删除 PVC 会连带删除本地磁盘所有数据,测试环境可用;生产数据库、重要业务必须修改为
Retain策略,删除 PVC 后保留数据目录。 -
完整工作链路 业务创建 PVC(指定 local-path 存储类)→ 创建 Pod 挂载 PVC → 插件监听 Pod 调度节点 → 节点自动生成本地目录 + PV → 容器读写宿主机本地磁盘 → 删除 PVC 自动清空本地目录。
-
适用 / 不适用场景 ✅ 适用:测试集群、单机缓存、开发环境、追求本地磁盘低延迟; ❌ 不适用:核心数据库、需要集群故障迁移、多节点高可用业务。
6.为什么同一个 PVC 生成的 PV 固定在某一台 worker 节点,应用所有副本只能跑在该节点,节点宕机数据不可访问?
1. 核心前提:Local Path 是节点本地硬盘 普通云存储 / 分布式存储(ceph、云盘)是集群共享存储,所有节点都能读写; 但 Local Path 用的是单个 worker 机器本地磁盘文件夹 /opt/local-path-provisioner,文件只存在这一台机器硬盘里,别的机器看不到。 2. 什么叫同一个 PVC 的 PV 固定一台 worker 你实验里创建了 1 个 PVC:local-path-pvc - 启动 Nginx Pod 时,调度器把 Pod 分到了 worker31; - Local Path 插件立刻只在 worker31 上创建存储目录、生成 PV; - 这个 PV 永久绑定 worker31,不会跑到其他节点(worker30、worker32 等)。 这个 PVC 对应的存储,永远只存在 worker31 本地。 3. 为什么应用所有副本只能跑在该节点 你的 Deployment 副本数是 2,两个 Pod 都必须读取同一个本地文件夹。 - 如果其中一个 Pod 调度到 worker32:worker32 本地没有这个 PVC 对应的存储目录,容器启动会直接报错、挂载失败; - K8s 调度器识别本地 PV 绑定节点,自动约束:所有副本只能调度到 PV 所在 worker31。 所以你执行 kubectl get pods -o wide 看到两个 Pod 都在 worker31。 4. 节点宕机数据不可访问(最大缺陷) 分两种场景理解: 场景 1:worker31 机器关机 / 故障、断电 - PV、数据全部存在 worker31 本地硬盘; - worker31 离线后,整个集群没有任何节点能读取这份存储; - 所有依赖这个 PVC 的 Pod 全部无法启动,业务直接瘫痪,直到 worker31 恢复开机。 场景 2:想把业务迁移到其他节点 集群没有自动数据同步机制,worker30/32 上没有这份数据,就算手动删除 Pod 重新调度,Pod 也不会跑到其他节点,就算强制调度也挂载失败、丢失数据。 5. 和共享存储对比区分,更好理解 1. 分布式共享存储(Ceph / 云硬盘) 盘是集群公共资源,Pod 可以在任意节点漂移,节点宕机,其他节点照样挂载磁盘读取数据,高可用。 2. Local Path 本地存储 盘属于单台机器私有资源,锁死节点;节点故障 = 存储失联,业务中断。 6. 结合你实验的现象总结 1. PVC 创建后无 Pod:Pending,不创建存储; 2. Pod 调度到 worker31:仅 worker31 生成本地目录,PV 绑定该节点; 3. 多副本 Pod 全部锁死在 worker31,不能分散到多节点; 4. 一旦 worker31 宕机,Nginx 读取不到index.html,页面完全打不开,数据无法访问。 7.补充使用建议 只适合测试、开发环境、临时缓存; 生产数据库、核心业务绝对不能用 Local Path,节点故障会直接丢业务服务。
部署 NFS provisioner
实验整体流程总结 1. 搭建基础 NFS 共享存储服务端,开放集群读写权限 2. 所有节点安装 NFS 客户端依赖 3. 创建独立命名空间隔离存储组件 4. 配置 RBAC 权限,给 provisioner 操作 PV/PVC 的集群权限 5. 部署 nfs-subdir-external-provisioner,监听 PVC 事件自动创建子目录 PV 6. 创建 StorageClass 绑定制备器,定义存储回收、扩容策略 7. 创建 PVC 指定存储类,自动生成 PV,验证 NFS 底层生成独立目录 8. 部署 Nginx 挂载 PVC,测试持久化数据读写 9. 清理应用与 PVC,验证存储回收策略,区分集群 PV 对象与底层真实文件生命周期
部署 NFS 服务
# 安装 NFS server [root@master30 ~ 15:00:09]# apt install -y nfs-kernel-server # 创建共享目录并赋予 777 权限,直接设置目录权限:所有用户可读、可写、可执行,解决容器挂载 NFS 后文件权限、root 用户映射报错问题 [root@master30 ~ 11:28:20]# mkdir -m 777 /shares/ # 配置共享,允许所有客户端访问 root@master30 ~ 15:42:12]# cat << EOF > /etc/exports /shares *(rw,sync,no_root_squash,no_all_squash,insecure) EOF #配置字段拆解: - /shares:要对外共享的本地目录 - *:允许所有客户端 IP挂载(生产环境建议限制集群网段如10.1.8.0/24) - rw:读写权限;ro = 只读 - sync:同步写入,数据落盘后再返回成功,数据更安全;async 异步性能高但丢电丢数据 - no_root_squash:客户端 root 用户不被压缩为匿名 nfsnobody,容器内 root 能正常读写文件(k8s 必备) - no_all_squash:普通客户端用户也不映射匿名用户,保留原有 UID/GID - insecure:允许客户端使用大于 1024 的随机端口挂载,k8s pod 随机端口必须开启,否则挂载失败 # 重启 NFS 服务生效配置:修改 exports 必须重启 nfs-server 才能加载新共享规则 [root@master30 ~ 15:42:12]# systemctl restart nfs-server.service # 客户端安装:所有 k8s 节点必须安装,否则 pod 无法挂载 NFS 存储,会出现卷挂载失败报错 [root@worker31 ~ 11:28:38]# apt install -y nfs-common [root@worker32 ~ 09:15:08]# apt install -y nfs-common
部署 NFS provisioner
NFS没有内置制备器,这里我们自定义外部分配器。
# 资源部署到命名空间:kube-storage [root@master30 ~ 15:42:37]# kubectl create ns kube-storage namespace/kube-storage created [root@master30 ~ 15:45:51]# kubectl config set-context --current --namespace kube-storage Context "kubernetes-admin@kubernetes" modified.
1. 创建 RBAC 权限
为什么要创建RBAC权限:NFS provisioner pod 需要权限去集群创建 / 删除 PV、监听 PVC 事件,无权限会无法自动创建 PV。
创建 nfs-rbac.yaml 文件,赋予 Provisioner 操作 PV/PVC 的权限。
[root@master30 ~ 15:46:03]# cat > nfs-rbac.yaml <<'EOF' # 1. 服务账号:给provisioner pod使用 apiVersion: v1 kind: ServiceAccount metadata: name: nfs-client-provisioner namespace: kube-storage --- # 2. 集群角色:定义允许操作的资源动作 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"] --- # 3. 集群角色绑定:把角色权限授予服务账号 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 #逐块解释 #1. ServiceAccount Pod 运行身份,provisioner 容器启动时指定serviceAccountName,所有集群操作都用该账号鉴权。 #2. ClusterRole(集群角色,全局权限,跨命名空间生效) - persistentvolumes:增删查看 PV(动态创建删除存储卷) - persistentvolumeclaims:监听 PVC 创建事件、更新 PVC 绑定状态 - storageclasses:读取存储类配置 - events:生成事件记录 PV/PVC 操作日志 - endpoints:内部通信所需资源权限 #3. ClusterRoleBinding 绑定规则:将 ClusterRole 的全套权限授予 kube-storage 命名空间下的 sa,全局生效。 [root@master30 ~ 15:47:23]# kubectl apply -f nfs-rbac.yaml serviceaccount/nfs-client-provisioner created clusterrole.rbac.authorization.k8s.io/nfs-client-provisioner-runner created clusterrolebinding.rbac.authorization.k8s.io/run-nfs-client-provisioner created
2. 部署 NFS Provisioner 应用
该程序核心功能:监听集群 PVC 创建事件,自动在 NFS 共享目录新建独立子目录,自动生成 PV 绑定 PVC,实现动态存储。
[root@master30 ~ 15:49:48]# cat > nfs-provisioner.yaml <<'EOF' apiVersion: apps/v1 kind: Deployment metadata: name: nfs-client-provisioner namespace: kube-storage spec: replicas: 1 #- replicas:1:单副本运行,provisioner 不支持多副本高可用 strategy: type: Recreate #- strategy: Recreate:更新 pod 时先删除旧 pod 再新建,避免同时读写 NFS 目录冲突 selector: matchLabels: app: nfs-client-provisioner template: metadata: labels: app: nfs-client-provisioner spec: serviceAccountName: nfs-client-provisioner #指定前面创建的 RBAC 服务账号 volumes: - name: nfs-client-root nfs: server: 10.1.8.30 # 同上 NFS 服务器 IP path: /shares # NFS共享根目录 containers: - name: nfs-client-provisioner image: registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2 volumeMounts: - name: nfs-client-root mountPath: /persistentvolumes env: - name: PROVISIONER_NAME value: fuseim.pri/ifs - name: NFS_SERVER value: 10.1.8.30 # 替换为你的 NFS 服务器 IP - name: NFS_PATH value: /shares # 核心:共享路径改为 /shares EOF # 部署 NFS Provisioner 应用 root@master30 ~ 15:51:00]# kubectl apply -f nfs-provisioner.yaml deployment.apps/nfs-client-provisioner created # 查看 NFS Provisioner 应用 [root@master30 ~ 15:51:17]# kubectl get deployments.apps -n kube-storage NAME READY UP-TO-DATE AVAILABLE AGE nfs-client-provisioner 1/1 1 1 106s
3. 创建 NFS StorageClass
StorageClass 是 k8s 存储模板,PVC 指定 storageClassName 即可触发对应 provisioner 自动创建 PV。
[root@master30 ~ 15:53:03]# cat > nfs-storageclass.yaml <<'EOF' apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-storage namespace: kube-storage provisioner: fuseim.pri/ifs # 必须和 Provisioner 名称一致 parameters: onDelete: retain # 存储底层:删除PVC后保留NFS真实业务目录 archiveOnDelete: "false" # 按需设置回收策略:Delete:回收策略,删除 PVC 时自动删除对应 PV;Retain 代表保留 PV 和 NFS 数据 #reclaimPolicy: Retain reclaimPolicy: Delete # 集群层面:删除PVC后清除集群PV资源,注意和onDelete:retain区分 allowVolumeExpansion: true volumeBindingMode: Immediate EOF # 创建 StorageClass [root@master30 ~ 15:53:32]# kubectl apply -f nfs-storageclass.yaml storageclass.storage.k8s.io/nfs-storage created # 查看 StorageClass root@master30 ~ 15:53:43]# kubectl get sc NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE nfs-storage fuseim.pri/ifs Delete Immediate true 17s
验证部署
在default命名空间测试。
[root@master30 ~ 15:55:01]# kubectl config set-context --current --namespace default Context "kubernetes-admin@kubernetes" modified.
1. 创建 pvc
[root@master30 ~ 15:55:24]# cat > nfs-pvc.yaml <<'EOF' apiVersion: v1 kind: PersistentVolumeClaim metadata: name: webclaim namespace: default spec: accessModes: - ReadWriteOnce resources: requests: storage: 5Gi storageClassName: "nfs-storage" #指定使用我们创建的 NFS 存储类,触发自动制备 PV EOF # 创建 pvc,查看绑定状态 [root@master30 ~ 15:55:36]# kubectl apply -f nfs-pvc.yaml persistentvolumeclaim/webclaim created # 查看 pvc 状态:PVC 状态变为 Bound:代表 provisioner 自动创建 PV 并完成绑定 [root@master30 ~ 15:55:54]# kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE webclaim Bound pvc-f8dd12bb-4dc7-4e18-9923-0b2301705839 5Gi RWO nfs-storage <unset> 5s # 查看 pv 状态:PV 名称为 UUID 格式,对应 NFS 服务器 /shares 下自动生成的独立文件夹 [root@master30 ~ 15:55:59]# kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE pvc-f8dd12bb-4dc7-4e18-9923-0b2301705839 5Gi RWO Delete Bound default/webclaim nfs-storage <unset> 11s # 查看 nfs 共享目录 [root@master30 ~ 15:56:05]# ls /shares/ default-webclaim-pvc-f8dd12bb-4dc7-4e18-9923-0b2301705839 #输出类似 default-webclaim-pvc-xxx #命名规则:命名空间-pvc名称-pvc唯一ID,每个 PVC 隔离独立目录,数据互不干扰 # 写入测试网页文件(NFS 服务端操作),直接在 NFS 底层目录写入静态页面,验证容器能读取共享存储数据 [root@master30 ~ 15:57:20]# echo Hello world From nginx > /shares/default-webclaim-pvc-f8dd12bb-4dc7-4e18-9923-0b2301705839/index.html
2. 创建 deployment(部署 Nginx 应用挂载 PVC)
[root@master30 ~ 15:57:59]# cat > nfs-deploy.yaml << EOF apiVersion: apps/v1 kind: Deployment metadata: labels: app: webapp name: webapp spec: replicas: 2 #启动 2 个 Nginx 副本,共享同一份 NFS 存储 selector: matchLabels: app: webapp template: metadata: labels: app: webapp spec: containers: - image: nginx name: nginx volumeMounts: - name: webapp-data mountPath: /usr/share/nginx/html #将 PVC 挂载到 Nginx 网页目录/usr/share/nginx/html volumes: - name: webapp-data persistentVolumeClaim: claimName: webclaim #绑定刚才创建的 PVC EOF [root@master30 ~ 15:58:14]# kubectl apply -f nfs-deploy.yaml deployment.apps/webapp created #- kubectl expose:为 deployment 创建 ClusterIP 内部服务 #--port:service 端口;--target-port:容器内 nginx 端口 80 [root@master30 ~ 15:58:26]# kubectl expose deployment webapp --target-port 80 --port 80 --protocol TCP service/webapp exposed #查看 service 分配的集群内网 IP [root@master30 ~ 15:59:00]# kubectl get svc webapp NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE webapp ClusterIP 10.104.143.59 <none> 80/TCP 12s # 访问测试 [root@master30 ~ 15:59:12]# curl 10.104.143.59 Hello world From nginx #输出Hello World From Nginx,证明 pod 成功读取 NFS 共享目录的文件,动态存储可用。
3. 清理资源
# 删除 deployment [root@master30 ~ 15:59:30]# kubectl delete deployments.apps webapp deployment.apps "webapp" deleted # 删除 pvc,pv也将一起删除 [root@master30 ~ 15:59:47]# kubectl delete pvc webclaim persistentvolumeclaim "webclaim" deleted [root@master30 ~ 15:59:55]# kubectl get pv No resources found # 查看后端存储中数据,仍然保留 [root@master30 ~ 16:00:00]# ls /shares/default-webclaim-pvc-f8dd12bb-4dc7-4e18-9923-0b2301705839/ index.html #文件夹和 index.html 文件仍然存在,原因:StorageClass 参数onDelete: retain控制删除 PVC 不清除 NFS 真实数据,避免误删业务文件;如需自动清理可修改参数。
保留 NFS provisioner ,下一章 statefulset 使用。
Kubernetes StatefulSet
环境准备
[root@master30 ~ 16:00:18]# kubectl create ns statefulset namespace/statefulset created [root@master30 ~ 16:13:18]# kubectl config set-context --current --namespace statefulset Context "kubernetes-admin@kubernetes" modified.
无状态应用 vs 有状态应用
| 维度 | 无状态应用(Stateless) | 有状态应用(Stateful) |
|---|---|---|
| 核心特征 | 不保存任何请求/会话数据,每次请求独立 | 依赖持久化的会话/数据,请求间有依赖关系 |
| 数据存储 | 数据仅在请求期间存在,不落地/仅落地外部存储 | 数据需持久化(本地/分布式存储),依赖固定存储 |
| 实例一致性 | 所有实例完全相同,可随意扩缩容/替换 | 实例有身份(如编号),启动/扩缩容有顺序要求 |
| 网络标识 | 共享 IP/域名,无固定网络标识 | 有固定网络标识(如 Headless Service + DNS) |
总结
-
无状态核心:实例“无记忆”,可随意替换/扩容,是云原生优先推荐的应用形态;
-
有状态核心:实例“有记忆”,依赖持久化和固定身份,需通过 StatefulSet 等组件保障稳定性;
-
最佳实践:尽可能将应用拆分为“无状态业务层 + 有状态数据层”,既提升扩展能力,又保障数据安全。
StatefulSet 介绍
StatefulSet 定义
StatefulSet 是 Kubernetes 中专门用于管理有状态应用的工作负载资源,与用于管理无状态应用的 Deployment 相比,其核心优势在于为每个 Pod 分配固定且唯一的身份标识(包括名称、网络标识、存储关联),即便 Pod 发生重启、迁移或重建,其身份信息和绑定的存储资源也不会改变,确保有状态应用的稳定性和连续性。
适用场景:数据库(MySQL、PostgreSQL)、分布式中间件(Redis 集群、ZooKeeper)、消息队列(Kafka)等依赖节点身份和数据持久化的服务。
StatefulSet 特性
-
固定身份标识:每个 Pod 拥有唯一的序号(从 0 开始递增),hostname 固定为「StatefulSet 名称-序号」(如 mysql-0),同时通过 Headless Service 提供固定 DNS 解析(如 mysql-0.mysql.default.svc.cluster.local),便于集群内节点间稳定通信和服务发现。
-
有序操作:
-
部署/扩容:按序号从 0 到 N-1 依次创建 Pod,必须等待前一个 Pod 处于 Ready 状态后,再创建下一个,确保集群启动顺序符合业务依赖;
-
缩容/删除:按序号从 N-1 到 0 依次删除 Pod,前一个 Pod 彻底删除(Terminating 状态结束)后,再删除下一个,避免数据丢失或集群异常;
-
更新:默认按序号从 N-1 到 0 滚动更新,确保更新过程中集群始终保持可用,避免服务中断。
-
-
-
稳定存储:通过
PersistentVolumeClaim(PVC)模板为每个 Pod 自动创建独立的 PVC,PVC 名称与 Pod 序号绑定(格式:模板名-StatefulSet名称-序号)。即使 Pod 被删除,PVC 及绑定的 PersistentVolume(PV)仍会保留,重新创建的 Pod 会自动复用原 PVC 和 PV,实现数据不丢失。 -
Headless Service 依赖:StatefulSet 必须关联一个 Headless Service(ClusterIP 设为 None),该服务不提供负载均衡功能,仅负责为每个 Pod 提供固定的 DNS 解析,是 Pod 间稳定通信的核心前提。
StatefulSet VS Deployment
| 控制器 | StatefulSet | Deployment |
|---|---|---|
| 适用场景 | 有状态应用(数据库、分布式集群、消息队列) | 无状态应用(Web 服务、API 接口、静态服务) |
| 网络 | Headless Service(固定 DNS 解析) | ClusterIP/LoadBalancer(共享 IP) |
| 存储 | 必须 PVC + PV(固定存储卷绑定) | 可选 PVC(无数据持久化要求) |
| 扩缩容 | 按实例编号顺序扩缩容(如从 0→1→2) | 无顺序,瞬间完成 |
| 更新策略 | 有序更新(如从最后一个实例开始) | 滚动更新/重建,无顺序 |
| 重启/重建 | 实例重启后需恢复原有数据/身份 | 实例重启后无影响 |
StatefulSet 关键组件
-
StatefulSet 控制器:Kubernetes 核心控制器之一,负责管理 Pod 的全生命周期,确保 Pod 按序号规则创建、更新、删除,维护 Pod 身份和存储的稳定性。
-
Headless Service:核心作用是为 Pod 提供固定 DNS 解析,无 ClusterIP,仅负责将 Pod 名称映射为 DNS 记录,支持 Pod 间通过 hostname 稳定通信。
-
PVC 模板:定义每个 Pod 所需的存储规格(如存储容量、访问模式),StatefulSet 会根据模板为每个 Pod 自动创建对应的 PVC,无需手动创建。
-
PV(PersistentVolume):实际的存储资源,与 PVC 一一绑定,为 Pod 提供持久化存储支持,可选用本地存储、云存储(AWS EBS、阿里云 EBS)、分布式存储(Ceph)等类型。
StatefulSet 实践操作
所有命令均在 statefulset 命名空间执行。
部署 NFS 服务器
# 安装 NFS server [root@master30 ~ 16:00:18]# apt install -y nfs-kernel-server # 安创建NFS目录 修改创建文件夹的权限 [root@master30 ~ 16:00:51]# mkdir -m 777 -p /shares # 配置共享,允许所有客户端访问 root@master30:~# cat << EOF >> /etc/exports /shares *(rw,sync,no_root_squash,no_all_squash,insecure) EOF # 重启 nfs server [root@master30 ~ 16:14:50]# systemctl restart nfs-server.service # 客户端安装 [root@worker31 ~ 16:15:16]# apt install -y nfs-common [root@worker32 ~ 16:15:25]# apt install -y nfs-common
示例 1:部署 Nginx 集群
需求:部署 3 节点 Nginx 集群,实现数据持久化、固定网络标识、有序部署,满足高可用。
步骤 1:创建 StatefulSet
创建 nginx-statefulset.yaml,通过 PVC 模板自动创建存储。
[root@master30 ~ 16:15:12]# cat > nginx-statefulset.yaml <<'EOF' apiVersion: apps/v1 kind: StatefulSet metadata: name: nginx spec: selector: matchLabels: app: nginx serviceName: "nginx" replicas: 3 minReadySeconds: 10 template: metadata: labels: app: nginx spec: terminationGracePeriodSeconds: 10 containers: - name: nginx image: docker.io/library/nginx:latest ports: - containerPort: 80 name: nginx volumeMounts: - name: nginx-data mountPath: /usr/share/nginx/html volumeClaimTemplates: - metadata: name: nginx-data spec: accessModes: [ "ReadWriteOnce" ] # 使用上一章动态卷置备nfs Provider storageClassName: "nfs-storage" resources: requests: storage: 10Gi EOF # 创建 statefulset [root@master30 ~ 16:16:16]# kubectl apply -f nginx-statefulset.yaml statefulset.apps/nginx created # 查看 pods [root@master30 ~ 16:16:35]# watch kubectl get pods NAME READY STATUS RESTARTS AGE nginx-0 1/1 Running 0 10m nginx-1 1/1 Running 0 10m nginx-2 1/1 Running 0 9m41s # 预期输出:Pod 按 nginx-0、nginx-1、nginx-2顺序创建 # 先创建 nginx-0,处于 Running 状态 # 等待20s,再创建 nginx-1,处于 Running 状态 # 等待20s,再创建 nginx-2,处于 Running 状态 # 最终 3 个 Pod 均为 Running 状态。 # 查看 statefulset [root@master30 ~ 16:17:33]# kubectl get sts NAME READY AGE nginx 3/3 75s # 查看 pvc 创建和pv绑定过程(可选) [root@master30 ~ 16:18:41]# kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE nginx-data-nginx-0 Bound pvc-8593d195-a5e7-4458-8155-324d3a9680d5 10Gi RWO nfs-storage <unset> 4m21s nginx-data-nginx-1 Bound pvc-4042fca6-e55f-4bb4-85d4-207fbfb80f2f 10Gi RWO nfs-storage <unset> 4m1s nginx-data-nginx-2 Bound pvc-84db5793-9338-48cc-8aa5-28fd07b69abc 10Gi RWO nfs-storage <unset> 3m41s
步骤 2:准备 nfs 存储
[root@master30 ~ 16:20:56]# kubectl get pvc|awk '{print $1,$3}'
NAME VOLUME
nginx-data-nginx-0 pvc-8593d195-a5e7-4458-8155-324d3a9680d5
nginx-data-nginx-1 pvc-4042fca6-e55f-4bb4-85d4-207fbfb80f2f
nginx-data-nginx-2 pvc-84db5793-9338-48cc-8aa5-28fd07b69abc
[root@master30 ~ 16:21:28]# echo nginx-data-nginx-0 > /shares/statefulset-nginx-data-nginx-0-pvc-8593d195-a5e7-4458-8155-324d3a9680d5/index.html
[root@master30 ~ 16:22:29]# echo nginx-data-nginx-1 > /shares/statefulset-nginx-data-nginx-1-pvc-4042fca6-e55f-4bb4-85d4-207fbfb80f2f/index.html
[root@master30 ~ 16:23:01]# echo nginx-data-nginx-2 > /shares/statefulset-nginx-data-nginx-2-pvc-84db5793-9338-48cc-8aa5-28fd07b69abc/index.html
步骤 3:创建 Headless Service
创建 nginx-service.yaml,为后续 StatefulSet 管理的 Pod 提供固定名称。
[root@master30 ~ 16:23:17]# cat > nginx-service.yaml <<'EOF' apiVersion: v1 kind: Service metadata: name: nginx namespace: statefulset spec: selector: app: nginx clusterIP: None ports: - port: 80 targetPort: 80 name: nginx-port EOF # 创建服务 [root@master30 ~ 16:24:03]# kubectl apply -f nginx-service.yaml service/nginx created # 没有 CLUSTER-IP [root@master30 ~ 16:24:17]# kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE nginx ClusterIP None <none> 80/TCP 3s
步骤 4:验证部署
# 通过pod名称验证 pod 内容 [root@master30 ~ 16:24:20]# kubectl run test --image nginx -it -- bash If you don't see a command prompt, try pressing enter. # pod名称后要加service后缀 root@test:/# curl nginx-0.nginx nginx-data-nginx-0 root@test:/# curl nginx-1.nginx nginx-data-nginx-1 root@test:/# curl nginx-2.nginx nginx-data-nginx-2 # 删除 nginx-2,等待重新创 [root@master30 ~ 16:27:45]# kubectl delete pod nginx-2 pod "nginx-2" deleted [root@master30 ~ 16:28:00]# kubectl get pods NAME READY STATUS RESTARTS AGE nginx-0 1/1 Running 0 49m nginx-1 1/1 Running 0 49m nginx-2 0/1 ContainerCreating 0 3s # 新创建的pod的 IP 可能变化 # 验证 nginx-2 内容 [root@master30 ~ 16:28:08]# kubectl exec test -it -- bash root@test:/# curl nginx-2.nginx nginx-data-nginx-2 # 预期输出:数据仍存在,说明 PV/PVC 实现了数据持久化,Pod 重建后可复用存储。
思考:如何为statefulset提供一个统一的IP入口呢?
答案:再创建一个普通的ClusterIP类型的service,暴漏statefulset。
[root@master30 ~ 16:29:23]# 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 [root@master30 ~ 16:29:37]# kubectl apply -f nginx-service-www.yaml service/www created [root@master30 ~ 16:29:48]# kubectl get svc www NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE www ClusterIP 10.107.127.191 <none> 80/TCP 10s # 通过svc访问后端pod [root@master30 ~ 16:29:58]# curl 10.107.127.191 nginx-data-nginx-0 [root@master30 ~ 16:30:22]# curl 10.107.127.191 nginx-data-nginx-2 [root@master30 ~ 16:30:23]# curl 10.107.127.191 nginx-data-nginx-1
步骤 5:清理环境
[root@master30 ~ 16:30:25]# kubectl delete sts nginx
statefulset.apps "nginx" deleted
[root@master30 ~ 16:32:04]# kubectl delete svc www nginx
service "www" deleted
service "nginx" deleted
[root@master30 ~ 16:32:22]# rm -rf /shares/statefulset-nginx-data-nginx-{0..2}-*
[root@master30 ~ 16:32:47]# kubectl delete pod test
pod "test" deleted
示例 2:部署 Etcd 集群
需求:部署 3 节点 etcd 集群,实现数据持久化、固定网络标识、有序部署,满足高可用。
步骤 1:准备 NFS 存储
# 准备应用目录和文件
[root@master30 ~ 16:33:26]# mkdir -m 777 -p /shares/etcd/data-{0..2}
步骤 2:创建 Headless Service
创建 etcd-service.yaml,为后续 StatefulSet 管理的 Pod 提供固定名称。
[root@master30 ~ 16:37:47]# 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 # 创建服务 [root@master30 ~ 16:38:35]# kubectl apply -f etcd-service.yaml service/etcd created # 没有 CLUSTER-IP [root@master30 ~ 16:38:48]# kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE etcd ClusterIP None <none> 2379/TCP,2380/TCP 12s
步骤 3:创建 PV
创建 3个 PV,使用 NFS 存储(服务器地址10.1.8.30,共享目录/shares/),指定存储容量和回收策略。
[root@master30 ~ 16:39:00]# cat > etcd-pv.yaml <<'EOF' apiVersion: v1 kind: PersistentVolume metadata: name: etcd-pv-0 spec: capacity: storage: 10Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: etcd-storage nfs: server: 10.1.8.30 path: /shares/etcd/data-0 --- apiVersion: v1 kind: PersistentVolume metadata: name: etcd-pv-1 spec: capacity: storage: 10Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: etcd-storage nfs: server: 10.1.8.30 path: /shares/etcd/data-1 --- apiVersion: v1 kind: PersistentVolume metadata: name: etcd-pv-2 spec: capacity: storage: 10Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: etcd-storage nfs: server: 10.1.8.30 path: /shares/etcd/data-2 EOF # 创建 pv [root@master30 ~ 16:39:56]# kubectl apply -f etcd-pv.yaml persistentvolume/etcd-pv-0 created persistentvolume/etcd-pv-1 created persistentvolume/etcd-pv-2 created [root@master30 ~ 16:40:09]# kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE etcd-pv-0 10Gi RWO Retain Available etcd-storage <unset> 4s etcd-pv-1 10Gi RWO Retain Available etcd-storage <unset> 4s etcd-pv-2 10Gi RWO Retain Available etcd-storage <unset> 4s pvc-4042fca6-e55f-4bb4-85d4-207fbfb80f2f 10Gi RWO Delete Bound statefulset/nginx-data-nginx-1 nfs-storage <unset> 23m pvc-84db5793-9338-48cc-8aa5-28fd07b69abc 10Gi RWO Delete Bound statefulset/nginx-data-nginx-2 nfs-storage <unset> 22m pvc-8593d195-a5e7-4458-8155-324d3a9680d5 10Gi RWO Delete Bound statefulset/nginx-data-nginx-0 nfs-storage <unset> 23m
建议:使用动态卷制备。
步骤 4:创建 StatefulSet
创建 etcd-statefulset.yaml,通过 PVC 模板自动创建存储。
[root@master30 ~ 16:40:13]# 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: registry.aliyuncs.com/google_containers/etcd:3.5.10-0 command: - /usr/local/bin/etcd ports: - containerPort: 2379 name: client - containerPort: 2380 name: peer env: - name: ETCD_NAME valueFrom: fieldRef: fieldPath: metadata.name - 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: "etcd-storage" resources: requests: storage: 10Gi EOF # 创建 statefulset [root@master30 ~ 16:40:50]# kubectl apply -f etcd-statefulset.yaml statefulset.apps/etcd created # 查看 pods [root@master30 ~ 16:41:11]# kubectl get pods NAME READY STATUS RESTARTS AGE etcd-0 0/1 ContainerCreating 0 5s [root@master30 ~ 16:41:16]# kubectl get pods NAME READY STATUS RESTARTS AGE etcd-0 1/1 Running 0 19s etcd-1 1/1 Running 0 9s etcd-2 0/1 Pending 0 1s [root@master30 ~ 16:41:30]# kubectl get pods NAME READY STATUS RESTARTS AGE etcd-0 1/1 Running 0 26s etcd-1 1/1 Running 0 16s etcd-2 1/1 Running 0 8s # 预期输出:Pod 按 etcd-0、etcd-1、etcd-2顺序创建 # 先创建 etcd-0,处于 Running 状态 # 再创建 etcd-1,处于 Running 状态 # 再创建 etcd-2,处于 Running 状态 # 最终 3 个 Pod 均为 Running 状态。 # 查看 statefulset [root@master30 ~ 16:41:37]# kubectl get sts NAME READY AGE etcd 3/3 43s
步骤 5:验证部署
验证集群状态
# 验证集群状态:建立集群 [root@master30 ~ 16:41:54]# kubectl exec etcd-2 -- etcdctl member list 8747632212a8465, started, etcd-2, http://etcd-2.etcd.statefulset.svc.cluster.local:2380, http://etcd-2.etcd.statefulset.svc.cluster.local:2379, false 881e7c876792f042, started, etcd-0, http://etcd-0.etcd.statefulset.svc.cluster.local:2380, http://etcd-0.etcd.statefulset.svc.cluster.local:2379, false b6d311da39fc1138, started, etcd-1, http://etcd-1.etcd.statefulset.svc.cluster.local:2380, http://etcd-1.etcd.statefulset.svc.cluster.local:2379, false # 删除 etcd-2,等待重新创 [root@master30 ~ 16:44:26]# kubectl delete pod etcd-2 pod "etcd-2" deleted # 验证集群状态:删了 etcd-2 Pod,但 member list 里节点信息完全没变 [root@master30 ~ 16:44:49]# kubectl exec etcd-0 -- etcdctl member list 8747632212a8465, started, etcd-2, http://etcd-2.etcd.statefulset.svc.cluster.local:2380, http://etcd-2.etcd.statefulset.svc.cluster.local:2379, false 881e7c876792f042, started, etcd-0, http://etcd-0.etcd.statefulset.svc.cluster.local:2380, http://etcd-0.etcd.statefulset.svc.cluster.local:2379, false b6d311da39fc1138, started, etcd-1, http://etcd-1.etcd.statefulset.svc.cluster.local:2380, http://etcd-1.etcd.statefulset.svc.cluster.local:2379, false #1. 先分清两个概念(核心关键点) ① StatefulSet 的 Pod etcd-2 是 StatefulSet 管理的 Pod,执行 kubectl delete pod etcd-2: - K8s 会立刻新建一个同名 Pod etcd-2,网络标识、Pod hostname 完全不变; - 只是底层容器进程重启,etcd 集群成员配置没有任何改动。 ② etcd 集群 Member(成员记录) etcdctl member list 展示的是 etcd 数据库内持久保存的集群成员清单,存在 etcd 数据盘里,不是实时 Pod 状态。 - 只有执行 etcdctl member remove <节点ID>,才会从集群里彻底删掉这条成员记录; - 单纯删 Pod、重启 Pod,不会修改 etcd 内部的成员列表。 #2. 你操作的完整流程拆解 1. 初始状态 3 个 etcd 成员:etcd-0 /etcd-1 /etcd-2,全部 started,集群健康。 2. 执行 kubectl delete pod etcd-2 StatefulSet 控制器检测副本数不足,马上调度创建新 Pod etcd-2: - Pod 名称、hostname、svc 域名 etcd-2.etcd.statefulset.svc.cluster.local 和原来完全一致; - 新 Pod 启动后,直接使用原有 etcd 成员 ID 加入集群。 #3. 再次执行 etcdctl member list etcd 内部成员记录没被删除,所以三条记录原样输出,etcd-2 那条依然存在、状态 started。 3. 为什么不会消失? 1. StatefulSet 有稳定网络身份,删除 Pod 只是重启实例,不是集群缩容; 2. etcd 的 member 列表独立于 Pod 生命周期,只记录集群注册过的节点; 3. 只要新建 Pod 能连上原有域名、原有数据目录,就复用原来的 member ID。
验证数据
[root@master30 ~ 16:45:49]# kubectl exec etcd-0 -- etcdctl put /users/user1/name zy OK #输出 OK,代表数据写入成功、集群已完成数据持久化。 #- kubectl exec etcd-0:进入集群中 etcd-0 节点 的容器内部执行命令 #- --:kubectl 参数分隔符,后面内容为容器内执行的原生命令 #- etcdctl put:etcd 原生写入指令,用于新增/更新 kv 键值对数据 #- /users/user1/name:etcd 自定义键(key),支持类文件系统层级路径格式,方便分类管理数据 #- laowang:对应 key 的值(value),即存储的具体数据 #- etcdctl get:etcd 原生读取指令,根据指定 key 查询对应存储值 [root@master30 ~ 17:52:07]# kubectl exec etcd-2 -- etcdctl get /users/user1/name zy /users/user1/name zy #核心底层逻辑(重点) #你仅在 etcd-0 节点写入数据,但 etcd 是分布式一致性集群:写入数据后,集群自动通过 Raft 协议同步数据到 etcd-1、etcd-2 所有节点,三个节点的底层数据完全一致,不存在单节点数据隔离。
步骤 6:清理环境
[root@master30 ~ 17:53:20]# kubectl delete sts etcd
statefulset.apps "etcd" deleted
[root@master30 ~ 17:53:33]# kubectl delete svc etcd
service "etcd" deleted
[root@master30 ~ 17:53:37]# kubectl delete pvc etcd-data-etcd-{0..2}
persistentvolumeclaim "etcd-data-etcd-0" deleted
persistentvolumeclaim "etcd-data-etcd-1" deleted
persistentvolumeclaim "etcd-data-etcd-2" deleted
[root@master30 ~ 17:53:41]# kubectl delete pv etcd-pv-{0..2}
persistentvolume "etcd-pv-0" deleted
persistentvolume "etcd-pv-1" deleted
persistentvolume "etcd-pv-2" deleted
[root@master30 ~ 17:53:44]# rm -fr /shares/etcd
示例 3:部署 Redis 集群
实验整体架构与核心目标
1. 需求定位
搭建1 主 2 从 Redis 哨兵高可用集群,不是 Redis Cluster 分片集群,是主从 + 哨兵故障自动转移架构; 依托 K8s StatefulSet 固定网络身份、Headless Service 稳定域名、NFS 持久存储实现:
-
固定 Pod 域名:
redis-0/1/2.redis.statefulset.svc.cluster.local,哨兵可稳定识别节点 -
AOF 持久化,宕机数据不丢失
-
主节点故障哨兵自动选举新主,自动切换读写
-
资源配额限制,生产级容器资源管控
-
PV/PVC 绑定 NFS 存储,数据持久落盘宿主机共享存储
2. 组件分工
| 组件 | 作用 |
|---|---|
| Headless Service(redis) | 无 ClusterIP,为 StatefulSet 提供固定 DNS 域名,Pod 之间通过域名互通 |
| PV(redis-pv-0/1/2) | 预创建 NFS 存储卷,对应 3 个独立数据目录,静态供给 PVC |
| StatefulSet redis | 有序创建 Pod redis-0、redis-1、redis-2,有序删除;绑定固定 PVC |
| Redis-Server | 主从复制:redis-0 默认主,1、2 自动设为从;开启 AOF 持久化、访问密码 |
| Redis-Sentinel | 3 节点哨兵集群,quorum=2,主节点故障自动故障转移 |
| NFS 存储 | 所有 Redis 数据落地共享存储,Pod 重建数据不丢失 |
3. 关键区分(容易踩坑)
-
本实验:Redis 主从 + Sentinel 高可用(主从复制,故障自动切主,无 hash 分片)
-
Redis Cluster 集群:16384 槽分片,多主多从,数据分散存储,本实验不涉及分片槽位
需求:部署 3 节点 Redis 集群(1 主 2 从),实现固定网络标识、数据持久化、集群自动组网,支持数据分片与故障自动切换,适用于高并发分布式缓存场景。
步骤 1:准备 NFS 存储
# 准备应用目录
[root@master30 ~ 17:53:49]# mkdir -m 777 -p /shares/redis/data-{0..2}
1. -m 777:NFS 客户端读写权限放开,避免 Redis 容器权限不足无法写入 AOF/RDB
2. data-0/1/2:3 个独立目录,分别对应 3 个 PV,每个 Redis 节点独占存储目录,数据隔离
3. 前置依赖:服务器已部署 NFS 服务,共享路径/shares/redis对外开放
步骤 2:创建 Headless Service
创建 redis-service.yaml,为后续 StatefulSet 管理的 Pod 提供固定名称。
[root@master30 ~ 18:13:14]# cat > redis-service.yaml <<'EOF' apiVersion: v1 kind: Service metadata: name: redis namespace: statefulset labels: app: redis spec: ports: - port: 6379 #- 6379:Redis 主从服务端口,数据读写、主从复制 name: redis - port: 26379 #- 26379:Sentinel 哨兵端口,哨兵之间互相通信、监控主节点 name: sentinel clusterIP: None #核心配置:无头服务,不分配集群 IP,K8s 自动为每个 StatefulSet Pod 生成唯一 DNS 域名: selector: app: redis EOF [root@master30 ~ 18:13:30]# kubectl apply -f redis-service.yaml service/redis created [root@master30 ~ 18:13:50]# kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE redis ClusterIP None <none> 6379/TCP,26379/TCP 4s
步骤 3:创建 PV
创建 3 个 PV,创建 redis-pv.yaml:
[root@master30 ~ 18:13:54]# cat > redis-pv.yaml <<'EOF' apiVersion: v1 kind: PersistentVolume metadata: name: redis-pv-0 spec: capacity: storage: 10Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: redis-storage nfs: server: 10.1.8.30 path: /shares/redis/data-0 --- apiVersion: v1 kind: PersistentVolume metadata: name: redis-pv-1 spec: capacity: storage: 10Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: redis-storage nfs: server: 10.1.8.30 path: /shares/redis/data-1 --- apiVersion: v1 kind: PersistentVolume metadata: name: redis-pv-2 spec: capacity: storage: 10Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: redis-storage nfs: server: 10.1.8.30 path: /shares/redis/data-2 EOF #1. 每个 PV 绑定独立 NFS 路径:/shares/redis/data-0 ~ data-2,一一对应 redis-0/1/2 #2. storageClassName: redis-storage:与 StatefulSet 里 PVC 存储类匹配,实现自动绑定 #3. accessModes: ReadWriteOnce:NFS 单节点读写(本实验每个 Pod 独占一个 NFS 目录,无需多写) #4. persistentVolumeReclaimPolicy: Retain:删除 PVC 后 PV 保留,数据不自动删除,防止误删丢失数据 #5. 容量 10Gi:为 Redis 缓存 + 持久化文件预留存储空间 [root@master30 ~ 18:14:09]# kubectl apply -f redis-pv.yaml persistentvolume/redis-pv-0 created persistentvolume/redis-pv-1 created persistentvolume/redis-pv-2 created [root@master30 ~ 18:14:30]# kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE redis-pv-0 10Gi RWO Retain Available redis-storage <unset> 3s redis-pv-1 10Gi RWO Retain Available redis-storage <unset> 3s redis-pv-2 10Gi RWO Retain Available redis-storage <unset> 3s
步骤 4:创建 StatefulSet
创建 redis-statefulset.yaml,部署 3 个 Redis 节点集群,实现集群自动组网。
[root@master30 ~ 18:14:33]# 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: "redis-storage"
resources:
requests:
storage: 10Gi
EOF
[root@master30 ~ 18:16:58]# kubectl apply -f redis-statefulset.yaml
statefulset.apps/redis created
[root@master30 ~ 18:25:16]# kubectl get pods
NAME READY STATUS RESTARTS AGE
redis-0 1/1 Running 0 36s
redis-1 1/1 Running 0 24s
redis-2 1/1 Running 0 14s
[root@master30 ~ 18:25:45]# kubectl get sts
NAME READY AGE
redis 3/3 55s
步骤 5:验证部署
验证主从状态(redis-0 为主,2 个从节点)
#验证主从复制 info replication [root@master30 ~ 18:26:04]# kubectl exec -it redis-0 -- redis-cli -a redis123 info replication Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe. # Replication role:master connected_slaves:2 slave0:ip=10.224.170.15,port=6379,state=online,offset=18757,lag=0 slave1:ip=10.224.170.16,port=6379,state=online,offset=18584,lag=1 master_failover_state:no-failover master_replid:b6f81a444581d1616f718971d474f2bad0dfe459 master_replid2:0000000000000000000000000000000000000000 master_repl_offset:19101 second_repl_offset:-1 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:1 repl_backlog_histlen:19101
验证哨兵状态(能识别主节点)
#哨兵状态验证 sentinel master mymaster [root@master30 ~ 18:28:19]# kubectl exec -it redis-0 -n statefulset -- redis-cli -p 26379 sentinel master mymaster 1) "name" 2) "mymaster" 3) "ip" 4) "redis-0.redis.statefulset.svc.cluster.local" 5) "port" 6) "6379" 7) "runid" 8) "e485d2b2a949be1302b9fb60e9bc7256fd72e93c" 9) "flags" 10) "master" 11) "link-pending-commands" 12) "0" 13) "link-refcount" 14) "1" 15) "last-ping-sent" 16) "0" 17) "last-ok-ping-reply" 18) "33" 19) "last-ping-reply" 20) "33" 21) "down-after-milliseconds" 22) "5000" 23) "info-refresh" 24) "2758" 25) "role-reported" 26) "master" 27) "role-reported-time" 28) "173403" 29) "config-epoch" 30) "0" 31) "num-slaves" 32) "2" 33) "num-other-sentinels" 34) "2" 35) "quorum" 36) "2" 37) "failover-timeout" 38) "10000" 39) "parallel-syncs" 40) "1" #- num-other-sentinels:2:当前节点能发现另外 2 个哨兵,集群完整 #- quorum:2:仲裁数量,满足故障切换条件 #- num-slaves:2:哨兵识别 2 台从节点 #- ip 字段:主节点使用域名而非 IP,得益于resolve-hostnames yes
验证集群数据分片与主从同步:
# 在 redis-0(主节点)设置测试数据,主节点 set 写入 key,从节点 get 读取,证明主从复制链路正常,数据实时同步。 [root@master30 ~ 18:29:32]# kubectl exec -it redis-1 -- redis-cli -a redis123 get test-key "redis-cluster-test" OK ## 在 redis-1(从节点)查看测试数据 [root@master30 ~ 18:29:58]# kubectl exec -it redis-1 -- redis-cli -a redis123 get test-key "redis-cluster-test"
验证集群故障切换。
# 删除一个主节点(如 redis-0),模拟节点故障 root@master30:~# kubectl delete pod redis-0 # 等待 redis-0 重新创建,查看集群状态:redis-0 的 role 变更为 slave [root@master30 ~ 18:32:10]# kubectl exec -it redis-0 -- redis-cli -a redis123 info replication Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe. # Replication role:slave master_host:10.224.170.15 master_port:6379 master_link_status:up master_last_io_seconds_ago:0 master_sync_in_progress:0 slave_read_repl_offset:106962 slave_repl_offset:106962 slave_priority:100 slave_read_only:1 replica_announced:1 connected_slaves:0 master_failover_state:no-failover master_replid:66ac381d832ca07c34a2a327a745e0f65dbd9890 master_replid2:0000000000000000000000000000000000000000 master_repl_offset:106962 second_repl_offset:-1 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:105654 repl_backlog_histlen:1309 #1. Pod 被删除,StatefulSet 控制器自动重建 redis-0; #2. redis-0 宕机,3 个哨兵 5 秒后判定主节点主观下线; #3. 超过 quorum=2,触发故障转移,在 redis-1/redis-2 中选举新主; #4. 新主取消 replicaof,自动接受写入; #5. 重建后的 redis-0 启动后,哨兵推送新主地址,redis-0 自动变为从节点; #6. 执行info replication可见role:slave,master_host 为新主 PodIP,证明切换生效。
步骤 6:清理环境
[root@master30 ~ 18:32:46]# kubectl delete svc redis
service "redis" deleted
[root@master30 ~ 18:33:07]# kubectl delete sts redis
statefulset.apps "redis" deleted
[root@master30 ~ 18:33:10]# kubectl delete pvc redis-data-redis-{0..2}
persistentvolumeclaim "redis-data-redis-0" deleted
persistentvolumeclaim "redis-data-redis-1" deleted
persistentvolumeclaim "redis-data-redis-2" deleted
[root@master30 ~ 18:33:41]# kubectl delete pv redis-pv-{0..2}
persistentvolume "redis-pv-0" deleted
persistentvolume "redis-pv-1" deleted
persistentvolume "redis-pv-2" deleted
[root@master30 ~ 18:33:44]# rm -fr /shares/redis/data-*/*
步骤七:实验核心知识点总结
1. StatefulSet 对比 Deployment 优势(本实验刚需)
-
有序创建 / 删除 Pod,保证主节点先启动
-
稳定网络身份(固定域名),哨兵必须依赖稳定标识监控主节点
-
独立持久存储,每个 Pod 独占 PVC/PV,重建不丢失数据 Deployment 无固定域名,Pod IP 随机变化,无法用于 Redis 哨兵集群。
2. Headless Service 核心价值
普通 Service 分配统一 ClusterIP,流量负载均衡; Headless 关闭 ClusterIP,直接返回所有 Pod A 记录,实现 Pod 点对点域名互通,是中间件集群标准搭配。
3. Redis 哨兵高可用核心条件
-
哨兵数量≥3,quorum = 哨兵数量 / 2 向上取整(3 哨兵 quorum=2)
-
开启域名解析支持
sentinel resolve-hostnames yes(K8s 环境必开) -
主从统一密码
requirepass/masterauth,否则同步 / 监控失败 -
持久化 AOF 开启,故障切换后数据完整
4. NFS 持久化作用
Pod 重建、节点驱逐、故障切换时,容器销毁但 NFS 目录数据留存,Redis 重启后加载原有 AOF 文件,不会丢失缓存数据。
StatefulSet 总结
-
生产环境中,敏感信息(如 MySQL 根密码、Redis 密码)需使用 Kubernetes Secret 管理,禁止明文写在 YAML 文件中,避免安全风险。
-
StatefulSet 缩容时,严格按序号从大到小删除 Pod,若需保留数据,建议提前备份 PV 中的数据,避免误操作导致数据丢失。
-
修改 StatefulSet 的 PVC 模板(如调整存储容量、修改
storageClassName)后,直接执行kubectl apply不会生效,需删除现有 PVC 和 Pod,重新创建(数据会保留,因 PV 为Retain策略)。 -
存储选型建议:当前已改为NFS存储(共享路径/shares/,适配WordPress+MySQL 5.7部署),测试环境可直接使用该NFS配置;生产环境建议选用高可用NFS集群或分布式存储(如Ceph),避免NFS单点故障,确保WordPress和MySQL 5.7数据高可用。
-
StatefulSet 不支持动态修改副本数以外的核心配置(如
serviceName、PVC 模板),若需修改,需删除 StatefulSet 后重新创建(Pod 和 PVC 可保留,数据不丢失)。
更多推荐

所有评论(0)