k8s调度原理以及自定义调度器

kube-scheduler 是 kubernetes 的核心组件之一,主要负责整个集群资源的调度功能,根据特定的调度算法和策略,将 Pod 调度到最优的工作节点上面去,从而更加合理、更加充分的利用集群的资源,这也是我们选择使用 kubernetes 一个非常重要的理由。如果一门新的技术不能帮助企业节约成本、提供效率,我相信是很难推进的。

1. 调度流程

默认情况下,kube-scheduler 提供的默认调度器能够满足我们绝大多数的要求,我们前面和大家接触的示例也基本上用的默认的策略,都可以保证我们的 Pod 可以被分配到资源充足的节点上运行。但是在实际的线上项目中,可能我们自己会比 kubernetes 更加了解我们自己的应用,比如我们希望一个 Pod 只能运行在特定的几个节点上,或者这几个节点只能用来运行特定类型的应用,这就需要我们的调度器能够可控。

kube-scheduler 的主要作用就是根据特定的调度算法和调度策略将 Pod 调度到合适的 Node 节点上去,是一个独立的二进制程序,启动之后会一直监听 API Server,获取到 PodSpec.NodeName 为空的 Pod,对每个 Pod 都会创建一个 binding。

这个过程在我们看来好像比较简单,但在实际的生产环境中,需要考虑的问题就有很多了:

  • 如何保证全部的节点调度的公平性?要知道并不是所有节点资源配置一定都是一样的

  • 如何保证每个节点都能被分配资源?

  • 集群资源如何能够被高效利用?

  • 集群资源如何才能被最大化使用?

  • 如何保证 Pod 调度的性能和效率?

  • 用户是否可以根据自己的实际需求定制自己的调度策略?

考虑到实际环境中的各种复杂情况,kubernetes 的调度器采用插件化的形式实现,可以方便用户进行定制或者二次开发,我们可以自定义一个调度器并以插件形式和 kubernetes 进行集成。

kubernetes 调度器的源码位于 kubernetes/pkg/scheduler 中,大体的代码目录结构如下所示:(不同的版本目录结构可能不太一样)

kubernetes/pkg/scheduler
-- scheduler.go         //调度相关的具体实现
|-- algorithm
|   |-- predicates      //节点筛选策略
|   |-- priorities      //节点打分策略
|-- algorithmprovider
|   |-- defaults         //定义默认的调度器

2. 自定义调度器说明

一般来说,我们有 4 种扩展 Kubernetes 调度器的方法。

  • 一种方法就是直接 clone 官方的 kube-scheduler 源代码,在合适的位置直接修改代码,然后重新编译运行修改后的程序,当然这种方法是最不建议使用的,也不实用,因为需要花费大量额外的精力来和上游的调度程序更改保持一致。

  • 第二种方法就是和默认的调度程序一起运行独立的调度程序,默认的调度器和我们自定义的调度器可以通过 Pod 的 spec.schedulerName 来覆盖各自的 Pod,默认是使用 default 默认的调度器,但是多个调度程序共存的情况下也比较麻烦,比如当多个调度器将 Pod 调度到同一个节点的时候,可能会遇到一些问题,因为很有可能两个调度器都同时将两个 Pod 调度到同一个节点上去,但是很有可能其中一个 Pod 运行后其实资源就消耗完了,并且维护一个高质量的自定义调度程序也不是很容易的,因为我们需要全面了解默认的调度程序,整体 Kubernetes 的架构知识以及各种 Kubernetes API 对象的各种关系或限制。

  • 第三种方法是调度器扩展程序,这个方案目前是一个可行的方案,可以和上游调度程序兼容,所谓的调度器扩展程序其实就是一个可配置的 Webhook 而已,里面包含 过滤器优先级 两个端点,分别对应调度周期中的两个主要阶段(过滤和打分)。

  • 第四种方法是通过调度框架(Scheduling Framework),Kubernetes v1.15 版本中引入了可插拔架构的调度框架,使得定制调度器这个任务变得更加的容易。调库框架向现有的调度器中添加了一组插件化的 API,该 API 在保持调度程序“核心”简单且易于维护的同时,使得大部分的调度功能以插件的形式存在,而且在我们现在的 v1.16 版本中上面的 调度器扩展程序 也已经被废弃了,所以以后调度框架才是自定义调度器的核心方式。

这里我们可以简单介绍下后面两种方式的实现。

3. 调度器扩展程序

