沃趣科技
熊中哲·联合创始人/产品研发团队总监
前文我们介绍了基于 Kubernetes 实现的下一代私有 RDS. 其中, 调度策略是具体实现时至关重要的一环, 它关系到 RDS 集群的服务质量和部署密度. 那么, RDS 需要怎样的调度策略呢?本文通过数据库的视角结合 Kubernetes 的源码, 分享一下我的理解.
It was the best of times, it was the worst of times.
--- by Dickens.
人类从爬行到直立用了几百万年, 但是我们这些码农从 Bare Metal 到 Container 只花了几万分之一的时间.
我有个朋友是维护 Mainframe 的, 他还在使用40年前的系统.
看看巨人们在干什么, 有助于我们更好的理解这个世界.
先看看 Google 是如何看待 Borg (Kubernetes 的前身) 的核心价值. 在 Google paper <Large-scale cluster management at Google with Borg>中,开篇就定义了 Borg :
It achieves high utilization by combining admission control, efficient task-packing, over-commitment, and machine sharing with process-level performance isolation.
PS : 里面还转门介绍了基于 CPI (Cycles Per Instruction)测量资源利用率的方式
再看看公有云的领头羊, AWS 是这样描述其 RDS 产品的:
可见, 不管是 Google Borg 还是 AWS RDS, 除了提供更灵活, 更开放, 更兼容, 更安全, 可用性更高的集群, 都将cost-efficient, high utilization 放到了极为重要的位置.
提高部署密度, 减少硬件的需求量, 最终达到降低硬件投入的目标. |
---|
同时,
必须满足业务需求. |
---|
本文尝试以数据库的视角, 从多个角度阐述 RDS 场景需要怎样的调度策略.
说明:○
●为了实现更精细化的调度策略, Kubernetes(版本1.7) 调度器提供了 17 个调度算法. 这些算法分为两类 Predicate 和 Priority, 通俗的描述是过滤和打分. 设计思路大致如下 :
○通过过滤算法, 先挑出出满足条件的节点
○通过打分算法, 对上一步挑出的节点进行打分并排名
总分 = (权重1 * 打分函数1) + (权重2 * 打分函数2) + … + (权重n * 打分函数n)
○选择分数最高的节点, 如果有分数相同的, 随机挑一个
●本文将基于 Kubernetes 的实现, 结合 RDS 场景展开, 并不会把所有的算法流水账似的写一遍, 相关资料很多, 有兴趣的同学可以去看文档.具体实现见
kubernetes/plugin/pkg/scheduler/algorithm/priorities
kubernetes/plugin/pkg/scheduler/algorithm/predicates
下面进入主题,
这里讨论的计算资源仅包含 CPU, Memory
需要特别说明, 毕竟Kubernetes 已经支持 GPU
看上去很简单, 挑选出一个满足资源要求的节点即可, 但是考虑到整合密度和数据库的业务特点并不简单, 我们还需要考虑到以下几点 :
●峰值和均值 :
数据库的负载随着业务, 时间, 周期不断变化, 到底是基于峰值调度还是均值调度呢? 这是一个有关部署密度的问题, 最好的办法就像 Linux 里面限定资源的方式, 让我们设置 Soft Limit 和 Hard Limit, 以Soft Limit分配资源, 同时Hard Limit又能限定使用的最大资源. Kubernetes 也是这么做的, 它会通过 Request 和 Limit 两个阈值来进行管理容器的资源使用.
Pod 是 Kubernetes 的调度单位
Requst 作为 Pod 初始分配值, Limit 限定了 Pod 能使用的最大值. 分配时采用 Requst 值进行调度, 这里有个假设 :
同一节点上运行的容器不会同时达到 Limit 阈值 |
---|
有效的实现了计算资源利用率的 high utilization, 非常适合数据库开发或测试场景.
如果假设不成立?
当某节点运行的所有容器同时接近 Limit, 并有将节点资源用完的趋势或者事实(在运行的过程中, 调度器会定期收集所有节点的资源使用情况, “搜集”用词不太准确, 但便于理解), 创建 Pod的请求也不会再调度到该节点.
以内存为例, 当 Pod 的请求超出 Node 可以提供的内存, 会以异常的方式告知调度器, 内存资源不足
同时, 基于优先级, 部分容器将会被驱逐到其他节点(例如通过重启 Pod 的方式). 所以并不适合生产环境
●资源的平衡 :
对于长期运行的集群, 在满足资源的同时还要考虑到集群中各节点资源分配的平衡性.
类似 Linux Buddy System, 仅仅分配进程需要的内存是不够的, 还要保障操作系统内存的连续性. |
---|
举个例子, RDS 集群有两个节点, 用户向 RDS 申请 2颗CPU和4GB内存 以创建 MySQL实例, 两节点资源使用情况如下:
节点名称 | 已用/总体(CPU核数) | 已用/总体(内存) |
---|---|---|
Node A | 8/16 | 8/32 |
Node B | 8/32 | 16/64 |
在资源同时满足的情况下, 调度会通过两个公式对节点打分.
基于已使用资源比率(BalancedResource)打分, 实现如下.
将节点资源输入公式, 可简化成 :
NodeA 分数 = int(1-math.Abs(8/16 - 8/32)) * float64(10) = 30/4 NodeB 分数 = int(1-math.Abs(8/32 - 16/64)) * float64(10) = 10 |
---|
基于该算法 NodeB 的分数更高.
再通过未使用资源(calculateUnused)持续打分.
该算法可简化成 :
cpu((capacity - sum(requested)) * 10 / capacity) + memory((capacity - sum(requested)) * 10 / capacity) / 2 |
---|
有兴趣的同学可以算一下, 不再赘述.
数据库会被调度到综合打分最高的节点.
存储资源是有状态服务中至关重要的一环, 也让有状态服务的实现难度远超无状态服务.
除了满足请求数据库的存储资源的容量要求, 调度策略必须要能够识别底层的存储架构和存储负载, 在提供存储资源的同时, 满足数据库的业务需求(比如数据零丢失和高可用)
从2017年年初开始, 基于分布式存储技术, 我们的 RDS 已经实现了计算和存储分离的架构.
计算存储分离
在实现数据库的数据零丢失,高可用的同时, 架构变得更通用, 更简单. 但对企业级用户, 还远远不够, cost-efficient 是考量产品成熟度的重要因素.
所以从一开始, 我们就以3种维度的存储 QoS 来思考这个问题 :
●从功能角度 :
存储资源分成两大类
●distribution, 基于分布式存储技术实现, 对 Flash 设备做了专门的优化, 提供数据冗余和弹性扩容功能
●local, 使用计算节点本地存储
对于生产环境, 我们会申请 distribution 资源. 而那些不太重要的或者临时性的, 譬如有的客户需要经常生成临时性的克隆库进行测试, 或者扩展备库以应对突发的业务高峰, 我们会申请 local资源.
●从性能角度 :
我们又将 distribution 分成了两类 high 和 medium , 以应业务不同的 IOPS, Throughput, Latency 需求.
IO 密集型业务, 我们会分配 high 类型. 对于计算密集型或者备库, 我们会分配 medium 类型.
●从数据库角度
比如, 不同的数据库物理卷的挂载参数也不同
如果能够调度器实现, 将极大的提高存储资源的 cost-efficient
这些特性带有明显的数据库业务特性, 原生的 Kubernetes 调度器并不支持. 但是, 我们通过二次开发, 以 Out of Cluster 的方式实现了外置的 Kubernetes storage provisoner. 并通过自定义的参数和代码实现和调度器的交互.
Kubernetes 会使用我们提供的 storage provisoner 创建存储资源.
这样 Kubernetes 的调度器就可以基于 RDS 的业务需求, 感知底层存储架构, 提供满足业务需求的调度服务.
除去需要的容量信息, 需要传递给调度器如下信息(就像请求 CPU, Memory 资源一样) :
…. volume.beta.kubernetes.io/mount-options: sync volume.orain.com/storage-type: "distribution" volume.orain.com/storage-qos: "high" volume.orain.com/dc-id: "278" ... |
---|
通过这四个参数将会告知
●从功能角度 :
volume.orain.com/storage-type: "distribution", 使用 distribution 类型存储资源 |
---|
●从性能角度 :
volume.orain.com/storage-qos: "high", 从高性能存储池获取 Volume |
---|
●从数据库角度
volume.beta.kubernetes.io/mount-options: sync, 使用特定 mount 参数 volume.orain.com/dc-id: "278", 使用编号为278的 Volume |
---|
关系型数据库是有状态服务, 但要求更加复杂. 比如我们提供了 MySQL 的 ReadWrite Cluster (分库分表集群) 和 Sharding Cluster (读写分离集群),每个数据库实例都有自己的角色. 调度器必须感知集群角色以实现业务特点 :
比如, 基于数据库角色, 我们有如下调度需求 :
1.ReadWrite Cluster 的 Master 和 Slave 不能调度到同一节点
2.Master 的多个 Slave 不能调度到同一节点
3.Sharding Cluster 的每个分片不能调度到同一节点
4.某些备份任务须调度到指定 Slave 所在的节点
5.…..
带有明显的业务(RDS)特点, 原生 Kuberentes 的调度策略并不能识别这些角色和关系.
与此同时, 容器的运行状态和RDS集群还在动态变化
因 Failover 迁移到其他节点
RDS 集群 Scale Out
首先, 我们将一系列的具体的业务需求抽象成 :
亲和性(Affinity), 反亲和性(Anti-Affinity), 分布度(Spread Width) |
---|
再通过我们的二次开发, 将数据库的角色和业务流程集成到调度器中, 以满足全部需求.
●亲和性(Affinity)
调度需求4 可以归纳到这里
需求4 : 某些备份任务须调度到指定 Slave 所在的节点 |
---|
以待调度备份任务为输入, 在所有节点中找到指定 Slave 所在节点, 以确定待调度备份任务调度到哪个节点. 该需求必须满足, 不然备份任务无法成功.
查找该节点所有数据库实例
确定该节点是否有指定 Slave
●反亲和性(Anti-Affinity)
调度需求1 可以归纳到这里
需求1 : ReadWrite Cluster 的 Master 和 Slave 不能调度到同一节点 |
---|
以待调度数据库的角色为输入, 建立已运行数据库和节点的关系, 再通过 Anti-Affinity 公式对所有节点打分, 以此决定待调度数据库是否要调度到该节点.
以需求1为例, 统计集群成员的分布情况, 该节点上同一数据库集群的成员越多, 分数越低
反亲和性(Anti-Affinity)公式
对所有节点打分
●分布度(Spread Width)
有种更时髦的叫法散射度(scatter width)
需求2,3 可以归纳到这里.
以需求2为例, 统计集群成员的分布情况, 该节点上同一数据库集群的成员越多, 分数越低
然后对所有节点打分, 公式如下 :
float64(schedulerapi.MaxPriority) * ((maxCountByNodeName - countsByNodeName[node.Name]) / maxCountByNodeName) |
---|
需要特别说明的是, 在 RDS 进行调度时
●需求 1,4 必须满足
●需求 2,3 尽量满足既可以.
必须和尽量也需要作为调度参数, 让调度器知晓.
本文仅以 RDS 的视角, 从三个层级讲述了对调度器的要求.
真实的场景会更复杂, 比如针对 ReadWrite Cluster , Slave 必须等待 Master 创建完毕.
而 Sharding Cluster, 所有分片可以并发创建以提高效率, ...
在设计产品和完成编码的过程中, 踩坑无数.不能否认的是, 站在巨人的肩膀上可以让我们看的更远. 不知道 Ending 怎么写, 就这样吧...
BTW, 如果你对我们正在做的事情感兴趣, 投简历我吧
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。