如今,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 controller。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 值即可。
六、后续工作
1)对于网络存储,将后端控制面由 cinder 切换到 SDS,届时将会对接新的 SDS api,实现新的 dynamic provision controller 和 csi 插件;
2)实现 Kubernetes 平台上的 volume snapshot(卷快照)功能;
3)将 in-tree 的 volume 插件全部迁移到 CSI,并将 CSI 升级到最新版本,方便部署和升级;
4)引入 cgroup v2, 以实现 blkio qos 控制;
5)实现本地存储的自动扩容能力。
本文转载自公众号 eBay 技术荟(ID:eBayTechRecruiting)。
原文链接:
https://mp.weixin.qq.com/s/VeyR4dSkH_bOH7YmbwSE2w
评论