在进入调度器扩展程序之前,我们再来了解下 Kubernetes 调度程序是如何工作的:

  1. 默认调度器根据指定的参数启动(我们使用 kubeadm 搭建的集群,启动配置文件位于 /etc/kubernetes/manifests/kube-schdueler.yaml

  2. watch apiserver,将 spec.nodeName 为空的 Pod 放入调度器内部的调度队列中

  3. 从调度队列中 Pop 出一个 Pod,开始一个标准的调度周期

  4. 从 Pod 属性中检索“硬性要求”(比如 CPU/内存请求值,nodeSelector/nodeAffinity),然后过滤阶段发生,在该阶段计算出满足要求的节点候选列表

  5. 从 Pod 属性中检索“软需求”,并应用一些默认的“软策略”(比如 Pod 倾向于在节点上更加聚拢或分散),最后,它为每个候选节点给出一个分数,并挑选出得分最高的最终获胜者

  6. 和 apiserver 通信(发送绑定调用),然后设置 Pod 的 spec.nodeName 属性以表示将该 Pod 调度到的节点。

我们可以通过查看官方文档,可以通过 --config 参数指定调度器将使用哪些参数,该配置文件应该包含一个 KubeSchedulerConfiguration 对象,如下所示格式:(/etc/kubernetes/scheduler-extender.yaml)

# 通过"--config" 传递文件内容
apiVersion: kubescheduler.config.k8s.io/v1alpha1
kind: KubeSchedulerConfiguration
clientConnection:kubeconfig: "/etc/kubernetes/scheduler.conf"
algorithmSource:policy:file:path: "/etc/kubernetes/scheduler-extender-policy.yaml" # 指定自定义调度策略文件

我们在这里应该输入的关键参数是 algorithmSource.policy,这个策略文件可以是本地文件也可以是 ConfigMap 资源对象,这取决于调度程序的部署方式,比如我们这里默认的调度器是静态 Pod 方式启动的,所以我们可以用本地文件的形式来配置。

该策略文件 /etc/kubernetes/scheduler-extender-policy.yaml 应该遵循 kubernetes/pkg/scheduler/apis/config/legacy_types.go#L28 的要求,在我们这里的 v1.16.2 版本中已经支持 JSON 和 YAML 两种格式的策略文件,下面是我们定义的一个简单的示例,可以查看 Extender 描述了解策略文件的定义规范:

apiVersion: v1
kind: Policy
extenders:- urlPrefix: "http://127.0.0.1:8888/"filterVerb: "filter"prioritizeVerb: "prioritize"weight: 1enableHttps: false

我们这里的 Policy 策略文件是通过定义 extenders 来扩展调度器的,有时候我们不需要去编写代码,可以直接在该配置文件中通过指定 predicatespriorities 来进行自定义,如果没有指定则会使用默认的 DefaultProvier

{"kind": "Policy","apiVersion": "v1","predicates": [{"name": "MatchNodeSelector"},{"name": "PodFitsResources"},{"name": "PodFitsHostPorts"},{"name": "HostName"}],"priorities": [{"name": "EqualPriority","weight": 2},{"name": "ImageLocalityPriority","weight": 4},{"name": "LeastRequestedPriority","weight": 2},{"name": "BalancedResourceAllocation","weight": 2}],"extenders": [{"urlPrefix": "/prefix","filterVerb": "filter","prioritizeVerb": "prioritize","weight": 1,"bindVerb": "bind","enableHttps": false}]
}

改策略文件定义了一个 HTTP 的扩展程序服务,该服务运行在 127.0.0.1:8888 下面,并且已经将该策略注册到了默认的调度器中,这样在过滤和打分阶段结束后,可以将结果分别传递给该扩展程序的端点 <urlPrefix>/<filterVerb><urlPrefix>/<prioritizeVerb>,在扩展程序中,我们可以进一步过滤并确定优先级,以适应我们的特定业务需求。

示例

我们直接用 golang 来实现一个简单的调度器扩展程序,当然你可以使用其他任何编程语言,如下所示:

func main() {router := httprouter.New()router.GET("/", Index)router.POST("/filter", Filter)router.POST("/prioritize", Prioritize)log.Fatal(http.ListenAndServe(":8888", router))
}

然后接下来我们需要实现 /filter/prioritize 两个端点的处理程序。

其中 Filter 这个扩展函数接收一个输入类型为 schedulerapi.ExtenderArgs 的参数,然后返回一个类型为 *schedulerapi.ExtenderFilterResult 的值。在函数中,我们可以进一步过滤输入的节点:

// filter 根据扩展程序定义的预选规则来过滤节点
func filter(args schedulerapi.ExtenderArgs) *schedulerapi.ExtenderFilterResult {var filteredNodes []v1.NodefailedNodes := make(schedulerapi.FailedNodesMap)pod := args.Podfor _, node := range args.Nodes.Items {fits, failReasons, _ := podFitsOnNode(pod, node)if fits {filteredNodes = append(filteredNodes, node)} else {failedNodes[node.Name] = strings.Join(failReasons, ",")}}result := schedulerapi.ExtenderFilterResult{Nodes: &v1.NodeList{Items: filteredNodes,},FailedNodes: failedNodes,Error:       "",}return &result
}

在过滤函数中,我们循环每个节点然后用我们自己实现的业务逻辑来判断是否应该批准该节点,这里我们实现比较简单,在 podFitsOnNode() 函数中我们只是简单的检查随机数是否为偶数来判断即可,如果是的话我们就认为这是一个幸运的节点,否则拒绝批准该节点。

var predicatesSorted = []string{LuckyPred}var predicatesFuncs = map[string]FitPredicate{LuckyPred: LuckyPredicate,
}type FitPredicate func(pod *v1.Pod, node v1.Node) (bool, []string, error)func podFitsOnNode(pod *v1.Pod, node v1.Node) (bool, []string, error) {fits := truevar failReasons []stringfor _, predicateKey := range predicatesSorted {fit, failures, err := predicatesFuncs[predicateKey](pod, node)if err != nil {return false, nil, err}fits = fits && fitfailReasons = append(failReasons, failures...)}return fits, failReasons, nil
}func LuckyPredicate(pod *v1.Pod, node v1.Node) (bool, []string, error) {lucky := rand.Intn(2) == 0if lucky {log.Printf("pod %v/%v is lucky to fit on node %v\n", pod.Name, pod.Namespace, node.Name)return true, nil, nil}log.Printf("pod %v/%v is unlucky to fit on node %v\n", pod.Name, pod.Namespace, node.Name)return false, []string{LuckyPredFailMsg}, nil
}

同样的打分功能用同样的方式来实现,我们在每个节点上随机给出一个分数:

// it's webhooked to pkg/scheduler/core/generic_scheduler.go#PrioritizeNodes()
// 这个函数输出的分数会被添加会默认的调度器
func prioritize(args schedulerapi.ExtenderArgs) *schedulerapi.HostPriorityList {pod := args.Podnodes := args.Nodes.ItemshostPriorityList := make(schedulerapi.HostPriorityList, len(nodes))for i, node := range nodes {score := rand.Intn(schedulerapi.MaxPriority + 1)  // 在最大优先级内随机取一个值log.Printf(luckyPrioMsg, pod.Name, pod.Namespace, score)hostPriorityList[i] = schedulerapi.HostPriority{Host:  node.Name,Score: score,}}return &hostPriorityList
}

然后我们可以使用下面的命令来编译打包我们的应用:

$ GOOS=linux GOARCH=amd64 go build -o app

本节调度器扩展程序完整的代码获取地址:https://github.com/cnych/sample-scheduler-extender。

构建完成后,将应用 app 拷贝到 kube-scheduler 所在的节点直接运行即可。现在我们就可以将上面的策略文件配置到 kube-scheduler 组件中去了,我们这里集群是 kubeadm 搭建的,所以直接修改文件 /etc/kubernetes/manifests/kube-schduler.yaml 文件即可,内容如下所示:

apiVersion: v1
kind: Pod
metadata:creationTimestamp: nulllabels:component: kube-schedulertier: control-planename: kube-schedulernamespace: kube-system
spec:containers:- command:- kube-scheduler- --authentication-kubeconfig=/etc/kubernetes/scheduler.conf- --authorization-kubeconfig=/etc/kubernetes/scheduler.conf- --bind-address=127.0.0.1- --kubeconfig=/etc/kubernetes/scheduler.conf- --leader-elect=true- --config=/etc/kubernetes/scheduler-extender.yaml- --v=9image: gcr.azk8s.cn/google_containers/kube-scheduler:v1.16.2imagePullPolicy: IfNotPresentlivenessProbe:failureThreshold: 8httpGet:host: 127.0.0.1path: /healthzport: 10251scheme: HTTPinitialDelaySeconds: 15timeoutSeconds: 15name: kube-schedulerresources:requests:cpu: 100mvolumeMounts:- mountPath: /etc/kubernetes/scheduler.confname: kubeconfigreadOnly: true- mountPath: /etc/kubernetes/scheduler-extender.yamlname: extenderreadOnly: true- mountPath: /etc/kubernetes/scheduler-extender-policy.yamlname: extender-policyreadOnly: truehostNetwork: truepriorityClassName: system-cluster-criticalvolumes:- hostPath:path: /etc/kubernetes/scheduler.conftype: FileOrCreatename: kubeconfig- hostPath:path: /etc/kubernetes/scheduler-extender.yamltype: FileOrCreatename: extender- hostPath:path: /etc/kubernetes/scheduler-extender-policy.yamltype: FileOrCreatename: extender-policy
status: {}

当然我们这个地方是直接在系统默认的 kube-scheduler 上面配置的,我们也可以复制一个调度器的 YAML 文件然后更改下 schedulerName 来部署,这样就不会影响默认的调度器了,然后在需要使用这个测试的调度器的 Pod 上面指定 spec.schedulerName 即可。对于多调度器的使用可以查看官方文档 配置多个调度器。

kube-scheduler 重新配置后可以查看日志来验证是否重启成功,需要注意的是一定需要将 /etc/kubernetes/scheduler-extender.yaml/etc/kubernetes/scheduler-extender-policy.yaml 两个文件挂载到 Pod 中去:

$ kubectl logs -f kube-scheduler-ydzs-master -n kube-system
I0102 15:17:38.824657       1 serving.go:319] Generated self-signed cert in-memory
I0102 15:17:39.472276       1 server.go:143] Version: v1.16.2
I0102 15:17:39.472674       1 defaults.go:91] TaintNodesByCondition is enabled, PodToleratesNodeTaints predicate is mandatory
W0102 15:17:39.479704       1 authorization.go:47] Authorization is disabled
W0102 15:17:39.479733       1 authentication.go:79] Authentication is disabled
I0102 15:17:39.479777       1 deprecated_insecure_serving.go:51] Serving healthz insecurely on [::]:10251
I0102 15:17:39.480559       1 secure_serving.go:123] Serving securely on 127.0.0.1:10259
I0102 15:17:39.682180       1 leaderelection.go:241] attempting to acquire leader lease  kube-system/kube-scheduler...
I0102 15:17:56.500505       1 leaderelection.go:251] successfully acquired lease kube-system/kube-scheduler

