干货 | eBay Kubernetes集群的存储实践

供稿 | TESS 高文俊&谢文利&沈涛

翻译&编辑 | 顾欣怡

本文3289字,预计阅读时间10分钟

更多干货请关注“eBay技术荟”公众号

导读

Kubernetes 作为eBay内部广泛使用的容器管理平台,承担着巨大的存储功能。本文将从 本地存储、网络存储、应用场景、磁盘监控、管理部署和后续工作 这几方面介绍eBay Kubernetes集群的存储实践。

如今,eBay已在内部广泛使用 Kubernetes 作为容器管理的平台,并自研了 AZ和联邦级别的控制平面 ,用以负责 50多个 集群 的创建、部署、监控、修复等工 作,并且规模在不断扩大。

我们的生产集群上,针对各种应用场景,大量使用了 本地存储网络存储 ,并通过原生的PV/PVC来使用。 其中本地存储分为 静态分区类型基于lvm的动态类型 ,支持ssd, hdd, nvme等介质。网络块存储使用ceph RBD和ISCSI,共享文件存储使用cephfs和nfs。

一、本地存储

01

静态分区

我们最早于 2016年 开始做 localvolume(本地卷) ,当时社区还没有本地的永久存储方案,为了支持内部的NoSQL应用使用 PV(Persistent Volume) ,开发了第一版的localvolume方案:

首先 ,在节点创建的时候,provision系统根据节点池flavor定义对数据盘做分区和格式化,并将盘的信息写入系统配置文件。

同时 ,我们在集群内部署了daemonset localvolume-provisioner,当节点加入集群后,provisioner会从配置文件中读取配置信息并生成相应的PV,其中包含相应的path,type等信息。 这样,每个PV对象也就对应着节点上的一个分区。

除此之外 ,我们改进了scheduler,将本地 PV/PVC的绑定(binding)延迟到scheduler里进行。 这也对应现在社区的volumeScheduling feature。

现在cgroup v1不能很好地支持buffer io的限流和隔离,对于一些io敏感的应用来说,需要尽可能防止这些“noisy neighbors”干扰。 同时对于disk io load很高的应用,应尽可能平均每块盘的负担,延长磁盘寿命。

因此,我们增加了PVC的反亲和性(anti affinity)调度特性 ,在满足节点调度的同时,会尽可能调度到符合反亲和性规则的盘上。

具体做法是 ,在PV中打上标签表明属于哪个节点的哪块盘,在PVC中指定反亲和性规则,如下图一所示。 scheduler里增加相应的预选功能,保证声明了同类型的反亲和性的PVC,不会绑定到处在同一块盘的PV上,并在最终调度成功后,完成选定PV/PVC的绑定。

图1(点击可查看大图)

02

LVM动态存储

对于上述静态存储的方案,PV大小是固定的,我们同时希望volume空间能够更灵活地按需申请,即动态分配存储。

类似地,我们在节点flavor里定义一个vg作为存储池,节点创建的时候,provision系统会根据flavor做好分区和vg的创建。同时集群内部署了 daemonset local-volume-dynamic-provisioner ,实现CSI的相应功能。

在CSI 0.4版本中,该daemonset由CSI的 四个 基本组件组成, 即:csi-attacher, csi-provisioner, csi-registrar以及csi-driver。其中csi-attacher, csi-provisioner和csi-registrar为社区的 sidecar controll er 。csi-driver是根据存储后端自己实现CSI接口, 目前支持xfs和ext4两种主流文件系统,也支持直接挂载裸盘供用户使用。

为了支持scheduler能够感知到集群的存储拓扑,我们在csi-registrar中把从csi-driver拿到的拓扑信息同步到Kubernetes节点中存储,供scheduler预选时提取,避免了在kubelet中改动代码。

如图2所示,pod创建后,scheduler将根据vg剩余空间选择节点、local-volume-dynamic-provisioner来申请相应大小的lvm logical volume,并创建对应的PV,挂载给pod使用。

图2 (点击可查看大图)

二、网络存储

01

块存储

对于网络块存储,我们使用ceph RBD和ISCSI作为存储后端,其中ISCSI为远端SSD,RBD为远端HDD,通过openstack的cinder组件统一管理。

