Resource

资源的分类

在实际裸机/虚拟机处理主要有 CPU,Memory 2类资源是 Eru 需要考虑的。在目前的计算机体系下,磁盘 IO 的隔离并不那么好做,要么会带来巨大的性能损失,要么聊胜于无。在重 IO 的用况下,独占将会是一个比较好的选择,如 MySQL。另外对于网络资源而言,Eru 倾向于交给上层网络控制层面来进行处理,一来便于和现有基础设施结合,二来降低宿主机本身的负担和复杂度,便于后期的运维。

资源的维度

以 CPU,Memory 为主的前提下,对于不同的应用对资源的侧重不用,Eru 支持 2 类资源维度处理。

  1. CPU 为主,Memory 默认为硬限制。但 Eru 也可以设定 Memory 为 memory-reservation,由 OS 控制内存软限制,在实际内存超出限制之后系统会不断尝试回收,因此这种模式主要用于基础服务而非应用,如 Redis/Memcache ,应用需要本身有很完善的内存控制并且对于 CPU 敏感。在这种情况下通过 CPUBind Flag 让 Eru 基于 CPU 资源来分配。在这里要指出的是 Eru 并非以单个核作为最小单位进行分配,在 Eru 配置中允许管理者控制运算量最小单位,如 1% 甚至 1‰。对于应用而言在这种模式下无论是 CPU 还是 Memory 均不会超售,每个容器/虚拟机均是独占其所需的资源。比如对于一个 1.7 CPU 需求,我们会先通过 cpuset 把一个整数核和一个碎片核绑定到这个容器/虚拟机上,再通过 cpushare 设定 7 乘以一个常数来满足这个容器/虚拟机在碎片核上的资源需求。对于整数核而言,因为没其他容器/虚拟机与其分享,因此 cpushare 会被会略。对于碎片核而言,cpushare 保证了容器/虚拟机最多不超过其拥有的运算量份数。

  2. Memory 为主,CPU 为软限制。Eru 保证在不超售的前提下容器/虚拟机一定在最繁忙的时候获得所申请的 CPU 运算量。超售的情况下就比较复杂了,一般来说是没法保证容器/虚拟机最繁忙的时候能获得所申请的 CPU 运力。但对于大规模分布式系统而言,我们认为配合运维监控做相应的迁移即可,如 web 服务。单个容器/虚拟机达不到预期 QPS 并不影响整体服务的可用性。这种模式下 Memory 默认是不会超售的,但是允许软限制内存,CPU 不受限的超售。

分配流程

对于 Eru 而言,每一次资源要约都是针对一个 Pod 进行的,为了保证数据的一致性我们会对 Pod 资源池加锁,待资源申请成功或失败后再释放。在不引入 MVCC 乐观锁的情况下,Eru 编排的并发能力就完全取决于锁定 Pod 后资源计算行为。

因此在分配流程中,Eru 本身不会处理分配-部署这个逻辑,而是将整个资源要约当做一个整体进行一次计算。由于是纯数学行为,在我们的测试中 10K 数量级的容器在 1K 数量级的 Node 上分配也只是毫秒级的开销,加之 Pod 进行了资源(节点)逻辑分割和 Core 的无状态设计,我们认为足以胜任日常需求。

在得到资源方案之后,Core 将会扣掉相应资源后解锁 Pod 的资源池,然后并发的执行部署行为。

分配算法

CPU 为主

假设所需 M bytes 内存和 X.Y 个 CPU,其中 X 表示整数位,Y 表示小数位。我们将单一节点上的 CPU 抽象成由 A 个整数核和 B 个碎片核组成。假如管理者设定的运算量为 P ,那么每个碎片核上的每一份能提供的就是 1/P 运算能力。因此对于单个 Node 而言最大可部署数量为 min( Memory/M,min( A/X,B*P/Y )) 个。

在 A+B = CPU总数量的前提下,我们要通过不断的尝试组合 A, B 的值来确定多个 min( A/X, B*P/Y ) 结果中最多的那个,再与内存计算结果进行比较。由于 cpushare 是单个核全局一致的,因此没法拆分 Y 为更小的多个运算量之和,所以我们只允许一次绑定一个碎片核。实际实现中还需要考虑已有碎片核的处理和碎片核运算量不能跨核进行,因此会更复杂一些。

Memory 为主

简单的进行 Memory / M 即可,对于 CPU 需求而言采用 CPU period 和 quota 进行全局控制。

以上 2 种算法在得到资源组合之后,Eru 会对 Nodes 上可部署数量排序,由少到多。然后再通过计算判断 Nodes 上的可部署数量是否满足用户需求。这样虽然会带来单一 Node 资源利用率不够高的可能性,但在大规模部署的情况下会在 Nodes 层面更加平均,分布上会更好。但是当能承载最少数的那个 Node 也能满足用户需求的时候,多次部署会产生堆积到一台 Node 的情况,因此 Eru 引入了更高级的分布算法。

分布算法

Eru 自身实现了 4 类分布算法,在 Cli 命令中默认是 auto 模式。

自动平均算法(auto 模式)

Core 会记录上一次某一类应用的部署情况,得到 Nodes 可部署方案之后将现有的应用部署情况也进行排序(基于容器/虚拟机数量由少到多)。排序完成之后,在现有容器/虚拟机部署情况下依次遍历各个 Node,然后逐台补齐,直到所补容器/虚拟机数量等同于用户所需总数为止。假如 Nodes 可部署方案并不能满足的话,则判定分布失败,转变为尽可能的满足用户所需。