到这里我们就创建并配置了一个非常简单的调度扩展程序,现在我们来运行一个 Deployment 查看其工作原理,我们准备一个包含 20 个副本的部署 Yaml:(test-scheduler.yaml)

apiVersion: apps/v1
kind: Deployment
metadata:name: pause
spec:replicas: 20selector:matchLabels:app: pausetemplate:metadata:labels:app: pausespec:containers:- name: pauseimage: gcr.azk8s.cn/google_containers/pause:3.1

直接创建上面的资源对象:

$ kuectl apply -f test-scheduler.yaml
deployment.apps/pause created

这个时候我们去查看下我们编写的调度器扩展程序日志:

$ ./app
......
2020/01/03 12:27:29 pod pause-58584fbc95-bwn7t/default is unlucky to fit on node ydzs-node1
2020/01/03 12:27:29 pod pause-58584fbc95-bwn7t/default is lucky to get score 7
2020/01/03 12:27:29 pod pause-58584fbc95-bwn7t/default is lucky to get score 9
2020/01/03 12:27:29 pod pause-58584fbc95-86w92/default is unlucky to fit on node ydzs-node3
2020/01/03 12:27:29 pod pause-58584fbc95-86w92/default is unlucky to fit on node ydzs-node4
2020/01/03 12:27:29 pod pause-58584fbc95-86w92/default is lucky to fit on node ydzs-node1
2020/01/03 12:27:29 pod pause-58584fbc95-86w92/default is lucky to fit on node ydzs-node2
2020/01/03 12:27:29 pod pause-58584fbc95-86w92/default is lucky to get score 4
2020/01/03 12:27:29 pod pause-58584fbc95-86w92/default is lucky to get score 8
......

我们可以看到 Pod 调度的过程,另外默认调度程序会定期重试失败的 Pod,因此它们将一次又一次地重新传递到我们的调度扩展程序上,我们的逻辑是检查随机数是否为偶数,所以最终所有 Pod 都将处于运行状态。

调度器扩展程序可能是在一些情况下可以满足我们的需求,但是他仍然有一些限制和缺点:

  • 通信成本:数据在默认调度程序和调度器扩展程序之间以 http(s)传输,在执行序列化和反序列化的时候有一定成本

  • 有限的扩展点:扩展程序只能在某些阶段的末尾参与,例如“ Filter”“ Prioritize”,它们不能在任何阶段的开始或中间被调用

  • 减法优于加法:与默认调度程序传递的节点候选列表相比,我们可能有一些需求需要添加新的候选节点列表,但这是比较冒险的操作,因为不能保证新节点可以通过其他要求,所以,调度器扩展程序最好执行“减法”(进一步过滤),而不是“加法”(添加节点)

  • 缓存共享:上面只是一个简单的测试示例,但在真实的项目中,我们是需要通过查看整个集群的状态来做出调度决策的,默认调度程序可以很好地调度决策,但是无法共享其缓存,这意味着我们必须构建和维护自己的缓存

由于这些局限性,Kubernetes 调度小组就提出了上面第四种方法来进行更好的扩展,也就是调度框架(Scheduler Framework),它基本上可以解决我们遇到的所有难题,现在也已经成官方推荐的扩展方式,所以这将是以后扩展调度器的最主流的方式

4. 调度框架

调度框架定义了一组扩展点,用户可以实现扩展点定义的接口来定义自己的调度逻辑(我们称之为扩展),并将扩展注册到扩展点上,调度框架在执行调度工作流时,遇到对应的扩展点时,将调用用户注册的扩展。调度框架在预留扩展点时,都是有特定的目的,有些扩展点上的扩展可以改变调度程序的决策方法,有些扩展点上的扩展只是发送一个通知。

我们知道每当调度一个 Pod 时,都会按照两个过程来执行:调度过程和绑定过程。