网络存储卷的管理主要包括provision/deletion/attach/detach等,在provision/deletion的时候,相比于localvolume(本地卷)需要以daemonset的方式部署, 网络存储只需要一个中心化的provisioner

我们利用了社区的cinder provisioner方案(详情可见: https://github.com/kubernetes/cloud-provider-openstack ),并加以相应的定制,比如支持利用已有 快照卷(snapshot volume) 来创建PV,secret统一管理等。

Provisioner的基本思路是

watch PVC创建请求 

→ 调用cinder api创建相应类型和大小的卷,获得卷id

→ 调用cinder的initialize_connection api,获取后端存储卷的具体连接信息和认证信息,映射为对应类型的PV对象 

→ 往apiserver发请求创建PV 

→ PV controller负责完成PVC和PV的绑定。

Delete 为逆过程。

Attach 由volume plugin或csi来实现,直接建立每个节点到后端的连接,如RBD map, ISCSI会话连接,并在本地映射为块设备。 这个过程是分立到每个节点上的操作,无法在controller manager里实现中心化的attach/detach。因此放到kubelet或csi daemonset来做,而controller manager主要实现逻辑上的accessmode的检查和volume接口的伪操作,通过节点的状态与kubelet实现协同管理。

Detach 为逆过程。

在使用RBD的过程中,我们也遇到过一些问题:

1) RBD map hang:

RBD map进程hang,然而设备已经map到本地并显示为/dev/rbdX。经分析,发现是RBD client端的代码在执行完attach操作后,会进入顺序等待udevd event的loop,分别为”subsystem=rbd” add event和”subsystem=block” add event。而udevd并不保证遵循kernel uevent的顺序,因此如果”subsystem=block” event先于 “subsystem=rbd” event, RBD client将一直等待下去。通过人为触发add event(udevadm trigger –type=devices –action=add),就可能顺利退出。

这个问题后来在社区得到解决,我们反向移植(backport)到所有的生产集群上。

(详情可见:

https://tracker.ceph.com/issues/39089

2) kernel RBD支持的RBD feature非常有限 ,很多后端存储的特性无法使用。

3) 当节点map了RBD device并被container使用,节点重启会一直hang住, 原因是network shutdown先于RBD umount,导致kernel在cleanup_mnt()的时候kRBD连接ceph集群失败,进程处于D状态。我们改变systemd的配置ShutdownWatchdogSec为 1分钟 ,来避免这个问题。

除了kernel RBD模块,Ceph也支持块存储的用户态librbd实现 rbd-nbd。 Kubernetes也支持使用rbd-nbd。

如图3所示,我们对kRBD和rbd-nbd做了对比:

图3 (点击可查看大图)

如上,rbd-nbd在使用上有 16个 device的限制,同时会耗费更多的cpu资源,综合考虑我们的使用需求,决定继续使用kRBD。

图4为三类块存储的性能比较:

图4 (点击可查看大图)

02

文件存储

我们主要使用 cephfs 作为 存储后端 ,cephfs可以使用 kernel mount ,也可以使用 cephfs-fuse mount ,类似于前述kRBD和librbd的区别。前者工作在 内核态 ,后者工作在 用户态

经过实际对比,发现性能上fuse mount远不如kernel mount,而另一方面,fuse能更好地支持后端的feature,便于管理。 目前社区cephfs plugin里默认使用ceph fuse,为了满足部分应用的读写性能要求,我们提供了pod annotation(注解)选项,应用可自行选择使用哪类mount方式,默认为fuse mount。

下面介绍一下在使用ceph fuse的过程中遇到的一些问题 (ceph mimic version 13.2.5, kernel 4.15.0-43)

1)ceph fuse internal type overflow导致mount目录不可访问

ceph fuse设置挂载目录dentry的attr_timeout为 0 ,应用每次访问时kernel都会重新验证该dentry cache是否可用,而每次lookup会对其对应inode的reference count + 1

