kubernetes 持久化存储

操作系统:ubuntu 16.04
kubernetes版本:1.10.0
ceph:10.2.10

kubernetes常见的三种应用
1、无状态应用
容器内的应用状态无需保持,
kubernetes通过replicaset保证pod的数量,一旦pod挂掉或崩溃,会基于image重建pod,此时pod内数据丢失。
2、有状态应用
和无状态应用相比,有状态应用多了一个应用状态保存的需求.
3、有状态集群应用
和有状态应用相比,多了集群管理的需求,那么需要解决的问题有两个,一个是应用状态的保存,一个是集群的管理。

有状态应用和有状态集群应用需要持久化存储里面的数据,比如数据库我们需要存储里面的数据库文件,直接用docker,我们可以使用docker的bind-mont、docker-managed-volume 、volume-container的方式,在kubernetes中解决方案是kubernetes volume和kubernetes persistent volume 。volume独立于pod,pod被销毁了数据没有了,但volume还是存在的,可以给其他pod使用。本质上,volume就是一个目录根目录的映射,它后端可以对应各种各样的存储,但pod在使用时,并不需要关心这些,对于pod来说,它看见的只是一个目录,这点根docker volume基本一样,当一个volume mount到pod中时,这个pod中所有容器都可以访问这个volume,kuberne volume支持多种类型的后端存如awsElasticBlockStore、azureDisk、ceph-rbd

完整参考列表
https://kubernetes.io/docs/concepts/storage/volumes/#types-of-volumes

kubernetes中volume分为两种类型静态供给的和动态供给的
静态供给volume:emptyDir、hostpath,Persistent volume
动态供给volume:storage-class

静态供给volume

缺点:
pod直接访问volume可移值性和可扩展性、安全性较差

emptyDir
emptydir:临时空目录,与pod紧密联接在创建pod是创建,在删除这个pod时,也会自动删除,pod迁移到其他节点数据会丢失。
用途:用于存储一些临时数据,如cookie等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
例:
apiVersion: v1
kind: Pod
metadata:
name: producer-consumer
spec:
containers:
- image: busybox
name: producer
volumeMounts:
- mountPath: /producer_dir
name: shared-volume
args:
- /bin/sh
- -c
- echo "hello world" >/producer_dir/hello; sleep 30000
volumes:
- name: shared-volume
emptyDir: {}

创建一个pod里面有两个container,生产者、消费者共享一个emptyDir,生产者向/producer_dir/hello写入hello world

通过docker inspect查看映射到宿主机哪个目录

1
2
docker inspect 5681556dfd95|grep Source
"Source": "/var/lib/kubelet/pods/a22e4253-8290-11e8-85ec-00163e04ede2/volumes/kubernetes.io~empty-dir/shared-volume",

hostpath:bind-mount与宿主机目录1:1映射,这类存储卷,当数据迁移到其他节点后,就会造成数据丢失。
用途:根DaemonSet配合使用如EFK,中fluentd 根容器日志目录映射,来达到收集日志效果。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: mysql
spec:
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- image: mysql:5.6
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
value: password
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: vol1
mountPath: /var/lib/mysql
volumes:
- hostPath:
path: /tmp/mysql
name: vol1

将pod内的/var/lib/mysql与宿主机的/tmp/mysql映射。

storage-private

不使用host存储空间,使用公有云厂商对象存储或分布式

persisten volume

volume虽然能提供很好的数据持久化,但在可管理性上,还是不足的,要使用volume,用户必须,知道当前的volume信息和提前创建好对应的volume,kubernetes推荐使用pv、pvc来解决存储持久化的问题。因此kubernetes给出的解决方案是pv(persistent volume)、pvc(persistent volume Claim)
persisten volume(pv)是k8s里面的一个资源对象,它是直接和底层存储关联的,pv具有持久性,生命周期独立于pod。
persisten volume claim(pvc)是对pv的具体实现,pod使用存储是直接使用pvc,然后pvc会根据pod需要的存储空间大小和访问模式在去寻找合适的pv然后绑定。

kubernetes支持的pv类型
https://kubernetes.io/docs/concepts/storage/persistent-volumes/#types-of-persistent-volumes
例子
这里以nfs做为pv后端存储为例讲解pv和pvc的使用
搭建nfs

1
apt install nfs-kernel-server nfs-common rpcbind