调度过程为 Pod 选择一个合适的节点,绑定过程则将调度过程的决策应用到集群中(也就是在被选定的节点上运行 Pod),将调度过程和绑定过程合在一起,称之为调度上下文(scheduling context)。需要注意的是调度过程是同步运行的(同一时间点只为一个 Pod 进行调度),绑定过程可异步运行(同一时间点可并发为多个 Pod 执行绑定)。

调度过程和绑定过程遇到如下情况时会中途退出:

  • 调度程序认为当前没有该 Pod 的可选节点

  • 内部错误

这个时候,该 Pod 将被放回到 待调度队列,并等待下次重试。

扩展点(Extension Points)

下图展示了调度框架中的调度上下文及其中的扩展点,一个扩展可以注册多个扩展点,以便可以执行更复杂的有状态的任务。

  1. QueueSort 扩展用于对 Pod 的待调度队列进行排序,以决定先调度哪个 Pod,QueueSort 扩展本质上只需要实现一个方法 Less(Pod1, Pod2) 用于比较两个 Pod 谁更优先获得调度即可,同一时间点只能有一个 QueueSort 插件生效。

  2. Pre-filter 扩展用于对 Pod 的信息进行预处理,或者检查一些集群或 Pod 必须满足的前提条件,如果 pre-filter 返回了 error,则调度过程终止。

  3. Filter 扩展用于排除那些不能运行该 Pod 的节点,对于每一个节点,调度器将按顺序执行 filter 扩展;如果任何一个 filter 将节点标记为不可选,则余下的 filter 扩展将不会被执行。调度器可以同时对多个节点执行 filter 扩展。

  4. Post-filter 是一个通知类型的扩展点,调用该扩展的参数是 filter 阶段结束后被筛选为可选节点的节点列表,可以在扩展中使用这些信息更新内部状态,或者产生日志或 metrics 信息。

  5. Scoring 扩展用于为所有可选节点进行打分,调度器将针对每一个节点调用 Soring 扩展,评分结果是一个范围内的整数。在 normalize scoring 阶段,调度器将会把每个 scoring 扩展对具体某个节点的评分结果和该扩展的权重合并起来,作为最终评分结果。

  6. Normalize scoring 扩展在调度器对节点进行最终排序之前修改每个节点的评分结果,注册到该扩展点的扩展在被调用时,将获得同一个插件中的 scoring 扩展的评分结果作为参数,调度框架每执行一次调度,都将调用所有插件中的一个 normalize scoring 扩展一次。

  7. Reserve 是一个通知性质的扩展点,有状态的插件可以使用该扩展点来获得节点上为 Pod 预留的资源,该事件发生在调度器将 Pod 绑定到节点之前,目的是避免调度器在等待 Pod 与节点绑定的过程中调度新的 Pod 到节点上时,发生实际使用资源超出可用资源的情况。(因为绑定 Pod 到节点上是异步发生的)。这是调度过程的最后一个步骤,Pod 进入 reserved 状态以后,要么在绑定失败时触发 Unreserve 扩展,要么在绑定成功时,由 Post-bind 扩展结束绑定过程。

  8. Permit 扩展用于阻止或者延迟 Pod 与节点的绑定。Permit 扩展可以做下面三件事中的一项:

    • approve(批准):当所有的 permit 扩展都 approve 了 Pod 与节点的绑定,调度器将继续执行绑定过程

    • deny(拒绝):如果任何一个 permit 扩展 deny 了 Pod 与节点的绑定,Pod 将被放回到待调度队列,此时将触发 Unreserve 扩展

    • wait(等待):如果一个 permit 扩展返回了 wait,则 Pod 将保持在 permit 阶段,直到被其他扩展 approve,如果超时事件发生,wait 状态变成 deny,Pod 将被放回到待调度队列,此时将触发 Unreserve 扩展

  9. Pre-bind 扩展用于在 Pod 绑定之前执行某些逻辑。例如,pre-bind 扩展可以将一个基于网络的数据卷挂载到节点上,以便 Pod 可以使用。如果任何一个 pre-bind 扩展返回错误,Pod 将被放回到待调度队列,此时将触发 Unreserve 扩展。

  10. Bind 扩展用于将 Pod 绑定到节点上:

    • 只有所有的 pre-bind 扩展都成功执行了,bind 扩展才会执行

    • 调度框架按照 bind 扩展注册的顺序逐个调用 bind 扩展

    • 具体某个 bind 扩展可以选择处理或者不处理该 Pod

    • 如果某个 bind 扩展处理了该 Pod 与节点的绑定,余下的 bind 扩展将被忽略

  11. Post-bind 是一个通知性质的扩展:

    • Post-bind 扩展在 Pod 成功绑定到节点上之后被动调用

    • Post-bind 扩展是绑定过程的最后一个步骤,可以用来执行资源清理的动作

  12. Unreserve 是一个通知性质的扩展,如果为 Pod 预留了资源,Pod 又在被绑定过程中被拒绝绑定,则 unreserve 扩展将被调用。Unreserve 扩展应该释放已经为 Pod 预留的节点上的计算资源。在一个插件中,reserve 扩展和 unreserve 扩展应该成对出现。

如果我们要实现自己的插件,必须向调度框架注册插件并完成配置,另外还必须实现扩展点接口,对应的扩展点接口我们可以在源码 pkg/scheduler/framework/v1alpha1/interface.go 文件中找到,如下所示:

