目录
- 前言
- 创建NFS存储卷
- 从底层持久化技术解耦pod,引入pvc和pv
- pv持久卷
- pvc持久卷声明
- 在pod中使用pvc
- pv的回收策略之 Retain策略
- pv的回收策略之 Delete 策略
- pv的回收策略之 Recycle策略(已弃用)
- 持久卷的动态卷配置、为什么需要StorageClass
- 什么是StorageClass(简写sc)
- 优化后的流程
- 创建ServiceAccount账号
- 创建provisioner(也可称为供应者、置备程序、存储分配器)
- 创建StorageClass
- 创建pvc来引用storageclass
- kube-apiserver.yaml 添加参数
- 创建业务pod、service
- 查看各个资源
- 默认存储类、不指定存储类的pvc、pvc中存储类名设置为空字符串
- 通过一幅图来完整了解动态持久卷供应全貌
- 配置动态卷出现的一些问题排查
- 总结
前言
环境:centos7.9 docker-ce-20.10.9 kubernetes-version v1.22.6
前面我们讲解了emptyDir和hostPath卷,前者只是临时数据,pod消失emptyDir卷的数据就没了,后者在工作节点的文件系统中保留卷的数据,但是无法保证pod异常挂掉之后重新创建之后还是调度到原来的节点上。
所以本篇来讲解持久化存储,其原理很简单,就是创建某种网络存储,这样即使pod异常挂掉之后,重新创建pod,pod还是会读取之前的存储卷数据。
一个pod可以定义多个不同类型的卷,一个容器也可以使用不同类型的多个卷。
创建NFS存储卷
我们最熟悉的远程网络存储之一就是NFS了,下面将演示pod中如何使用nfs卷;
1、首先需要创建一个nfs 服务器,作为存储服务器,可参考https://blog.youkuaiyun.com/MssGuo/article/details/116381308
;以下是简单演示:
#创建共享目录
mkdir -p /data/k8s_data
#安装nfs-utils rpcbind
yum -y install nfs-utils rpcbind
cat >> /etc/exports <<'EOF'
/data/k8s_data 192.168.xx.0/24(rw,sync,no_root_squash)
EOF
#启动rpcbind.service
systemctl enable --now rpcbind.service
#启动nfs-server
systemctl enable --now nfs-server.service
#查看安装nfs服务默认生成的用户
grep -i nfs /etc/passwd
#把共享目录授权给上面查到的用户
chown -R nfsnobody:nfsnobody /data/k8s_data
#查看服务端有哪些共享目录
showmount -e 192.168.118.140
2、每个k8s节点安装mount.nfs
命令
当pod创建后,需要挂载远程的nfs存储,而使用的是mount.nfs命令去挂载的,所有每个节点都需要有这个命令;
mount.nfs命令是nfs-utils软件包提供的,所以安装nfs-utils软件包但不启动nfs-server服务:
yum install -y nfs-utils -y
systemctl disable nfs-server
3、nfs 服务器搭建好之后,我们开始创建pod;
#创建deployment和server资源
cat > deplyment_nginx_nfs.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-nginx-nfs
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: nginx-nfs
template:
metadata:
labels:
app: nginx-nfs
spec:
containers:
- image: nginx:1.7.9
name: nginx-container
ports:
- name: http
containerPort: 80
volumeMounts:
- name: nfs-volume
mountPath: /var/log/nginx
volumes:
- name: nfs-volume
nfs: #创建nfs存储卷
server: 192.168.118.128 #nfs服务器ip
path: /home/k8s_data #nfs服务器的共享数据目录
---
apiVersion: v1
kind: Service
metadata:
labels:
app: nginx-nfs
name: svc-nginx-nfs-nodeport
spec:
sessionAffinity: ClientIP
ports:
- port: 80
protocol: TCP
targetPort: 80
nodePort: 30068
selector:
app: nginx-nfs
type: NodePort
EOF
kubectl apply -f deplyment_nginx_nfs.yaml
从底层持久化技术解耦pod,引入pvc和pv
以上,我们在pod创建nfs存储卷时,是直接在pod定义存储卷的,这显然需要开发pod的工程师知道nfs服务器的ip地址及其他的参数,这违背了kubernetes的基本理念,kubernetes的基本理念旨在向应用程序及其开发人员隐藏真实的底层基础设施,使他们不必担心基础设施的具体状态,并使应用程序可在大量云服务商和数据企业之间进行功能迁移。
理想的情况是,在Kubernetes上部署应用程序的开发人员不需要知道底层使用的是哪种存储技术,同理他们也不需要了解应该使用哪些类型的物理服务器来运行pod,与基础设施相关的交互是集群管理员独有的控制领域。
当开发人员需要一定数量的持久化存储来进行应用时,可以向Kubernetes请求,就像在创建pod时可以请求CPU、内存和其他资源一样。系统管理员可以对集群进行配置让其可以为应用程序提供所需的服务。
在Kubernetes集群中,为了使应用能够正常请求存储资源,同时避免处理基础设施细节,引入了两个新的资源,分别是持久卷和持久卷声明。
pv持久卷
PersistentVolume(持久卷,简称PV),PV不属于任何命名空间,PV是集群层面的资源。
简单的来说,持久卷就是定义存储的一个k8s资源对象;
#查看k8s集群中的pv命令,pv没有命名空间的隔离,pv是集群层面的资源
kubectl get pv
下面来创建一个持久卷:
cat > pv-nfs.yaml <<'EOF'
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-nfs
labels:
pv: nfs
spec:
capacity: #容量为500M
storage: 500M
accessModes: #访问模式为允许多节点以读写方式挂载,可以有多个访问模式
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain #回收策略
nfs: #定义nfs服务器的信息
server: 192.168.118.128
path: /home/k8s_data
readOnly: false
EOF
kubectl apply -f pv-nfs.yaml
#查看PV,状态是可用,容量大小是500M,访问模式时ROX
[root@master ~]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pv-nfs 500M ROX Retain Available 2m42s
pv 的 persistentVolumeReclaimPolicy
字段用于定义当 pvc 被删除后,使用哪一种pv 的保留策略,k8s提供了以下3种pv的回收策略:
回收策略 | 说明 |
---|---|
Retain | Retain策略,当 PersistentVolumeClaim 被删除时,PersistentVolume 仍然存在,该卷被认为是“已释放”, 但它还不能用于另一个pvc,因为前一个pvc的数据仍在卷上。 管理员可以手动通过删除持久卷pv, 删除 pv 后,数据仍然存在于存储服务器磁盘里,所以还需要相应地手动清理相关存储服务器磁盘上的数据;如果要重用相同的存储,请创建具有相同存储资源的新 pv。 |
Delete | Delete 策略,删除被pvc释放的pv和后端存储数据 |
Recycle | 保留pv,但清空pv上的数据(已废弃) |
pv 的访问模式有4种,使用accessModes字段进行设置,一个pv可以设置多个访问模式,pv的4种访问模式为:
模式 | 说明 |
---|---|
ReadWriteOnce,简写:RWO | 只仅允许单个节点以读写方式挂载 |
ReadOnlyMany,简写:ROX | 可以被许多节点以只读方式挂载 |
ReadWriteMany,简写:RWX | 可以被多个节点以读写方式挂载 |
ReadWriteOncePod,简写:RWOP | 该卷可以由单个 Pod 以读写方式挂载。如果要确保整个集群中只有一个 pod 可以读取或写入 PVC,请使用 ReadWriteOncePod 访问模式。这仅支持 CSI 卷和 Kubernetes 1.22+ 版本 |
创建pv后,pv的的状态有以下4种:
Available(可用)、Bound(已绑定)、Released(已释放)、Failed(失败)
Available,表示pv已经创建正常,是可用状态;
Bound,表示pv已经被某个pvc绑定,注意,一个pv一旦被某个pvc绑定,那么该pvc就独占该pv,其他pvc不能再与该pv绑定;
Released,表示pvc被删除了,pv状态就会变成已释放;
Failed,表示pv的自动回收失败;
再次声明,pv并不属于任何命名空间,它使集群层面上的资源,如下图所示:
pvc持久卷声明
当集群用户需要在其pod中使用持久化存储时,他们并不是直接使用pv的,而是,首先创建一个持久卷声明(PersistentVolumeClaim,简称PVC),指定所需要的最低容量要求和访问模式,然后用户将pvc清单提交给Kubernetes API服务器,Kubernetes将找到可匹配的pv并将其绑定到pvc。
pvc可以当作pod中的一个卷来使用,其他用户不能使用相同的持久卷,除非先通过删除持久卷声明绑定来释放。
创建pvc:
#创建一个pvc,用于绑定我们上面创建的pv
cat > pvc-nfs.yaml <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim #资源类型
metadata:
name: pvc-nfs
namespace: default #所属命名空间,注意,pvc是有命名空间的,pv是集群层面的没有命名空间
labels:
pvc: nfs
spec:
resources: #定义pvc所需的资源
requests:
storage: 500M #需要500M的存储空间
accessModes:
- ReadWriteMany #访问模式为ReadWriteMany
storageClassName: "" #这个写上空字符串,后面动态持久卷将解释为什么
EOF
pvc创建好之后,我们来查看会发现什么:
[root@master ~]# kubectl get pv,pvc
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
persistentvolume/pv-nfs 500M RWX Retain Bound default/pvc-nfs 108m
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
persistentvolumeclaim/pvc-nfs Bound pv-nfs 500M RWX 4m46s
我们发现,pvc与pv进行了绑定,现在pvc和pv的状态都是Bound表示绑定了,既能查看到pv被哪个pvc绑定,也能查看得到pvc绑定了哪个pv
那么,pvc是如何指定该与哪个pv进行绑定呢?
答案是:通过pvc申请的容量大小和访问模式来匹配最佳的pv。
在pod中使用pvc
创建一个deployment,如下所示:
#创建一个资源清单
cat > deplyment_nginx_pvc.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-nginx-pvc
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: nginx-pvc
template:
metadata:
labels:
app: nginx-pvc
spec:
containers:
- image: nginx:1.7.9
name: nginx-container
ports:
- name: http
containerPort: 80
volumeMounts:
- name: pvc-volume
mountPath: /var/log/nginx
volumes: #这里直接引用pvc
- name: pvc-volume
persistentVolumeClaim:
claimName: pvc-nfs
readOnly: false
---
apiVersion: v1
kind: Service
metadata:
labels:
app: nginx-pvc
name: svc-nginx-pvc-nodeport
spec:
sessionAffinity: ClientIP
ports:
- port: 80
protocol: TCP
targetPort: 80
nodePort: 30058
selector:
app: nginx-pvc
type: NodePort
EOF
访问nginx:
#查看pod,service是否正常
kubectl get pod,svc
#访问nginx应用,正常访问
curl localhost:30058
#查看nfs服务器上的共享数据目录,发现数据已经过来了
tail -22f access.log
pv的回收策略之 Retain策略
Retain策略,当 PersistentVolumeClaim 被删除后,保留PersistentVolume,该pv就会被认为是“已释放”, 但它仍不能用于另一个pvc,因为前一个pvc的数据仍在卷上。 管理员可以通过手动删除持久卷pv, 删除 pv 后,数据仍然存在于存储服务器磁盘里,如果彻底不需要数据了,此时可以手动清理相关存储服务器磁盘上的数据;如果需要重用之前的数据,此时重新创建具有相同存储资源的新 pv即可复用之前的数据。
#现在pvc已经和pv绑定了,pv回收策略是Retain,下面进行删除pvc
[root@master ~]# kubectl get pv,pvc
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
persistentvolume/pv-nfs 500M RWX Retain Bound default/pvc-nfs 42h
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
persistentvolumeclaim/pvc-nfs Bound pv-nfs 500M RWX 42h
[root@master ~]# kubectl delete pvc pvc-nfs #删除pvc
persistentvolumeclaim "pvc-nfs" deleted
[root@master ~]# kubectl get pv #查看pv,现在pv的状态是Released
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
persistentvolume/pv-nfs 500M RWX Retain Released default/pvc-nfs 42h
[root@mysql k8s_data]# ll #查看nfs服务器上的数据,仍存在
total 12
-rw-r--r--. 1 nfsnobody nfsnobody 1064 Mar 22 15:17 access.log
-rw-r--r--. 1 nfsnobody nfsnobody 268 Mar 20 23:45 error.log
#我们重新创建pvc,发现pvc状态显示Pending,pvc并没有绑定到之前的pv上
[root@master ~]# kubectl apply -f pvc-nfs.yaml
persistentvolumeclaim/pvc-nfs created
[root@master ~]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
pvc-nfs Pending 7s
[root@master ~]# kubectl delete pv pv-nfs #下面我们把pv删掉
persistentvolume "pv-nfs" deleted
#重新创建pv,可以看到刚开始创建的pv状态是Available,然后过了一会被我们之前创建pvc绑定了
[root@master ~]# kubectl apply -f pv-nfs.yaml ;kubectl get pv
persistentvolume/pv-nfs created
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pv-nfs 500M RWX Retain Available 0s
[root@master ~]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pv-nfs 500M RWX Retain Bound default/pvc-nfs 15s
[root@master ~]#
#此时,我们重新创建一个pod,进入pod查看发现还是可以读取之前上一个pod的数据
#以上,我们就演示了pv的回收策略是persistentVolumeReclaimPolicy: Retain时,当删除pvc后,pv的状态就会变成Released,后端存储服务器的数据仍保留存在;Released状态的pv将不会被任何pvc绑定,如果需要重新使用该pv的定义,只能删除该pv,重新创建一个和它一模一样的pv,后端数据的删除与否取决于管理员的决定,如果不删,那么新创建的pv将继承数据,pod也将继承数据,即新的pod将可以访问之前pod的旧数据。
pv的回收策略之 Delete 策略
Delete 策略,当pvc被删除之后,级联删除pv和后端存储数据。例如 AWS EBS、GCE PD、Azure Disk、OpenStack Cinder Volume
等,NFS不支持delete策略。下面不在做代码演示。
pv的回收策略之 Recycle策略(已弃用)
Recycle策略,已弃用,只需了解即可,当删除pvc之后,pv的状态由Released 变为Available,可用再次被新的pvc绑定,此时存储服务器的数据将会被彻底删除(很危险,所以不建议使用该回收策略,所以 Recycle策略被弃用了)。演示如下:
[root@master pv]# kubectl delete pvc pvc-nfs #删除pvc
persistentvolumeclaim "pvc-nfs" deleted
[root@master pv]# kubectl get pv #查看pv,这时pv的状态是Released已释放
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
persistentvolume/pv-nfs 500M RWX Recycle Released default/pvc-nfs 17m
[root@master pv]# kubectl get pv #等了一会儿,查看pv,这时pv的状态是Available 可用
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
persistentvolume/pv-nfs 500M RWX Recycle Available 18m
#查看后端的存储服务器上的数据
[root@mysql k8s_data]# ll #数据已经被删除掉了
total 0
以上3种pv的回收策略,目前,AWS EBS、GCE PD、Azure Disk 和 Cinder 卷支持Delete删除策略,而只有 NFS 和 HostPath 支持Recycle回收策略,也不建议使用Recycle策略,因为会直接删除存储数据。
持久卷的动态卷配置、为什么需要StorageClass
以上,我们创建pv和使用pv的流程是:
1、创建一个持久卷;
2、再创建一个持久卷声明,持久卷声明根据访问模式和容量大小来自动适配到PV,并进行绑定;
3、pod中应用持久卷声明即可实现使用存储。
假如在一个大规模的Kubernetes集群里,可能有成千上万个PVC,这就意味着运维人员必须实现创建出这个多个PV,此外,随着项目的需要,会有新的PVC不断被提交,那么运维人员就需要不断的添加新的,满足要求的PV,否则新的Pod就会因为PVC绑定不到PV而导致创建失败。而且通过 PVC 请求到一定的存储空间也很有可能不足以满足应用对于存储设备的各种需求。
而且不同的应用程序对于存储性能的要求可能也不尽相同,比如读写速度、并发性能等,为了解决这一问题,Kubernetes 又为我们引入了一个新的资源对象:StorageClass,通过 StorageClass 的定义,管理员可以将存储资源定义为某种类型的资源,比如快速存储、慢速存储等,用户根据 StorageClass 的描述就可以非常直观的知道各种存储资源的具体特性了,这样就可以根据应用的特性去申请合适的存储资源了。
什么是StorageClass(简写sc)
Kubernetes提供了一套可以自动创建PV的机制,即:Dynamic Provisioning。而这个机制的核心在于StorageClass这个API对象。
StorageClass对象会定义下面两部分内容:
1、PV的属性。比如,存储类型,Volume的大小等。
2、创建这种PV需要用到的存储插件,即存储制备器。
有了这两个信息之后,Kubernetes就能够根据用户提交的PVC,找到一个对应的StorageClass,之后Kubernetes就会调用该StorageClass声明的存储插件,进而创建出需要的PV。
优化后的流程
引入了持久卷的动态配置之后,整个流程就变成了如下:
1、管理员首选需要提供或创建Provisioner(供应者),Provisioner负责为外部请求提供pv(持久化存储卷)实例(相当于定义哪个是nfs存储服务器),说白了,Provisioner作用就是,决定使用哪个卷插件分配 PV;
2、StorageClass向绑定的Provistioner发出创建pv(持久化存储卷)请求;
3、PVC绑定到StorageClass,StorageClass就会根据PVC指定的容量、访问模式来自动创建与PVC匹配的PV,无需人工干预;
4、Pod应用PVC。
正向流程:pod引用pvc,pvc向sc发出请求,sc向provisioner发出创建pv请求并成功创建pv。
以上4步就是整个动态卷的流程,现在只需要我们运维人员前期创建多个StorageClass存储类,然后就不用管了,开发人员他们自己创建pvc,pvc就会自动向sc发出创建pv请求,这样pv自动创建成功,就不需要运维人员手动一个个创建pv了。
从官网https://kubernetes.io/zh-cn/docs/concepts/storage/storage-classes/#provisioner
得知,k8s并没有内置nfs的Provisioner,所以需要我们自己安装一个NFS的Provisioner。
声明:我们以最常用的nfs来做演示,同时以下创建账号和provisioner、StorageClass存储类都是参考:https://github.com/kubernetes-retired/external-storage/blob/master/nfs-client/deploy/
来做的。
创建ServiceAccount账号
#先创建账号和角色,默认在default命名空间中创建
cat > nfs-rbac.yaml <<'EOF'
apiVersion: v1
kind: ServiceAccount
metadata:
name: nfs-client-provisioner
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
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
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
namespace: default
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
namespace: default
roleRef:
kind: Role
name: leader-locking-nfs-client-provisioner
apiGroup: rbac.authorization.k8s.io
EOF
kubectl apply -f nfs-rbac.yaml
创建provisioner(也可称为供应者、置备程序、存储分配器)
需要先准备好nfs存储服务器,然后我们使nfs来作为Provisioner(供应者、置备程序),创建一个deploy,使用pod来运行置备程序provisioner,其大概配置的就是定义nfs服务器的ip,共享路径等信息;
#创建nfs类型的置备程序
cat > nfs-client-provisioner.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: nfs-client-provisioner
labels:
app: nfs-client-provisioner
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 #这个serviceAccountName就是上面创建ServiceAccount账号
containers:
- name: nfs-client-provisioner
image: quay.io/external_storage/nfs-client-provisioner:latest
volumeMounts:
- name: nfs-client-root
mountPath: /persistentvolumes
env:
- name: PROVISIONER_NAME #PROVISIONER_NAME的值就是本清单的顶部定义的name
value: nfs-client-provisioner
- name: NFS_SERVER #这个NFS_SERVER参数的值就是nfs服务器的IP地址
value: 192.168.118.128
- name: NFS_PATH #这个NFS_PATH参数的值就是nfs服务器的共享目录
value: /data/k8s_data
volumes:
- name: nfs-client-root
nfs: #这里就是配置nfs服务器的ip地址和共享目录
server: 192.168.118.128
path: /data/k8s_data
EOF
kubectl apply -f nfs-client-provisioner.yaml
创建StorageClass
置备程序provisioner创建好了,下面现在我们就可以创建一个StorageClass存储类了。
#存储类和pv一样,也是集群层面的资源,所以没有命名空间的限制,如下命令可以看的出来:
kubectl api-resources | grep storageclasses
#创建StorageClass存储类
cat > nfs-storageclass.yaml <<'EOF'
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-storageclass
provisioner: nfs-client-provisioner #provisioner参数定义置备程序
reclaimPolicy: Retain #回收策略,默认是Delete
parameters:
archiveOnDelete: "false"
EOF
kubectl apply -f nfs-storageclass.yaml
创建pvc来引用storageclass
pvc中使用存储类很简单,通过一个参数即可实现:
#创建pvc,pvc使用sc
cat > pvc.yaml <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-nfs
namespace: default
labels:
pvc: nfs
spec:
resources:
requests:
storage: 200M
accessModes:
- ReadWriteMany
storageClassName: nfs-storageclass #指定要使用哪个存储类
EOF
kube-apiserver.yaml 添加参数
RemoveSelfLink=false
参数在k8s集群 v1.20之前都存在,在v1.20之后被删除,需要在/etc/kubernetes/manifests/kube-apiserver.yaml
添加参数增加 - --feature-gates=RemoveSelfLink=false
,如果不添加,存储动态供给将出现问题:
#不添加RemoveSelfLink=false参数,存储动态供给将出现问题:
[root@master01 ~]# kubectl logs -f nfs-client-provisioner-f49bd9bc5-8qqnh
e reference'. Will not report event: 'Normal' 'LeaderElection' 'nfs-client-provisioner-f49bd9bc5-8qqnh_db245699-6338-11ee-b4a8-02df0c95d22b became leader'
I1005 04:37:14.104241 1 leaderelection.go:194] successfully acquired lease default/nfs-client-provisioner
I1005 04:37:14.104279 1 controller.go:631] Starting provisioner controller nfs-client-provisioner_nfs-client-provisioner-f49bd9bc5-8qqnh_db245699-6338-11ee-b4a8-02df0c95d22b!
I1005 04:37:14.204517 1 controller.go:680] Started provisioner controller nfs-client-provisioner_nfs-client-provisioner-f49bd9bc5-8qqnh_db245699-6338-11ee-b4a8-02df0c95d22b!
I1005 04:40:29.682332 1 controller.go:987] provision "default/pvc-nfs" class "nfs-storageclass": started
E1005 04:40:29.685280 1 controller.go:1004] provision "default/pvc-nfs" class "nfs-storageclass": unexpected error getting claim reference: selfLink was empty, can't make reference
#添加参数RemoveSelfLink=false参数
spec:
containers:
- command:
- kube-apiserver
- --advertise-address=192.168.544.151
...............................
- --feature-gates=RemoveSelfLink=false #添加参数RemoveSelfLink=false参数
#等待 kube-apiserver pod重启完成就绪状态即可
创建业务pod、service
pvc以及创建好了,下面就是创建业务pod和service了。
cat > deployment.yaml <<'EOF'
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy-nginx-pv-pvc
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: nginx-pv-pvc
template:
metadata:
labels:
app: nginx-pv-pvc
spec:
containers:
- image: nginx:1.7.9
name: nginx-container
ports:
- name: http
containerPort: 80
volumeMounts:
- name: pv-pvc-volume
mountPath: /var/log/nginx
volumes:
- name: pv-pvc-volume
persistentVolumeClaim:
claimName: pvc-nfs
readOnly: false
---
apiVersion: v1
kind: Service
metadata:
labels:
app: svc-nginx-pv-pvc
name: svc-nginx-nodeport-pv-pvc
spec:
sessionAffinity: ClientIP
ports:
- port: 80
protocol: TCP
targetPort: 80
nodePort: 30058
selector:
app: nginx-pv-pvc
type: NodePort
---
EOF
查看各个资源
#查看deploy
kubectl get deployments.apps
#查看存储类
kubectl get sc
#查看pvc和pv
kubectl get pv,pvc
默认存储类、不指定存储类的pvc、pvc中存储类名设置为空字符串
管理员可以创建多个存储类,如果存在多个存储类,可以将某个存储类设置为默认存储类:
#将指定的存储类设置为默认存储类
kubectl patch storageclass nfs-storageclass -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
有了默认存储类,在定义pvc如果不指定 pvc.spec.storageClassName,则将使用默认存储类进行创建pv。
以上我们讨论了写上storageClassName和不写storageClassName的两种情况,provisioner(存储分配器)都会创建pv,那么如果不想让其创建pv,而是像让pvc与我们手动创建的pv进行绑定怎么办呢?细心观察的同学可能已经发现,文章最开始创建pvc的时候将storageClassName:""
,设置为空字符串,是的,当storageClassName值设置为空字符串时,provisioner将不会创建pv,pvc自动寻找匹配你手动创建的pv。
管理员可以实现配置多个provisioner,然后再创建多个storageClass,然后由开发人员根据他们存储需求来创建pvc引用对应的storageClass。
通过一幅图来完整了解动态持久卷供应全貌
配置动态卷出现的一些问题排查
1、必须要创建ServiceAccount,因为Provisioner(存储分配器)中用到了ServiceAccount;
2、如果deployment创建了,但是pod没创建成功(deployment底层原理是使用rc去创建pod),此时可以查看controller-manager的日志,kubeadm安装的controller-manager是使用pod启动的,kubectl logs -f kube-controller-manager-master -n kube-system
;
3、如果出现问题,顺着这样的思路一层一层的查看pod的来排查问题:业务pod调用pvc,pvc调用storageClass,sc调用provisioner去创建pv;
4、业务pod一直是“Pending”状态,排查发现pvc一直是“等待创建pv”,继续排查provisioner,查看provisioner的日志, kubectl logs nfs-client-provisioner-686c9994b5-p6h6n
发现报错信息“unexpected error getting claim reference: selfLink was empty, can't make reference”
,百度,使用以下解决办法:
总结
1、pod中直接使用nfs卷。
必须先创建一个nfs服务器,然后启动nfs-server,设置共享数据目录等前期工作,同时在node节点还需要安装nfs-utils
服务,即`yum install -y nfs-utils`,主要是为了让node节点上有mount.nfs命令,该命令是挂载时需要的命令,可以不启动node节点上nfs-server.
[root@master pv]# cat deployment-nfs.yaml
.............
spec:
containers:
- image: nginx:1.7.9
name: nginx-container
ports:
- name: http
containerPort: 80
volumeMounts: #挂载点
- name: nfs-volume #要挂载的卷的名称
mountPath: /var/log/nginx #挂载的路径
volumes:
- name: nfs-volume
nfs: #创建nfs存储卷
server: 192.168.118.128 #nfs服务器ip
path: /home/k8s_data #nfs服务器的共享数据目录
.............
kubectl apply -f deployment-nfs.yaml
2、创建一个pv。
为了屏蔽存储服务器的相关配置,即向开发人员屏蔽后端的nfs存储服务器的ip,共享目录等情况,kubernetes能将卷的定义从pod中解耦出来,从而有
了pv和pvc资源对象,pv是persistentVolume的简写,即持久化存储,它是集群层面的资源,所以pv不属于任何一个命名空间。
cat > pv-nfs.yaml <<'EOF'
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-nfs
labels:
pv: nfs
spec:
capacity: #容量为500M
storage: 500M
accessModes: #访问模式为允许多节点以读写方式挂载,可以有多个访问模式
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain #回收策略
nfs: #定义nfs服务器的信息
server: 192.168.118.128
path: /data/k8s_data
readOnly: false
EOF
kubectl apply -f pv-nfs.yaml
3、创建一个pvc。
创建好后的pvc会自动与最匹配的pv进行绑定,这种绑定机制其实是通过pvc申请的容量大小和访问模式来寻找最佳的pv进行绑定的。
[root@master ~]# vim pvc-nfs.yaml #创建一个pvc,用于绑定我们上面创建的pv
apiVersion: v1
kind: PersistentVolumeClaim #资源类型
metadata:
name: pvc-nfs
namespace: default #所属命名空间,注意,pvc是有命名空间的,pv是集群层面的没有命名空间
labels:
pvc: nfs
spec:
resources: #定义pvc所需的资源
requests:
storage: 500M #需要500M的存储空间
accessModes:
- ReadWriteMany #访问模式为ReadWriteMany
storageClassName: "" #这个写上空字符串,后面动态持久卷将解释为什么
4、pod中引用pvc。
定义后pvc之后,pod中可以直接引用pvc,如下所示,pv定义了存储空间大小,pvc绑定pv,pod中引用pvc,这样就能实现pod持久化存储数据的目的。
..............
spec:
containers:
- image: nginx:1.7.9
name: nginx-container
ports:
- name: http
containerPort: 80
volumeMounts:
- name: pvc-volume
mountPath: /var/log/nginx
volumes:
- name: pvc-volume #卷名
persistentVolumeClaim: #这里直接引用已经定义好的pvc-nfs这个pvc
claimName: pvc-nfs
readOnly: false
.......................
5、问题思考,如果k8s中存在大量的应用pod都需要持久化存储数据,那要管理员一个个的创建成千上万的pv吗?k8s为我们引入了一种自动创建pv的机
制,存储类StorageClass。
StorageClass对象会定义下面两部分内容:
1、PV的属性。比如,存储类型,Volume的大小等。
2、创建这种PV需要用到的存储插件,即存储制备器。
有了这两个信息之后,Kubernetes就能够根据用户提交的PVC,找到一个对应的StorageClass,之后Kubernetes就会调用该StorageClass声明的存储
插件,进而创建出需要的PV。
引入了持久卷的动态配置之后,整个流程就变成了如下:
1、管理员首选需要提供或创建Provisioner(供应者),Provisioner负责为外部请求提供pv(持久化存储卷)实例(相当于定义哪个是nfs存储服务
器),说白了,Provisioner作用就是,决定使用哪个卷插件分配 PV;
2、StorageClass向绑定的Provistioner发出创建pv(持久化存储卷)请求;
3、PVC绑定到StorageClass,StorageClass就会根据PVC指定的容量、访问模式来自动创建与PVC匹配的PV,无需人工干预;
4、Pod应用PVC。
正向流程:pod引用pvc,pvc向sc发出请求,sc向provisioner发出创建pv请求并成功创建pv。
以上4步就是整个动态卷的流程,现在只需要我们运维人员前期创建多个StorageClass存储类,然后就不用管了,开发人员他们自己创建pvc,pvc就会自
动向sc发出创建pv请求,这样pv自动创建成功,就不需要运维人员手动一个个创建pv了。
6、创建一个nfs类型的存储类。
整个流程为:创建账号和角色-->创建Provisioner供应者/置备程序(Provisioner供应者指定了后端存储的类型,ip地址等信息)-->创建
StorageClass(sc中会指明使用哪个Provisioner以及回收策略)-->开发人员创建pvc-->pod中引用pvc。
使用最常用的nfs来做演示,同时以下创建账号和provisioner、StorageClass存储类都是参考:https://github.com/kubernetes-
retired/external-storage/blob/master/nfs-client/deploy/来做的。
(1)、创建账号与角色
cat > nfs-rbac.yaml <<'EOF'
apiVersion: v1
kind: ServiceAccount
metadata:
name: nfs-client-provisioner
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
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
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
namespace: default
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
namespace: default
roleRef:
kind: Role
name: leader-locking-nfs-client-provisioner
apiGroup: rbac.authorization.k8s.io
EOF
kubectl apply -f nfs-rbac.yaml
(2)、创建provisioner供应者、置备程序、存储分配器.
需要先准备好nfs存储服务器,然后我们使nfs来作为Provisioner(供应者、置备程序),创建一个deploy,使用pod来运行置备程序provisioner,其
大概配置的就是定义nfs服务器的ip,共享路径等信息;
#创建nfs类型的置备程序
cat > nfs-client-provisioner.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: nfs-client-provisioner
labels:
app: nfs-client-provisioner
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 #这个serviceAccountName就是上面创建ServiceAccount账号
containers:
- name: nfs-client-provisioner
image: quay.io/external_storage/nfs-client-provisioner:latest
volumeMounts:
- name: nfs-client-root
mountPath: /persistentvolumes
env:
- name: PROVISIONER_NAME #PROVISIONER_NAME的值就是本清单的顶部定义的name
value: nfs-client-provisioner
- name: NFS_SERVER #这个NFS_SERVER参数的值就是nfs服务器的IP地址
value: 192.168.118.128
- name: NFS_PATH #这个NFS_PATH参数的值就是nfs服务器的共享目录
value: /home/k8s_data
volumes:
- name: nfs-client-root
nfs: #这里就是配置nfs服务器的ip地址和共享目录
server: 192.168.118.128
path: /home/k8s_data
EOF
kubectl apply -f nfs-client-provisioner.yaml
(3)、 创建StorageClass存储类。置备程序provisioner创建好了,下面开始创建一个StorageClass存储类。
cat > nfs-storageclass.yaml <<'EOF'
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-storageclass
provisioner: nfs-client-provisioner #provisioner参数定义置备程序
reclaimPolicy: Retain #回收策略,默认是Delete
parameters:
archiveOnDelete: "false"
(4)、创建pvc来引用storageclass
[root@master ~]# vim pvc.yaml #创建pvc,pvc引用sc
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-nfs
namespace: default
labels:
pvc: nfs
spec:
resources:
requests:
storage: 200M
accessModes:
- ReadWriteMany
storageClassName: nfs-storageclass #指定要使用哪个存储类
EOF
kubectl apply -f nfs-storageclass.yaml
5、创建业务pod调用pvc就是能实现动态分配存储了。
cat > deployment.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy-nginx-pv-pvc
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: nginx-pv-pvc
template:
metadata:
labels:
app: nginx-pv-pvc
spec:
containers:
- image: nginx:1.7.9
name: nginx-container
ports:
- name: http
containerPort: 80
volumeMounts:
- name: pv-pvc-volume
mountPath: /var/log/nginx
volumes:
- name: pv-pvc-volume
persistentVolumeClaim:
claimName: pvc-nfs
readOnly: false
---
apiVersion: v1
kind: Service
metadata:
labels:
app: svc-nginx-pv-pvc
name: svc-nginx-nodeport-pv-pvc
spec:
sessionAffinity: ClientIP
ports:
- port: 80
protocol: TCP
targetPort: 80
nodePort: 30058
selector:
app: nginx-pv-pvc
type: NodePort
EOF
kubectl apply -f deployment.yaml