得物 App 作为互联网行业的后起之秀,在快速的业务发展过程中基础设施规模不断增长,继而对效率和成本的关注度也越来越高。我们在云原生技术上的推进历程如图所示,整体上节奏还是比较快的。
从 2021 年 8 月开始,我们以提升资源使用率和资源交付效率为目标,开始基于云原生技术建设整个服务体系的高可用性、可观测性和高运维效率,同时要保证成本可控。在容器化过程中我们遇到了很多的挑战,包括:如何将存量的服务在保持已有研发流程不变的情况下,做到容器化部署和管理;容器化之后如何做到高效地运维;如何针对不同的业务场景,提供不同的容器化方案等等。此外,通过技术手段实现持续的成本优化是我们的长期目标,我们先后建设落地了画像系统、混部方案和调度优化等方案。本文把得物在推进云原生容器技术落地过程中相关方案和实践做一些总结和梳理,欢迎阅读和交流。
容器与 ECS 的资源形态是有差异的,所以会造成在管理流程上也会有不同之处。但是为了尽可能降低容器化带来的使用体验上的差异,我们参考业内容器应用 OAM 模型的设计模式,对容器的相关概念做了屏蔽和对等解释。例如:以“应用集群”的概念代表 CloneSet 工作负载(Kruise 提供的一种 Kubernetes 扩展工作负载);将单个 Pod 约定为一个应用集群的实例;以“应用路由/域名配置”的概念代表针对 Ingress/Service 的设置。
在应用集群的构造上(即如何构造出 Kubernetes 工作负载对象),我们设计了“配置/特征分层”的方案,将一个应用集群所处归属的应用、环境组、环境上的配置进行叠加后,使用 Helm 工具渲染生成 Kubernetes 资源对象,提交给容器平台。
CI 和 CD 过程均使用这种配置/特征分层的方式,一方面可以解决应用依赖的中间件信息的管理问题(由相应的提供者统一维护);另一方面,这种管理方式可以让中间件组件/服务变更时按照不同维度进行,整体上降低了配置变更带来的风险。
Sidecar 容器在应用集群实例中除了扮演“协作者”的角色外,我们还基于它做了权限管理,以便对应在 ECS 形态下的不同用户的登陆权限,也算是一举两得。当然,在容器场景下也是可以定义不同的用户,赋予不同的角色,但是强依赖基础镜像的维护。
云原生场景下的解决方案对应用集群而言本身就是高可用的,比如:容器编排引擎 Kubernetes 中支持 Pod 实例的拓扑分布设置、支持可用区设置、副本数设置、 Service 负载均衡的设计等,这些都能保证应用集群的高可用。那如果单个 Kubernetes 集群不可用了,会有什么的影响呢,该如何解决?多集群管理方案就是我们解决 Kubernetes 的可用性问题的思路。
如果 Kubernetes 控制面不可用了,会导致应用发布受损,较严重的情况也会影响容器服务的可用性。所以,为了保证 Kubernetes 的可用性,一方面要保证 Kubernetes 各组件的健壮性,另一方面要适当控制单个 Kubernetes 集群的规模,避免集群过大造成系统性风险升高。我们的解决思路就是“不要把鸡蛋放在一个篮子里”,用联邦的方式管理多个 Kubernetes,将业务分散到不同的 Kubernetes 集群。
联邦的思想在 Kubernetes 诞生不久就被开始讨论,逐步设计实现,从最初社区的 KubeFate V1.0 到 V2.0,再到企业开源的 Karmada、KubeAdmiral 逐渐成熟起来,并实际应用到了生产场景。那如果没有集群联邦,多个 Kubernetes 集群就没法管理了吗?当然不是的,容器管控平台其实也能做这件事情,笔者在几年之前还对此深以为然,但现在已经完全改变看法了。因为在实际的生产落地过程中我们发现,相比在管控中用 if...else/switch 的方式,亦或配置的方式相比,基于 CRD 的方式来管理多集群效率更高、逻辑更清晰。
落地云原生容器技术的目标是期望在敏捷、弹性和可用的基础上,最终实现资源利用率上的提升、成本上的节省。这通常有 2 个实现途径,一个是通过技术的手段,另一个则是通过治理方法。本章重点介绍我们在容器精细化调度和混部实践方面的技术方案设计和落地过程。
应用服务的研发人员在部署应用集群实例时,通常会申请超过应用集群本身承载业务流量时所要消耗的资源量,这是可以理解的(要确保系统的资源利用率安全水位,防止过载造成系统夯住),但是不同的研发人员对这个“度”把握是不一样的,因为合理地设置应用集群的资源用量是依赖研发人员经验的,也就是说主观性会更强。
为了解决上述问题,业内的做法通常是通过分析应用集群的过往资源利用率数据,来刻画出应用集群在业务流量下的实际资源利用率曲线,这就是应用画像。如下图所示是我们建设的画像系统的架构框图,该画像系统不仅负责应用的画像分析,也负责宿主机、Kubernetes 集群的画像分析,用来指导整个容器平台对资源的管理。
容器的监控数据通过 Prometheus 方案进行采集和管理,自研的 KubeRM 服务将它作为数据源,周期性计算产出应用画像、宿主机画像和 Kubernetes 集群画像(资源池画像)。容器平台部署在线服务服务时,可参考画像值来配置应用集群的资源规格,这里的画像值就是指 Pod 的 Request 值,计算公式如下:
公式中“指标周期性利用率”是画像系统通过统计学手段、AI 模型等方法计算分析出的资源指标(CPU/内存/GPU显存)在实际业务流量下所表现出的周期性的规律。画像值的生效我们通过以下 4 个策略进行实施:
交由用户决定画像是否生效时,如何让用户更倾向于去生效画像呢?我们使用差异化计费的策略:生效了画像的应用集群实例按照其 Pod 配置的 Request 值计费,未生效画像的应用集群实例按照其 Pod 配置的 Limit 值计费。用户可以根据自己服务的实际情况选择生效画像,以降低成本;平台也因为画像而拿到了更多可以调度的资源,用于更多的场景。
此外,画像系统也接入了 KubeAutoScale 自动伸缩器,在业务低峰期,可以指导自动伸缩器对部分场景在线服务做副本缩容操作,以便释放出更多的资源供给场景使用(比如:混部任务场景),后面的章节会详细介绍。
当整个容器集群的资源冗余量不是很充足的时候,在以下几种情况下是会出现 “虽然集群层面总量资源是够的,但是业务 Pod 却无法调度”的问题,影响业务发布效率和体验。
在集群中容器实例变更比较频繁的时候,某个大规格的业务集群在做滚动更新时,释放的旧的实例很可能被小规格的容器实例所抢占,导致无法调度。
研发同学负责 2 个应用服务 A 和 B,它们的规格都是一样的。为了保证总体成本不变会,选择将 A 服务的实例缩掉一些,然后扩容 B 服务的实例。因为 Kubernetes 默认调度会按照 Pod 创建时间来依次调度新 Pod,当用户缩容完 A 服务的实例再去扩容 B 服务实例的时候,A 服务释放的资源很可能被其他容器实例抢占,导致 B 的实例无法调度。
在大促、全链路压测等业务需要紧急扩容的情况下,容器平台会新扩宿主机节点以满足业务需求,不曾想新扩容的机器资源却被那些“小而快(拉起频繁,执行时间短)”的任务给见缝插针地抢占了,一方面会导致大规格的服务实例无法调度,另一方面还造成了较多的资源碎片。
为了解决以上问题,我们在调度器中自定义实现了资源预占的调度插件(通过 CRD 定义资源预占期望,影响调度决策),用来提升用户体验和提高调度效率。
为了更好地平衡集群中节点的水位,以避免过热节点的出现、尽量减少碎片资源等为目标来思考和设计,我们基于 Kubernetes 提供的调度器扩展框架,自定义实现了多个调度插件:
CoolDownHotNode 插件:给最近调度过 Pod 的节点降低优先级,避免热点节点。
从今年 1 月份开始,我们着手做在离线混部的落地,一期的目标着眼于将在线服务与 Flink 任务进行混部。之所以选择 Flink 任务做混部,是因为它与在线服务有一个相似之处,那就是它是一种常驻的离线任务,在它启动之后如果没有特殊情况,一般不会下线,这种特质会使得我们的容器集群调度频次、Pod 的变更程度会低一些,进而对稳定性的挑战也会小一点,整体混部风险也会低一些。
在没有混部的情况下,我们的集群整体利用率较低,即便画像功能能帮助用户尽可能合理的为自己的服务实例设置资源规格,但对容器平台而言这依然很被动。所以为了挖掘出可以用来混部的资源,我们为不同等级的服务设置不同的绑核策略。如下表所示定义了 4 种应用类型(LSX、LSR、LS、BE),适用于 P0~P4 范围和离线任务,绑核策略从完全绑核到部分绑核,再到完全共享。
离线任务(Flink 任务)属于 BE 类型,可以使用的资源是在宿主机所有 CPU 核心里面单独划分出来的一部分专用 CPU 核心,再加上 LS 的共享 CPU 核心,以及 LSR、LS 类型的应用上共享出来的部分 CPU 核心。
基于 Kubernetes 的 Device-Plugin 机制我们自研实现了 Kube-Agent 组件,该组件在集群中的所有节点上以 Damonset 的方式部署,一方面负责根据自定义策略将本节点上可用的 BE 资源上报给 API-Server(通过 Kubelet 组件间接上报。