配置共享目录
修改/etc/exports文件

1
/nfsdata *(rw,sync,no_root_squash)

1
2
3
4
*:表示所有用户都可以访问
rw:挂接此目录的客户端对该共享目录具有读写权限
sync:资料同步写入内存和硬盘
no_root_squash:root用户具有对根目录的完全管理访问权限

启动rpcbind

1
systemctl status rpcbind

启动nfs

1
systemctl start nfs-kernel-server

验证

1
showmount -e nfs_server_ip

挂载

1
mount -t nfs 172.31.164.57:/nfsdata /mnt/

配置pv

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: PersistentVolume
metadata:
name: mysql-pv
spec:
accessModes:
- ReadWriteOnce #可读写模式支持单节点挂载
capacity:
storage: 1Gi
persistentVolumeReclaimPolicy: Retain
storageClassName: nfs
nfs:
path: /nfsdata/mysql-pv
server: 172.31.164.57

pv的回收模式

  • persistentVolumeReclaimPolicy 为当pvc删除后pv的回收策略。
  • Retain – pvc删除后pv和数据仍然保留但此时不可以在创建pvc了,需要管理员手工回收。
  • Recycle – pvc删除后回自动起一个pod将pv内的数据全部清空,可以创建新的pvc。
  • Delete – 删除 Storage Provider 上的对应存储资源,例如 AWS EBS、GCE PD、Azure Disk、OpenStack Cinder Volume 等。

pv的访问模式
在pvc绑定pv时通常根据两个条件绑定,一个是存储的大小,另外一个是存储的模式

  • ReadWriteOnce(RWO):可读写模式支持单节点挂载。
  • ReadOnlyMany(ROX): 只读模式支持多节点挂载。
  • ReadWriteMany(RWX):可读写模式支持多节点挂载,目前只有少数存储支持这种方式,像ceph-rbd目前只能当个节点挂载。

创建pvc

1
2
3
4
5
6
7
8
9
10
11
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: mysql-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: nfs

pvc只需要指定大小,访问模式和className就会根pv关联。

创建mysql使用pvc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: mysql
spec:
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- image: mysql:5.6
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
value: password
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: mysql-persistent-storage
mountPath: /var/lib/mysql
volumes:
- name: mysql-persistent-storage
persistentVolumeClaim:
claimName: mysql-pvc


进入container创建些数据

将pod所在宿主机关闭了,过几分钟kubernetes会在另外宿主机上启动,并且还是使用这个pvc,因为nfs支持多个host同时挂载.

动态供给volume(storage_class)

storage-class:
直接使用pv方式都是静态供给,需要管理员提前将pv创建好,然后再与pvc绑定,在kubernetes中动态卷是通过storage-class去实现的。配好storage-class与backend对接,当没有满足pvc条件的pv时,storage-class会动态的去创建一个pv
动态卷的优势

1、不需要提前创建好pv,提高效率和资源利用率

2、封装不同的存储类型给pvc使用,在StorageClass出现以前,PVC绑定一个PV只能根据两个条件,一个是存储的大小,另一个是访问模式。在StorageClass出现后,等于增加了一个绑定维度。


在PVC里除了常规的大小、访问模式的要求外,还通过annotation指定了Storage Class的名字为fast,这样这个PVC就会绑定一个SSD,而不会绑定一个普通的磁盘。

例这里我们以ceph-rbd为例配置一个storage-class
因为
操作系统加载rbd module
modprobe rbd
记得加入开机启动脚本,不然重启module又没加载
创建一个ceph-rbd的storage-class
获取admin的secret并用base64加密

1
ceph auth get-key client.admin |base64

创建个secret对象

1
2
3
4
5
6
7
apiVersion: v1
kind: Secret
metadata:
name: ceph-secret-admin
type: "kubernetes.io/rbd"
data:
key: QVFEMDJ1VmE0L1pxSEJBQUtTUnFwS3JFVjErRjFNM1kwQ2lyWmc9PQ==

创建storage-class

1
2
3
4
5
6
7
8
9
10
11
12
13
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: rbd
provisioner: kubernetes.io/rbd
parameters:
monitors: 172.31.164.57:6789
adminId: admin
adminSecretName: ceph-secret-admin
adminSecretNamespace: default
pool: rbd
userId: admin
userSecretName: ceph-secret-admin

