云平台的资源分配算法

869 阅读15分钟

题目

实现一个云平台的资源分配算法: 目前如阿里云或字节云等弹性云系统,用户在申请服务的时候,可以指定需要多少个容器,每个容器多少个核的CPU,多少G的内存,多少G的硬盘,然后弹性云系统在具体的物理机上创建对应的容器Container,目前的服务器一般的配置是 48核CPU,120G内存,1T硬盘,那么请你实现一个资源分配算法,目标是让最终整体服务器硬件的利用率越高越好。

目标

  1. 能够满足用户签订服务时约定的SLA
  2. 整体硬件服务器的数量更少,总利用率更高,降低成本

思路

尽可能使用数量更少/规模更小的硬件服务用户,但面对突然到来的激增流量也需要进行适当扩容(不超过初始约定值)。

基础概念解释

容器 container

cloud.google.com/learn/what-…

包含在任何环境中运行所需的所有元素的软件包,特点:

  1. 可以虚拟化操作系统,并在任何地方运行
  2. 在操作系统级别虚拟化 CPU、内存、存储和网络资源,为开发者提供在逻辑上与其他应用相隔离的操作系统接口

控制组 cgroup

zh.wikipedia.org/wiki/Cgroup…

Linux 内核的一个特性,度量、控制分配到容器的资源,避免当多个容器同时运行时的对系统资源的竞争。

Pod

Pod,其实是一组共享了某些资源的容器。具体的说:Pod 里的所有容器,共享的是同一个 Network Namespace,并且可以声明共享同一个 Volume。Pod 是可以在 Kubernetes 中创建和管理的、最小的可部署的计算单元。

水平扩缩容

Brief

一个思路是,通过水平自动容量控制 (Horizontal Auto Scaling),实现极致的成本控制。当业务需求弱时,减少实例的数量;相反,当业务需求高峰来临,恢复一定实例数量。

Infrastructure

HPA 可以根据自定义或外部提供的指标做出缩放决策,并在初始配置后自动运行。我们需要定义实例数量的最小值和最大数量。

HPA 控制器将通过Metrics Server 定期地检查指标,然后相应地增加或者减少实例。Metrics Server 提供了标准的资源使用测量数据,例如CPU和内存使用量。

Scaling Process

  1. 指标获取:

    1. 每过一段时间,通过 Metrics Server 收集到所有pod的资源利用率或负载(CPU、内存、硬盘)作为资源指标
  2. 期望计算:

    1. HPA Scaler 计算调整后的期望实例数量。
  3. 机器部署:

    1. Deployment 调整实例数量。

Algorithm Detail

指标监控弹性 (Metrics HPA)

主要公式

期望实例数量 = ceil(已有数量 * 当前资源指标 / 期望资源指标)

例如,当前平均 CPU 负载为 30%,期望 CPU 负载为 60%,当前实例数量为5,则ceil为 5 * 30% / 60% = 3。需要减少 2 个实例。

矫正实例

在计算前,scaler 对 pod 进行状态检查,对一些错误的实例做特殊处理,如缺失某些指标、正在被删除的实例。

在特殊处理时,尽可能采取更为保守的策略来保证服务能正常运行,如缺失指标时,对于需要缩容的情况,假设实例的负载为100%。

如果大量实例的指标出现缺失,采取 fallback 机制,根据历史周期性分配数据进行调控。

基于预测的弹性 (Predicted HPA)

通过算法来对未来的流量进行预测。

预测弹性可以实现提前扩缩容,更好地降低成本和保障服务质量,并减少无效缩容扩容。

DSP 预测

使用离散傅里叶变换和自相关函数等手段,识别和预测周期时序。

