前面提到过,Pod类似于一个逻辑主机,它内部的一组容器是共享一些资源的,比如CPU、RAM、网络接口等。但是当涉及到文件系统时,各个容器就是隔离的,因为大多数容器的文件系统来自容器镜像,因此默认情况下, 每个容器的文件系统与其他容器完全隔离。
我们知道,K8s中的Pod是有自愈能力的,一个Pod内的容器是有可能被重新启动的(如果出现了异常)。而在Docker中我们也学的过,容器内的数据都是临时存储的,当容器被删除,它里面的数据也会被删除。那么问题来了,一个运行的容器被重启了,那么重启后产生的新容器中该怎么恢复他原来在旧容器中产生的数据呢???
在Kubernetes中,通过定义存储卷来满足这个需求。
假设有一个Pod中包含了三个容器,一个容器运行了一个web服务器,该web服务器要展示的HTML页面路径位于/var/htdocs,同时他产生的日志会存储到/var/logs目录中。第二个容器运行了一个代理服务,他的作用就是用来创建HTML文件,并将它存放在/var/html中。第三个容器中运行的应用主要用来处理这个容器中/var/logs目录中的日志。
每个容器都有一个很明确的用途,但是如果每个容器单独使用就没多大用处了,因为他们的文件系统是完全隔离的,代理服务创建的HTML文件WebServer访问不到,WebServer产生的日志LogRotator处理不了。
但是,如果我们在Pod中添加两个存储卷,并且在三个容器的适当路径上挂载它们,这样就能让这三个容器间协同工作了。
在Linux中是允许在文件树的任意位置挂载文件系统的,当这样做的时候,挂载的文件系统的内容在目录中是可以访问的。通过将相同的卷挂载到两个容器中,他们可以对相同的文件进行操作。
上面图中,Pod中存在两个卷:publicHtml和logVol。
- publicHtml被挂载在WebServer容器的
/var/htdocs/
中,因为这是WebServer的服务目录。在AgentServer容器中也挂载了相同的卷,但在这个容器中挂载的路径是/var/html/
,因为代理将Html文件写入/var/html/
这个路径中。通过这种方式挂载这个卷,WebServer现在就能读取到AgentServer容器中产生的Html页面了。- logVol卷用来存放日志,此卷被挂载在WebServer容器和AgentServer容器的
/var/logs/
路径下。注意,不是说同一个Pod中的所有容器都可以任意访问Pod中存在的卷。如果某个容器想访问某个卷,则需要在定义容器的时候通过volumeMounts来进行挂载声明。
- 通过volumeMounts进行挂载声明只是用来指定容器中哪个路径要挂载,但是对外要挂在哪里是在Pod中指定的,在Pod中声明的是容器中的挂载的卷的定义。
配置信息(用于将K8s部分资源和集群信息公开给Pod的特殊类型的卷,下一章会介绍)
非持久性存储
emptyDir:用来存储临时数据的简单空目录
hostPath:用来将目录从工作节点的文件系统挂载到Pod中
其实他是属于半持久性存储,因为他跟被调度在哪个节点上有关。(详细看下面的介绍)
持久性存储
一个emptyDir卷对于同一个Pod中运行的容器之间共享文件特别有用。但是它也可以被单个容器用于将数据临时写入磁盘。例如在大型数据集上执行排序操作时,没有那么多内存可供使用。这时数据就需要被写到容器的文件系统中,但是将数据写入内存和写入容器的文件系统还是有区别的,甚至容器的文件系统还可能是不可写的,所以写到挂载的卷可能是唯一的选择。
apiVersion: v1
kind: Pod
metadata:
name: fortune
labels:
app: test
spec:
volumes: # 一个名为html的单独emptyDir卷,挂载到下面的两个容器中
- name: html #
emptyDir: {} #
containers:
- image: luksa/fortune # 第一个容器名为html-generator,运行luksa/fortune镜像
name: html-generator
volumeMounts: # 名为html的卷挂载在容器的/var/htdocs路径中
- name: html
mountPath: /var/htdocs
- image: nginx:alpine # 第二个容器名为web-server,运行nginx:alpine镜像
name: web-server
volumeMounts: # 名为html的卷挂载在容器的/usr/share/nginx/html路径中,他被设置为只读
- name: html
mountPath: /usr/share/nginx/html
readOnly: true
ports:
- containerPort: 80
protocol: TCP
在这个Pod中包含两个容器和一个挂载在两个容器中的共用的卷,但这个卷挂载在容器的不同路径上。当html-generator容器启动时,他每10s启动一次fortune命令输出到
/var/htdocs/index.html
文件(这是这个定制的镜像的功能)。因为卷是在/var/htdocs上挂载的,所以index.html
文件被写入卷中(而不是容器的可写层(这是关于Docker的知识))。一旦web-server容器启动,他就开始为/usr/share/nginx/html
目录中的任意HTML文件提供服务(这是Nginx服务的默认服务文件目录)。因为我们将卷挂载在那个确切的位置,Nginx将为运行fortune循环的容器输出的index.html
文件提供服务。最终效果是:一个客户端向Pod上的80端口发送一个Http请求,将接收到当前的fortune消息作为响应。
如果想测试效果,可通过
kubectl expose pod fortune --port=8888 --target-port=80 --name=testemptyDir
命令将这个Pod暴露出去,然后再每十秒访问一次服务集群的8888端口,查看效果。
大多数Pod不应该去访问节点文件系统上的任何文件,但是某些系统级别的Pod(由DaemonSet管理的Pod)确实需要读取节点的文件或使用节点文件系统来访问节点设备。Kubernetes通过hostPath卷实现了这一点。
hostPath卷指向节点文件系统上的特定文件或目录,在同一个节点上运行并在其hostPath卷中使用相同路径的Pod可以看到相同的文件。
hostPath卷是一个持久化卷,如果删除一个Pod,并且下一个Pod使用了指向主机上相同路径的hostPath卷,则新Pod将会发现上一个Pod留下的数据。(前提是下一个Pod要和上一个Pod部署在同一个节点上!)
切记,仅当需要在节点上读取或写入系统文件时才使用hostPath,切勿使用它们来持久化跨Pod的数据。
### 解决容器时间问题
apiVersion: v1
kind: Pod
metadata:
name: busy-box-test
namespace: default
spec:
restartPolicy: OnFailure
containers:
- name: busy-box-test
image: busybox
imagePullPolicy: IfNotPresent
volumeMounts:
- name: date-config
mountPath: /etc/localtime
command: ["sleep", "60000"]
volumes:
- name: date-config
hostPath:
path: /etc/localtime
要想使用NFS,需要自己先搭建一个NFS服务。
安装NFS服务
# 在任意机器执行下面的命令 yum install -y nfs-utils #执行命令 vi /etc/exports,创建 exports 文件,文件内容如下: echo "/nfs/data/ *(insecure,rw,sync,no_root_squash)" > /etc/exports # 执行以下命令,启动 nfs 服务;创建共享目录 mkdir -p /nfs/data systemctl enable rpcbind systemctl enable nfs-server systemctl start rpcbind systemctl start nfs-server exportfs -r #检查配置是否生效 exportfs # 输出结果如下所示 /nfs/data /nfs/data
使用NFS类型的卷(他是完全持久性的卷,不用但是新的Pod被调度到了哪个节点上)其实和使用其他类型的卷没什么区别,只是需要改下Pod中的定义信息即可,如下:
apiVersion: v1
kind: Pod
metadata:
name: test-nfs
labels:
app: test
spec:
volumes: # 一个名为html的单独nfs卷,挂载到下面的容器中
- name: html
nfs:
server: 10.170.11.8 # 指定NFS服务器的IP
path: /nfs/some/path # 服务器提供的路径
containers:
- image: luksa/fortune # 第一个容器名为html-generator,运行luksa/fortune镜像
name: html-generator
volumeMounts: # 名为html的卷挂载在容器的/var/htdocs路径中
- name: html
mountPath: /var/htdocs
我们发现所有的持久卷类型都要求Pod的开发人员了解集群中可用的真实网络存储的基础结构。例如,要创建支持NFS协议的卷,开发人员必须知道NFS节点所在的实际服务器。而Kubernetes的理念是向应用程序及其开发人员隐藏真实的基础设施,使他们不必担心基础设施的具体状态。(比如,开发人员要部署的应用程序到底部署在哪个节点上他根本不用关心,直接交给Kubernetes就行了。)
所以,理想情况下是,在Kubernetes上部署应用程序的开发人员不需要知道底层使用的技术细节(应用程序部署在哪台机器、存储技术等)。这些是交给集群管理员去考虑的。开发人员需要一定数量的持久化存储为应用持久化数据时,只需要向Kubernetes申请就行了(PV和PVC的概念就诞生了!),就像在创建Pod时可以请求CPU、内存和其他机器资源一样。集群管理员可以对集群进行配置,让其可以为应用程序提供所需的服务。
首先由集群管理员创建持久卷PV,在创建时可以指定这个PV的大小和所支持的访问模式。当开发者(集群用户)需要在他的Pod中添加特定的卷时,他们首先需要创建持久卷声明PVC清单,指定所需要的最低容量要求和访问模式,然后这个PVC会交给K8s,K8s将找到可匹配的持久卷并将其绑定到持久卷声明。
接下来我们分别担任管理员的角色,创建一个PV。然后在承担开发者的角色,首先声明PVC,然后在Pod中使用。
apiVersion: v1
kind: PersistentVolume
metadata:
name: test-pv-volume
labels:
type: nfs
spec:
storageClassName: my-nfs-storage
capacity:
storage: 10Gi ### 定义PV的大小
accessModes: ### 指定访问模式
- ReadWriteOnce ### 允许单个客户端访问(同时支持读取和写入操作)
persistentVolumeReclaimPolicy: Retain ### 当声明被释放后,PV将会被保留(不清理和删除)
nfs: ### PV指定支持之前创建的NFS服务
server: 10.170.11.8
path: "/nfs/data/haha"
马上我们要部署一个需要持久化存储的Pod,将要用到之前创建的持久卷,但是不能直接在Pod内使用,需要先声明一个PVC。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nginx-pvc
namespace: default
labels:
app: nginx-pvc
spec:
storageClassName: "" ## 存储类的名字,后面会讲到他的使用方式。
accessModes: ### 指定访问模式
- ReadWriteOnce
resources:
requests:
storage: 50m ### 表示需要申请50m的磁盘空间
上面这个PVC表示:请求50m的存储空间和ReadWriteOnce访问模式。
当我们将这个PVC创建好以后,Kubernetes就会找到适合的PV并将其和该PVC进行绑定。注意:PV的容量必须是大于PVC申请的容量的,而且卷的访问模式(PVC中指定的)也必须包含声明中指定的访问模式(PV中指定的)。
要在Pod中使用PV,则需要在Pod的卷定义中引用PVC,如下:
apiVersion: v1
kind: Pod
metadata:
name: "nginx-pvc"
namespace: default ### 需要和PVC在同一命名空间
labels:
app: "nginx-pvc"
spec:
volumes:
- name: html
persistentVolumeClaim: ### 指定PVC
claimName: nginx-pvc ### PVC的名字
containers:
- name: nginx-pvc
image: "nginx"
ports:
- containerPort: 80
name: http
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
restartPolicy: Always
当PV被Pod使用了他就不能再被分配给别人了,而且绑定了Pod的PVC,而且Pod正在运行,PVC是不能被删除的,PVC会一直等到Pod删除了后再去删除
注意:
当我们删除Pod和他引用的PVC,然后再重新创建一个新的PVC,这时这个PVC是不能被绑定到刚刚被释放的那个PV的,因为这个PV已经被使用过,所以它可能还包含前一个声明人的数据。所以如果集群管理员还没来得及清理那个不再被使用PV,那么他就不应该将这个PV绑定到PVC中。
我们可以通过persistentVolumeReclaimPolicy
属性指定PV的回收策略
rm -rf /thevolume/*
):删除PVC时同时清除PV关联的卷里面的内容
学习了上面的技术后,我们发现这个PV总是需要有个集群管理员来进行创建,能不能不要人来做这件事呢?可以,K8s可以通过动态配置持久卷来自动执行此任务。
集群管理员只需要创建一个持久卷配置,并定义一个或多个StorageClass资源,从而让用户选择他们想要的持久卷类型而不是仅仅只是创建持久卷。用户可以在其持久卷声明中引用StorageClass,而配置程序在配置持久存储时将采用这一点。
在用户创建PVC之前,管理员需要创建一个或多个StorageClass资源,然后集群自动根据PVC创建出对应PV进行使用。
## 创建了一个存储类
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: my-nfs-storage
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner ### 指定用于动态配置PV的卷插件
#### provisioner指定一个供应商的名字。 必须匹配 k8s-deployment 的 env PROVISIONER_NAME的值
parameters:
archiveOnDelete: "false" ### 该参数会传递给provisioner
StorageClass资源指定当PVC请求此StorageClass时应该使用哪个动态配置供应商来创建PV。
## 创建了一个存储类
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: managed-nfs-storage
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner #### provisioner指定一个供应商的名字。 必须匹配 下面Deployment中指定的env的PROVISIONER_NAME的值
parameters:
archiveOnDelete: "false"
---
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
containers:
- name: nfs-client-provisioner
image: registry.cn-hangzhou.aliyuncs.com/lfy_k8s_images/nfs-subdir-external-provisioner:v4.0.2
volumeMounts:
- name: nfs-client-root
mountPath: /persistentvolumes
env:
- name: PROVISIONER_NAME
value: k8s-sigs.io/nfs-subdir-external-provisioner ### 这个值在创建StorageClass时会用到
- name: NFS_SERVER
value: 10.170.11.8 ## 指定自己nfs服务器地址
- name: NFS_PATH
value: /nfs/data ## nfs服务器共享的目录
volumes:
- name: nfs-client-root
nfs:
server: 10.170.11.8
path: /nfs/data
---
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
创建StorageClass资源后,用户可以在其PVC中按名称引用StorageClass。
创建PVC,使用StorageClass
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nginx-pvc
namespace: default
labels:
app: nginx-pvc
spec:
storageClassName: my-nfs-storage ## 该PVC请求自定义存储类
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50m
在创建这个PVC时,PV会由指定的StorageClass资源中引用的provisioner创建。
集群管理员可以创建具有不同性能或其他特性的多个StorageClass,然后开发人员再在PVC中指明他想要的StorageClass。
这样看来其实StorageClass就是将PV按照不同特性分了个组。
当我们在创建PVC时如果不明确的指定storageClassName属性,那么他会使用一个默认的StorageClass!如果我们就是想让这个PVC和我们自己手动创建的PV进行绑定,那么只需要将storageClassName的值填为 “” (空字符串)就行。
未完,待续>>>
参考:Kubernetes in Action