monitors: ceph-monitor地址+端口
adminId: secret的用户名
adminSecretName: 上面创建secret的名字
pool:rbd 在ceph哪个pool
userSecretName:上面创建secret的名字

1
2
3
4
5
6
7
8
9
10
11
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: mysql-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: rbd

storageClassName指向我们刚刚创建storage-class
可以看见已经Bound了

查看ceph的pool,发现块设备已经创建好了

继续使用上面的yml创建mysql,会直接用这个pvc

到rke-node3上去
可以看见kernel rbd module将pool里面的块映射出来了,挂载到pod里面了

如果非hyperkube部署的kubernetes集群,如kubeadm部署的,在创建pvc时会报错如下

1
persistentvolume-controller Warning ProvisioningFailed Failed to provision volume with StorageClass "rbd": failed to create rbd image: executable file not found in $PATH, command output:

因为我们的k8s集群是使用kubeadm创建的,k8s的几个服务也是跑在集群静态pod中,而kube-controller-manager组件会调用rbd的api,但是因为它的pod中没有安装rbd,所以会报错。解决办法如下
使用kubernetes上的存储扩展卷来解决
https://github.com/kubernetes-incubator/external-storage

1
git clone https://github.com/kubernetes-incubator/external-storage

执行

1
2
3
4
kubectl apply -f ./
kubectl get pods
NAME READY STATUS RESTARTS AGE
rbd-provisioner-23232323-i6223 1/1 Running 0 13s

然后在重新创建storage-class

1
cat ceph-storageclass.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: rbd
#provisionen: kubernetes.io/rbd
provisioner: ceph.com/rbd
parameters:
monitors: 172.31.164.57:6789
adminId: admin
adminSecretName: ceph-secret-admin
adminSecretNamespace: default
pool: rbd
userId: admin
userSecretName: ceph-secret-admin

注意provisioner由provisionen: kubernetes.io/rbd改成provisioner: ceph.com/rbd
然后在创建pvc就没有问题了。

使用rbd做为pv的backend或storagclass的backend整体原理如下:
创建pvc—-对应在ceph pool内创建一个rbd对象。
pod挂载这个pvc—-将对应的rbd块通过内核rbd模块map到宿主机上。然后在mount到宿主机 /var/lib/kubelet/pods/xxxx目录,在将这个目录根pod对bind mount。
删除pod—-在对应的host上umount目录然后将rbd块与宿主机unmap。
在kubernetes1.11之前的版本有个问题就是删除pod后,不会自动将rbd块从host上unmap。
PR如下 1.11版本已经修复这个问题
https://github.com/kubernetes/kubernetes/pull/63579

就是在使用pod如果挂载后端为rbd的pvc后,在底层实际上是会从ceph的pool里面将一个rbd块设备map的host上然后在mount到pod内,
需要注意的是:

  • 因为在k8s里面是直接用的是内核的rbd块,openstack用的是librbd接口,所以这里需要在每个host上内核要加载rbd模块,不然挂载不上去的,modprobe rbd。
  • ceph-rbd 在AccessMode为ReadWriteOnce情况下只支持单节点挂载,不能被多个pod同时使用这个pvc。也就是说一个host挂了,如果上面pod使用了ceph-rbd,哪里在另外一个host上重建这个pod时会报这个pvc已经被使用了,但在AccessMode为ReadWriteOnce+ReadOnlyMany的情况下支持一个pvc被多个pod挂载。

local-pv

Local-pv是让用户使用标准的k8s pv和pvc的接口使用node节点的本地存储,kubernetes1.7为alpha版本,1.14正式GA,这和我们上面说的host-path和empty-Volume有个共同点都是使用node节点的本地存储,但区别在于hostpath是单节点的本地存储,也就是说只有当pod调度到哪台节点便使用那台节点对应的存储,无法提供node亲和性和POD调度支持,但通过localpv可以在pv的定义中配置节点亲和性,这样对应的使用这个pvc的POD也会根据亲和性配置调度到对应的节点上,比如在local-pv的定义配置了主机亲和性为node-2,那么使用这个pvc的pod也会调度到node-2上。

使用场景:
1、分布式数据库和分布式文件系统(redis、ElasticSearch、memcache、ceph),这类应用在应用本身能实现高可用,只是依靠localpv实现高性能存储。
2、需要临时缓存存储的应用,当应用删除后对应的数据也能随之清理,因为使用localpv可以灵活设置pv的回收策略。

