K8S HPA(Horizontal Pod Autoscaler)资源实现了基于CPU利用率的弹性伸缩功能,但并不支持基于内存的弹性伸缩功能。我们自己实现了该功能,在此与各位分享。
实现原理
我之前有一篇文章分析了其源码,参考此处。我的实现也基本参考了K8S HPA的思路,源码可以参考此处。
MemHpa资源
首先需要一个类似HPA的MemHpa资源来定义弹性伸缩的相关规则:replicas的上下限、利用率阈值和引用的pod controller。可参考这里
MemHpa是自定义资源,如何让K8S将其管理起来呢?这里就涉及到K8S ThirdPartyResource的概念,可参考这里。原理是:
- 通过ThirdPartyResource类型的资源可以将自定义的资源注册到K8S中,注册后K8S API便会为该自定义资源暴露对应的endpoint,URL为/apis/<group>/v1/namespaces/<namespace>/<kind>/
- ThirdPartyResource只需指定.metadata.name和.versions,name格式为kind.group。K8S会将kind中的‘-’号转为驼峰式,如mem-hpa转为MemHpa。
- 自定义资源除了apiVersion、kind和metadata之外的其他字段都是自定义的。
- 以我的实现为例,ThirdPartyResource name为mem-hpa.xinhuang.com,version添加了v1。注册后endpoint为/apis/xinhuang.com/v1/namespaces/<namespace>/memhpas。访问该endpoint返回资源的apiVersion为xinhuang.com/v1,kind为MemHpa和MemHpaList。
有了MemHpa资源后,还需要封装对应的API来访问。可以参考源码,这里我只封装了Create、Update、Delete、Get、List和Watch操作。
metrics获取
我的实现中使用了Prometheus来获取metrics,主要是因为我们团队准备使用Prometheus作为K8S的监控组件,其能通过Alertmanager实现用户级别的监控告警功能。部署方法可以参考我之前的一篇文章,查看这里
Prometheus client可以参考源码。Prometheus的query语法可以参考这里。
K8S API访问
我的思路是将MemHpa controller运行在kube-system下的Pod中,这样可以使用service account便可以访问K8S API,还带来几点好处:
- 可将Pod交由RC、Deployment等资源管理起来
- 可通过Service访问Prometheus,解决了服务发现问题
使用k8s.io/client-go/1.4/rest的InClusterConfig()方法就可以获得我们需要的配置,从而创建client。
scale算法
我基本上参考了HPA的实现逻辑。每隔30秒或MemHpa资源有变动时,便会判断是否需要进行scale。
内存使用率计算
公式是利用率=所有pod的所有容器的metrics总和/内存limit总和。
metrics是通过MemHpa的.spec.scaleTargetRef.name查询Prometheus得到的,这里使用了模糊查询,因此可能存在多余的pod,因此在计算总和时会进行过滤检查。
limit是用过.spec.scaleTargetRef查询到pod controller的scale子资源,并通过其selector来查询出所有Pod,再统计limit总和。
查询出来的Pod也会存在干扰项:尚未运行的Pod以及未查到metrics的Pod。对于干扰项的处理可以参考replica-calculator.go的GetReplicas方法。
scale条件判断
得到当前内存利用率之后,再根据其与MemHpa的.spec.targetUtilizationPercentage的比值来决定是否触发scale,比值小于0.9则缩容,比值大于1.1则扩容。伸缩后的replicas=Ceil(比值*当前replicas)。此外,为避免频繁的伸缩还添加了时间窗口机制,距离上次伸缩的一段时间内是无法再次触发伸缩的。
满足条件后,通过修改scale子资源的replicas便可以完成伸缩操作。
踩过的坑
基本实现原理大概就是这些内容了,接下来说说实现期间踩到的一些坑。
这里有两个坑。在封装MemHpa client时,底层使用了client-go的RESTClient对象,这样可以很方便的封装API。但其内部还存在一些校验机制,我在使用封装好的API访问MemHpa资源时,在解码时会报错。这里需要我们先将自定义的MemHpa资源注册,可参考源码。
另一个坑是在注册之后,我可以获取到MemHpa对象,其字段均被初始化为零值。这里有个bug,详情参考这里。解决办法是将ObjectMeta对象组合到MemHpa对象而不是嵌入,并添加相关接口函数,具体参考源码