预处理
  1. 填充缺失数据

    1. 由于metrics可能有缺失,这一步做线性函数填充处理。
    2. 令斜率k=vnvmnmk= {v_n-v_m \over n-m},则填充数据为:vm+k,vm+2k,v_m+k , v_m+2k , …
  2. 去除异常点

    1. 在metrics中有时存在一些突刺极端数据,可能原因包括监控系统bug、0值填充、应用启动等。为了减少对周期判断造成的干扰,要进行去除
    2. 在整个时序中,取到 P99.9 和 P0.1 作为极值,将更大/更小的采样值缩放平滑。
  3. 离散傅立叶变换

    1. 将时序进行快速离散傅里叶变换(FFT)得到频谱图
    2. 实际生产环境中,应用通常以天/周为单位呈现周期性,所以这里可以采用 1d 或者 7d 作为固定周期来观察
    3. 如下图,在几个点上明显高于其它点,这些点便可以作为我们的「候选周期」,待进一步的验证
  4. 自相关函数

    1. ACF 是一个信号与其自身在不同时间点的互相关。通俗的讲,它就是两次观察之间的相似度对它们之间的时间差的函数。
    2. ACF的图像如下所示,横轴代表信号平移的时间长度 kk;纵轴代表自相关系数 rkr_k,反映了平移信号与原始信号的相似程度
    3. 依次验证每一个候选周期对应的自相关系数是否位于极值上;并且选择对应极值的那个候选周期为整个时间序列的主周期(基波周期),并以此为基础进行预测。
预测

根据上一个步骤得到的主周期,可以进行拟合了:

选取过去几个周期中同一个时间的最大值,作为预测值。

指标聚合

由于资源指标不止一个,需要采取特定算法来确定扩容/缩容的最终数量,具体体现为:根据设定的 target ,按照每个指标计算期望不同资源类型下期望的实例数量,并取其中最大值。

例如,按照CPU计算需要缩容3个实例,但是按照内存计算只能缩容2个实例,那么结果为缩容实例数量 2。

期望实例数量 = max( CPU期望,内存期望,硬盘期望... )

时间窗口

维护一个期望实例数量的时间序列,对每一个连续的时间节点,记录该时间计算出来的期望值。取一段时间范围内的最大值,作为最新的期望值,从而防止频繁的缩容造成服务指标的抖动、影响业务。

最终兜底

在上述计算完成后,要最后进行一次实例数量的兜底处理,使得服务扩容后的实例数量<=签约数量;缩容后的实例数量 > 1。

垂直扩缩容

case : xx公司突发大批量的物理机都出现负载上升的情况,经过排查发现是低版本的 Java 程序无法准确识别容器里的规格,导致 GC 时频繁发生资源争抢。当时正在做业务迁移,这个问题导致部分业务的稳定性感受不太好。 由于无法确定其他语言是否会出现同样的问题,研发团队紧急开发了垂直扩缩容,确保 GC 可以使用更多的计算资源。

Brief

垂直自动伸缩(VPA,Vertical Pod Autoscaler),基于历史数据、集群可使用资源数量和实时的事件(如 OMM)来自动设置Pod所需资源并且能够在运行时自动调整资源基础服务。

Target

  1. 通过自动配置资源请求来减少运维成本,比如可以随时调整CPU和内存请求,无需人为操作,因此可以减少维护时间。
  2. 不必运行基准测试任务来确定 CPU 和内存请求的合适值
  3. 在提高集群资源利用率的同时最小化容器出现内存溢出或 CPU 饥饿的风险。
  • 当 VPA 重启 Pod 时,它必须考虑中断服务的成本。
  • 用户能够配置 VPA 的在资源上的固定限制,特别是最小和最大资源请求。

Infrastructure

VPA API Object

API资源:VertivalPodAutoscaler。它包括一个标签识别器 label selector(匹配Pod)、资源策略 resources policy(控制VPA如何计算资源)、更新策略 update policy(控制资源变化应用到Pod)和推荐资源信息。

推荐(Recommender)

Recommender 考虑集群中来自 Metrics Server 的所有 Pod 的资源利用率信号和内存溢出事件。会监控所有 Pod,为每个 Pod 持续计算新的推荐资源,并将它们存储到 VPA Object 中。会暴露一个同步 API 获取 Pod 详细信息并返回推荐信息。

历史存储(History Storage )

History Storage 是从 API Server 中获取资源利用率信号和内存溢出并将它们永久保存的组件。Recommender 在一开始用这些历史数据来初始化状态。

准入控制器(Admission Controller)

所有的 Pod 创建请求都会通过 Admission Controller,即VPA Admission Controller 负责拦截 Pod 创建请求。如果 Pod 与 VPA 配置匹配且模式未设置为 Off,则控制器通过将建议的资源应用于 Pod Spec 来重写资源请求。否则它会使 Pod Spec 保持不变。