经过分析,发现在kernel fuse driver里count是uint_64类型,而ceph-fuse里是int32类型。 当反复访问同一路径时,ref count一直增加,如果节点内存足够大,kernel没能及时触发释放 dentry缓存,会导致ceph-fuse里ref count值溢出。

针对该问题,临时的解决办法是周期性释放缓存(drop cache) ,这样每次会生成新的dentry,重新开始计数。同时我们存储的同事也往ceph社区提交补丁,将ceph-fuse中该值改为uint_64类型,同kernel 匹配起来。 (详情可见:

https://tracker.ceph.com/issues/40775

2)kubelet du hang

kubelet会周期性通过du来统计emptydir volume使用情况,我们发现在部分节点上存在大量du进程hang,并且随着时间推移数量越来越多,一方面使系统load增高,另一方面耗尽pid资源,最终导致节点不响应。

经分析,du会读取到cephfs路径,而cephfs不可达是导致du hang的根本原因,主要由以下两类问题导致:

a. 网络问题导致mds连接断开。 如图5所示,通过ceph admin socket,可以看到存在失效链接(stale connection),原因是client端没有主动去重连,导致所有访问mount路径的操作hang在等待fuse answer上,在节点启用了client_reconnect_stale选项后,得到解决。

b. mds连接卡在opening状态,同样导致du hang。 原因是服务端打开了mds_session_blacklist_on_evict,导致连接出现问题时客户端无法重连。

图5 (点击可查看大图)

3)性能

kernel mount性能远高于fuse性能,经过调试,发现启用了fuse_big_write后,在大块读写的场景下,fuse性能几乎和kernel差不多。

三、应用场景

01

本地数据备份还原

本地存储相比网络存储,具有成本低,性能高的优点,但是如果节点失效,将会导致数据丢失,可靠性比网络存储低。

为了保证数据可靠性,应用实现了自己的备份还原机制。使用本地PV存储数据,同时挂载RBD类型的PV,增量传输数据至远端备份集群。同时远端会根据事先定义规则,周期性地在这些RBD盘上打 snapshot(快照) ,在还原的时候,选定特定snapshot,provision出对应PV,并挂载到节点上,恢复到本地PV。

02

盘加密

对于安全要求级别高的应用,如支付业务,我们使用了kata安全容器方案,同时对kata container的存储进行加密。如图6所示,我们使用了kernel dm-crypt对盘进行加密,并将生成的key对称加密存入eBay的密钥管理服务中,最后给container使用的是解密后的盘,在pod生命周期结束后,会关闭加密盘,防止数据泄漏。

图6 (点击可查看大图)

四、磁盘监控

对于本地存储来说,节点坏盘,丢盘等错误,都会影响到线上应用,需要实时有效的监控手段。我们基于社区的node-problem-detector项目,往其中增加了硬盘监控(disk monitor)的功能。

(详情可见: https://github.com/Kubernetes/node-problem-detector )

主要监控手段有三类:

1) smart工具检测每块盘的健康状况。

2) 系统日志中是否有坏盘信息。 根据已有的模式(pattern)对日志进行匹配,如图7所示。

3) 丢盘检测,对比实际检测到的盘符和节点flavor定义的盘符。

图7 (点击可查看大图)

以上检测结果以 metrics(指标) 的形式被prometheus收集,同时也更新到自定义crd computenode的状态中,由专门的 remediation controller(修复控制器) 接管,如满足预定义的节点失效策略,将会进入后续修复流程。

对于有问题的盘,monitor会对相应PV标记taint,scheduler里会防止绑定到该类PV,同时对于已绑定的PV,会给绑定到的PVC发event,通知应用。

五、管理部署

以上提到了几类组件,local-volume-provisioner,local-volume-dynamic-provisioner,cinder-provisioner,node-problem-detector等, 我们开发了gitops + salt的方案对其进行统一管理。

首先把每个组件作为一个 salt state ,定义对应的salt state文件和pillar,写入git repo,对于key等敏感信息则存放在secret中。这些manifest文件通过AZ控制面同步到各个集群并执行。我们将所有的组件视为 addon ,salt会生成最终的yaml定义文件,交由kube addon manager进行apply。在需要更新的时候,只需更新相应的salt文件和pillar值即可。