// Plugin is the parent type for all the scheduling framework plugins.
type Plugin interface {Name() string
}type QueueSortPlugin interface {PluginLess(*PodInfo, *PodInfo) bool
}// PreFilterPlugin is an interface that must be implemented by "prefilter" plugins.
// These plugins are called at the beginning of the scheduling cycle.
type PreFilterPlugin interface {PluginPreFilter(pc *PluginContext, p *v1.Pod) *Status
}// FilterPlugin is an interface for Filter plugins. These plugins are called at the
// filter extension point for filtering out hosts that cannot run a pod.
// This concept used to be called 'predicate' in the original scheduler.
// These plugins should return "Success", "Unschedulable" or "Error" in Status.code.
// However, the scheduler accepts other valid codes as well.
// Anything other than "Success" will lead to exclusion of the given host from
// running the pod.
type FilterPlugin interface {PluginFilter(pc *PluginContext, pod *v1.Pod, nodeName string) *Status
}// PostFilterPlugin is an interface for Post-filter plugin. Post-filter is an
// informational extension point. Plugins will be called with a list of nodes
// that passed the filtering phase. A plugin may use this data to update internal
// state or to generate logs/metrics.
type PostFilterPlugin interface {PluginPostFilter(pc *PluginContext, pod *v1.Pod, nodes []*v1.Node, filteredNodesStatuses NodeToStatusMap) *Status
}// ScorePlugin is an interface that must be implemented by "score" plugins to rank
// nodes that passed the filtering phase.
type ScorePlugin interface {PluginScore(pc *PluginContext, p *v1.Pod, nodeName string) (int, *Status)
}// ScoreWithNormalizePlugin is an interface that must be implemented by "score"
// plugins that also need to normalize the node scoring results produced by the same
// plugin's "Score" method.
type ScoreWithNormalizePlugin interface {ScorePluginNormalizeScore(pc *PluginContext, p *v1.Pod, scores NodeScoreList) *Status
}// ReservePlugin is an interface for Reserve plugins. These plugins are called
// at the reservation point. These are meant to update the state of the plugin.
// This concept used to be called 'assume' in the original scheduler.
// These plugins should return only Success or Error in Status.code. However,
// the scheduler accepts other valid codes as well. Anything other than Success
// will lead to rejection of the pod.
type ReservePlugin interface {PluginReserve(pc *PluginContext, p *v1.Pod, nodeName string) *Status
}// PreBindPlugin is an interface that must be implemented by "prebind" plugins.
// These plugins are called before a pod being scheduled.
type PreBindPlugin interface {PluginPreBind(pc *PluginContext, p *v1.Pod, nodeName string) *Status
}// PostBindPlugin is an interface that must be implemented by "postbind" plugins.
// These plugins are called after a pod is successfully bound to a node.
type PostBindPlugin interface {PluginPostBind(pc *PluginContext, p *v1.Pod, nodeName string)
}// UnreservePlugin is an interface for Unreserve plugins. This is an informational
// extension point. If a pod was reserved and then rejected in a later phase, then
// un-reserve plugins will be notified. Un-reserve plugins should clean up state
// associated with the reserved Pod.
type UnreservePlugin interface {PluginUnreserve(pc *PluginContext, p *v1.Pod, nodeName string)
}// PermitPlugin is an interface that must be implemented by "permit" plugins.
// These plugins are called before a pod is bound to a node.
type PermitPlugin interface {PluginPermit(pc *PluginContext, p *v1.Pod, nodeName string) (*Status, time.Duration)
}// BindPlugin is an interface that must be implemented by "bind" plugins. Bind
// plugins are used to bind a pod to a Node.
type BindPlugin interface {PluginBind(pc *PluginContext, p *v1.Pod, nodeName string) *Status
}

对于调度框架插件的启用或者禁用,我们同样可以使用上面的 KubeSchedulerConfiguration 资源对象来进行配置。下面的例子中的配置启用了一个实现了 reservepreBind 扩展点的插件,并且禁用了另外一个插件,同时为插件 foo 提供了一些配置信息:

apiVersion: kubescheduler.config.k8s.io/v1alpha1
kind: KubeSchedulerConfiguration---
plugins:reserve:enabled:- name: foo- name: bardisabled:- name: bazpreBind:enabled:- name: foodisabled:- name: bazpluginConfig:- name: fooargs: >foo插件可以解析的任意内容      

扩展的调用顺序如下:

  • 如果某个扩展点没有配置对应的扩展,调度框架将使用默认插件中的扩展

  • 如果为某个扩展点配置且激活了扩展,则调度框架将先调用默认插件的扩展,再调用配置中的扩展

  • 默认插件的扩展始终被最先调用,然后按照 KubeSchedulerConfiguration 中扩展的激活 enabled 顺序逐个调用扩展点的扩展

  • 可以先禁用默认插件的扩展,然后在 enabled 列表中的某个位置激活默认插件的扩展,这种做法可以改变默认插件的扩展被调用时的顺序

假设默认插件 foo 实现了 reserve 扩展点,此时我们要添加一个插件 bar,想要在 foo 之前被调用,则应该先禁用 foo 再按照 bar foo 的顺序激活。示例配置如下所示:

apiVersion: kubescheduler.config.k8s.io/v1alpha1
kind: KubeSchedulerConfiguration---
plugins:reserve:enabled:- name: bar- name: foodisabled:- name: foo

在源码目录 pkg/scheduler/framework/plugins/examples 中有几个示范插件,我们可以参照其实现方式。

示例

其实要实现一个调度框架的插件,并不难,我们只要实现对应的扩展点,然后将插件注册到调度器中即可,下面是默认调度器在初始化的时候注册的插件:

func NewRegistry() Registry {return Registry{// FactoryMap:// New plugins are registered here.// example:// {//  stateful_plugin.Name: stateful.NewStatefulMultipointExample,//  fooplugin.Name: fooplugin.New,// }}
}

但是可以看到默认并没有注册一些插件,所以要想让调度器能够识别我们的插件代码,就需要自己来实现一个调度器了,当然这个调度器我们完全没必要完全自己实现,直接调用默认的调度器,然后在上面的 NewRegistry() 函数中将我们的插件注册进去即可。在 kube-scheduler 的源码文件 kubernetes/cmd/kube-scheduler/app/server.go 中有一个 NewSchedulerCommand 入口函数,其中的参数是一个类型为 Option 的列表,而这个 Option 恰好就是一个插件配置的定义:

// Option configures a framework.Registry.
type Option func(framework.Registry) error// NewSchedulerCommand creates a *cobra.Command object with default parameters and registryOptions
func NewSchedulerCommand(registryOptions ...Option) *cobra.Command {......
}

所以我们完全就可以直接调用这个函数来作为我们的函数入口,并且传入我们自己实现的插件作为参数即可,而且该文件下面还有一个名为 WithPlugin 的函数可以来创建一个 Option 实例:

// WithPlugin creates an Option based on plugin name and factory.
func WithPlugin(name string, factory framework.PluginFactory) Option {return func(registry framework.Registry) error {return registry.Register(name, factory)}
}

所以最终我们的入口函数如下所示:

func main() {rand.Seed(time.Now().UTC().UnixNano())command := app.NewSchedulerCommand(app.WithPlugin(sample.Name, sample.New),)logs.InitLogs()defer logs.FlushLogs()if err := command.Execute(); err != nil {_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)os.Exit(1)}}

其中 app.WithPlugin(sample.Name, sample.New) 就是我们接下来要实现的插件,从 WithPlugin 函数的参数也可以看出我们这里的 sample.New 必须是一个 framework.PluginFactory 类型的值,而 PluginFactory 的定义就是一个函数:

type PluginFactory = func(configuration *runtime.Unknown, f FrameworkHandle) (Plugin, error)

所以 sample.New 实际上就是上面的这个函数,在这个函数中我们可以获取到插件中的一些数据然后进行逻辑处理即可,插件实现如下所示,我们这里只是简单获取下数据打印日志,如果你有实际需求的可以根据获取的数据就行处理即可,我们这里只是实现了 PreFilterFilterPreBind 三个扩展点,其他的可以用同样的方式来扩展即可:

// 插件名称
const Name = "sample-plugin"type Args struct {FavoriteColor  string `json:"favorite_color,omitempty"`FavoriteNumber int    `json:"favorite_number,omitempty"`ThanksTo       string `json:"thanks_to,omitempty"`
}type Sample struct {args   *Argshandle framework.FrameworkHandle
}func (s *Sample) Name() string {return Name
}func (s *Sample) PreFilter(pc *framework.PluginContext, pod *v1.Pod) *framework.Status {klog.V(3).Infof("prefilter pod: %v", pod.Name)return framework.NewStatus(framework.Success, "")
}func (s *Sample) Filter(pc *framework.PluginContext, pod *v1.Pod, nodeName string) *framework.Status {klog.V(3).Infof("filter pod: %v, node: %v", pod.Name, nodeName)return framework.NewStatus(framework.Success, "")
}func (s *Sample) PreBind(pc *framework.PluginContext, pod *v1.Pod, nodeName string) *framework.Status {if nodeInfo, ok := s.handle.NodeInfoSnapshot().NodeInfoMap[nodeName]; !ok {return framework.NewStatus(framework.Error, fmt.Sprintf("prebind get node info error: %+v", nodeName))} else {klog.V(3).Infof("prebind node info: %+v", nodeInfo.Node())return framework.NewStatus(framework.Success, "")}
}//type PluginFactory = func(configuration *runtime.Unknown, f FrameworkHandle) (Plugin, error)
func New(configuration *runtime.Unknown, f framework.FrameworkHandle) (framework.Plugin, error) {args := &Args{}if err := framework.DecodeInto(configuration, args); err != nil {return nil, err}klog.V(3).Infof("get plugin config args: %+v", args)return &Sample{args: args,handle: f,}, nil
}

完整代码可以前往仓库 GitHub - cnych/sample-scheduler-framework: This repo is a sample for Kubernetes scheduler framework. 获取。

实现完成后,编译打包成镜像即可,然后我们就可以当成普通的应用用一个 Deployment 控制器来部署即可,由于我们需要去获取集群中的一些资源对象,所以当然需要申请 RBAC 权限,然后同样通过 --config 参数来配置我们的调度器,同样还是使用一个 KubeSchedulerConfiguration 资源对象配置,可以通过 plugins 来启用或者禁用我们实现的插件,也可以通过 pluginConfig 来传递一些参数值给插件:

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:name: sample-scheduler-clusterrole
rules:- apiGroups:- ""resources:- endpoints- eventsverbs:- create- get- update- apiGroups:- ""resources:- nodesverbs:- get- list- watch- apiGroups:- ""resources:- podsverbs:- delete- get- list- watch- update- apiGroups:- ""resources:- bindings- pods/bindingverbs:- create- apiGroups:- ""resources:- pods/statusverbs:- patch- update- apiGroups:- ""resources:- replicationcontrollers- servicesverbs:- get- list- watch- apiGroups:- apps- extensionsresources:- replicasetsverbs:- get- list- watch- apiGroups:- appsresources:- statefulsetsverbs:- get- list- watch- apiGroups:- policyresources:- poddisruptionbudgetsverbs:- get- list- watch- apiGroups:- ""resources:- persistentvolumeclaims- persistentvolumesverbs:- get- list- watch- apiGroups:- ""resources:- configmapsverbs:- get- list- watch- apiGroups:- "storage.k8s.io"resources:- storageclasses- csinodesverbs:- get- list- watch- apiGroups:- "coordination.k8s.io"resources:- leasesverbs:- create- get- list- update- apiGroups:- "events.k8s.io"resources:- eventsverbs:- create- patch- update
---
apiVersion: v1
kind: ServiceAccount
metadata:name: sample-scheduler-sanamespace: kube-system
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:name: sample-scheduler-clusterrolebindingnamespace: kube-system
roleRef:apiGroup: rbac.authorization.k8s.iokind: ClusterRolename: sample-scheduler-clusterrole
subjects:- kind: ServiceAccountname: sample-scheduler-sanamespace: kube-system
---
apiVersion: v1
kind: ConfigMap
metadata:name: scheduler-confignamespace: kube-system
data:scheduler-config.yaml: |apiVersion: kubescheduler.config.k8s.io/v1alpha1kind: KubeSchedulerConfigurationschedulerName: sample-schedulerleaderElection:leaderElect: truelockObjectName: sample-schedulerlockObjectNamespace: kube-systemplugins:preFilter:enabled:- name: "sample-plugin"filter:enabled:- name: "sample-plugin"preBind:enabled:- name: "sample-plugin"pluginConfig:- name: "sample-plugin"args:favorite_color: "#326CE5"favorite_number: 7thanks_to: "thockin"    
---
apiVersion: apps/v1
kind: Deployment
metadata:name: sample-schedulernamespace: kube-systemlabels:component: sample-scheduler
spec:replicas: 1selector:matchLabels:component: sample-schedulertemplate:metadata:labels:component: sample-schedulerspec:serviceAccount: sample-scheduler-sapriorityClassName: system-cluster-criticalvolumes:- name: scheduler-configconfigMap:name: scheduler-configcontainers:- name: scheduler-ctrlimage: cnych/sample-scheduler:v0.1.6imagePullPolicy: IfNotPresentargs:- sample-scheduler-framework- --config=/etc/kubernetes/scheduler-config.yaml- --v=3resources:requests:cpu: "50m"volumeMounts:- name: scheduler-configmountPath: /etc/kubernetes

直接部署上面的资源对象即可,这样我们就部署了一个名为 sample-scheduler 的调度器了,接下来我们可以部署一个应用来使用这个调度器进行调度:

apiVersion: apps/v1
kind: Deployment
metadata:name: test-scheduler
spec:replicas: 1selector:matchLabels:app: test-schedulertemplate:metadata:labels:app: test-schedulerspec:schedulerName: sample-schedulercontainers:- image: nginximagePullPolicy: IfNotPresentname: nginxports:- containerPort: 80

