本文根据美团基础架构部/弹性策略团队负责人涂扬在2019 QCon(全球软件开发大会)上的演讲内容整理而成。本文涉及Kubernetes集群管理技术,美团相关的技术实践可参考此前发布的

在早期,HULK 1.0是基于OpenStack演进的一个集群调度系统版本。这个阶段工作的重点是将容器和美团的基础设施进行融合,比如打通CMDB系统、公司内部的服务治理平台、发布平台以及监控平台等等,并验证容器在生产环境的可行性。2018年,基础架构部将底层的OpenStack升级为容器编排标准Kubernetes,然后我们把这个版本称之为HULK 2.0,新版本还针对在1.0运营过程中遇到的一些问题,对系统专门进行了优化和打磨,主要包括以下几个方面:

截止发稿时,美团生产环境超过1万个应用在使用容器,容器数过10万。

二、HULK2.0集群调度系统总体架构图

上图中,最上层是集群调度系统对接的各个平台,包括服务治理、发布平台、测试部署平台、CMDB系统、监控平台等,我们将这些系统打通,业务就可以无感知地从VM迁移到容器中。其中:

最底层的HULK Agent是我们在每个Node上的代理程序。此前,在美团技术团队官方博客上,我们也分享过底层的镜像管理和容器运行时相关内容,参见

可以看到,一次扩缩容请求基本上会经历以下这些流程:

a. 用户或者上层系统发起扩缩容请求。 b. 扩缩容组件从策略配置中心获取对应服务的配置信息。 c. 将对应的配置信息提交到美团自研的一个API服务(扩展的K8s组件),然后K8s各Master组件就按照原生的工作流程开始Work。 d. 当某个实例调度到具体的Node上的时候,开始通过IP分配服务获取对应的Hostname和IP。 e. Container-init是一号进程,在容器内部拉起各个Agent,然后启动应用程序。针对已经标准化接入的应用,会自动进行服务注册,从而承载流量。

而这些模块是由美团内部的不同同学分别进行维护,每次遇到问题时,就需要多个同学分别核对日志信息。可想而知,这种排查问题的方式的成本会有多高。

解法:类似于分布式调用链中的traceId,每次扩缩容会生成一个TaskId,我们在关键链路上进行打点的同时带上TaskId,并按照约定的格式统一接入到美团点评日志中心,然后在可视化平台HULK Portal进行展示。

落地效果:

3.2 业务定制化需求

痛点:每次业务的特殊配置都可能变更核心链路代码,导致整体系统的灵活性不够。

具体业务场景如下:

解法:建设一体化的调度策略配置中心,通过调度策略配置中心,可定制化调度规则。

在策略配置中心,我们会将这些策略进行Manifest组装,然后转换成Kubernetes可识别的YAML文件。

落地效果:实现了平台自动化配置,运维人员得到解放。

3.3 调度策略优化

接下来,介绍一下Kubernetes调度器Scheduler的默认行为:它启动之后,会一直监听ApiServer,通过ApiServer去查看未Bind的Pod列表,然后根据特定的算法和策略选出一个合适的Node,并进行Bind操作。具体的调度策略分为两个阶段:Predicates预选阶段和Priorities打分阶段。

Predicates 预选阶段(一堆的预选条件):PodFitsResources检查是否有足够的资源(比如CPU、内存)来满足一个Pod的运行需求,如果不满足,就直接过滤掉这个Node。

Priorities 打分阶段(一堆的优先级函数):

将以上优先级函数算出来的值加权平均算出来一个得分(0-10),分数越高,节点越优。

痛点一:当集群达到3000台规模的时候,一次Pod调度耗时5s左右(K8s 1.6版本)。如果在预选阶段,当前Node不符合过滤条件,依然会判断后续的过滤条件是否符合。假设有上万台Node节点,这种判断逻辑便会浪费较多时间,造成调度器的性能下降。

解法:当前Node中,如果遇到一个预选条件不满足(比较像是短路径原则),就将这个Node过滤掉,大大减少了计算量,调度性能也得到大幅提升。

成效:生产环境验证,提升了40%的性能。这个方案目前已经成为社区1.10版本默认的调度策略,技术细节可以参考GitHub上的PR

痛点二:资源利用率最大化和服务SLA保障之间的权衡。

解法:我们基于服务的行为数据构建了服务画像系统,下图是我们针对某个应用进行服务画像后的树图展现。

调度前:可以将有调用关系的Pod设置亲和性,竞争相同资源的Pod设置反亲和性,相同宿主机上最多包含N个核心应用。 调度后:经过上述规则调度后,在宿主机上如果依然出现了资源竞争,优先保障高优先级应用的SLA。

3.4 重编排问题

痛点:

(1)容器重启/迁移场景:

  • 容器和系统盘的信息丢失。
  • 容器的IP变更。

(2)驱逐场景:Kubelet会自动杀死一些违例容器,但有可能是非常核心的业务。

解法:

(1)容器重启/迁移场景:

  • 新增Reuse策略,保留原生重启策略(Rebuild)。
  • 定制化CNI插件,基于Pod标识申请和复用IP。

(2)关闭原生的驱逐策略,通过外部组件来做决策。

四、弹性伸缩平台痛点、解法

弹性伸缩平台整体架构图如下:

注:Raptor是美团点评内部的大监控平台,整合了

如上图所示,一个业务配置了2条监控策略和1条周期策略:

早期的设计是各条策略独自决策,扩容顺序有可能是:缩5台、缩2台、扩10台,也有可能是:扩10台、缩5台、缩2台,就可能造成一些无效的扩缩行为。

解法:增加了一个聚合层(或者把它称之为策略协商层),提供一些聚合策略:默认策略(多扩少缩)和权重策略(权重高的来决策扩缩行为),减少了大量的无效扩缩现象。

4.2 扩缩不幂等

如上图所示,聚合层发起具体扩缩容的时候,因之前采用的是增量扩容方式,在一些场景下会出现频繁扩缩现象。比如,原先12台,这个时候弹性伸缩平台告诉调度系统要扩容8台,在返回TaskId的过程中超时或保存TaskId失败了,这个时候弹性伸缩平台会继续发起扩容8台的操作,最后导致服务下有28台实例(不幂等)。

解法:采用按目标扩容方式,直接告诉对端,希望能扩容到20台,避免了短时间内的频繁扩缩容现象。

4.3 线上代码多版本

如上图所示,一个业务线上有30台机器,存在3个版本(A、B、C)。之前我们弹性扩容的做法是采用业务构建的最新镜像进行扩容,但在实际生产环境运行过程中却遇到问题。比如一些业务构建的最新镜像是用来做小流量测试的,本身的稳定性没有保障,高峰期扩容的时候会提升这个版本在线上机器中的比例,低峰期的时候又把之前稳定版本给缩容了,经过一段时间的频繁扩缩之后,最后线上遗留的实例可能都存在问题。

解法:基于约定优于配置原则,我们采用业务的稳定镜像(采用灰度发布流程将线上所有实例均覆盖过一遍的镜像,会自动标记为稳定镜像)进行扩容,这样就比较好地解决了这个问题。

4.4 资源保障问题

如上图所示,存量中有2个服务,一个需要扩容20台,一个需要扩容15台,这个时候如果新接入一个服务,同一时间需要扩容30台,但是资源池只剩余50台实例了。这个时候就意味着,谁先扩容谁就可以获得资源保障,后发起的请求就无法获得资源保障。

解法:

(1)存量资源水位检测:当存量资源的使用水位超过阈值的时候,比如达到80%的时候会有报警,告诉我们需要做资