比如现有的部署方案是 A:3 B:1 C:5 D:4,排序后得到 B:1 A:3 D:4 C:5,资源池有 A:10 B:13 C:7 D:2。

第一轮 A-B=2 B+2 部署情况变为 B:3 A:3 D:4 C:5 第二轮 D-A=1 B+1 A+1 部署情况变为 B:4 A:4 D:4 C:5 第三轮 C-D=1 B+1 A+1 D+1 部署情况变为 B:5 A:5 D:5 C:5 第四轮 B+1 A+1 D+1 C+1 部署情况变为 B:6 A:6 D:6 C:6 第五轮因为 D 机已经没资源,分布算法只考虑 ABC 平均,直到满足用户需求。

因此在用户所需小于等于 11 个新增容器/虚拟机的时候,资源池是满足平均分布的,大于 11 个新增容器/虚拟机也不会完全平均。

每台机器均等部署算法(each 模式)

Core 得到 Nodes 部署方案后,会把计算每个 Node 的可部署量是否满足需求 N,筛选出满足的 Nodes 最后在这些 Nodes 上部署 N 个容器/虚拟机。

比如现有 A:5 B:3 C:7 D:4 (数字代表 Node 能部署的容器/虚拟机数量),根据容量排序后得到 B:3 D:4 A:5 C:7。这时候如果 N 是 3,则 4 台机器均会部署 3 个新的容器/虚拟机,总共是 12 个。如果 N 取 5 ,那么只有 A C 两台机器会部署 5 个容器/虚拟机。

Each 模式可以用于一些大规模基础设施的快速部署。

填充算法(fill 模式)

在每台机器均等部署算法里面,部署的 Nodes 池只跟 Node 本身的容量有关。如果对于某些设施在扩容之后需要针对已有的部署进行填充部署,那么这个模式会比较合适。

比如现在 A:10:2 B:10:3 C:10:5 D:10:7 (Node:剩余容量:已有容器/虚拟机),我们需要将每台补充到 10 个容器/虚拟机,那么执行这个模式部署之后机器状态会变成 A:2:10 B:3:10 C:5:10 D:7:10。如果我们需要补充到 5 个,那么最终会变成 A:7:5 B:8:5 C:10:5 D:10:7。

如果每台机器所需超过某一台可容纳上限,那么这次 fill 部署会失败,因为没办法局部分配,所有节点不会有变化。如 A:3:5 B:1:5 C:1:5 D:1:5 需要补充到 7 个,B, C, D 3 机的剩余容量加上已有的少于所需量 7 ,因此这次分配会失败,即使 A 拥有足够的容量也不会进行分配。

当某些节点的已有容器/虚拟机已经超过本次 fill 所需的容器/虚拟机数量时,分配操作会忽略这些节点。如 A:10:2 B:5:3 C:7:4 D:9:5 需要补充到每台机器到 4 个容器/虚拟机,那么 A 节点会扩容 2 个,B 节点会扩容 1 个,C, D 节点会忽略并维持现状。

全局平均(global 模式)

根据机器实际分配出去的资源量得到机器当前的资源剩余量,由多到少排列,然后进行分布。这种方式主要用于部署容器/虚拟机太少,机器资源过多的情况,从而实现全局上的资源平均。

实时重分配

基于 Docker 的 Update API,我们可以在不下线容器的前提下进行资源的重分配。在 Eru 中重分配的过程是变量的而非定量,也就是说假如 A 类容器有 1.0 个 CPU 和 1024M 内存的情况下,用户需要通过传入目标资源量与当前量的差传入 Eru 来进行操作。就目前的实现来说,Eru 支持 CPU 和 Memory 增(减)量资源实时变动。

需要注意的是,yavirt 目前不支持实时重分配。

无论容器是否绑定了 CPU 核,第一步要做的是针对最终所需 CPU 和 内存进行归类,如使用了 3.2 个核的有 A, B, D 3个容器,使用了 2.7 个核的有 C, E 2个容器,当然真实情况会比这个更复杂。在这个基础上第一步要做的就是按照 Node 维度规整后把资源「还」回去,再对资源经行重新计算。要么就这个 Node 没法支持这一组容器新的资源需求,要么就按照容器个数一个个执行重分配。如果重分配失败,则 Node 得减掉容器原始的资源配额,如果成功,则减掉成功后的配额,最后更新 Node 的元数据。

更新容器

更新容器不会导致某一类容器资源变化,老的容器会先暂停然后复用其资源配额启动新容器。如果新容器启动成功并且工作正常,这时候才会干掉老的容器。如果新容器启动中并不正常,则会重启老容器删掉新容器。这个过程中资源是不需要再次分配的。

需要注意的是,yavirt 目前不支持更新。

NUMA 支持

在 CPUBind 模式下,如果 Node 注册的时候包含 NUMA 信息,CPU 分配策略会尽可能的使得目标使用同一个 NUMA Node 上的 CPU。如果所需容器数超过 NUMA Node 能分配的数量,则会自动跨 NUMA Node 计算还能部署多少容器,尽可能的满足部署需求。比如一个 NUMA Node 包含 2 核 1G 内存,有 2 个 NUMA Node。这时候需要 1 核 600M 的容器,则会计算出最多能部署 3 个。分别是 Node 0 和 1 各自 1 个,跨 Node 1 个。

动态修改资源

Eru 支持通过 SetNode 接口动态修改资源的额度,修改不会对现有已经占有资源的容器造成影响。