这里需要注意的是我们现在手动指定了一个 schedulerName 的字段,将其设置成上面我们自定义的调度器名称 sample-scheduler

我们直接创建这个资源对象,创建完成后查看我们自定义调度器的日志信息:

$ kubectl get pods -n kube-system -l component=sample-scheduler
NAME                               READY   STATUS    RESTARTS   AGE
sample-scheduler-7c469787f-rwhhd   1/1     Running   0          13m
$ kubectl logs -f sample-scheduler-7c469787f-rwhhd -n kube-system
I0104 08:24:22.087881       1 scheduler.go:530] Attempting to schedule pod: default/test-scheduler-6d779d9465-rq2bb
I0104 08:24:22.087992       1 plugins.go:23] prefilter pod: test-scheduler-6d779d9465-rq2bb
I0104 08:24:22.088657       1 plugins.go:28] filter pod: test-scheduler-6d779d9465-rq2bb, node: ydzs-node1
I0104 08:24:22.088797       1 plugins.go:28] filter pod: test-scheduler-6d779d9465-rq2bb, node: ydzs-node2
I0104 08:24:22.088871       1 plugins.go:28] filter pod: test-scheduler-6d779d9465-rq2bb, node: ydzs-node3
I0104 08:24:22.088946       1 plugins.go:28] filter pod: test-scheduler-6d779d9465-rq2bb, node: ydzs-node4
I0104 08:24:22.088992       1 plugins.go:28] filter pod: test-scheduler-6d779d9465-rq2bb, node: ydzs-master
I0104 08:24:22.090653       1 plugins.go:36] prebind node info: &Node{ObjectMeta:{ydzs-node3   /api/v1/nodes/ydzs-node3 1ff6e228-4d98-4737-b6d3-30a5d55ccdc2 15466372 0 2019-11-10 09:05:09 +0000 UTC <nil> <nil> ......}
I0104 08:24:22.091761       1 factory.go:610] Attempting to bind test-scheduler-6d779d9465-rq2bb to ydzs-node3
I0104 08:24:22.104994       1 scheduler.go:667] pod default/test-scheduler-6d779d9465-rq2bb is bound successfully on node "ydzs-node3", 5 nodes evaluated, 4 nodes were found feasible. Bound node resource: "Capacity: CPU<4>|Memory<8008820Ki>|Pods<110>|StorageEphemeral<17921Mi>; Allocatable: CPU<4>|Memory<7906420Ki>|Pods<110>|StorageEphemeral<16912377419>.".

可以看到当我们创建完 Pod 后,在我们自定义的调度器中就出现了对应的日志,并且在我们定义的扩展点上面都出现了对应的日志,证明我们的示例成功了,也可以通过查看 Pod 的 schedulerName 来验证:

$ kubectl get pods
NAME                                      READY   STATUS    RESTARTS   AGE
test-scheduler-6d779d9465-rq2bb           1/1     Running   0          22m
$ kubectl get pod test-scheduler-6d779d9465-rq2bb -o yaml
......
restartPolicy: Always
schedulerName: sample-scheduler
securityContext: {}
serviceAccount: default
......

在最新的 Kubernetes v1.17 版本中,Scheduler Framework 内置的预选和优选函数已经全部插件化,所以要扩展调度器我们应该掌握并理解调度框架这种方式。

5. 疑问和思考

5.1 在k8s环境中,由于调度对于机器的资源只考虑request类型,实际上一台服务器的cpu使用率可能已经很高,但是依然被分配了pod上去,能否实现一种调度器,将cpu使用率作为一种考虑?该如何实现?

是的。

在实际情况中,pod申请到的资源并不一定会被pod使用,而k8s层面多数并没有进行资源绑核,因此就会导致cpu被分配出去实际上并没有被使用的情况。对于k8s而言,紧紧依赖request资源调度pod到运行的pod,这种类型的策略有点单薄,但是当前的调度策略中并没有该类型的调度策略方式。 因此需要自定义研发调度策略。

自定义研发调度策略最难的还是指标采集链路,因为k8s容器调度是核心链路环节,而k8s调度器在申请调度时如果依赖外部的指标采集,从而获取cpu、内存的使用率情况,这会引入一个依赖,并且通常该依赖的链条都比较长。同时由于是监控系统,通常公司级别对于监控系统的优先级定位并不高,因此对应的服务性难以保证,因此在实现策略时可以这样。

  1. 定时采集外部指标,并进行缓存机器指标

  2. 在调度策略中,对于机器的优选和过滤环节,加入cpu、内存使用率的指标考虑,参与调度逻辑

  3. 充分考虑降级策略,如果出现指标延迟、脏数据、坏数据等情况,能够及时的降级到默认调度策略,避免核心链路环节异常或者调度缓慢。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/8884.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java--io流(一)

1. 前置知识 字符集是什么&#xff1f; 字符集&#xff08;Character Set&#xff09;是一组字符的集合&#xff0c;它定义了可以在计算机系统中使用的所有字符。字符集可以包括字母、数字、标点符号、控制字符、图形符号等。字符集使得计算机能够存储、处理和显示各种语言和…

嵌入式Linux学习第四天启动方式学习

嵌入式Linux学习第四天 今天学习I.MX6U 启动方式详解。I.MX6U有多种启动方式&#xff0c;可以从 SD/EMMC、NAND Flash、QSPI Flash等启动。 启动方式选择 BOOT 的处理过程是发生在 I.MX6U 芯片上电以后&#xff0c;芯片会根据 BOOT_MODE[1:0]的设置来选择 BOOT 方式。BOOT_M…

Linux——基础IO2

引入 之前在Linux——基础IO(1)中我们讲的都是(进程打开的文件)被打开的文件 那些未被打开的文件呢&#xff1f; 大部分的文件都是没有被打开的文件&#xff0c;这些文件在哪保存&#xff1f;磁盘(SSD) OS要不要管理磁盘上的文件&#xff1f;(如何让OS快速定位一个文件) 要…

数据仓库与数据挖掘实验练习3-4(实验二2024.5.8)

练习3 1.简单文件操作练习 import pandas as pd # 读取文件 pd.read_csv(pokemon.csv) # 读取 CSV 文件的函数调用&#xff0c;它将文件中的数据加载到 DataFrame 中&#xff0c;并指定了 Pokemon 列作为索引列。 pd.read_csv(pokemon.csv,index_colPokemon)#查看类型 type(p…