控制器通过从 Recommender 中来获取推荐的资源。如果呼叫超时或失败,控制器将回退到 VPA object 中缓存的建议。如果这也不可用,则控制器允许资源请求传递最初指定的资源。

更新器(Updater)

Updater 是一个负责将推荐资源应用于现有 Pod 的组件。它监视集群中的所有 VPA object 和 Pod ,通过调用 Recommendation API 定期获取由 VPA 控制的 Pod 的建议。当推荐的资源与实际配置的资源明显不同时,Updater 可能会决定更新 Pod。这需要通过删除 Pod 然后依据新的资源重建 Pod 来实现,这种方法需要 Pod 属于一个Replica Set副本集,或者其他能够重新创建它的组件。重新创建或者重新分配Pod对服务是很有破坏性的,必须尽量减少这种操作。

虽然终止Pod是破坏性的并且通常是不期望的,但有时也是合理的:

  • 避免 CPU 饥饿
  • 随机降低跨多个 Pod 的相关 OOM 的风险
  • 在长时间内节省资源

因此update policy mode可以设置3种:

  • Initial: VPA 只在创建 Pod 时分配资源,在 Pod 的其他生命周期不改变Pod的资源。
  • Auto(默认):VPA 在 Pod 创建时分配资源,并且能够在 Pod 的其他生命周期更新它们,包括淘汰和重新调度 Pod。
  • Off:VPA 从不改变Pod资源。

Algorithms

推荐模型算法

资源请求是基于对容器的当前和先前运行以及具有类似属性的其他容器(名称,图像,命令,args)的分析来计算的。推荐的模型假设内存和CPU消耗是独立的随机变量,其分布等于在过去 N 天中观察到的分布(推荐 N 值取为 N =8 以捕获每周峰值)。更先进的模型可能会尝试检测趋势,周期性和其他与时间相关的模式。

  • 对于CPU,目标是保证容器使用的CPU超过容器请求的 CPU 资源的高百分比(如95%)时间低于某个特定的阈值(如保证只有1%的时间内容器的CPU使用高于请求的 CPU 资源的95%)在此模型中,“CPU 使用”定义为在短时间间隔内测量的平均值。测量间隔越短,对尖峰,延迟敏感的工作负载的建议质量越好。最低合理间隔为 1/min,建议为 1/sec。
  • 对于内存,目标是保证在特定时间窗口内容器使用的内存超过容器请求的内存资源的概率低于某个阈值(例如,在 24 小时内低于 1%)。窗口必须很长( ≥24h ),以确保 OOM 引起的驱逐不会明显影响服务应用程序的可用性和批量计算的进度。更高级的模型可以允许用户指定 SLO 来控制它。

调度算法

调度决策总是基于resource requests,调度程序都会用它来为你的 pod 分配位置。用户可以选择配置resource limits,但调度程序永远不会使用这些。limit 只是( 如 Kubelet中 )一个硬性限制,如果它超过这些 cgroup 限制,它知道何时限制或杀死你的 pod。因此,由于唯一真正重要的是requests参数,Vertical Pod Autoscaler 将始终使用它。每当您为应用程序定义垂直自动缩放时,您就是在定义请求应该是什么。

可以为自动缩放设置一个范围:请求的最小值和最大值。当触及 request 边界时,limits 也会进行调整。VPA 将按比例缩放limits。

比如默认设置:

requests:
  cpu: 50m
  memory: 100Mi
limits:
  cpu: 200m
  memory: 250Mi

推荐引擎将确定需要120mCPU 和300Mi内存以使 pod 正常工作。因此它将提出以下新设置:

requests:
  cpu: 120m
  memory: 300Mi
limits:
  cpu: 480m
  memory: 750Mi

如上所述,这是比例缩放:

  • CPU: 50m → 200m: 1:4 ratio
  • memory: 100Mi → 250Mi: 1:2.5 ratio

算法会尊重并保持最初配置的相同比例,并根据用户原始比例按比例设置新值。

因此,如果想确保 Pod 上的内存限制永远不会超过 250Mi,可以:

  • 始终为 request 配置最小值和最大值
  • 使用 1:1 的request-to-limit ratio,所以即使你得到maximum request,limit也不会超过这个值
  • 以上的任意组合,调比率

