k8s存储
- 在容器中的磁盘文件是短暂的,当容器挂掉时候,k8s会将它重启,而重启后容器文件会丢失,并且有的pod会有多容器进行文件交互。为了解决这个问题,k8s引入数据卷概念。
- k8s中Volume提供在容器中挂在外部存储的能力。
- Pod需要设置卷来源(spec.volume)和挂载点(spec.containers.volumeMounts)两个信息后才可以使用相应的Volume
- 常用数据卷:
本地:hostPath, emptyDir
网络:NFS, Ceph, GlusterFS
公有云:AWS EBS
k8s资源: configmap, secret
1.存储数据卷
1.1emptyDir
-
是一个临时存储卷。与pod生命周期绑定一起,如果pod删除卷也会被删除
-
主要应用:pod中容器之间数据共享(比如一个容器负责写,一个容器负责读的场景)。
-
示例:
# 在pod 中启动2个容器一个读一个写 apiVersion: v1 kind: Pod metadata: name: volume1 spec: containers: - name: write image: centos command: ["bash", "-c", "for i in {1..100};do echo $i >> /data/hello;sleep 1;done"] volumeMounts: - name: data mountPath: /data - name: read image: centos command: ["bash", "-c", "tail -f /data/hello"] volumeMounts: - name: data mountPath: /data volumes: - name: data emptyDir: {} # 如有启动异常可执行 [root@k8s-master ~]# kubectl logs <pod name> -c <容器name> # 通过exec在容器执行shell命令,查看写的执行状态 kubectl exec volume1 -c write -- tail /data/hello # 通过如下命令可以查看刚才部署的pod跑在哪个节点上 kubectl get pods -o wide # 到相应节点过滤查看容器 docker ps | grep volume1 #刚才设置数据共享目录在宿主机 /var/lib/kubelet/pods/<NAME 的ID>/volumes/kubernetes.io~empty-dir/data/hello
1.2Hostpath
-
挂载node文件系统(pod所在节点)上文件或者目录到pod中的容器。
-
应用场景:pod中容器需要访问宿主机文件(比如应用程序读取宿主机的物理资源,日志)
-
示例:
apiVersion: v1 kind: Pod metadata: name: pod-hostpath spec: containers: - name: busybox image: busybox args: - /bin/sh - -c - sleep: 36000 volumeMounts: - name: data mountPath: /data# 挂载容器中/data下 volumes: - name: data hostPath: path: /tmp# 挂载宿主机的/tmp下 type: Directory # 进入容器往容器/data下新增文件 kubectl exec -it pod-hostpath -- sh touch hahat.txt # 查看pod运行的节点 kubectl get pods -o wide # 在node01节点上查看/tmp目录下,可以查看新创建的haha.txt
1.3NFS
-
网络存储卷,NFS提供对NFS挂载支持,可以自动将NFS共享路径挂载到Pod中。
-
准备一台服务器安装NFS,注意:每个Node上都要安装nfs-utils:需要让节点pod挂载共享目录,需要linux支持nfs内部协议的模块
yum install -y nfs-utils vi /etc/exports /ifs/kubernetes *(rw,no_root_squash) # /ifs/kubernetes 指定暴漏目录 # * 表示暴漏的网段为所有,也可以设置 # rw,no_root_squash 表示权限 # 创建文件夹 mkdir -p /ifs/kubernetes # 启动 systemctl start nfs systemctl enable nfs # 测试nfs是否能用,另一台机器 mount -t nfs 172.16.215.141:/ifs/kubernetes /mnt cd /mnt mount -t nfs 172.16.215.141:/ifs/kubernetes /mnt cd /mnt touch 111.txt # 查看nfs服务器/ifs/kubernetes下是否存在111.txt # 卸载nfs命令: mount /mnt/
1.4 ConfigMap
-
创建ConfigMap后,数据实际会存储在k8s中etcd,然后通过创建pod时引用该数据。
-
应用场景:应用程序配置文件
-
pod使用configmap数据有两种方式:
1.变量注入 2.数据卷挂载
-
示例:
[root@k8s-master ~]# vi configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: configmap-demo data:# 定义键值方式变量 a: "123" b: "456" # 定义多行变量 redis.properties: | port: 6379 host: 192.168.31.10 # 将configmap保存到k8中etcd中 [root@k8s-master ~]# kubectl apply -f configmap [root@k8s-master ~]# kubectl get configmap NAME DATA AGE configmap-demo 3 7s # 定义一个pod apiVersion: v1 kind: Pod metadata: name: configmap-demo-map spec: containers: - name: demo image: nginx # 变量注入 env: - name: ZZZ# 自定义变量 value: "zzz" - name: ABCD# 变量名字 valueFrom: configMapKeyRef: name: configmap-demo# 来自于configmap key: a - name: CDEF valueFrom: configMapKeyRef: name: configmap-demo key: b # 数据卷挂载 volumeMounts: - name: config mountPath: "/config"# 存放容器中/config目录下 readOnly: true volumes: - name: config configMap: name: configmap-demo items: - key: "redis.properties"# key与对应configmap.yaml对应 path: "redis.properties"# 保存在容器中路径 # 执行pod [root@k8s-master ~]# kubectl apply -f pod-configmap.yaml # 进入容器 [root@k8s-master ~]# kubectl exec -it configmap-demo-map -- bash # 验证数据卷挂载 root@configmap-demo-map:/# cat config/redis.properties port: 6379 host: 192.168.31.10 # 验证变量注入 root@configmap-demo-map:/# echo $CDEF 456
1.5 Secret
- 与ConfigMap类似,区别于Secret主要存储敏感数据,所有数据要经过base64编码。
- 应用场景:凭据
- kubectl create secret 支持三种数据类型
docker-registry: 存储镜像仓库认证信息
generic:从文件,目录或者字符串创建,例如存储用户名,密码
tls:存储证书,例如HTTPS证书
- 示例
[root@k8s-master ~]# echo -n 'admin' | base64
[root@k8s-master ~]# echo -n '1f2d1e2e67df' | base64
[root@k8s-master ~]# vi secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: db-user-pass
type: Opaque
data:
username: YWRtaW4=
password: MWYyZDFlMmU2N2Rm
# 应用
[root@k8s-master ~]# kubectl apply -f secret.yaml
# 查看secret
[root@k8s-master ~]# kubectl get secret
NAME TYPE DATA AGE
db-user-pass Opaque 2 12s
# 创建一个pod
[root@k8s-master ~]# vi pod-secret.yaml
apiVersion: v1
kind: Pod
metadata:
name: secret-demo-pod
spec:
containers:
- name: demo
image: nginx
# 变量注入
env:
- name: USER
valueFrom:
secretKeyRef:
name: db-user-pass
key: username
- name: PASS
valueFrom:
secretKeyRef:
name: db-user-pass
key: password
# 数据卷方式
volumeMounts:
- name: config
mountPath: "/config"
readOnly: true
volumes:
- name: config
secret:
secretName: db-user-pass
items:
- key: username
path: my-username
# 应用:
[root@k8s-master ~]# kubectl apply -f pod-secret.yaml
# 进入pod中
[root@k8s-master ~]# kubectl exec -it secret-demo-pod -- bash
# 变量注入方式验证
root@secret-demo-pod:/# echo $USER
admin
root@secret-demo-pod:/# echo $PASS
1f2d1e2e67df
# 数据卷方式验证
root@secret-demo-pod:/# cat /config/my-username
admin
2.pv和pvc管理存储
-
PersistentVolume(PV):对存储资源创建和使用的抽象,使得存储作为集群中的资源管理。
-
PersistentVolumeClaim(PVC): 让用户不需要关系具体的Volume实现细节。
-
示例
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: deply-pvc
name: deploy-pvc
spec:
replicas: 2
selector:
matchLabels:
app: deploy-pvc
strategy: {}
template:
metadata:
labels:
app: deploy-pvc
spec:
containers:
- image: nginx
name: nginx
resources: {}
volumeMounts:
- name: data
mountPath: /usr/share/nginx/html
volumes:
- name: data
persistentVolumeClaim:# 卷资源pvc
claimName: my-pvc# 申请空间pvc名字
---
# pvc分配的存储资源
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
# 此时创建pod处于Pending状态 查看pod详情
kubectl describe pod deploy-pvc-6994b7ff4b-k8wgb
# Events报错:
Warning FailedScheduling 7m27s default-scheduler 0/2 nodes are available: 2 pod has unbound immediate PersistentVolumeClaims.
# 表示PVC没有绑定PV,需要创建一个PV
# 创建的PV,指定存储的资源
vi pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv00001
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
nfs:
path: /ifs/kubernetes
server: 172.16.215.141
# 应用:挂载存储
kubectl apply -f pv.yaml
# 此时刚才创建的pvc开始创建,运行。如何查看pv与pvc绑定
kubectl get pv,pvc
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
persistentvolume/pv00001 1Gi RWX Retain Bound default/my-pvc 96s
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
persistentvolumeclaim/my-pvc Bound pv00001 1Gi RWX 15m
# 可以看到pv与pvc已经进行绑定
- 当创建多个pv为保证多个pv之间数据隔离,会在nfs上创建多个目录
/ifs/
└── kubernetes
├── pv00001
├── pv00002
└── pv00003
- 此时pv.yaml应该如下定义多个yaml块
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv00001
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
nfs:
path: /ifs/kubernetes/pv00001
server: 172.16.215.141
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv00002
spec:
capacity:
storage: 2Gi
accessModes:
- ReadWriteMany
nfs:
path: /ifs/kubernetes/pv00002
server: 172.16.215.141
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv00003
spec:
capacity:
storage: 3Gi
accessModes:
- ReadWriteMany
nfs:
path: /ifs/kubernetes/pv00003
server: 172.16.215.141
- 当又创建一个应用。
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: deply-pvc
name: deploy-pvc2
spec:
replicas: 2
selector:
matchLabels:
app: deploy-pvc
strategy: {}
template:
metadata:
labels:
app: deploy-pvc
spec:
containers:
- image: nginx
name: nginx
resources: {}
volumeMounts:
- name: data
mountPath: /usr/share/nginx/html
volumes:
- name: data
persistentVolumeClaim:
claimName: my-pvc2
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc2
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1.5Gi
- 此时通过
kubectl get pvc
[root@k8s-master ~]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
my-pvc Bound pv00001 1Gi RWX 32m
my-pvc2 Bound pv00002 2Gi RWX 2m12s
# 可以看到创建第一个和第二个应用分别绑定了pv00001和pv00002
- PV生命周期
accessModes:访问模式
AccessModes是用来对PV进行访问模式的设置,用于描述用户应用对存储资源访问权限,访问权限包括下面几种。
ReadWriteOnce(RWO): 读写权限,但是只能被单个节点挂载
ReadOnlyMany(ROX): 只读权限,可以被多个节点挂载
ReadWriteMany(RWX): 读写权限,可以被多个节点挂载
RECLAIM POLICY(回收策略)
通过 kubectl get pv 命令,查看RECLAIM POLICY列,目前有3种策略
Retain(保留):保留数据,需要管理员手工清理数据
Recycle(回收):清除PV中的数据,效果相当于rm -rf
Delete(删除): 与PV相连后端存储同时删除
STATUS 状态
表示一个PV生命周期状态。
Available(可用): 表示可用状态,还未被任何PVC绑定
Bound(已绑定): 表示PV已经被PVC绑定
Released(已释放): PVC被删除,但是资源还未被集群重新声明
Failed(失败): 表示该PV的自动回收失败
- PV与PVC常见问题
1.PV与PVC之间关系?
一个PV对应一个PVC
2.PV与PVC怎么匹配?
根据存储空间,访问模式进行匹配
3.容器匹配策略是什么样的?
匹配最接近的PV容量,如果满足不了Pod会处于pending状态
4. 存储空间(1Gi,2Gi,...)字段限制使用容量?
这个字段主要用于做匹配,实际的使用限制取决于后端存储。
- 现在我们在使用PV为静态供给,需要k8s工程师创建一大堆PV供开发者使用。PV静态供给维护成本过高,通过StorageClass动态进行PV供给可减少维护成本。
3.PV动态供给(StorageClass)
-
基于NFS实现PV动态供给流程图
-
GitHub复制文件
class.yaml
,deployment.yaml
,rbac.yaml
。yaml文件下载地址- class.yaml 用于存储类
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: managed-nfs-storage provisioner: k8s-sigs.io/nfs-subdir-external-provisioner # or choose another name, must match deployment's env PROVISIONER_NAME' parameters: archiveOnDelete: "false"
- 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 image: lizhenliang/nfs-subdir-external-provisioner:v4.0.1 volumeMounts: - name: nfs-client-root mountPath: /persistentvolumes env: - name: PROVISIONER_NAME value: k8s-sigs.io/nfs-subdir-external-provisioner - name: NFS_SERVER value: 172.16.215.141# NFS服务器IP - name: NFS_PATH value: /ifs/kubernetes# 共享目录 volumes: - name: nfs-client-root nfs: server: 172.16.215.141# NFS服务器IP path: /ifs/kubernetes# 挂载容器的目录
- rbac.yaml为了连接k8s的api进行授权
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: ["nodes"] verbs: ["get", "list", "watch"] - 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
-
目录结构
nfs-external-rpovisioner/ ├── class.yaml ├── deployment.yaml └── rbac.yaml
-
应用
kubectl apply -f .
-
查看存储类
[root@k8s-master nfs-external-rpovisioner]# kubectl get sc NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE managed-nfs-storage k8s-sigs.io/nfs-subdir-external-provisioner Delete Immediate false 10s
-
查看pod
[root@k8s-master nfs-external-rpovisioner]# kubectl get pods NAME READY STATUS RESTARTS AGE nfs-client-provisioner-6f5bf84fc9-srrf8 0/1 Running 0 50s
-
创建一个deployment看是否能动态供给
# storageClassName 设置存储类名字,通过 kubectl get sc 获取存储类的名字。 vi deploy-storageclass.yaml apiVersion: apps/v1 kind: Deployment metadata: labels: app: deply-pvc name: deploy-storageclass spec: replicas: 2 selector: matchLabels: app: deploy-pvc strategy: {} template: metadata: labels: app: deploy-pvc spec: containers: - image: nginx name: nginx resources: {} volumeMounts: - name: data mountPath: /usr/share/nginx/html volumes: - name: data persistentVolumeClaim: claimName: my-sc --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: my-sc spec: storageClassName: "managed-nfs-storage" accessModes: - ReadWriteMany resources: requests: storage: 1.5Gi
-
创建pod
[root@k8s-master ~]# kubectl apply -f deploy-storageclass.yaml
-
在NFS服务器
172.16.215.141
可以看到/ifs/kubernetes
目录下有default-my-sc-pvc-d94fb9a6-d6ed-4511-9077-18487bfd1985
可以知道自动创建一个目录,这个目录就是PV动态供给创建目录 -
我们进入一个容器内
kubectl exec -it deploy-storageclass-7584589f8c-ltqg4 bash cd /usr/share/nginx/html/ # 创建一个文件 touch 1.txt # 此时查看NFS服务器上的default-my-sc-pvc-d94fb9a6-d6ed-4511-9077-18487bfd1985目录下也多了一个文件1.txt
-
动态供给分配的存储资源,是看你pod要求分配的是多少存储资源。
-
需要知道是动态存储的类不是一对一关系,可以重复使用,它就好像提供一个接口一样,重复调用。
4.有状态应用部署:StatefulSet控制器
-
Deployment控制器设计原则:管理所有pod一模一样,提供同一个服务,不需要考虑多副本之间的关系是否有独立存储,也不考虑在哪台Node运行,可随意扩容和缩容,这种应用称为‘无状态’,例如web服务。
-
在实际场景中,这并不能满足所有应用,尤其是分布式应用,会部署多个实例,这些实例之间往往有依赖关系,例如:主从关系,主备关系,这种应用称为‘有状态’,例如MySQL主从,Etcd集群。
-
StatefulSet
-
用于部署有状态应用
-
解决pod独立生命周期,保持pod启动顺序和唯一性。
1.稳定,唯一网络标识符,持久存储 2.有序, 优雅的部署和扩展,删除和终止。 3.有序,滚动更新
-
应用场景:分布式应用,数据库集群
-
-
稳定的网络ID
- 使用Headless Service (相比普通Service只是将spec.clusterIP定义为None)来维护Pod网络身份。并且添加ServiceName字段指定StatefulSet控制器要使用这个Headless Service。DNS解析名称:
. , .svc.cluster.local
- 使用Headless Service (相比普通Service只是将spec.clusterIP定义为None)来维护Pod网络身份。并且添加ServiceName字段指定StatefulSet控制器要使用这个Headless Service。DNS解析名称:
-
稳定的存储
- StatefulSet存储卷使用VolumeClaimTemplate创建,称为卷申请模版,当StatefulSet使用VolumeClaimTemplate创建一个PV(PersistentVolume)时,同样也会为每个pod分配并创建一个编号的PVC
-
如部署一个MySQL主从:
- 主从实例启动顺序
- 主从实例数据目录唯一
- 从连接主实例需要指定IP或主机名
-
这里拿nginx做实例,部署statefulSet
# 新增一个statefulSet的基本模版yaml文件
[root@k8s-master ~]# kubectl create deployment sts --image=nginx --dry-run=client -o yaml > sts.yaml
# 暴漏一个service
[root@k8s-master ~]# kubectl expose deployment deploy-pvc --port=80 --target-port=80 --dry-run=client -o yaml > svc.yaml
# 编写demployment的service为StatefulSet
# vi sts.yaml
apiVersion: apps/v1
kind: StatefulSet#设置StatefulSet控制器
metadata:
labels:
app: sts
name: sts
spec:
serviceName: "sts"# 指定serviceName,用户指定Headless Service
replicas: 2
selector:
matchLabels:
app: sts
template:
metadata:
labels:
app: sts
spec:
containers:
- image: nginx
name: nginx
resources: {}
volumeMounts:# 定义挂载目录
- name: www
mountPath: /usr/share/nginx/html
volumeClaimTemplates:# volumeClaimTemplates 为每个pod分配独立pv。只支持StatefulSet
- metadata:
name: www
spec:
accessModes: [ "ReadWriteOnce" ]#单pod读写模式,一个pvc只能被一个pod独享,
storageClassName: "managed-nfs-storage"# 存储类,为上面提前设置好的。
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Service
metadata:
labels:
app: sts
name: sts
spec:
clusterIP: None# Headless Service是将clusterIP置为None,因为是一个有状态的service。
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: sts
# 应用
kubectl apply -f sts.yaml
# kubectl get pods 可以看到,pod进行有序的启动(部署)
sts-0 1/1 Running 0 10m
sts-1 1/1 Running 0 9m53s
# 为每个pod分配稳定身份
[root@k8s-master ~]# kubectl get ep
NAME ENDPOINTS AGE
sts 10.244.85.204:80,10.244.85.205:80 25m
[root@k8s-master ~]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
sts ClusterIP None <none> 80/TCP 27m
# 创建一个沙箱
[root@k8s-master ~]# kubectl run -it --rm --image=busybox:1.28.4 -- sh
# deploy-nfs 通过 CLUSTER-IP 进行解析
/ # nslookup deploy-nfs
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
# 而sts并没有 CLUSTER-IP 通过pod IP进行解析
/ # nslookup sts
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: sts
Address 1: 10.244.85.205 sts-0.sts.default.svc.cluster.local
Address 2: 10.244.85.204 sts-1.sts.default.svc.cluster.local
- 当我们对
sts.yaml
进行升级时候比如nginx版本升级更高版本
# 重新apply
kubectl apply -f sts.yaml
# 升级的顺序是从后向前升级,也就是先sts-1再sts-0
- StatefulSet和Deployment区别
主要是身份区别:
域名
主机名
存储(PVC)
- 总结
1如果是不需额外数据依赖或者状态维护的部署,或者replicas是1,优先考虑使用Deployment;
2如果单纯的要做数据持久化,防止pod宕掉重启数据丢失,那么使用pv/pvc就可以了;
3如果要打通app之间的通信,而又不需要对外暴露,使用headlessService即可;
4如果需要使用service的负载均衡,不要使用StatefulSet,尽量使用clusterIP类型,用serviceName做转发;
5如果是有多replicas,且需要挂载多个pv且每个pv的数据是不同的,因为pod和pv之间是 一 一对应的,如果某个pod挂6掉再重启,还需要连接之前的pv,不能连到别的pv上,考虑使用StatefulSet
6能不用StatefulSet,就不要用