React:Router-2. createBrowserRouter函数式

参考文档&#xff1a;ReactRouter官网 前边的文章 BrowserRouter组件式路由 提供了组件式路由的方式&#xff0c;在react-router6.4.0及以上版本&#xff0c;提供了 createBrowserRouter 函数式路由创建方式。 一、创建路由 1. 新建router.js文件&#xff0c;使用createBrow…

十二届蓝桥杯Python组1月中/高级试题 第五题

** 十二届蓝桥杯Python组1月中/高级试题 第五题 ** 第五题&#xff08;难度系数 5&#xff0c;35 个计分点&#xff09; 提示信息&#xff1a; 平均数&#xff1a;是指在一组数据中所有数据之和再除以这组数据的个数。 如&#xff1a;“1&#xff0c;2&#xff0c;3&#xf…

命令行方式将mysql数据库迁移到达梦数据库(全步骤)

因项目需求&#xff0c;需要将mysql数据库转换为国产达梦数据库&#xff0c;但由于安全问题&#xff0c;正式环境只能用命令行方式连接&#xff0c;下列是操作全步骤 目录 一、操作逻辑二、操作步骤1、本地安装达梦相关工具2、将服务器mysql导出到本地a) 服务器命令行导出mysql…

如何添加、编辑、调整WordPress菜单

我们最近在使用WordPress建站建设公司网站。我们是使用的hostease的主机产品建设的WordPress网站。在建设网站使用遇到了一些WordPress菜单使用方面的问题。好在hostease提供了不少帮助。 下面把WordPress菜单使用心得分享一下。 本文将详细介绍WordPress菜单的各种功能&#x…

音频录制软件有哪些?这2种推荐给你

音频在当今数字化世界中扮演着至关重要的角色。无论是音乐创作、语音记录、会议记录还是其他任何需要捕捉声音的场景&#xff0c;音频录制软件都是能为我们提供强大功能的工具。 那么到底哪一种软件才最适合您呢&#xff1f;下面小编将为您介绍2款音频录制软件。 方法一&#…

Ansys Mechanical|绳索仿真技术

绳索&#xff0c;俗称绳子&#xff0c;是通过扭或编等方式加强后&#xff0c;连成一定长度的纤维。其拉伸强度很好但没有压缩强度&#xff0c;可用来做连接、牵引的工具。绳索的用途数不胜数&#xff1a;从建筑中的材料固定、到岩土工程中的柔性锚索、汽车门窗的升降拉索、电缆…

在系统学习C语言之前所需要了解的知识

C语言常见概念 前言1. C语言是什么2. C语言的历史和辉煌3. 编译器的选择VS20223.1 编译和链接3.2 编译器的对比3.3 VS2022的优缺点优点&#xff1a;缺点&#xff1a; 4. VS项目和源文件、头文件介绍5. 第⼀个C语言程序6. main函数7. printf和库函数8. 关键字介绍9. 字符和ASCII…

Spring Cloud Kubernetes 本地开发环境调试

一、Spring Cloud Kubernetes 本地开发环境调试 上面文章使用 Spring Cloud Kubernetes 在 k8s 环境中实现了服务注册发现、服务动态配置&#xff0c;但是需要放在 k8s 环境中才能正常使用&#xff0c;在本地开发环境中可能没有 k8s 环境&#xff0c;如何本地开发调试呢&#…

【优选算法】—Leetcode—11—— 盛最多水的容器

1.题目 11. 盛最多水的容器 给定一个长度为 n 的整数数组 height 。有 n 条垂线&#xff0c;第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线&#xff0c;使得它们与 x 轴共同构成的容器可以容纳最多的水。 返回容器可以储存的最大水量。 说明&#…

探索MySQL数学宝库:常用数学函数的秘密操作

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 探索MySQL数学宝库&#xff1a;常用数学函数的秘密操作 前言ABS函数&#xff08;绝对值&#xff09;ROUND函数&#xff08;四舍五入&#xff09;CEIL和FLOOR函数&#xff08;向上取整和向下取整&#…

酷开科技AI技术支持,酷开系统根据你的喜好量身定制节目

在当今数字化时代&#xff0c;个性化推荐已成为提升消费者体验的关键因素。酷开科技的智慧AI&#xff0c;为消费者提供了精彩的内容推荐服务&#xff0c;更大地丰富了消费者的娱乐生活。 酷开系统中的AI推荐引擎通过学习消费者的观看习惯和偏好&#xff0c;能够快速识别其兴趣…

【C++】-【QT】类库使用-001

1主窗口创建 1.1【makefile】配置 1 源码 QT widgetsSOURCES main.cpp2 图示 1.2源码 1 源码 #include <QWidget> #include <QApplication>using namespace std;int main(int argc,char *argv[]) {QApplication a(argc,argv);QWidget w;w.show();return a…

nginx代理原理(端口复用)探究

前言&#xff1a;对于一些常用的插件&#xff0c;我们应该学会如何使用。同时&#xff0c;其实现原理也要进行深究&#xff0c;可以为其他的项目开发做借鉴。 探究方案&#xff1a; 一、发布两个不同的服务&#xff0c;这两个服务的端口不致 二、配置nginx&#xff0c;让这两…

C语言——文件描述符、系统调用操作文件

文件描述符 在Unix-like操作系统中&#xff0c;文件描述符&#xff08;file descriptor&#xff09;是一个用于标识打开文件或I/O设备的整数值。它是对底层文件系统的抽象&#xff0c;用于在应用程序和操作系统之间传递文件信息。 文件描述符是一个非负整数&#xff0c;通常是…

透明桌面便签便笺怎么设置 透明的便签在哪

在繁忙的工作与生活中&#xff0c;我一直渴望找到一种能够轻松管理待办事项的方式。最近&#xff0c;我发现了一个让我爱不释手的工具。它不仅有着出色的提醒功能&#xff0c;更独特的是&#xff0c;它提供了透明便签的设计。 那么&#xff0c;透明便签的好处究竟有哪些呢&…

第十三届蓝桥杯决赛(国赛)真题 Java A 组【原卷】

文章目录 发现宝藏【考生须知】试题 A: 火柴棒数字试题 B: 小蓝与钥匙试题 C: 内存空间试题 D: 斐波那契数组试题 E: 交通信号试题 F: 数组个数试题 G: 六六大顺试题 H : \mathrm{H}: H: 选素数试题 I: 图书借阅试题 J \mathrm{J} J : 括号序列树 发现宝藏 前些天发现了一个…