kubernetes 与LVM的结合
本文主要介绍k8s与LVM结合使用的场景,在原生的k8s中对于本地存储提供了hostPath与emptyDir两种volme,hostPath是直接将文件存储在本地主机上,存在的问题是无法进行quota管理,单个用户就可能将所有的磁盘打满;在kubernetes 1.10 local ephemeral storage
成为beta版本,可以对emptyDir进行磁盘限制,但是这里的磁盘配额不仅包括用户写入epmtyDir的数据,也包括writable layer和logs,并且emptyDir会随着pod的删除而删除,无法自定义生命周期,如果应用程序需要在pod退出后保留这些数据,emptyDir就无法胜任了。鉴于k8s原生volume的以上特点无法满足复杂的业务需求,我们可以根据LVM提供一种本地存储的volume方案。本文只介绍LVM与k8s结合实现方式,具体LVM及k8s的知识请自行查阅相关资料。
Out-of-Tree Volume Plugins
k8s对于额外的存储提供良好的支持,参见 Out-of-Tree Volume Plugins, 有两种方式实现volume plugin: CSI和FlexVolume,第一种是现在官方推荐的方式,不过第二种官方还继续提供支持而且使用起来更加简单,官方提供了FelxVolme LVM shell版的简单实现,参见github地址,其实就是一个binary或脚本来提供特定的调用,对于LVM来说需要实现基本的mount和umount subcommand供kubelet回调。mount subcommand创建LVM,分为以下几个步骤:
- 调用
lvcreate
创建LVM设备,大小为用户在yaml文件中指定的磁盘大小,会在kubelet调用plugin的时候作为参数传递进来 - 调用mkfs格式化LVM为某种文件类型
- 创建挂载点目录,该参数一般都由plugin作为参数传递进来,一般就是$KUBELET_ROOT_DIR/pods/$POD_UID/volumes/...目录下
- mount LVM设备到挂载点上
- 汇报LVM VG的使用情况,因为后面调度pod的时候需要使用这些信息
上述是kubelet在回调plugin mount subcommand的操作,对于umount subcommand因为需要自定义删除策略,我们不能直接将LVM 删除掉,但挂载点目录必须删除掉,否则kubelet会认为该volume没有被正确移除,pod一直处于terminating无法删除,此时有种tricky的方式就是move挂载点目录,这样LVM正常mount到挂载点上,pods也可以正常被删除,至于后续的自定义删除策略就直接操作move之后的挂载点设备了。需要注意的是应用程序如果要查看退出pod的LVM中的数据必须去移动之后的挂载点目录查看。
LVM GC policy
前面了解了如何在k8s中提供LVM支持,umount的时候不删除LVM来为自定义删除策略提供支持,自定义删除策略就需要写一个旁路清理程序,可以是daemon或cron的形式运行,可以基于磁盘容量或pod退出时间进行GC。比较好的方式是不直接清理LVM, 先lvreduce
对LVM进行缩容,将未使用的空间还给VG, 这样即提高磁盘使用率,又保留了用户数据,等到合适的时候再删除。注意此处缩容指的是LVM和文件系统两方面,且不能损坏用户的数据为前提。每次GC之后都需要及时上报VG信息,为后面调度提供信息。
schedule pod based on LVM
上面的实现为集群中的每个node提供了一种额外的资源(LVM)的支持, 既然是node级别的资源,就要考虑在调度pod的时候如何选择合适的node assign,需要自定义调度策略,上文中所有对LVM的操作中都提到了要及时上报VG信息,就是为了调度的时候使用这些信息。官方对于node resource提供了调度的支持,参见Advertise Extended Resources for a Node,但是这种方式只适合随着pod生命周期结束而结束的资源,例如emptyDir,memroy,cpu等。还有一种扩展是device plugin,可以考虑将LVM作为一种磁盘device使用,这种方式也不适合,因为对于device设备的申请是有粒度的,例如可以说申请一个磁盘设备,但不能说申请1G的空间。
不能通过上述两种声明node resource的方式利用内置的调度策略,我们只能自己实现调度策略了,k8s对于调度的扩展也有很好的支持,可以定义一个scheduler extender提供一个http webhook。要实现一个scheudler extender, 首先要了解kube-scheduler的调度过程,分为三步,首先是做筛选(predict)或称为filter,过滤掉不能调度到其上的node。其次是排序(priorities),为可以调度的node进行打分,决定最适合调度的node;最后是绑定(bind),绑定操作将该pod“分配”给某台node。scheduler-extender相当于上述步骤的“补充协议”, 可选的为上述步骤补充自己的调度逻辑, 当创建一个pod需要调度的时候,首先会执行内置的调度策略,然后将内置调度器的结果传递过来给你的extender server,调度逻辑完成之后将结果返回给kube-scheduler进行下一个过程。
除了kube-scheduler的调度步骤外,kube-scheduler为了提高调度性能有一个“资源占用”的概念: kube-scheduler会在本地有一个cache存放所有的node信息,该cache会与api-server进行同步,调度pod的信息直接从cache中取,当pod调度到某个node上后,会将cache中该node的资源进行“预先占用”,假定资源已经被pod消耗了,其实pod只是调度到了该node上面,还未真正启动起来,未真正占用资源,通过假定调度上去的pod已经占用了该资源,为后面调度的pod提供变化的信息,如果在指定的timeout内,pod还未启动则将预先占用的资源返回给该node,通过这种异步的最终一致性的方案能够有效提高pod的调度效率。此外在高可用的方案中需要配置多个kube-scheduler,因为每个kube-scheduler的cache并不同步,无法多个kube-scheduler同时对外提供服务,此时只能通过选主协议选择其中的一个进行处理请求。
明白了kube-scheduler的实现之后,来仿照kube-scheduler实现我们的scheduler extender, extender的httpserver位置通过kube-scheduler的--policy-config-file
或--policy-configmap
来配置,前者指定一个config file,后者通过一个configmap来配置,笔者建议用configmap来管理,因为更新配置只需要改变一个地方对于多个kube-scheduler同时生效。并且可以单独定义上述三步骤中的其中某一步。
apiVersion: v1
kind: ConfigMap
metadata:
name: lvm-scheduler-extender
namespace: kube-system
data:
policy.cfg : |
{
"kind" : "Policy",
"apiVersion" : "v1",
"extenders" : [
{
"urlPrefix": "http://localhost:8384/lvm-scheduler",
"apiVersion": "v1alpha1",
"filterVerb": "predicates/lvm_resource",
"bindVerb": "bind",
"prioritizeVerb": "",
"weight": 1,
"enableHttps": false,
"nodeCacheCapable": false
}
],
"hardPodAffinitySymmetricWeight" : 10
}
上面的配置configmap指定了extender webhook的位置,并且定义了filter和bind这两个操作,之所以定义这两个操作是因为我们只关心哪些node可以调度上去,将LVM剩余空间不足以创建pod的node过滤掉,以及最后到底调度到了那台node上以便在scheduler extender的cache中将该node的资源进行占用。需要注意的是系统中只能有一个执行bind操作的binder, 如果extender中指定了binder,kube-scheduler内置的就不会生效了,需要通过k8s client-go来绑定:
clientset.CoreV1().Pods(podNamespace).Bind(&v1.Binding{
ObjectMeta: metav1.ObjectMeta{Namespace: podNamespace, Name: podName, UID: podUID},
Target: v1.ObjectReference{
Kind: "Node",
Name: nodeName,
}})
前面在介绍plugin和GC的时候每次操作LVM都需要及时将LVM信息上报来提供extender进行调度决策,那么该向哪里汇报这些VG信息?如何汇报? 这个问题根据不同的场景下有不同的解决方案,首先肯定不能直接向extender上报,因为高可用集群中往往不只有一个exteder示例,所以需要提供一个分离的统一后端存储,所有的extender直接从后端存储里取,可以存储在Mysql或redis中,比较tricky的方式是直接存储在k8s的etcd中,这些数据量比较小不会占用太多空间,也免了额外的操作数据库,但是直接操作etcd有点不妥,可以定义一种CRD资源,为每台node创建一个crd资源实体表示该node可用LVM空间的大小,这样就可以像其他资源一样方便查询,在LVM资源发生变化之后对该crd对象进行patch。在extender中watch该crd资源即可。此外需要提供一个cache进行资源的预先占用,同时提供选主协议的实现,以上都可以参考kube-scheduler中的实现。
summary
麻雀虽小,五脏俱全上述通过实现LVM这种node resource的支持,分别涉及到了storage plugin,scheduelr extender,CRD等,虽然内容较多但是实际编码并不多,感谢kubernetes提供了一种灵活的扩展,感兴趣的可以自己去实现一下。