DockOne微信分享(二二五):eBay Kubernetes集群的存储实践


【编者的话】存储是云平台最核心最基本的功能之一,如何在Kubernetes容器平台上对接不同的后端类型,并根据实际需求做选择和定制,会遇到哪些问题,怎么管理?本次分享将介绍eBay存储的实现,使用方式,并介绍我们在生产环境的一些实践案例,遇到的问题及解决方案。

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

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

本地存储

静态分区

我们最早于2016年开始做Local Volume(本地卷),当时社区还没有本地的永久存储方案,为了支持内部的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 neighbor”干扰,同时对于disk io load很高的应用,应尽可能平均每块盘的负担,延长磁盘寿命。因此,我们增加了PVC的反亲和性(anti affinity)调度特性,在满足节点调度的同时,会尽可能调度到符合反亲和性规则的盘上。具体做法是,在PV中打上标签表明属于哪个节点的哪块盘,在PVC中指定反亲和性规则,如下图一所示。scheduler里增加相应的预选功能,保证声明了同类型的反亲和性的PVC,不会绑定到处在同一块盘的PV上,并在最终调度成功后,完成选定PV/PVC的绑定。
1.png

图 1

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.png

图 2

网络存储

块存储

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

网络存储卷的管理主要包括provision/deletion/attach/detach等,在provision/deletion的时候,相比于Local Volume(本地卷)需要以DaemonSet的方式部署,网络存储只需要一个中心化的provisioner。我们利用了社区的cinder provisioner方案,并加以相应的定制,比如支持利用已有快照卷(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的过程中,我们也遇到过一些问题:
  • 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
  • kernel RBD支持的RBD feature非常有限,很多后端存储的特性无法使用。
  • 当节点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.png

图 3

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

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

图 4

文件存储

我们主要使用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的根本原因,主要由以下两类问题导致:
  • 网络问题导致mds连接断开,如图5所示,通过ceph admin socket,可以看到存在失效链接(stale connection),原因是client端没有主动去重连,导致所有访问mount路径的操作hang在等待fuse answer上,在节点启用了client_reconnect_stale选项后,得到解决。
    5.png

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


3、性能:

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

应用场景

本地数据备份还原

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

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

盘加密

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

图 6

磁盘监控

对于本地存储来说,节点坏盘,丢盘等错误,都会影响到线上应用,需要实时有效的监控手段。我们基于社区的node-problem-detector项目(详情可见https://github.com/Kubernetes/ ... ector),往其中增加了硬盘监控(disk monitor)的功能。主要监控手段有三类:
  1. smart工具检测每块盘的健康状况。
  2. 系统日志中是否有坏盘信息。根据已有的模式(pattern)对日志进行匹配,如图7所示:
    7.png

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


以上检测结果以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. 实现本地存储的自动扩容能力。


Q&A

Q:分布式数据库例如MongoDB在Kubernetes上有实现方案吗?
A:有的,我们内部NoSQL就是完全运行在容器云上的,Pod部署由应用自己管理,通过svc暴露服务,存储上使用local PV,并实现了backup restore。社区应该也有比较多的实现参考。

Q:由于环境,如网络因素,出现短时间暂时大规模掉Node的情况怎么处理?
A:如网络问题导致Node连不通,对于网络存储来说,需要在网络恢复之后重连,比如cephfs kernel client和fuse都实现了reconnect。

Q:etcd集群中,v2和数据和v3的数据备份方式不一样,如何备份整个etcd数据呢?
A:etcd server只能有一种版本,不会并存,所以按照各自版本的方式备份即可。

Q:PVC的anti affinity调度特性是Kubernetes原生支持的吗?自研方案有计划贡献到Kubernetes仓库吗?
A:不是,我们是在使用MongoDB的过程中,发现master pod的io load很高,所以基于此自己开发了这个功能。

Q:数据如何做容灾?
A:网络存储自己有多replica和rack awareness的分布,本地存储需要应用自己实现多拷贝,对于可靠性要求比较高的数据,需要做备份还原。

Q:本地存储能说的更清楚点么?比如registar是怎么把信息同步到kubernetesnode中的。PV的删除是csi那个组件来做的?信息有哪些信息。
A:registar在注册节点的时候会将vg的相关信息以annotation的方式写到node对象中,pv的删除由csi-provisioner sidecar完成,大体思路可参考社区的design doc。

Q:容器镜像如何存储和管理?
A:我们目前用的是quay,用swift存储镜像层。

Q:Redis集群,3主3从这种,如何跑在Kubernetes上?
A:可以用statefulset的方式,具体可以参考社区的做法。

Q:使用ceph rbd会出现multiattach error,导致新Pod一直处于creating状态,需要人工介入,有无自动处理方案?比如,kubelet挂掉。
A:如出现kubelet挂掉或者node hung导致kubelet不工作,有可能出现这种情况,需要实现节点的remediation,监控这些情况,重启或者下架节点,保证原来的连接断掉。

Q:请问日志存储是在专有的节点吗?如果不是会和业务数据存储产生影响吗?空间占用,CPU,内存方面的影响。
A:每个节点组件本身的日志和容器的日志都是通过beats来收集并上报到监控系统,不会和业务数据冲突或干扰。

Q:存储限制是怎么做的?
A:对于emptydir,我们使用xfs quota限制。对于PV/PVC,我们在controller层面做了每个namespace的quota limit。

Q:ceph rbd和本地磁盘有做过benchmark么?cg v2应该只能限制本地盘吧?
A:有的,如下:
8.png


Q:kernel network storage有没有什么好的学习材料?
A:具体是哪类存储类型,可以参见https://www.oreilly.com/librar ... ivers

Q:有没有可能通过StatfulSet 实现分布式存储?来做异地容灾。
A:异地容灾是federation层面的部署,感觉和用哪类workload api没太大关系。

Q:本地存储不需要另外的scheduler-extender么?用原有的scheduler就可以了?
A:我们是直接在原有的scheduler基础上做了修改,当时还没有extender机制,后续会考虑以extend方式放到外部。

以上内容根据2019年8月27日晚微信群分享内容整理。 分享人高文俊,eBay中国研发中心云计算工程师,负责Kubernetes平台的研发,内部host-runtime sig成员,主要负责存储,集群管理等工作。 DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiese,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。

0 个评论

要回复文章请先登录注册