第七章 Kubernetes存储
1、数据卷与数据持久卷
为什么需要数据卷?
容器中的文件在磁盘上是临时存放的,这给容器中运行比较重要的应用程序带来一些问题。
-
问题1:当容器升级或者崩溃时,kubelet会重建容器,容器内文件会丢失;
-
问题2:一个Pod中运行多个容器并需要共享文件;
Kubernetes 卷(Volume) 这一抽象概念能够解决这两个问题。
1. 数据持久化
- 容器的无状态性:Kubernetes 中的容器是无状态的,这意味着它们在重启或重新调度时,容器内的数据会丢失。
- 持久化数据:通过使用数据卷,可以将数据持久化到外部存储系统中,确保数据在容器生命周期外仍然存在。
2. 数据共享
- 多容器共享数据:在某些情况下,同一个 Pod 中的多个容器需要共享数据。数据卷允许这些容器共享同一个存储卷。
- 跨容器协作:例如,一个容器生成日志文件,另一个容器需要读取这些日志文件。
3. 数据隔离
- 独立存储:不同的 Pod 可以使用不同的数据卷,确保数据隔离,避免数据冲突。
- 安全性:通过将敏感数据存储在独立的数据卷中,可以提高安全性。
4. 简化配置
- 统一管理:使用数据卷可以简化应用程序的配置,因为存储配置可以在 Kubernetes 中集中管理。
- 灵活性:可以根据需要选择不同的存储后端(如本地存储、云存储、网络文件系统等),而不需要修改应用程序代码。
5. 生命周期管理
- 数据生命周期:数据卷可以独立于 Pod 的生命周期管理,确保数据在 Pod 重启或删除后仍然可用。
- 自动挂载:Kubernetes 可以自动挂载和卸载数据卷,简化操作。
常用的数据卷:
- 节点本地存储(hostPath,emptyDir)
- 网络存储(NFS,Ceph,GlusterFS)
- 公有云存储(AWS EBS)
- K8S资源存储(configmap,secret)
1.1 临时数据卷:emptyDir
emptyDir卷是一个临时存储卷,与Pod生命周期绑定一起,如果 Pod删除了卷也会被删除。
**应用场景:**Pod中容器之间数据共享
选项:
- sizeLimit:500Mi //限制共享空间大小(比较少用)
示例:Pod内容器之间共享数据
javascript
apiVersion: v1
kind: Pod
metadata:
name: emptydir-test
spec:
containers:
- name: write-container //程序讲数据写入到文件
image: centos
command: ["bash","-c","for i in {1..100};do echo $i >>
/data/hello;sleep 1;done"]
volumeMounts:
- name: data-volume //通过volume卷名称引用
mountPath: /data
- name: read-container //程序从文件中读取数据
image: centos
command: ["bash","-c","tail -f /data/hello"]
volumeMounts:
- name: data-volume
mountPath: /data
volumes: //定义卷的来源
- name: data-volume
emptyDir: {} //{}中为空值
**验证1:**验证容器之间是否能够共享数据
javascript
[root@k8s-master-1-71 ~]# kubectl apply -f emptydir-test.yaml
[root@k8s-master-1-71 ~]# kubectl get pods
NAME READY STATUS RESTARTS AGE
emptydir-test 2/2 Running 1 (21s ago) 3m14s
# 进入write-container查看
[root@k8s-master-1-71 ~]# kubectl exec -it emptydir-test -- bash
[root@emptydir-test /]# tail -f /data/hello
...
99
100
command terminated with exit code 137
[root@emptydir-test /]# touch /data/aaa ; ls /data/ //往容器中写入临时文件
aaa hello
## 注:脚本循环结束后退出容器,按照默认的策略Always进行容器重启。
# 进入read-container查看
[root@k8s-master-1-71 ~]# kubectl exec -it emptydir-test -c read-container -- bash
[root@emptydir-test /]# ls /data/
aaa hello
**验证2:**实际存储位置(基于节点的存储)
补充:Kubelet的工作目录为/var/lib/kubelet/,负载维护Pod数据的
javascript
[root@k8s-master-1-71 ~]# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
emptydir-test 2/2 Running 2 (45s ago) 5m3s 10.244.114.48 k8s-node2-1-73 <none> <none>
# 根据查看到的Pod所在节点,找到相应的容器ID
[root@k8s-node2-1-73 ~]# docker ps | grep emptydir-test(例如:7c26307b-b290-4bac-9a3b-6f18ffc26776)
6deb6dc27ab2 centos "bash -c 'for i in {..." 44 seconds ago Up 43 seconds k8s_write-container_emptydir-test_default_7c26307b-b290-4bac-9a3b-6f18ffc26776_3
7bb46657087b centos "bash -c 'tail -f /d..." 7 minutes ago Up 7 minutes k8s_read-container_emptydir-test_default_7c26307b-b290-4bac-9a3b-6f18ffc26776_0
fd4c6c671807 registry.aliyuncs.com/google_containers/pause:3.7 "/pause" 7 minutes ago Up 7 minutes k8s_POD_emptydir-test_default_7c26307b-b290-4bac-9a3b-6f18ffc26776_0
# 找到相关Pod关联的empty-dir目录,即可看到挂载的empty-dir目录内容
[root@k8s-node2-1-73 kubernetes.io~empty-dir]# pwd
/var/lib/kubelet/pods/7c26307b-b290-4bac-9a3b-6f18ffc26776/volumes/kubernetes.io~empty-dir
[root@k8s-node2-1-73 kubernetes.io~empty-dir]# ls data-volume/
aaa bbb hello
Pod是节点级别的,Pod中的容器都是捆绑在一个节点上,所以 Pod删除了,卷也会被删除
javascript
[root@k8s-master-1-71 ~]# kubectl delete -f emptydir-test.yaml
[root@k8s-node2-1-73 kubernetes.io~empty-dir]# ls data-volume/
ls: 无法访问data-volume/: 没有那个文件或目录
1.2 节点数据卷:hostPath
hostPath卷挂载Node的文件系统(即Pod所在节点)上文件或者目 录到Pod中的容器。
**应用场景:**Pod中容器需要访问宿主机的文件
选项:
-
path: //将宿主机的目录或文件映射到容器中去
-
type: //指定类型(目录或文件)
**示例:**将宿主机/tmp目录挂载到容器/data目录
javascript
apiVersion: v1
kind: Pod
metadata:
name: hostpath-test
spec:
containers:
- name: busybox
image: busybox
args:
- /bin/sh
- -c
- sleep 36000
volumeMounts:
- name: data-volume
mountPath: /data
volumes:
- name: data-volume
hostPath:
path: /tmp //Pod所在节点的/tmp目录
type: Directory
验证:
javascript
[root@k8s-master-1-71 ~]# kubectl apply -f hostpath-test.yaml
[root@k8s-master-1-71 ~]# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
hostpath-test 1/1 Running 0 86s 10.244.114.49 k8s-node2-1-73 <none> <none>
# 查看Pod所在节点的/tmp目录
[root@k8s-node2-1-73 ~]# ls /tmp/
systemd-private-85c79618aae44d7cbb01bccc046ecb10-chronyd.service-UMOhQf
systemd-private-fdf2b8d40ff841319feb738a463a8f6f-chronyd.service-GJT2Mn
# 进入容器查看/data目录是否有相关映射文件
[root@k8s-master-1-71 ~]# kubectl exec -it hostpath-test -- sh
/ # ls /data/
systemd-private-85c79618aae44d7cbb01bccc046ecb10-chronyd.service-UMOhQf
systemd-private-fdf2b8d40ff841319feb738a463a8f6f-chronyd.service-GJT2Mn
1.3 网络数据卷:NFS
**NFS:**是一个主流的文件共享服务器(注:每个Node上都要安装nfs-utils包)
javascript
# 服务端部署
[root@k8s-node1-1-72 ~]# yum install -y nfs-utils
[root@k8s-node1-1-72 ~]# vi /etc/exports //NFS共享配置目录
/ifs/kubernetes *(rw,no_root_squash)
# 解释:
共享目录 访问来源限制(权限)
[root@k8s-node1-1-72 ~]# mkdir -p /ifs/kubernetes
[root@k8s-node1-1-72 ~]# systemctl enable nfs --now
# 客户端测试
[root@k8s-node2-1-73 ~]# yum install -y nfs-utils
[root@k8s-node2-1-73 ~]# mount -t nfs 192.168.1.72:/ifs/kubernetes /mnt
[root@k8s-node2-1-73 ~]# df -Th | grep /ifs/kubernetes
192.168.1.72:/ifs/kubernetes nfs4 37G 4.3G 33G 12% /mnt
**NFS卷:**提供对NFS挂载支持,可以自动将NFS共享路径 挂载到Pod中
选项:
-
server: //指定NFS地址
-
path: /指定NFS共享目录
**示例:**将Nginx网站程序根目录持久化到 NFS存储,为多个Pod提供网站程序文件
javascript
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nfs-pod
name: nfs-pod
spec:
selector:
matchLabels:
app: nginx
replicas: 3
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: web
image: nginx
volumeMounts:
- name: data-volume
mountPath: /usr/share/nginx/html //挂载的目录
volumes:
- name: data-volume //volume卷名称
nfs:
server: 192.168.1.72 //指定NFS地址
path: /ifs/kubernetes //NFS共享目录
验证1:容器之间的数据是否共享
javascript
[root@k8s-node2-1-73 ~]# kubectl apply -f nfs-pod.yaml
[root@k8s-master-1-71 ~]# kubectl get pods
NAME READY STATUS RESTARTS AGE
nfs-pod-9b57f886-d8rzx 1/1 Running 0 10m
nfs-pod-9b57f886-tk99z 1/1 Running 0 55s
nfs-pod-9b57f886-wpr9q 1/1 Running 0 10m
# 进入容器1,创建文件测试
[root@k8s-node2-1-73 ~]# kubectl exec -it nfs-pod-9b57f886-d8rzx -- bash
root@nfs-pod-9b57f886-d8rzx:/# df -Th |grep /ifs/kubernetes
192.168.1.72:/ifs/kubernetes nfs4 37G 4.3G 33G 12% /usr/share/nginx/html
root@nfs-pod-9b57f886-d8rzx:~# touch /usr/share/nginx/html/aaaaa
root@nfs-pod-9b57f886-d8rzx:~# ls /usr/share/nginx/html/
aaaaa
# 进入容器2,查看文件
[root@k8s-node2-1-73 ~]# kubectl exec -it nfs-pod-9b57f886-tk99z -- bash
root@nfs-pod-9b57f886-tk99z:/# ls /usr/share/nginx/html/
aaaaa
验证2:增加/删除Pod是否能继续使用共享存储数据
javascript
# 删除Pod,查看是否能继续使用共享存储数据
[root@k8s-master-1-71 ~]# kubectl delete pod nfs-pod-9b57f886-tk99z
[root@k8s-master-1-71 ~]# kubectl get pods
NAME READY STATUS RESTARTS AGE
nfs-pod-9b57f886-d8rzx 1/1 Running 0 10m
nfs-pod-9b57f886-fg7n7 1/1 Running 0 55s //新增Pod
nfs-pod-9b57f886-wpr9q 1/1 Running 0 10m
[root@k8s-node2-1-73 ~]# kubectl exec -it nfs-pod-9b57f886-fg7n7 -- bash
root@nfs-pod-9b57f886-fg7n7:/# ls /usr/share/nginx/html/
aaaaa
# 增加Pod,查看是否能继续使用共享存储数据
[root@k8s-master-1-71 ~]# kubectl scale deployment nfs-pod --replicas=5
[root@k8s-node2-1-73 ~]# kubectl exec -it nfs-pod-9b57f886-sj7m6 -- bash
root@nfs-pod-9b57f886-sj7m6:/# df -Th | grep /ifs/kubernetes
192.168.1.72:/ifs/kubernetes nfs4 37G 4.3G 33G 12% /usr/share/nginx/html
root@nfs-pod-9b57f886-sj7m6:/# ls /usr/share/nginx/html/
aaaaa
1.4 持久数据卷概述
持久卷(Persistent Volumes, PV) 是用于管理存储的重要概念。
**安全性:**如果要设置安全方面的认证,都需要提前将安全配置写入YAML,因此将会暴露在YAML文件中,导致安全性减低;
**专业性:**在应用的部署上,使用者对K8S、存储的不了解,对于建设者来说,倡导职责上的分离。
1.4.1 PV、PVC
持久卷(Persistent Volumes, PV)
- 定义:持久卷是集群中的一块存储,由管理员配置和管理。它们独立于 Pod 的生命周期,可以被多个 Pod 使用。
- 生命周期:持久卷的生命周期独立于 Pod,即使 Pod 被删除,数据仍然保留。
对存储资源创建和使用的抽象,使得存储作为集群中的资源管理(定义后端存储)
持久卷声明(Persistent Volume Claims, PVC)
- 定义:持久卷声明是用户对持久卷的请求。用户不需要了解底层存储的细节,只需要声明所需的存储大小和访问模式。
- 生命周期:持久卷声明的生命周期与 Pod 的生命周期无关,可以独立存在。
- 绑定:持久卷声明会被绑定到一个满足其请求的持久卷上。一旦绑定,PVC 和 PV 之间是一对一的关系。
让用户不需要关心具体的Volume实现细节(访问模式、存储容量大小)
Pod申请PVC作为卷来使用,Kubernetes通过PVC查找绑定的PV,并Mount给Pod。
支持持久卷的存储插件: Persistent Volumes | Kubernetes
1.4.2 PV与PVC使用流程
PVC配置示例:
javascript
--- //容器应用
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: my-pod
name: my-pod
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: web
image: nginx
volumeMounts:
- name: data-volume
mountPath: /usr/share/nginx/html //挂载的目录
volumes:
- name: data-volume
persistentVolumeClaim:
claimName: my-pvc
--- //PVC 卷需求模板
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc //与claimName对应进行关联
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 5Gi
- 查看PVC命令: kubectl get pvc
javascript
[root@k8s-master-1-71 ~]# kubectl get pods
NAME READY STATUS RESTARTS AGE
my-pod-6fbc98b678-42m29 0/1 Pending 0 10s
[root@k8s-master-1-71 ~]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
my-pvc Pending 18s
注意:如果没有可用的PV,PVC无法进行资源分配会处于在Pending状态
PV配置示例:
javascript
apiVersion: v1
kind: PersistentVolume
metadata:
name: my-pv //随便定义
spec:
capacity:
storage: 5Gi //后端存储定义资源
accessModes:
- ReadWriteMany
nfs:
path: /ifs/kubernetes
server: 192.168.1.72
- 查看PV命令: kubectl get pv
javascript
[root@k8s-master-1-71 ~]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
my-pv 5Gi RWX Retain Bound default/my-pvc 11s
[root@k8s-master-1-71 ~]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
my-pvc Bound my-pv 5Gi RWX 2m30s
[root@k8s-master-1-71 ~]# kubectl get pods
NAME READY STATUS RESTARTS AGE
my-pod-6fbc98b678-42m29 1/1 Running 0 3m5s
测试:
javascript
[root@k8s-master-1-71 ~]# kubectl exec -it my-pod-6fbc98b678-42m29 -- bash
root@my-pod-6fbc98b678-42m29:/# df -Th | grep /ifs/kubernetes
192.168.1.72:/ifs/kubernetes nfs4 37G 4.3G 33G 12% /usr/share/nginx/html
root@my-pod-6fbc98b678-42m29:/# ls /usr/share/nginx/html/
aaaaa
思考: 多PV配置
挂载PV的时候,不能将挂载点挂到同一目录,为保证应用的唯一性,需要在挂载点的节点上创建各自的目录,避免冲突
多PV配置示例:
javascript
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv001 //随便定义
spec:
capacity:
storage: 5Gi //后端存储定义资源
accessModes:
- ReadWriteMany
nfs:
path: /ifs/kubernetes/pv001
server: 192.168.1.72
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv002 //随便定义
spec:
capacity:
storage: 15Gi //后端存储定义资源
accessModes:
- ReadWriteMany
nfs:
path: /ifs/kubernetes/pv002
server: 192.168.1.72
总结:
1、PV与PVC怎么匹配?
主要根据PVC的存储容量和访问模式进行匹配
2、存储容量怎么匹配?
容量只会向上匹配,如已有未使用PV有10G、20G,申请5G,向上取最近的PV容量10G
3、PV与PVC的关系?
一对一,存在绑定关系
4、容量请求是否有实际的限制?
目前容量请求主要用作于PVC与PV进行匹配的,只是抽象的存在,而具体的限制取决于后端存储,即请求容量不能超过共享存储的实际容量
1.4.3 PV 生命周期
1)AccessModes(访问模式):
AccessModes 是用来对 PV 进行访问模式的设置,用于描述用户应用对存储资源的访问权限,访问权限包括下面几种方式:
-
ReadWriteOnce(RWO):读写权限,但是只能被单个节点挂载
-
ReadOnlyMany(ROX):只读权限,可以被多个节点挂载
-
ReadWriteMany(RWX):读写权限,可以被多个节点挂载
备注:块存储(单节点)、文件系统、对象存储(多节点)
2)RECLAIM POLICY(回收策略):
目前 PV 支持的策略有三种:
-
Retain(保留): 保留数据,需要管理员手工清理数据(默认策略)
-
Recycle(回收):清除 PV 中的数据,效果相当于执行 rm -rf /ifs/kuberneres/*(一般结合StorageClass使用,NFS暂时无法看出效果)
-
Delete(删除):与 PV 相连的后端存储同时删除(一般结合StorageClass使用,NFS暂时无法看出效果)
persistentVolumeReclaimPolicy : 回收策略
3)STATUS(状态):
一个 PV 的生命周期中,可能会处于4中不同的阶段:
-
Available(可用):表示可用状态,还未被任何 PVC 绑定
-
Bound(已绑定):表示 PV 已经被 PVC 绑定
-
Released(已释放):PVC 被删除,但是资源还未被集群重新声明
-
Failed(失败): 表示该 PV 的自动回收失败
示例:观察回收状态和默认的 Retain回收策略
javascript
[root@k8s-master-1-71 ~]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
my-pv 5Gi RWX Retain Bound default/my-pvc 7h7m
## 解释:目前回收策略为 Retain ,状态为 Bound(表示 PV 已经被 PVC 绑定)
[root@k8s-master-1-71 ~]# kubectl delete -f my-pvc.yaml
[root@k8s-master-1-71 ~]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
my-pv 5Gi RWX Retain Released default/my-pvc 7h18m
## 解释:连着删除Pod和PCV后,回收策略为 Retain ,状态为 Released(PVC 被删除,但是资源还未被集群重新声明)
[root@k8s-node1-1-72 ~]# ls /ifs/kubernetes/ //PV中的资源依旧保留,需要管理员手工清理数据
aaaaa
注意: 删除PVC后,原来Bound的PV就无法继续使用,即使重新apply pvc.yaml,也是Pending状态。
思考: 现在PV使用方式称为静态供给,需要K8s运维工程师提前创 建一堆PV,供开发者使用
1.5 PV 动态供给(StorageClass)
PV静态供给明显的缺点是维护成本太高了,需要提前创建PV且不灵活! 因此,K8s开始支持PV动态供给,使用StorageClass 对象实现。StorageClass可以根据客户的PVC需求,通过PVC需求自动创建后端存储PV,且自动去绑定,无需像NFS还要创建目录隔离应用。
优点:
-
PV无需额外的提前独立创建;
-
PVC直接获取,也不用等待合适的PV;
支持动态供给的存储插件: Storage Classes | Kubernetes
相关GitHub部署: https://github.com/kubernetes-sigs/sig-storage-lib-external-provisioner
**了解:**Volume Plugin是支持存储的类型,Internal Provisioner内部是否支持
例如NFS内部是不支持的(不能直接PVC动态供给),且K8s默认不支持NFS动态供给,需要单独部署社区开发的插件
项目地址:https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner
部署StorageClass插件需要的3个文件:
bash
cd deploy
kubectl apply -f rbac.yaml # 授权访问apiserver
kubectl apply -f deployment.yaml # 部署插件,需修改里面NFS服务器地址与共享目录
kubectl apply -f class.yaml # 创建存储类(标识使用哪个存储)
kubectl get sc # 查看存储类
**补充:**一个集群中可以有多个存储类,而一个存储类一般对应一个存储
流程图:
- rbac.yaml 示例:
javascript
[root@k8s-master-1-71 nfs-external-provisioner]# cat rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: default
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
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"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: run-nfs-client-provisioner
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: default
roleRef:
kind: ClusterRole
name: nfs-client-provisioner-runner
apiGroup: rbac.authorization.k8s.io
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: leader-locking-nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: default
rules:
- apiGroups: [""]
resources: ["endpoints"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: leader-locking-nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: default
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: default
roleRef:
kind: Role
name: leader-locking-nfs-client-provisioner
apiGroup: rbac.authorization.k8s.io
- class.yaml 配置示例:
javascript
[root@k8s-master-1-71 nfs-external-provisioner]# cat class.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: managed-nfs-storage //StroagaClass存储类,在PVC中需要指定的标识(类似ingressclass选择Nginx)
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner # or choose another name, must match deployment's env PROVISIONER_NAME' //与Deployment的变量PROVISIONER_NAME保持一致
parameters:
archiveOnDelete: "false"
- deployment.yaml 配置示例:
javascript
[root@k8s-master-1-71 nfs-external-provisioner]# cat deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nfs-client-provisioner
labels:
app: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: default
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: nfs-client-provisioner
template:
metadata:
labels:
app: nfs-client-provisioner
spec:
serviceAccountName: nfs-client-provisioner
containers:
- name: nfs-client-provisioner //部署了NFS供给程序的容器
image: lizhenliang/nfs-subdir-external-provisioner:v4.0.1 //镜像地址
volumeMounts:
- name: nfs-client-root
mountPath: /persistentvolumes
env:
- name: PROVISIONER_NAME //class.yaml中的PROVISIONER_NAME
value: k8s-sigs.io/nfs-subdir-external-provisioner
- name: NFS_SERVER
value: 192.168.1.72 //需要指定后端NFS存储
- name: NFS_PATH
value: /ifs/kubernetes
volumes:
- name: nfs-client-root
nfs:
server: 192.168.1.72 //需要指定后端NFS存储
path: /ifs/kubernetes
- PVC指定存储类配置示例:
javascript
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc
spec:
storageClassName: "managed-nfs-storage" //指定StorageClass存储类(class.yaml)
accessModes:
- ReadWriteMany
resources:
requests:
storage: 5Gi
测试1:基于NFS提供PVC动态供给
步骤1:部署NFS-StorageClass 存储类插件
javascript
[root@k8s-master-1-71 nfs-external-provisioner]# kubectl apply -f .
storageclass.storage.k8s.io/managed-nfs-storage created
deployment.apps/nfs-client-provisioner created
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
role.rbac.authorization.k8s.io/leader-locking-nfs-client-provisioner created
rolebinding.rbac.authorization.k8s.io/leader-locking-nfs-client-provisioner created
# 创建的Pod为deployment.yaml中的NFS供给程序容器
[root@k8s-master-1-71 nfs-external-provisioner]# kubectl get pods
NAME READY STATUS RESTARTS AGE
nfs-client-provisioner-5848c9cddc-zkts2 1/1 Running 0 2m27s
# 创建的storageclass为class.yaml中指定的存储类
[root@k8s-master-1-71 nfs-external-provisioner]# kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
managed-nfs-storage k8s-sigs.io/nfs-subdir-external-provisioner Delete Immediate false 2m54s
步骤2:在PVC中指定存储类名称
javascript
[root@k8s-master-1-71 ~]# kubectl apply -f my-pvc.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: my-pod
name: my-pod
spec:
selector:
matchLabels:
app: my-pod
template:
metadata:
labels:
app: my-pod
spec:
containers:
- name: my-pod
image: nginx
volumeMounts:
- name: data-volume
mountPath: /usr/share/nginx/html
volumes:
- name: data-volume
persistentVolumeClaim:
claimName: my-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc
spec:
storageClassName: "managed-nfs-storage"
accessModes:
- ReadWriteMany
resources:
requests:
storage: 5Gi
[root@k8s-master-1-71 ~]# kubectl get pods
NAME READY STATUS RESTARTS AGE
my-pod-6fbc98b678-t2gvg 1/1 Running 0 4m11s
nfs-client-provisioner-5848c9cddc-zkts2 1/1 Running 0 24m
[root@k8s-master-1-71 ~]# kubectl get pvc //查看PVC
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
my-pvc Bound pvc-51d00de4-52ea-4f1a-988a-bc3e5b7e961e 5Gi RWX managed-nfs-storage 3s
[root@k8s-master-1-71 ~]# kubectl get pv //查看PV
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-51d00de4-52ea-4f1a-988a-bc3e5b7e961e 5Gi RWX Delete Bound default/my-pvc managed-nfs-storage 6s
# 进入容器查看挂载
[root@k8s-master-1-71 ~]# kubectl exec -it my-pod-6fbc98b678-t2gvg -- bash
root@my-pod-6fbc98b678-t2gvg:/# df -Th | grep pvc-51d00de4-52ea-4f1a-988a-bc3e5b7e961e
192.168.1.72:/ifs/kubernetes/default-my-pvc-pvc-51d00de4-52ea-4f1a-988a-bc3e5b7e961e nfs4 37G 4.4G 33G 12% /usr/share/nginx/html
查看NFS服务器的挂载目录,可看到随机生成的PV目录
**结论:**通过基于NFS提供PVC动态供给,无需再定义单独的PV,也无需为了指定到某个NFS目录手动创建子目录,动态供给会自动帮忙实现PV及随机生成NFS子目录
**注意:**由于NFS提供PVC动态供给的PV的默认回收策略是 Delete ,所以在删除PVC的同时,也会将PV一起删除;在NFS目录上的文件也一并删除。
测试2:
javascript
[root@k8s-master-1-71 ~]# kubectl apply -f my-pvc2.yaml
[root@k8s-node1-1-72 ~]# ls /ifs/kubernetes/
aaaaa default-my-pvc2-pvc-b7769112-aea6-4533-9e5c-5d14cd67fc65 default-my-pvc-pvc-51d00de4-52ea-4f1a-988a-bc3e5b7e961e
[root@k8s-master-1-71 ~]# kubectl delete -f my-pvc2.yaml
[root@k8s-node1-1-72 ~]# ls /ifs/kubernetes/
aaaaa default-my-pvc-pvc-51d00de4-52ea-4f1a-988a-bc3e5b7e961e
由于考虑数据的重要性,希望删除PVC时保留数据备份
javascript
[root@k8s-master-1-71 nfs-external-provisioner]# vi class.yaml
...
archiveOnDelete: "True" //将archiveOnDelete修改为 True,即可在删除PVC时保留备份
# 需要删除class.yaml并重新apply应用
[root@k8s-master-1-71 nfs-external-provisioner]# kubectl delete -f class.yaml
[root@k8s-master-1-71 nfs-external-provisioner]# kubectl apply -f class.yaml
# 测试删除PVC
[root@k8s-master-1-71 ~]# kubectl delete -f my-pvc.yaml
查看NFS服务器的挂载目录,原来的PV目录已重新命名(备份)
2、有状态应用部署初探
无状态与有状态:
Deployment控制器设计原则:管理的所有Pod一模一样且提供同一个服务(replicas),使用共享存储,之间没有连接关系,也不考虑在哪台Node运 行,可随意扩容和缩容。这种应用称为"无状态",例如Web服务
在实际的场景中,这并不能满足所有应用,尤其是分布式应用,会部署多个实例,这些实例之间往往有 依赖关系,部署的角色也不一样,例如主从关系、主备关系,这种应用称为"有状态",例如MySQL主从、Etcd集群
无状态特点:
-
每个Pod都一样,且提供同一种服务
-
Pod之间没有连接关系
-
使用共享存储
有状态特点:
-
每个Pod不对等,角色属性也不同
-
Pod之间有连接关系(类似数据库主从)
-
每个Pod的数据都是有差异化的,需要独立的存储进行持久化,否则会产生冲突
2.1 StatefulSet 控制器介绍
StatefulSet控制器用于部署有状态应用,满足一些有状态应用的需求:
-
Pod有序的部署、扩容、删除和停止
-
Pod分配一个稳定的且唯一的网络标识
-
Pod分配一个独享的存储
2.2 StatefulSet 部署应用实践
1)稳定的网络ID(域名)
使用 Headless Service(相比普通Service只是将spec.clusterIP定义为None)来维护Pod网络身份。 并且添加 serviceName: "headless-svc" 字段指定 StatefulSet控制器 要使用这个Headless Service。让StatefulSet控制器为其创建每个Pod固定的域名解析地址。
DNS解析名称:<statefulsetName-index>.<service-name> .<namespace-name>.svc.cluster.local
javascript
[root@k8s-master-1-71 ~]# kubectl create deployment stateful-pod --image=nginx
[root@k8s-master-1-71 ~]# kubectl expose deployment stateful-pod --port=80 --target-port=80 --dry-run=client -o yaml > headless-svc.yaml
[root@k8s-master-1-71 ~]# kubectl apply -f headless-svc.yaml
apiVersion: v1
kind: Service
metadata:
labels:
app: headless-svc
name: headless-svc
spec:
clusterIP: None //指定SVC的clusterIP为None
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: stateful-pod
[root@k8s-master-1-71 ~]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
headless-svc ClusterIP None <none> 80/TCP 8s
**补充:**Cluster-IP为None,标识该Service为headlress无头服务,这种Service将没有负载均衡转发的功能;
2)稳定的存储
StatefulSet的存储卷 使用VolumeClaimTemplate创建,称为卷申请模板(针对StatefulSet专门设置的类型),当StatefulSet使用VolumeClaimTemplate创建 一个PersistentVolume时,同样也会为每个Pod分配并创建一个编号的PVC(一个PV对应一个PVC)
参考: https://github.com/lizhenliang/k8s-statefulset
示例:StatefulSet部署(包括无头服务、StatefulSet+卷申请模)
javascript
[root@k8s-master-1-71 ~]# kubectl apply -f statefulset-test.yaml
# headless Service 服务配置示例
apiVersion: v1
kind: Service
metadata:
name: nginx-headless //headlessService服务名称,需要和StatefulSet的ServiceName保持一致
labels:
app: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None //设置cluster-IP为None
selector:
app: nginx //指定StatefulSet的Pod
# StatefulSet 配置示例
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
selector:
matchLabels:
app: nginx # has to match .spec.template.metadata.labels
serviceName: "nginx-headless" # 指定 headless Service 服务名称
replicas: 3 # by default is 1
minReadySeconds: 10 # by default is 0
template:
metadata:
labels:
app: nginx # has to match .spec.selector.matchLabels
spec:
terminationGracePeriodSeconds: 10
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
name: web
volumeMounts:
- name: www-data
mountPath: /usr/share/nginx/html
volumeClaimTemplates: //卷申请模板(为StatefulSet专门设置的类型)
- metadata:
name: www-data
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "managed-nfs-storage" # 指定 storageClass(kubectl get sc)
resources:
requests:
storage: 1Gi
查看信息:
javascript
[root@k8s-master-1-71 ~]# kubectl get pods
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 4m32s
web-1 1/1 Running 0 3m31s
web-2 1/1 Running 0 2m51s
# 查看 headless Service
[root@k8s-master-1-71 ~]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-headless ClusterIP None <none> 80/TCP 9m52s
# 查看 headless Service 对应后端的Pod
[root@k8s-master-1-71 ~]# kubectl get ep
NAME ENDPOINTS AGE
nginx-headless 10.244.114.10:80,10.244.117.50:80,10.244.117.51:80 13m
# 查看 PVC
[root@k8s-master-1-71 ~]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
www-data-web-0 Bound pvc-c8f06f51-5048-4a43-b822-9f6bcd1bd20c 1Gi RWO managed-nfs-storage 10m
www-data-web-1 Bound pvc-ae9c2a66-0bcb-489e-98d0-0350d480fb68 1Gi RWO managed-nfs-storage 9m37s
www-data-web-2 Bound pvc-50d69a6e-d3ef-42e2-bd13-e6c954d78677 1Gi RWO managed-nfs-storage 8m57s
# 查看 PV
[root@k8s-master-1-71 ~]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAG ECLASS REASON AGE
pvc-50d69a6e-d3ef-42e2-bd13-e6c954d78677 1Gi RWO Delete Bound default/www-data-web-2 manage d-nfs-storage 9m1s
pvc-ae9c2a66-0bcb-489e-98d0-0350d480fb68 1Gi RWO Delete Bound default/www-data-web-1 manage d-nfs-storage 9m41s
pvc-c8f06f51-5048-4a43-b822-9f6bcd1bd20c 1Gi RWO Delete Bound default/www-data-web-0 manage d-nfs-storage 10m
测试网络:
- ① 一个普通的headless-service对应Deplyment的域名解析,以及headless-service对应statefulset的域名解析
- ② 测试 statefulset的域名 网络连通性
javascript
# 创建普通的headless-service对应Deplyment的域名解析
[root@k8s-master-1-71 ~]# kubectl create deployment web --image=nginx
[root@k8s-master-1-71 ~]# kubectl expose deployment web --port=80 --target-port=80 --dry-run=client -o yaml > test-svc.yaml
[root@k8s-master-1-71 ~]# kubectl apply -f test-svc.yaml( //修改 clusterIP: None
[root@k8s-master-1-71 ~]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-headless ClusterIP None <none> 80/TCP 30m
web ClusterIP None <none> 80/TCP 6m45s
[root@k8s-master-1-71 ~]# kubectl get ep
NAME ENDPOINTS AGE
nginx-headless 10.244.114.10:80,10.244.117.50:80,10.244.117.51:80 31m
web 10.244.117.52:80 7m34s
# 创建测试bs镜像pod
[root@k8s-master-1-71 ~]# kubectl run bs --image=busybox:1.28.4 -- sleep 24h
[root@k8s-master-1-71 ~]# kubectl exec -it bs -- sh
/ # nslookup web
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: web
Address 1: 10.244.117.52 10-244-117-52.web.default.svc.cluster.local
## DNS解析名称:<PodIP>.<service-name> .<namespace-name>.svc.cluster.local
/ # nslookup nginx-headless
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: nginx-headless
Address 1: 10.244.114.10 web-1.nginx-headless.default.svc.cluster.local
Address 2: 10.244.117.50 web-0.nginx-headless.default.svc.cluster.local
Address 3: 10.244.117.51 web-2.nginx-headless.default.svc.cluster.local
## DNS解析名称:<statefulsetName-index>.<service-name> .<namespace-name>.svc.cluster.local
[root@k8s-master-1-71 ~]# kubectl exec -it bs -- sh
/ # ping web-1.nginx-headless.default.svc.cluster.local
PING web-1.nginx-headless.default.svc.cluster.local (10.244.114.10): 56 data bytes
64 bytes from 10.244.114.10: seq=0 ttl=62 time=0.539 ms
64 bytes from 10.244.114.10: seq=1 ttl=62 time=0.474 ms
测试存储:
javascript
[root@k8s-node1-1-72 ~]# ls /ifs/kubernetes/ //自动生成独立的PV,
default-www-data-web-1-pvc-ae9c2a66-0bcb-489e-98d0-0350d480fb68
default-www-data-web-2-pvc-50d69a6e-d3ef-42e2-bd13-e6c954d78677
default-www-data-web-0-pvc-c8f06f51-5048-4a43-b822-9f6bcd1bd20c
[root@k8s-master-1-71 ~]# kubectl exec -it web-0 -- bash
root@web-0:/# cd /usr/share/nginx/html/ ; echo 111 > index.html
[root@k8s-node1-1-72 ~]# cat /ifs/kubernetes/default-www-data-web-0-pvc-c8f06f51-5048-4a43-b822-9f6bcd1bd20c/index.html
111
[root@k8s-node1-1-72 ~]# ls /ifs/kubernetes/default-www-data-web-2-pvc-50d69a6e-d3ef-42e2-bd13-e6c954d78677/
//目录为空
## 因此Statefulset控制器创建的每个Pod的存储为独立的
思考: 在有状态环境部署下,分布式应用组件的角色不同,每个Pod也不相同,而在上述 StatefulSet部署示例中,假设运行一个etcd数据集群,有3个副本且肯定只有一个指定的镜像(镜像相同),如何区分这3个Pod的角色?
解答: 通过配置文件区分(每个节点的名称、IP、存储目录区分),需要保证启动的3个副本容器,每一个都能按照自己的角色(配置文件)进行启动。而配置文件则需要根据statefulset控制器部署的Pod容器编号进行区分,判断当前启动的是第几个容器,就使用哪个配置文件进行启动,从而实现Pod的角色。例如:etcd-0.conf、etcd-1.conf、etcd-2.conf
参考同类型的案例: 运行 ZooKeeper,一个分布式协调系统 | Kubernetes
**--- StatefulSet 与 Deployment区别:**有身份的!
身份三要素(唯一性):
-
域名
-
主机名
-
存储(PVC)
3、应用程序数据存储
- ConfigMap:存储配置文件
- Secret:存储敏感数据
3.1 ConfigMap 存储应用配置
创建ConfigMap后,数据实际会持久化存储在K8s中Etcd,然后通过创建Pod时引用该数据。
应用场景:应用程序配置(类似配置管理中心如apollo、nacos)
Pod使用configmap数据有两种方式:
-
变量注入到容器里
-
数据卷挂载
两种数据类型:
- 键值
- 多行数据
相关命令:
创建ConfigMap**:** kubectl create configmap --from-file=path/to/bar
bash
kubectl create configmap my-configmap --from-literal=abc=123 --from-literal=cde=456
查看ConfigMap:kubectl get configmap
官方:ConfigMap | Kubernetes
ConfigMap 配置示例:
1)部署ConfigMap的YAML
javascript
[root@k8s-master-1-71 ~]# kubectl apply -f configmap-demo.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: configmap-demo
data:
abc: "123" //key/value键值方式(字符串需要"")
cde: "456"
redis.properties: | //多行数据方式
port: 6379
host: 192.168.31.10
# 查看创建的ConfigMap
[root@k8s-master-1-71 ~]# kubectl get configmap
NAME DATA AGE
configmap-demo 3 7s
## 备注:DATA显示为3,表示存储了3个数据,包括abc、cde、redis.properties
2)部署Pod的YAML
javascript
[root@k8s-master-1-71 ~]# kubectl apply -f configmap-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: configmap-demo-pod
spec:
containers:
- name: nginx
image: nginx
# 定义环境变量
env:
- name: ABCD # 容器中的变量,请注意这里和 ConfigMap 中的键名是不一样的
valueFrom:
configMapKeyRef:
name: configmap-demo # 这个值来自 ConfigMap
key: abc # 需要取值的键
- name: CDEF
valueFrom:
configMapKeyRef:
name: configmap-demo
key: cde
volumeMounts:
- name: config
mountPath: "/config" # 挂载到哪个目录
readOnly: true # 挂载文件为只读
volumes:
# 在 Pod 级别设置卷,然后将其挂载到 Pod 内的容器中
- name: config
configMap:
name: configmap-demo # 提供想要挂载的 ConfigMap 的名字
# 来自 ConfigMap 的一组键,将被创建为文件
items:
- key: "redis.properties" # configmap-demo的Key
path: "redis.conf" # 挂载后的名字
**测试:**进入pod中验证是否注入变量和挂载
javascript
[root@k8s-master-1-71 ~]# kubectl exec -it configmap-test-pod-5dff5f64c6-ncmnd -- bash
root@configmap-test-pod-5dff5f64c6-ncmnd:/# echo $ABCD
123
root@configmap-test-pod-5dff5f64c6-ncmnd:/# echo $CDEF
456
root@configmap-test-pod-5dff5f64c6-ncmnd:/# cat /config/redis.conf
port: 6379
host: 192.168.31.10
3.2 Secret 存储敏感信息
与ConfigMap类似,区别在于Secret主要存储敏感数据,所有的数据要经过base64编码。
应用场景:凭据
kubectl create secret 支持三种数据类型:
- docker-registry:存储镜像仓库认证信息(镜像仓库需要账户密码认证)
- generic:存储用户名、密码:
- 例如:kubectl create secret generic ssh-key-secret --from-file=ssh-privatekey=/path/to/.ssh/id_rsa --from-file=ssh-publickey=/path/to/.ssh/id_rsa.pub
- 例如:kubectl create secret generic my-secret --from-literal=username=produser --from-literal=password=123456
- tls:存储证书:kubectl create secret tls --cert=path/to/tls.cert --key=path/to/tls.key
- 例如 :++05 K8s网络++ 使用ingress - tls 部署https
相关命令:
- 查看Secret:kubectl get secret
相关base64命令:
-
echo -n 'admin' | base64 //加密(YWRtaW4=)
-
echo YWRtaW4= | base64 -d //解密(admin)
补充: 在Secret 配置文件中未作显式设定时,默认的 Secret 类型是 Opaque
命令示例:
javascript
[root@k8s-master ~]# kubectl create secret generic my-secret --from-literal=username=admin --from-literal=password=123456
YAML示例:
1)将用户名密码进行编码
javascript
[root@k8s-master-1-71 ~]# echo -n 'admin' | base64 //用户名加密
YWRtaW4=
[root@k8s-master-1-71 ~]# echo -n '123456' | base64 //密码加密
MTIzNDU2
2)部署Secret的YAML
javascript
[root@k8s-master-1-71 ~]# vi secret-demo.yaml
apiVersion: v1
kind: Secret
metadata:
name: my-secret
type: Opaque //默认的 Secret 类型是 Opaque
data:
username: YWRtaW4=
password: MTIzNDU2
# 查看创建的ConfigMap
[root@k8s-master-1-71 ~]# kubectl get secret
NAME TYPE DATA AGE
secret-demo Opaque 2 5s
TYPE类型参考:Secrets | Kubernetes
3)部署Pod的YAML
javascript
apiVersion: v1
kind: Pod
metadata:
name: secret-demo-pod
spec:
containers:
- name: nginx
image: nginx
env:
- name: USERNAME # 容器中的变量
valueFrom:
secretKeyRef:
name: secret-demo
key: username
- name: PASSWORD # 容器中的变量
valueFrom:
secretKeyRef:
name: secret-demo
key: password
volumeMounts:
- name: config
mountPath: "/config"
readOnly: true
volumes:
- name: config
secret:
secretName: secret-demo
items:
- key: username
path: username.txt
- key: password
path: password.txt
备注:Pod使用Secret数据与ConfigMap方式一样
**测试:**进入pod中验证是否注入变量和挂载
javascript
[root@k8s-master-1-71 ~]# kubectl exec -it secret-test-pod-6554b98c8-8hrbb -- bash
root@secret-test-pod-6554b98c8-8hrbb:/# echo $USERNAME
admin
root@secret-test-pod-6554b98c8-8hrbb:/# echo $PASSWORD
123456
root@secret-test-pod-6554b98c8-8hrbb:/# ls /config/
password.txt username.txt
课后作业
1、创建一个secret,并创建2个pod,pod1挂载该secret,路径为/secret,pod2使用环境变量引用该 secret,该变量的环境变量名为ABC
- secret名称:my-secret
- pod1名称:pod-volume-secret
- pod2名称:pod-env-secret
2、 创建一个pv,再创建一个pod使用该pv
- 容量:5Gi
- 访问模式:ReadWriteOnce
3、创建一个pod并挂载数据卷,不可以用持久卷
- 卷来源:emptyDir、hostPath任意
- 挂载路径:/data
4、将pv按照名称、容量排序,并保存到/opt/pv文件
小结
本篇为 【Kubernetes CKA认证 Day7】的学习笔记,希望这篇笔记可以让您初步了解到 数据卷与数据持久卷、有状态应用部署、应用程序数据存储案例 ;课后还有扩展实践,不妨跟着我的笔记步伐亲自实践一下吧!
Tip:毕竟两个人的智慧大于一个人的智慧,如果你不理解本章节的内容或需要相关笔记、视频,可私信小安,请不要害羞和回避,可以向他人请教,花点时间直到你真正的理解。