测试版本:kubernetesv1.14.3
部署storageclass

1
2
3
4
5
6
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer

volumeBindingMode: WaitForFirstConsumer :配置延迟绑定,也就是pv control并不会立刻将pvc与pv关联,而是等待pod调度后才去bond,还有种默认的Immediate模式则是当pvc创建时立刻绑定。

创建pv

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: v1
kind: PersistentVolume
metadata:
name: example-pv
spec:
capacity:
storage: 100Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: delete
storageClassName: local-storage
local:
path: /mnt/
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- rke-node2

注意点 : volumeMode: Filesystem为文件系统模式,也就是对应的目录,这里也可以直接设置为BlockVolume模式直接对应主机上的块设备,当需要在kube-apiserver、kube-controlmanager、kubelet开启–feature-gates=BlockVolume=true参数,但这个参数在1.13版本已经默认是true了,参考:https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/

local path设置的就是对应宿主机的供挂载的目录,nodeselectorTerms设置的节点的亲和性。
创建pvc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
labels:
cattle.io/creator: norman
name: example-local-claim
namespace: default
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
storageClassName: local-storage
volumeMode: Filesystem
volumeName: example-pv

创建POD使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kind: Pod
apiVersion: v1
metadata:
name: example-pv-pod
spec:
volumes:
- name: example-pv-storage
persistentVolumeClaim:
claimName: example-local-claim
containers:
- name: example-pv-container
image: nginx
ports:
- containerPort: 80
name: "http-server"
volumeMounts:
- mountPath: "/usr/share/nginx/html"
name: example-pv-storage

局限性
1、目前local-pv不支持对空间申请管理,需要手动对空间进行配置和管理.
2、默认local pv的StorageClass的provisioner是kubernetes.io/no-provisioner , 这是因为local pv不支持Dynamic Provisioning, 所以它没有办法在创建出pvc的时候, 自动创建对应pv.

static-provisioner配置

可以自己实现一个static-provisioner来创建和管理PV,可以直接使用社区已经写的static-provisioner:https://github.com/kubernetes-incubator/external-storage/tree/master/local-volume#option-1-using-the-local-volume-static-provisioner
当然也可以自己实现,这里使用Rancher实现的static-provisioner
https://github.com/rancher/local-path-provisioner
部署

1
kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml

默认pv的回收模式为delete模式,需要修改为recycle模式直接修改yaml即可。
执行后会部署

1
2
3
4
5
6
namespace/local-path-storage created
serviceaccount/local-path-provisioner-service-account created
clusterrole.rbac.authorization.k8s.io/local-path-provisioner-role created
clusterrolebinding.rbac.authorization.k8s.io/local-path-provisioner-bind created
deployment.apps/local-path-provisioner created
storageclass.storage.k8s.io/local-path created

查看POD

1
2
3
kubectl get pod -n local-path-storage
NAME READY STATUS RESTARTS AGE
local-path-provisioner-848fdcff-tqwc2 1/1 Running 0 35s

查看storage-class

1
2
3
kubectl get storageclass
NAME PROVISIONER AGE
local-path rancher.io/local-path 4m17s

创建PVC

1
kubectl create -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/examples/pvc.yaml

在没创建pod使用此PVC前,默认模式为 WaitForFirstConsumer,所以pvc的状态会是pending状态。

创建POD

1
kubectl create -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/examples/pod.yaml

默认目录为/opt/local-path-provisioner目录下,可以在POD所在节点上查看
当我们需要进行细粒化配置时,如在node1节点上使用/data目录,在node2节点使用/data2目录,其他节点使用/data3目录时可以修改 local-path-storage命名空间内的local-path-config这个configmap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kind: ConfigMap
apiVersion: v1
metadata:
name: local-path-config
namespace: local-path-storage
data:
config.json: |-
{
"nodePathMap":[
{
"node":"DEFAULT_PATH_FOR_NON_LISTED_NODES",
"paths":["/opt/local-path-provisioner"]
},
{
"node":"rke-node2",
"paths":["/data2"]
}
]
}

更新成功后,查看

1
kubectl logs pod/local-path-provisioner-848fdcff-lr2pj -n local-path-storage

可以看见其加载配置的信息。

参考链接
https://www.kubernetes.org.cn/3462.html
https://blog.csdn.net/liukuan73/article/details/60089305