kubernetes挂载cephfs带来的mds卡顿问题及引入cephfs storageClass

kubernetes挂载cephfs带来的mds卡顿问题及引入cephfs storageClass

前言

前面的文章中,有写过如何在kubernets中组合pv/pvc,使用cephfs进行数据的持久化存储:

cephfs 在kubernetes中的使用

但是在经过一段时间的使用后,发现此方式意外地会造成ceph mds严重的性能问题,本文将介绍如何使用cephfs storageClass避免这个问题。

回顾

回顾此前的pv/pvc结合使用的模式,这里直接贴yaml文件:

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
# cat cephfs-pv.yaml 
apiVersion: v1
kind: PersistentVolume
metadata:
name: cephfs-pv
labels:
pv: cephfs-pv
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
cephfs:
monitors:
- 192.168.20.112:6789
- 192.168.20.113:6789
- 192.168.20.114:6789
# 注意这个路径,这个路径必须提前创建好,否则pv会绑定失败
path: /app
user: admin
secretRef:
name: ceph-secret
readOnly: false
persistentVolumeReclaimPolicy: Delete
---
# cat cephfs-pvc.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: cephfs-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
selector:
matchLabels:
pv: cephfs-pv

注意看上方的注释,pv中指定的挂载路径,若不指定,将存放在cephfs的”/“根路径,当多个服务都使用cephfs时,很可能出现目录重名的问题,因此统一放在同级目录显然不行;但若手动指定不同的目录,则此路径必须是存在的,否则pv绑定会出错,所以使用挂载之前要去手动创建目录,这的确带来了不少麻烦。

为了实现创建pod时,自动创建子级的挂载目录,在此前时怎么办的呢?答案是使用subPath:

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
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
labels:
app: cephfs-test
name: cephfs-test
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: cephfs-test
template:
metadata:
spec:
containers:
- image: php-fpm
imagePullPolicy: Always
name: cephfs-test
volumeMounts:
- mountPath: /var/www/
name: home
subPath: cephfs-test/home/
volumes:
- name: home
persistentVolumeClaim:
claimName: cephfs-pvc

指定subPath后,容器在运行之前会自动在挂载的路径上创建声明中所指定的subPath路径,如此,就解决了上面所说的问题。

部署运行起来后,查看此时的挂载:

容器层面:

1
2
3
4
5
oot@cephfs-test-765bb76d75-gsm79:/var/www# mount | grep "/var/www"
192.168.20.112:6789,192.168.20.113:6789,192.168.20.114:6789:/app on /var/www type ceph (rw,relatime,name=admin,secret=<hidden>,acl)
root@cephfs-test-765bb76d75-gsm79:/var/www#
root@cephfs-test-765bb76d75-gsm79:/var/www# ls
check_health.php

cephfs视角:

1
2
3
4
5
# cephfs里的路径和文件
[root@host020112 home]# pwd
/mnt/cephfs/app/cephfs-test/home
[root@host020112 home]# ls
check_health.php

宿主机层面:

1
2
[root@host020107 ~]# mount | grep cephfs
192.168.20.112:6789,192.168.20.113:6789,192.168.20.114:6789:/app on /var/lib/kubelet/pods/8126f599-2f6e-11ea-af20-3440b5a2bb9c/volumes/kubernetes.io~cephfs/cephfs-test type ceph (rw,relatime,name=admin,secret=<hidden>,acl)

可以看到,确实在cephfs中创建了子目录,容器中所挂载路径下的文件,也如预期的存放在子目录下,实现了隔离。看似很正常?但注意看宿主机层面的挂载详情,整个cephfs的/app目录全部都挂载到了宿主机上,这就是隐患所在,随之而来的,就是文首所说的cephfs mds严重的性能问题

问题/解决思路

在ceph服务端,来看看cephfs mds的会话信息,找到测试例子中容器所在宿主机与cephfs mds建立的会话详情,通过ip过滤一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@yskp020113 ~]# ceph daemon mds.host020113 session ls | grep -10 192.168.20.107
{
"id": 3355940,
"num_leases": 0,
"num_caps": 5,
"state": "open",
"replay_requests": 0,
"completed_requests": 0,
"reconnecting": false,
"inst": "client.3355940 192.168.20.107:0\/3585027522",
"client_metadata": {
"entity_id": "admin",
"hostname": "host020107",
"kernel_version": "3.10.0-693.21.1.el7.x86_64"
}
},

注意看这里的一个选项:num_caps,目前数量只有5,非常小,但若以此方式运行一段时间后,数值会飙升到恐怖的百万级别,之前忘记截图保存了,比较遗憾。此数值较大后,会引起cephfs mds的性能问题。

先简单描述一下num_caps这个是做什么的:

