概述
https://zhangchenchen.github.io/2017/11/17/kubernetes-integrate-with-ceph/
Kubernetes中的存储方案
对于有状态服务,存储是一个至关重要的问题。k8s提供了非常丰富的组件来支持存储,这里大致列一下:
- volume: 就是直接挂载在pod上的组件,k8s中所有的其他存储组件都是通过volume来跟pod直接联系的。volume有个type属性,type决定了挂载的存储是什么,常见的比如:emptyDir,hostPath,nfs,rbd,以及下文要说的persistentVolumeClaim等。跟docker里面的volume概念不同的是,docker里的volume的生命周期是跟docker紧紧绑在一起的。这里根据type的不同,生命周期也不同,比如emptyDir类型的就是跟docker一样,pod挂掉,对应的volume也就消失了,而其他类型的都是永久存储。详细介绍可以参考Volumes
- Persistent Volumes:顾名思义,这个组件就是用来支持永久存储的,Persistent Volumes组件会抽象后端存储的提供者(也就是上文中volume中的type)和消费者(即具体哪个pod使用)。该组件提供了PersistentVolume和PersistentVolumeClaim两个概念来抽象上述两者。一个PersistentVolume(简称PV)就是后端存储提供的一块存储空间,具体到ceph rbd中就是一个image,一个PersistentVolumeClaim(简称PVC)可以看做是用户对PV的请求,PVC会跟某个PV绑定,然后某个具体pod会在volume 中挂载PVC,就挂载了对应的PV。关于更多详细信息比如PV,PVC的生命周期,dockerfile 格式等信息参考Persistent Volumes
- Dynamic Volume Provisioning: 动态volume发现,比如上面的Persistent Volumes,我们必须先要创建一个存储块,比如一个ceph中的image,然后将该image绑定PV,才能使用。这种静态的绑定模式太僵硬,每次申请存储都要向存储提供者索要一份存储快。Dynamic Volume Provisioning就是解决这个问题的。它引入了StorageClass这个概念,StorageClass抽象了存储提供者,只需在PVC中指定StorageClass,然后说明要多大的存储就可以了,存储提供者会根据需求动态创建所需存储快。甚至于,我们可以指定一个默认StorageClass,这样,只需创建PVC就可以了。
Kubernetes与Ceph整合
首先要有一个k8s集群,一个ceph集群,k8s集群的搭建与ceph集群的搭建不再赘述。
预备工作
在每个k8s node中安装ceph-common
1
|
yum install -y ceph-common
|
将ceph配置文件ceph.conf,ceph admin的认证文件ceph.client.admin.keyring复制到k8s node的/etc/ceph/ 目录下。
配置ceph secret
1
|
grep key /etc/ceph/ceph.client.admin.keyring |awk '{printf "%s", $NF}'|base64
|
获取base64加密的key:QVFCZmdTcFRBQUFBQUJBQWNXTmtsMEFtK1ZkTXVYU21nQ0FmMFE9PQ==
利用该key创建ceph-secret,这里我们单独创建了一个test-ceph namespace,所有操作都在该namespace下。
ceph-secret.yaml
1
2
3
4
5
6
7
8
|
apiVersion: v1
kind: Secret
metadata:
name: ceph-secret
namespace: test-ceph
type: "kubernetes.io/rbd"
data:
key: QVFCZmdTcFRBQUFBQUJBQWNXTmtsMEFtK1ZkTXVYU21nQ0FmMFE9PQ==
|
Persistent Volumes测试
首先在ceph 中创建一个2G 的image,这里为了方便直接在rbd pool中创建。
查看新建image信息:
1
2
3
4
5
6
7
8
|
# rbd info test-image
rbd image 'test-image':
size 2048 MB in 512 objects
order 22 (4096 kB objects)
block_name_prefix: rbd_data.5ed8238e1f29
format: 2
features: layering
flags:
|
注:这里有个ceph的坑,在jewel版本下默认format是2,开启了rbd的一些属性,而这些属性有的内核版本是不支持的,会导致map不到device的情况,可以在创建时指定feature(我们就是这样做的),也可以在ceph配置文件中关闭这些新属性:rbd_default_features = 2。参考rbd无法map(rbd feature disable)。
创建PV,需要指定ceph mon节点地址,以及对应的pool,image等:
test.pv.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
apiVersion: v1
kind: PersistentVolume
metadata:
name: test-pv
namespace: test-ceph
spec:
capacity:
storage: 2Gi
accessModes:
- ReadWriteOnce
rbd:
monitors:
- 172.16.21.250:6789
- 172.16.21.251:6789
- 172.16.21.252:6789
pool: rbd
image: test-image
user: admin
secretRef:
name: ceph-secret
persistentVolumeReclaimPolicy: Recycle
|
创建PVC:
test.pvc.yml
1
2
3
4
5
6
7
8
9
10
11
|
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: test-pvc
namespace: test-ceph
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
|
查看PV,PVC,如果状态是bound那么两者绑定成功。
创建一个pod验证:
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
|
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: nginx-dm
namespace: test-ceph
spec:
replicas: 1
template:
metadata:
labels:
name: nginx
spec:
containers:
- name: nginx
image: nginx:alpine
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
volumeMounts:
- name: ceph-rbd-volume
mountPath: "/usr/share/nginx/html"
volumes:
- name: ceph-rbd-volume
persistentVolumeClaim:
claimName: test-pvc
|
进入该容器,利用dh -h 命令验证是否挂载成功。
Dynamic Volume Provisioning测试
创建一个StorageClass:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ceph-storage
namespace: test-ceph
provisioner: ceph.com/rbd
parameters:
monitors: 172.16.21.250:6789,172.16.21.251:6789,172.16.21.252:6789
adminId: admin
adminSecretName: ceph-secret
adminSecretNamespace: test-ceph
pool: rbd
userId: admin
userSecretName: ceph-secret
|
创建一个PVC,指定StorageClass:
1
2
3
4
5
6
7
8
9
10
11
12
|
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: ceph-claim-dynamic
namespace: test-ceph
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
storageClassName: ceph-storage
|
注:这里又趟到了一个大坑,如果这样就直接创建pod挂载的话会报错如下:
1
|
Error creating rbd image: executable file not found in $PATH
|
这是因为我们的k8s集群是使用kubeadm创建的,k8s的几个服务也是跑在集群静态pod中,而kube-controller-manager组件会调用rbd的api,但是因为它的pod中没有安装rbd,所以会报错,如果是直接安装在物理机中,因为我们已经安装了ceph-common,所以不会出现这个问题。我们在该issue下找到了解决方案。如下:
创建一个 rbd-provisioner :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: rbd-provisioner
namespace: kube-system
spec:
replicas: 1
template:
metadata:
labels:
app: rbd-provisioner
spec:
containers:
- name: rbd-provisioner
image: "quay.io/external_storage/rbd-provisioner:v0.1.0"
serviceAccountName: persistent-volume-binder
|
这样就可以直接用PVC了,创建 pod测试下:
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
|
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: nginx-test-dynamic
namespace: test-ceph
spec:
replicas: 1
template:
metadata:
labels:
name: nginx
spec:
containers:
- name: nginx
image: nginx:alpine
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
volumeMounts:
- name: ceph-rbd-volume
mountPath: "/usr/share/nginx/html"
volumes:
- name: ceph-rbd-dynamic-volume
persistentVolumeClaim:
claimName: ceph-claim-dynamic
|
实战:创建一个mysql-galera集群
在进入具体实战之前,先介绍一下k8s针对有状态服务推出的一个组件,statefulset(1.5之前叫做petset),statefulset与deployment,replicasets是一个级别的。不过Deployments和ReplicaSets是为无状态服务而设计。statefulset则是为了解决有状态服务的问题。它的应用场景如下:
- 稳定的持久化存储,即Pod重新调度后还是能访问到相同的持久化数据,基于PVC来实现
- 稳定的网络标志,即Pod重新调度后其PodName和HostName不变,基于Headless Service(即没有Cluster IP的Service)来实现。
- 有序部署,有序扩展,即Pod是有顺序的,在部署或者扩展的时候要依据定义的顺序依次依次进行(即从0到N-1,在下一个Pod运行之前所有之前的Pod必须都是Running和Ready状态),基于init containers来实现。
- 有序收缩,有序删除(即从N-1到0)。
由应用场景可知,statefuleset特别适合mqsql,redis等数据库集群。相应的,一个statefuleset有以下三个部分:
- 用于定义网络标志(DNS domain)的Headless Service
- 用于创建PersistentVolumes的volumeClaimTemplates
- 定义具体应用的StatefulSet
以下是从github上找到的一个示例dockerfile:
首先创建headless service:
galera-service.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
apiVersion: v1
kind: Service
metadata:
annotations:
service.alpha.kubernetes.io/tolerate-unready-endpoints: "true"
name: galera
namespace: galera
labels:
app: mysql
spec:
ports:
- port: 3306
name: mysql
# *.galear.default.svc.cluster.local
clusterIP: None
selector:
app: mysql
|
创建StatefulSet,这里存储直接用的storageclass指定ceph rbd,镜像下载需要科学上网。
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
|
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
name: mysql
namespace: galera
spec:
serviceName: "galera"
replicas: 3
template:
metadata:
labels:
app: mysql
spec:
initContainers:
- name: install
image: gcr.io/google_containers/galera-install:0.1
imagePullPolicy: Always
args:
- "--work-dir=/work-dir"
volumeMounts:
- name: workdir
mountPath: "/work-dir"
- name: config
mountPath: "/etc/mysql"
- name: bootstrap
image: debian:jessie
command:
- "/work-dir/peer-finder"
args:
- -on-start="/work-dir/on-start.sh"
- "-service=galera"
env:
- name: POD_NAMESPACE
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
volumeMounts:
- name: workdir
mountPath: "/work-dir"
- name: config
mountPath: "/etc/mysql"
containers:
- name: mysql
image: gcr.io/google_containers/mysql-galera:e2e
ports:
- containerPort: 3306
name: mysql
- containerPort: 4444
name: sst
- containerPort: 4567
name: replication
- containerPort: 4568
name: ist
args:
- --defaults-file=/etc/mysql/my-galera.cnf
- --user=root
readinessProbe:
# TODO: If docker exec is buggy just use gcr.io/google_containers/mysql-healthz:1.0
exec:
command:
- sh
- -c
- "mysql -u root -e 'show databases;'"
initialDelaySeconds: 15
timeoutSeconds: 5
successThreshold: 2
volumeMounts:
- name: datadir
mountPath: /var/lib/
- name: config
mountPath: /etc/mysql
volumes:
- name: config
emptyDir: {}
- name: workdir
emptyDir: {}
volumeClaimTemplates:
- metadata:
name: datadir
annotations:
volume.beta.kubernetes.io/storage-class: "ceph-web"
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi
|
以上涉及到三个镜像,前两个镜像在initContainers 下,表示这两个镜像创建的是Init Container,顾名思义,就是完成一些初始化的工作。这些 Init Container 按照定义的顺序依次执行,只有所有的Init Container 执行完后,主容器才启动。由于一个Pod里的存储卷是共享的,所以 Init Container 里产生的数据可以被主容器使用到。
这里两个 initContainer 主要完成以下两个工作,安装 mysql-galera 等组件,生成配置文件等。
最终结果如下:
1
2
3
4
5
|
# kubectl get pods -n galera
NAME READY STATUS RESTARTS AGE
mysql-0 1/1 Running 0 47m
mysql-1 1/1 Running 0 24m
mysql-2 1/1 Running 0 2m
|
警告
本文最后更新于 2017年2月1日,文中内容可能已过时,请谨慎参考。