其实limits几乎无关紧要,因为调度决策(以及因此的资源争用)将始终基于requests。限制仅在存在资源争用或想避免无法控制的内存泄漏时才有用。

Predicates 和 Priorities 这两个调度策略

在 Kubernetes 项目中,默认调度器的主要职责,就是为一个新创建出来的 Pod,寻找一个最合适的节点(Node)。

而这里“最合适”的含义,包括两层:

  • 从集群所有的节点中,根据调度算法挑选出所有可以运行该 Pod 的节点;
  • 从第一步的结果中,再根据调度算法挑选一个最符合条件的节点作为最终结果。

所以在具体的调度流程中,默认调度器会首先调用一组叫作 Predicate 的调度算法,来检查每个 Node。然后,再调用一组叫作 Priority 的调度算法,来给上一步得到的结果里的每个 Node 打分。最终的调度结果,就是得分最高的那个 Node。

Predicate 的调度算法
  • 第一种类型,叫作 GeneralPredicates。这一组过滤规则,负责的是最基础的调度策略。比如,PodFitsResources 计算的就是宿主机的 CPU 和内存资源等是否够用。PodFitsHostPorts 检查的是,Pod 申请的宿主机端口(spec.nodePort)是不是跟已经被使用的端口有冲突。
  • 第二种类型,是与 Volume 相关的过滤规则。这一组过滤规则,负责的是跟容器持久化 Volume 相关的调度策略。比如,NoDiskConflict 检查的条件,是多个 Pod 声明挂载的持久化 Volume 是否有冲突。比如,AWS EBS 类型的 Volume,是不允许被两个 Pod 同时使用的。所以,当一个名叫 A 的 EBS Volume 已经被挂载在了某个节点上时,另一个同样声明使用这个 A Volume 的 Pod,就不能被调度到这个节点上了。
  • 第三种类型,是宿主机相关的过滤规则。这一组规则,主要考察待调度 Pod 是否满足 Node 本身的某些条件。比如,NodeMemoryPressurePredicate,检查的是当前节点的内存是不是已经不够充足,如果是的话,那么待调度 Pod 就不能被调度到该节点上。
  • 第四种类型,是 Pod 相关的过滤规则。这一组规则,跟 GeneralPredicates 大多数是重合的。而比较特殊的,是 PodAffinityPredicate。这个规则的作用,是检查待调度 Pod 与 Node 上的已有 Pod 之间的亲密(affinity)和反亲密(anti-affinity)关系。比如,某个 Pod 不希望跟任何携带了 security=S2 标签的 Pod 存在于同一个 Node 上,就可以在podAntiAffinity规则里面指定。

在具体执行的时候, 当开始调度一个 Pod 时,Kubernetes 调度器会同时启动 16 个 Goroutine,来并发地为集群里的所有 Node 计算 Predicates,最后返回可以运行这个 Pod 的宿主机列表。

Priority 的调度算法

在 Predicates 阶段完成了节点的“过滤”之后,Priorities 阶段的工作就是为这些节点打分。这里打分的范围是 0-10 分,得分最高的节点就是最后被 Pod 绑定的最佳节点。

  • Priorities 里最常用到的一个打分规则,是 LeastRequestedPriority。它的计算方法,可以简单地总结为如下所示的公式:
score = (cpu((capacity-sum(requested))10/capacity) + memory((capacity-sum(requested))10/capacity))/2

可以看到,这个算法实际上就是在选择空闲资源(CPU 和 Memory)最多的宿主机。

  • 还有 BalancedResourceAllocation,它的计算公式如下所示:
score = 10 - variance(cpuFraction,memoryFraction,volumeFraction)*10

其中,每种资源的 Fraction 的定义是 :Pod 请求的资源 / 节点上的可用资源。

而 variance 算法的作用,则是计算每两种资源 Fraction 之间的“距离”。而最后选择的,则是资源 Fraction 差距最小的节点。

所以说,BalancedResourceAllocation 选择的,其实是调度完成后,所有节点里各种资源分配最均衡的那个节点,从而避免一个节点上 CPU 被大量分配、而 Memory 大量剩余的情况。

参考资料

www.kubecost.com/kubernetes-…

medium.com/infrastruct…

gocrane.io/docs/tutori…

www.lixueduan.com/posts/kuber…