ceph mds元数据服务器,为了高速响应客户端对文件系统的读写操作,会将客户端操作过的文件描述符(句柄),缓存在内存中,每一个cap,对应一个客户端的文件句柄缓存,num_caps即代表总数。当所有会话的总num_caps很大时(据个人观察,超过200W时),mds响应时间会比较久,客户端这时的文件读写操作会感受到明显的卡顿,当num_caps到达千万级别时,mds可能会出现崩溃现象,此时备mds监听到主mds异常会,会开始主备切换,但mds主备切换过程涉及也异常的缓慢,观察mds切换日志会发现,备mds会在handle_mds_map state change up:rejoin这一步停留很久的时间才能进入active状态,原因也是因为这些为了恢复这庞大的客户端caps数据,这期间客户端读写操作完全阻塞,进程iowait,cpu load飞速飙升,影响巨大。总而言之,num_caps数值庞大会给mds带来巨大的压力,会造成挂载使用的客户端普遍读写卡顿现象很严重

关于num_caps造成mds问题的详细描述和分析解决过程,可参考此文章:

https://github.com/lidaohang/ceph_study/blob/master/Ceph%20MDS%E9%97%AE%E9%A2%98%E5%88%86%E6%9E%90.md

这种挂载方式为什么会造成num_caps飙升的现象呢?

如上面所描述,宿主机中的mount的是整个/app的根目录,而不是容器实际使用的/app/cephfs-test/home子目录,整个/app目录下有非常多的文件,猜测是客户端使用ceph-lib库,在某些情况下会对挂载目录做一些扫描操作,服务端mds则铁憨憨地将这些操作句柄全部缓存下来,假设/app下共100W文件,此时再有多台宿主机出现这种现象,所有session的总num_caps很轻松就上到了千万级别。由此,问题出现了。

解决思路

既然问题原因找到了,那就好办了,不挂载整个根目录,挂载相应的子目录就行了,但pv里面指定的目录都是要提前创建好的,难道每创建pv之前都要手动去cephfs目录下创建相应的子目录吗,这一点都不自动化。

不要着急,k8s团队的扩展存储项目孵化了有很多种对接不同类型的存储的provisioner插件,其中也包括cephfs。

##cephfs provisioner

项目地址

https://github.com/yinwenqin/external-storage/tree/master/ceph/cephfs

如果你还不了解storageClass/pv/pvc的关系,请参考这篇文章:

https://blog.upweto.top/gitbooks/useKubernetes/part5/%E5%88%86%E5%B8%83%E5%BC%8F%E5%AD%98%E5%82%A8Ceph-RBD%E4%BD%BF%E7%94%A8.html

代码结构

使用cephfs provisioner之前,先来看看它的代码结构。如果你不熟悉go语言,那么下面的这两个部分你可以直接跳过,直接使用仓库里的部署模板即可。

代码结构很简单,代码总共只有两个代码文件:

cephfs-provisioner.go,主要作用是使用client-go,与k8s apiserver的连接,监听相应资源的增删改查。

cephfs_provisioner.py,主要作用是连接storageClass中指定的cephfs服务器后端,创建实际的存储空间并提供给pv使用。

修改源代码

在部署cephfs provisioner之前,根据使用场景以及个人的偏好,对cephfs provisioner源码进行了微小地修改,然后重新编译打包成自制的镜像。这么修改目的是为了使pv/pvc/挂载路径 三者的名称一一对应,使得清晰一致,方便后续维护管理。

如果你也想像我这样修改,但又觉得配置环境改代码麻烦,可以直接pull我打包好的镜像:

1
docker pull ywq935/my-cephfs-provisioner:latest

修改前,指定storageClass为cephfs并创建pvc后,自动创建并绑定的pv名字是这样子的:

1
2
3
// ceph/cephfs/cephfs-provisioner.go:135
// "kubernetes-dynamic-pvc-%s", uuid.NewUUID()
kubernetes-dynamic-pvc-xxxxxxxxxxxx-xxxxxxxxxxxxxxx

cephfs端挂载的根路径是:

1
2
ceph/cephfs/cephfs-provisioner.go:305
pvcRoot = "/volumes/kubernetes"

这自动创建的一大串随机字符结尾的pv名称,以及预设的cephfs挂载路径看起来很不喜欢,那么就来改改它吧

仅仅需要修改如下几个小地方即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ceph/cephfs/cephfs-provisioner.go:303
// 注释的是原来的代码
// pvcRoot = "/volumes/kubernetes"
pvcRoot = "/app"
// deterministicNames = false
deterministicNames = true

