关于rust:DatenLord-Rust-实现K8S调度扩展

21次阅读

共计 5619 个字符,预计需要花费 15 分钟才能阅读完成。

背景介绍

K8S 调度器 (kube-scheduler) 是 K8S 调度 pod 的控制器,它作为 K8S 外围组件运行。调度器基于肯定的调度策略为 pod 找到适合的运行节点。

调度流程

K8S 调度器调度一个 pod 时候次要分为三个阶段

  1. 过滤阶段:调用一系列 predicate 函数或者叫 filter 函数过滤掉不适合的节点,比如说某些节点上不满足 pod 申明的运行所须要的资源,如 CPU,内存等资源,那么这些节点会在这个阶段被过滤掉。
  2. 打分阶段:调用一系列 priority 函数或者叫 score 函数给通过第一步过滤的节点打分排序。比方有三个节点通过了过滤阶段,当三个节点通过资源平衡函数的打分,那么最终残余 CPU,内存等资源更多的节点会取得更高的优先级评分。
  3. 调度阶段:把 pod 调度到抉择优先级最高的节点上

调度过程如下图所示:

调度算法

前文提到,K8S 调度器会调用一系列 filter 函数和 priority 函数,这些函数是 K8S 内置的调度算法, K8S 会依据默认的调度算法将 pod 调度到最合适的节点上。filter 函数的目标是过滤掉不合乎 pod 要求的节点,通常会有多个节点通过 filter 阶段,prioriry 函数的目标就是在剩下的节点中选出最合适的节点。

咱们来介绍一下 K8S 默认的调度策略有哪些。

filter 策略: 

  • PodFitsResources: 这条策略是过滤掉所剩资源不满足 pod 要求的节点
  • MatchNodeSelector: 这条策略是过滤掉不匹配 pod nodeSelector 要求的节点
  • PodFitsHostPorts: 这条策略是过滤掉 pod 申明的 HostPort 曾经被占用的节点
  • 等等

priority 策略: 

  • BalancedResourceAllocation: 这条策略心愿当 pod 被调度到一个节点后 CPU,内存使用率是平衡的。
  • LeastRequestedPriority: 这条策略基于节点的闲暇局部,闲暇局部根本算法就是(节点总容量 – 已有 pod 的申请 – 将要被调度的 pod 申请)/ 节点总容量,有更多的闲暇局部的节点会取得更高的优先级。
  • 等等

K8S 提供了很多调度策略,咱们在这里就不一一列举,K8S 并不会应用所有的这些调度策略,它会默认应用局部调度策略,咱们能够通过 K8S 配置文件来组合应用这些策略来达到最合乎咱们利用场景的调度策略。

调度扩大

前文中介绍到 K8S 会提供很多调度策略,它们很大水平上容许咱们管制 K8S 调度器的行为,然而 K8S 提供的调度策略也有它的局限性,它只能依据通用的规范如 CPU,内存的使用率来设计调度策略,当咱们的利用须要更有针对性的调度策略时,比如说咱们想把一个 pod 调度到网路带宽更高,提早更低的节点上,这时候 K8S 提供的调度策略很难满足咱们的需要。侥幸的是 K8S 提供了 K8S 调度扩大机制。K8S 调度扩大提供给用户扩大调度策略的办法。K8S 调度扩大容许用户提供定制的 predicate/filter 函数和 priority/score 函数来扩大的调度策略。调度扩大的实现形式是用户提供一个 web 服务,并将 web 服务的 url 通过配置文件告诉 K8S,K8S 会在调度 pod 的过滤阶段和打分阶段通过 RESTful API 的 POST 办法来调用用户提供的自定义扩大函数,从而达到扩大 K8S 调度策略的成果。具体架构如下图所示:

在 K8S 过滤阶段,kube-scheculer 发送 POST ${URL}/filter 到 scheduler-extender,申请中蕴含 Json 格局序列化的数据 ExtenderArgs,scheduler-extender 收到申请后,通过本地自定义的 filter 函数,产生 filter 后果 ExtenderFilterResult 并序列化成 Json 格局返回。
在打分阶段,kube-scheculer 发送 POST ${URL}/priority 到 scheduler-extender,申请中蕴含 Json 格局序列化的数据 ExtenderArgs,scheduler-extender 收到申请后,通过本地自定义的 priority 函数,产生 score 后果 List<HostPriority> 并序列化成 Json 格局返回。

数据结构

如上所示,K8S 在和用户调度扩大交互的过程中波及到三个数据结构,K8S 应用 go 语言来定义它们,在通过 http 发送的时候会把它们序列化成 Json 格局。在 Rust 实现的调度扩大中需用将这些数据结构用 Rust 语言定义,并进行序列化和反序列化。

调度扩大

申请这个数据结构是 K8S 发送的调度扩大申请,filter 和 priority 申请的构造是雷同的。go 语言定义如下:

type ExtenderArgs struct {
    // Pod being scheduled
    Pod *v1.Pod
    // List of candidate nodes where the pod can be scheduled; to be populated
    // only if Extender.NodeCacheCapable == false
    Nodes *v1.NodeList
    // List of candidate node names where the pod can be scheduled; to be
    // populated only if Extender.NodeCacheCapable == true
    NodeNames *[]string}
  • Pod 示意须要调度的 pod
  • Nodes 示意候选的节点列表
  • NodeNames 示意候选的节点名字列表

这里须要留神的是 Nodes 和 NodeNames 只有一个会被填写,所以在 Rust 语言中须要将这两个域定义成 Option,Rust 的定义如下:

#[derive(Clone, Debug, Serialize, Deserialize)]
struct ExtenderArgs {
    /// Pod being scheduled
    pub Pod: Pod,
    /// List of candidate nodes where the pod can be scheduled; to be populated
    /// only if Extender.NodeCacheCapable == false
    pub Nodes: Option<NodeList>,
    /// List of candidate node names where the pod can be scheduled; to be
    /// populated only if Extender.NodeCacheCapable == true
    pub NodeNames: Option<Vec<String>>,
}

filter 申请的应答

这个数据结构作为 predicate 申请的应答,它蕴含了通过过滤的节点列表,节点失败的起因和错误信息。go 语言的定义如下:

type ExtenderFilterResult struct {
    // Filtered set of nodes where the pod can be scheduled; to be populated
    // only if Extender.NodeCacheCapable == false
    Nodes *v1.NodeList
    // Filtered set of nodes where the pod can be scheduled; to be populated
    // only if Extender.NodeCacheCapable == true
    NodeNames *[]string
    // Filtered out nodes where the pod can't be scheduled and the failure messages
    FailedNodes FailedNodesMap
    // Error message indicating failure
    Error string
}
  • Nodes 示意通过 filter 函数的节点列表
  • NodeNames 示意通过 filter 函数的节点名字列表
  • FailedNodes 是一个 hashmap,保留了没有通过 filter 函数的节点和没有通过的起因
  • Error 示意 filter 函数过程中的失败起因

同样 Nodes 和 NodesNames 只有一个会被填写,也须要定义成 Option,Rust 的定义如下:

#[derive(Clone, Debug, Serialize, Deserialize)]
struct ExtenderFilterResult {
    /// Filtered set of nodes where the pod can be scheduled; to be populated
    /// only if Extender.NodeCacheCapable == false
    pub Nodes: Option<NodeList>,
    /// Filtered set of nodes where the pod can be scheduled; to be populated
    /// only if Extender.NodeCacheCapable == true
    pub NodeNames: Option<Vec<String>>,
    /// Filtered out nodes where the pod can't be scheduled and the failure messages
    pub FailedNodes: HashMap<String, String>,
    /// Error message indicating failure
    pub Error: String,
}

priority 申请的应答

priority 申请的应答是一个 HostPriority 的列表,HostPrority 蕴含节点的名字和节点的分数,go 的定义如下:

type HostPriority struct {
    // Name of the host
    Host string
    // Score associated with the host
    Score int64
}
  • Host 示意节点的名字
  • Score 示意 priority 函数给该节点打的分数

对应的 Rust 定义如下:

#[derive(Clone, Debug, Serialize, Deserialize)]
struct HostPriority {
    /// Name of the host
    pub Host: String,
    /// Score associated with the host
    pub Score: i64,
}

在后面这三个数据结构中应用的类型比如说 Pod, NodeList 在 K8S-openapi 这个库中有 Rust 版本的定义,能够通过在 Cargo.toml 中退出依赖来应用它

K8S-openapi = {version = "0.11.0", default-features = false, features = ["v1_19"] }

这个库是依据 K8S openapi 的定义主动生成的 Rust 定义,节俭了大家把 go 定义的数据结构转成 Rust 定义的麻烦,然而这个库只蕴含 core API 局部的数据结构,ExtenderArgs,ExtenderFilterResult 和 HostPriority 属于 extender 的 API,所以须要本人定义。在应用过程中发现了 go 语言定义的一个问题, 次要起因是 []string 在 go 语言中,[] 和 nil 都是无效的值,序列化对应到 Json 中别离是 [] 和 null,一个域能够是 null 代表了它是 optional 的,所以类型是 []string 的域须要加上 +optional 标记,相应的在 Rust 定义中也须要定义成 Option。
具体探讨请参考 issue, 目前这个问题在 K8S 中曾经失去修复。

调度扩大 Web 服务

有了数据结构就须要实现一个 web 服务来解决 K8S 发动的申请,web 服务 Rust 有丰盛的库能够应用,这里应用的是一个轻量级同步 http 的库(tiny-http),具体 filter 函数和 priority 函数的实现与具体业务逻辑相干,这里就不具体展现,示意代码如下:

match *request.method() {Method::Post => match request.url() {
        "/filter" | "/prioritize" => {let body = request.as_reader();
            let args: ExtenderArgs = try_or_return_err!(
                request,
                serde_json::from_reader(body),
                "failed to parse request".to_string());

            let response = if request.url() == "/filter" {info!("Receive filter");
                // 解决 `filter` 申请
                let result = self.filter(args);
                try_or_return_err!(
                    request,
                    serde_json::to_string(&result),
                    "failed to serialize response".to_string())
            } else {info!("Receive prioritize");
                // 解决 `priority` 申请
                let result = Self::prioritize(&args);
                try_or_return_err!(
                    request,
                    serde_json::to_string(&result),
                    "failed to serialize response".to_string())
            };
            Ok(request.respond(Response::from_string(response))?)
        }
        _ => Self::empty_400(request),
    },
    ... 省略
}

总结

通过本文的介绍咱们对 K8S 的调度流程,K8S 的调度算法,K8S 调度扩大机制有了根本的理解。并且咱们用 Rust 语言实现了 K8S 调度扩大,用 Rust 语言定义了 K8S 和调度扩大之间交互的数据结构,以及介绍了 Rust 定义中须要定义成 Option 的域以及相干须要留神的问题。

作者 | 潘政

转自《Rust Magazine 中文精选》

正文完
 0