Volume
本节我们讨论 Kubernetes 的存储模型 Volume,学习如何将各种持久化存储映射到容器。
我们经常会说:容器和 Pod 是短暂的。其含义是它们的生命周期可能很短,会被频繁地销毁和创建。容器销毁时,保存在容器内部文件系统中的数据都会被清除。为了持久化保存容器的数据,可以使用 Kubernetes Volume。
Volume 的生命周期独立于容器,Pod 中的容器可能被销毁和重建,但 Volume 会被保留。
本质上,Kubernetes Volume 是一个目录,这一点与 Docker Volume 类似。当 Volume 被 mount 到 Pod,Pod 中的所有容器都可以访问这个 Volume。Kubernetes Volume 也支持多种 backend 类型,包括 emptyDir、hostPath、GCE Persistent Disk、AWS Elastic Block Store、NFS、Ceph 等,完整列表可参考 https://kubernetes.io/docs/concepts/storage/volumes/#types-of-volumes
Volume 提供了对各种 backend 的抽象,容器在使用 Volume 读写数据的时候不需要关心数据到底是存放在本地节点的文件系统中呢还是云硬盘上。对它来说,所有类型的 Volume 都只是一个目录。
我们将从最简单的 emptyDir 开始学习 Kubernetes Volume。
为什么需要存储卷
容器部署过程中一般有以下三种数据:
- 启动时需要的初始数据,例如配置文件
- 启动过程中产生的临时数据,该临时数据需要多个容器间共享
- 启动过程中产生的持久化数据,例如MySQL的data目录
数据卷概述
- Kubernetes中的Volume提供了在容器中挂载外部存储的能力
- Pod需要设置卷来源(spec.volume)和挂载点(spec.containers.volumeMounts)两个信息后才可以使用相应的Volume
数据卷类型大致分类
- 本地(hostPath,emptyDir等)
- 网络(NFS,Ceph,GlusterFS等)
- 公有云(AWS EBS等)
- K8S资源(configmap,secret等)
emptyDir存储卷
Kubernetes支持存储卷类型中,emptyDir存储卷的生命周期与其所属的Pod对象相同,它无法脱离Pod对象的生命周期提供数据存储功能,因此emptyDir通常仅用于数据缓存或临时存储。不过基于emptyDir构建的gitRepo存储卷可以在Pod对象的生命周期起始时从响应的Git仓库中复制相应的数据文件到底层的emptyDir中,从而使得它具有了一定意义上的持久性。
emptyDir存储卷是Pod对象生命周期中的一个临时目录,类似于Docker上的docker挂载卷
,在Pod对象启动时即被创建,而在Pod对象被移除时会被一并删除。不具有持久能力的emptyDir存储卷只能用于某些特殊场景中,例如,用一Pod内的多个容器间文件的共享,或者作为容器数据的临时存储目录用于数据缓存系统等。
emptyDir存储卷则定义于.spec.volumes.emptyDir
嵌套字段中,可用字段主要包含两个,具体如下:
•medium
:此目录所在存储介质的类型,可取值为default
或Memory
,默认为default,表示使用节点的默认存储介质:Memory
表示基于RAM的临时文件系统tmpfs,空间受于内存,但性能非常好,通常用于为容器中的应用提供缓存空间
•sizeLimit
:当前存储卷的空间限额,默认值为 nil,表示不限制;不过在 medium 字段为 Memory
时,建议定义此限额。
emptyDir
emptyDir 是最基础的 Volume 类型。正如其名字所示,一个 emptyDir Volume 是 Host 上的一个空目录。
emptyDir Volume 对于容器来说是持久的,对于 Pod 则不是。当 Pod 从节点删除时,Volume 的内容也会被删除。但如果只是容器被销毁而 Pod 还在,则 Volume 不受影响。emptyDir一般用于测试,或者缓存场景。
也就是说:emptyDir Volume 的生命周期与 Pod 一致。
Pod 中的所有容器都可以共享 Volume,它们可以指定各自的 mount 路径。下面通过例子来实践 emptyDir,配置文件如下:
[root@k8s-master ~]# cat emptyDir.yml
apiVersion: v1
kind: Pod
metadata:
name: producer-consumer
spec:
containers:
- name: producer
image: busybox
volumeMounts:
- name: shared-volume
mountPath: /producer_dir
args:
- /bin/sh
- -c
- echo "hello this is producer" > /producer_dir/hello ; sleep 3600
- name: consumer
image: busybox
volumeMounts:
- name: shared-volume
mountPath: /consumer_dir
args:
- /bin/sh
- -c
- cat /consumer_dir/hello ; sleep 3600
volumes:
- name: shared-volume
emptyDir: {}
[root@k8s-master ~]# kubectl apply -f emptyDir.yml
pod/producer-consumer created
这里我们模拟了一个 producer-consumer 场景。Pod 有两个容器 producer
和 consumer
,它们共享一个 Volume。producer
负责往 Volume 中写数据,consumer
则是从 Volume 读取数据。
- 文件最底部
volumes
定义了一个emptyDir
类型的 Volumeshared-volume
。 -
producer
容器将shared-volume
mount 到/producer_dir
目录。 -
producer
通过echo
将数据写到文件hello
里。 -
consumer
容器将shared-volume
mount 到/consumer_dir
目录。 -
consumer
通过cat
从文件hello
读数据。
[root@k8s-master ~]# kubectl logs producer-consumer consumer
hello this is producer
kubectl logs
显示容器 consumer
成功读到了 producer
写入的数据,验证了两个容器共享 emptyDir Volume。
因为 emptyDir 是 Docker Host 文件系统里的目录,其效果相当于执行了 docker run -v /producer_dir
和 docker run -v /consumer_dir
。通过 docker inspect
查看容器的详细配置信息,我们发现两个容器都 mount 了同一个目录:
"Mounts": [
{
"Type": "bind",
"Source": "/var/lib/kubelet/pods/620cd011-1d40-47af-8a1a-1beb131e135f/volumes/kubernetes.io~empty-dir/shared-volume",
"Destination": "/consumer_dir",
"Mode": "Z",
"RW": true,
"Propagation": "rprivate"
},
{
"Type": "bind",
"Source": "/var/lib/kubelet/pods/620cd011-1d40-47af-8a1a-1beb131e135f/volumes/kubernetes.io~empty-dir/shared-volume",
"Destination": "/producer_dir",
"Mode": "Z",
"RW": true,
"Propagation": "rprivate"
},
这里 /var/lib/kubelet/pods/620cd011-1d40-47af-8a1a-1beb131e135f/volumes/kubernetes.io~empty-dir/shared-volume
就是 emptyDir 在 Host 上的真正路径。
emptyDir 是 Host 上创建的临时目录,其优点是能够方便地为 Pod 中的容器提供共享存储,不需要额外的配置。但它不具备持久性,如果 Pod 不存在了,emptyDir 也就没有了。根据这个特性,emptyDir 特别适合 Pod 中的容器需要临时共享存储空间的场景,比如前面的生产者消费者用例。
[root@k8s-master ~]# kubectl describe pod producer-consumer
Volumes:
shared-volume:
Type: EmptyDir (a temporary directory that shares a pod's lifetime)
下面是一个使用了emptyDir存储卷的简单示例
1.创建Pod对象配置清单
apiVersion: v1
kind: Pod
metadata:
name: vol-emptydir-pod
spec:
volumes:
- name: html
emptyDir: { }
containers:
- name: nginx
image: nginx:latest
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
- name: pagegen
image: alpine
volumeMounts:
- name: html
mountPath: /html
command: [ "/bin/sh", "-c" ]
args: #定义循环,每10秒向/html/文件中追加写入当前主机名和时间
- while true; do
echo $(hostname) $(date) >> /html/index.html;
sleep 10;
done
上面示例中定义的存储卷名称为html,挂载于容器nginx的 /usr/share/nginx/html目录,以及容器pagegen的/html目录。容器pagegen每隔10秒向存储卷上的index.html文件中追加一行信息,而容器nginx中的nginx进程则以其站点主页。如下图所示:
2.创建Pod对象
kubectl apply -f vol-emptydir.yaml
3.查看Pod状态 Pod对象的详细信息中会显示存储卷的相关状态,包括其是否创建成功(在Events字段中输出)、相关的类型及参数(在Volumes字段中输出)以及容器中挂载状态等信息(在Containers字段中输出),如下面命令所示:
kubectl describe pods/vol-emptydir-pod
Containers:
nginx:
Mounts:
/usr/share/nginx/html from html (rw)
/var/run/secrets/kubernetes.io/serviceaccount from default-token-xxqkj (ro)
pagegen:
Command:
/bin/sh
-c
Args:
while true; do echo $(hostname) $(date) >> /html/index.html; sleep 10; done
Mounts:
/html from html (rw)
/var/run/secrets/kubernetes.io/serviceaccount from default-token-xxqkj (ro)
4.访问Pod中Nginx pagegen容器每隔10秒向 html/index.html 追加写入信息,Nginx容器挂载的也是此临时存储,所以Nginx的网页文件也是从这里获取。
curl http://172.20.1.18
vol-emptydir-pod Fri Jun 12 02:47:29 UTC 2020
vol-emptydir-pod Fri Jun 12 02:47:39 UTC 2020
vol-emptydir-pod Fri Jun 12 02:47:49 UTC 2020
vol-emptydir-pod Fri Jun 12 02:47:59 UTC 2020
vol-emptydir-pod Fri Jun 12 02:48:09 UTC 2020
vol-emptydir-pod Fri Jun 12 02:48:19 UTC 2020
5.进入容器 以下分别进入Nginx容器以及pagegen容器查看其挂载
通过 -c 来指定容器名称进入指定容器
kubectl exec -it pods/vol-emptydir-pod -c nginx -- /bin/sh
# ls /usr/share/nginx/html
index.html
# head -3 /usr/share/nginx/html/index.html
vol-emptydir-pod Fri Jun 12 02:47:29 UTC 2020
vol-emptydir-pod Fri Jun 12 02:47:39 UTC 2020
vol-emptydir-pod Fri Jun 12 02:47:49 UTC 2020
进入pagegen容器
kubectl exec -it pods/vol-emptydir-pod -c pagegen -- /bin/sh
/ # ls /html/
index.html
/ # head -3 /html/index.html
vol-emptydir-pod Fri Jun 12 02:47:29 UTC 2020
vol-emptydir-pod Fri Jun 12 02:47:39 UTC 2020
vol-emptydir-pod Fri Jun 12 02:47:49 UTC 2020
/ # ps aux
PID USER TIME COMMAND
1 root 0:00 /bin/sh -c while true; do echo $(hostname) $(date) >> /html/index.html; sleep 10; done
286 root 0:00 /bin/sh
303 root 0:00 sleep 10
304 root 0:00 ps aux
作为边车 (sidecar)的容器pagegen,其每隔10秒生成一行信息追加到存储卷上的index.html文件中,因此,通过主容器nginx的应用访问到文件内存也会处理不停的变动中。另外,emptyDir存储卷也可以基于RAM创建tmpfs文件系统的存储卷,常用于为容器的应用提高高性能缓存,下面是一个配置示例:
cat vol-emptydir.yaml
apiVersion: v1
kind: Pod
metadata:
name: vol-emptydir-pod
spec:
volumes:
- name: html
emptyDir:
medium: Memory #指定临时存储到内存
sizeLimit: 256Mi #给予的内存空间大小
containers:
- name: nginx
image: nginx:latest
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
- name: pagegen
image: alpine
volumeMounts:
- name: html
mountPath: /html
command: [ "/bin/sh", "-c" ]
args:
- while true; do
echo $(hostname) $(date) >> /html/index.html;
sleep 10;
done
emptyDir卷简单易用,但仅能用于临时存储,另外还存在一些类型的存储卷构建在emptyDir之上,并额外提供了emptyDir没有的功能。
如果还是不懂可以看下面这个例子
定义一个emptyDir存储大小为1G,将其挂载到redis的/data目录中
[root@k8s-master emptydir]# cat emptydir-redis.yaml
apiVersion: v1
kind: Pod
metadata:
name: emptydir-redis
labels:
volume: emptydir
annotations:
kubernetes.io/storage: emptyDir
spec:
containers:
- name: emptydir-redis
image: redis:latest
imagePullPolicy: IfNotPresent
ports:
- name: redis-6379-port
protocol: TCP
containerPort: 6379
volumeMounts: #将定义的驱动emptydir-redis挂载到容器的/data目录,通过名字方式关联
- name: emptydir-redis
mountPath: /data
volumes: #定义一个存储,驱动类型为emptyDir,大小1G
- name: emptydir-redis
emptyDir:
sizeLimit: 1Gi
生成redis pod,并查看describe pod的详情信息
[root@k8s-master emptydir]# kubectl apply -f emptydir-redis.yaml
pod/emptydir-redis created
[root@k8s-master emptydir]# kubectl describe pod emptydir-redis #执行kubectl describe pods emptydir-redis查看容器的存储挂载信息
[root@k8s-master emptydir]# kubectl describe pod emptydir-redis
Name: emptydir-redis
Namespace: default
Priority: 0
Node: k8s-master/192.168.179.99
Start Time: Thu, 29 Oct 2020 15:43:57 +0800
Labels: volume=emptydir
Annotations: kubernetes.io/storage: emptyDir
Status: Running
IP: 10.244.0.15
IPs:
IP: 10.244.0.15
Containers:
emptydir-redis:
Container ID: docker://7e568233b435ad661971463cf7bb4acb87de7df99fc53b451732e81ec8cb0e7c
Image: redis:latest
Image ID: docker-pullable://redis@sha256:a0494b60a0bc6de161d26dc2d2f9d2f1c5435e86a9e5d48862a161131affa6bd
Port: 6379/TCP
Host Port: 0/TCP
State: Running
Started: Thu, 29 Oct 2020 15:44:52 +0800
Ready: True
Restart Count: 0
Environment: <none>
Mounts: #挂载信息,将emptydir-redis挂载到/data目录,且是rw读写状态
/data from emptydir-redis (rw)
/var/run/secrets/kubernetes.io/serviceaccount from default-token-dfmvc (ro)
Conditions:
Type Status
Initialized True
Ready True
ContainersReady True
PodScheduled True
Volumes: #定义了一个EmptyDir类型的存储,大小为1Gi
emptydir-redis:
Type: EmptyDir (a temporary directory that shares a pod's lifetime)
Medium:
SizeLimit: 1Gi
default-token-dfmvc:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-dfmvc
Optional: false
向redis中写入数据
[root@k8s-master emptydir]# kubectl get pod emptydir-redis -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
emptydir-redis 1/1 Running 0 11m 10.244.0.15 k8s-master <none> <none>
#安装客户端redis-cli
[root@k8s-master emptydir]# yum install redis -y
[root@k8s-master emptydir]# redis-cli -h 10.244.0.15 -p 6379
10.244.0.15:6379>
#向redis中写入两个key
10.244.0.15:6379> set volume emptydir
OK
10.244.0.15:6379> set username happylu
OK
10.244.0.15:6379> get volume
"emptydir"
10.244.0.15:6379> get username
"happylu"
登陆到pod中,可以直接kill redis-server进程,进程一般为1,进程被kill后kubelet会自动将进程重启
[root@k8s-master emptydir]# kubectl exec -it emptydir-redis -- /bin/sh
# kill 1
# command terminated with exit code 137
pod异常重启后,再次登录redis并查看redis中的数据内容,发现数据没有丢失。
[root@k8s-master ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
emptydir-redis 0/1 Completed 1 27m
[root@k8s-master ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
emptydir-redis 1/1 Running 2 30m
#pod重启后,再次登录redis并查看redis中的数据内容,发现数据没有丢失。
10.244.0.15:6379> keys *
1) "volume"
2) "username"
emptyDir实际是宿主机上创建的一个目录,将目录以bind mount的形势挂载到容器中,跟随容器的生命周期。查看存储内容如下:
[root@k8s-master ~]# docker inspect d64ee8367b49
"Mounts": [
{
"Type": "bind",
"Source": "/var/lib/kubelet/pods/28800e94-3474-4a0e-9e35-d5585b60a133/volumes/kubernetes.io~empty-dir/emptydir-redis",
"Destination": "/data",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
},
查看目录的信息:
[root@k8s-master ~]# cd /var/lib/kubelet/pods/28800e94-3474-4a0e-9e35-d5585b60a133/volumes/kubernetes.io~empty-dir/emptydir-redis
[root@k8s-master emptydir-redis]# ls
dump.rdb
Pod删除后,volume的信息也随之删除
[root@k8s-master emptydir]# kubectl delete -f emptydir-redis.yaml
pod "emptydir-redis" deleted
[root@k8s-master emptydir]# cd /var/lib/kubelet/pods/28800e94-3474-4a0e-9e35-d5585b60a133/volumes/kubernetes.io~empty-dir/emptydir-redis
-bash: cd: /var/lib/kubelet/pods/28800e94-3474-4a0e-9e35-d5585b60a133/volumes/kubernetes.io~empty-dir/emptydir-redis: No such file or directory
总结
emptyDir是host上定义的一块临时存储,通过bind mount的形式挂载到容器中使用,容器重启数据会保留,容器删除则volume会随之删除。