// ceph/cephfs/cephfs-provisioner.go:127
// 加一行代码
if deterministicNames {
share = options.PVC.Name
user = fmt.Sprintf("k8s.%s.%s", options.PVC.Namespace, options.PVC.Name)
// 自定义修改pv名称
options.PVName = options.PVC.Name

修改后,创建的pv/pvc名称一致,完全对应,易于管理,挂载的cephfs根路径为自定义的/app路径。

编译打包

完成上面修改后,要编译并打包成新的镜像并推向自己的私有仓库,那么你需要修改一下makefile文件,将REGISTRY替换成自己的私有仓库地址:

1
2
# REGISTRY = quay.io/external_storage/
REGISTRY = your.personal.registry

然后在目录内执行(前提是你的GOPATH等go语言环境需要配置好):

1
make push

代码便会编译并推送打包成docker镜像到你的私人仓库,打包吃来完整的镜像路径是:

1
your.personal.registry/cephfs-provisioner:latest

部署

rbac授权的部署文件在这里已经全部包含了,把这几个yaml文件下载下来,kubectl apply -f ./ 一口气创建好就行,如果你修改了镜像,记得将deployment内的image替换成自己的:

https://github.com/yinwenqin/external-storage/tree/master/ceph/cephfs/deploy/rbac

使用cephfs storageClass

上面部署好cephfs provisioner后,需要创建一个sc来指向它:

1
2
3
4
5
6
7
8
9
10
11
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: cephfs
provisioner: ceph.com/cephfs
reclaimPolicy: Retain
parameters:
monitors: 192.168.20.112:6789,192.168.20.113:6789,192.168.20.114:6789
adminId: admin
adminSecretName: cephfs-secret
adminSecretNamespace: "default"

创建一个测试用的部署来使用它们:

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
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
labels:
app: cephfs-test
name: cephfs-test
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: cephfs-test
template:
metadata:
creationTimestamp: null
labels:
app: cephfs-test
spec:
containers:
- image: php-fpm
imagePullPolicy: Always
name: cephfs-test
volumeMounts:
- mountPath: /var/www/
name: home
subPath: home/
volumes:
- name: home
persistentVolumeClaim:
claimName: cephfs-test

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: cephfs-test
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 50Gi
storageClassName: cephfs

创建完成后,来看看现在的挂载详情:

宿主机层面:

1
2
[root@host020203 ~]# mount | grep cephfs-test
192.168.20.112:6789,192.168.20.113:6789,192.168.20.114:6789:/app/default/cephfs-test on /var/lib/kubelet/pods/c310cf7f-2f96-11ea-b3e0-e4434b415914/volumes/kubernetes.io~cephfs/cephfs-test type ceph (rw,relatime,name=k8s.default.cephfs-test,secret=<hidden>,acl,wsize=33554432)

容器层面:

1
2
3
4
root@cephfs-test-56455f75c4-5kcqd:/var/www# mount | grep "/var/www"
192.168.20.112:6789,192.168.20.113:6789,192.168.20.114:6789:/app/default/cephfs-test on /var/www type ceph (rw,relatime,name=k8s.default.cephfs-test,secret=<hidden>,acl,wsize=33554432)
root@cephfs-test-56455f75c4-5kcqd:/var/www# ls
check_health.php

cephfs视角:

1
2
3
4
[root@host020112 home]# ls
check_health.php
[root@host020112 home]# pwd
/mnt/cephfs/app/default/cephfs-test/home

这次可以看到,不再是将整个根路径挂载在宿主机上了。

查看session信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@yskp020113 ~]# ceph daemon mds.host020113 session ls | grep -10 cephfs-test
{
"id": 3365682,
"num_leases": 0,
"num_caps": 4,
"state": "open",
"replay_requests": 0,
"completed_requests": 0,
"reconnecting": false,
"inst": "client.3365682 192.168.20.203:0\/1842590235",
"client_metadata": {
"entity_id": "k8s.default.cephfs-test",
"hostname": "host020203",
"kernel_version": "4.14.108",
"root": "\/app\/default\/cephfs-test"
}
},

对比上面一开始的挂载方式,可以再次确认,client挂载的路径已经从整个根路径变为了细分子目录,并且带来了一个额外的好处是可以观察到每一个使用挂载的应用的详情,不再是向此前一样整个宿主机共用一个连接session。

结果

经过一段时间的运行观察,上面出现的cephfs mds client session 中的num_caps值没有再出现过很大的数值,读写卡顿的现象得到了很好的解决。

总结

其实最本质的需求,无非就是希望能够在pod绑定pv时,自动创建隔离的子目录。之前指定pv/pvc的方式,单纯地使用subPath的实现,就满足这个需求,此前是为了避免增加复杂度,尽量避免引入更多的插件,因此没有使用cephfs provisioner,但未曾想到误打误撞地因为cephfs mds的工作机制,引发了这个问题,而今引入cephfs provisioner,由它来自动创建、挂载子目录,就可以避免这个问题,这一顿折腾,也算是对cephfs了解也更为深入了不少。

赏一瓶快乐回宅水吧~
-------------本文结束感谢您的阅读-------------