乐趣区

关于kubernetes:从k8s集群e2e调度慢告警看kubescheduler源码

k8s 教程阐明

  • k8s 底层原理和源码解说之精髓篇
  • k8s 底层原理和源码解说之进阶篇

prometheus 全组件的教程

  • 01_prometheus 全组件配置应用、底层原理解析、高可用实战
  • 02_prometheus-thanos 应用和源码解读
  • 03_kube-prometheus 和 prometheus-operator 实战和原理介绍
  • 04_prometheus 源码解说和二次开发

go 语言课程

  • golang 根底课程
  • golang 运维平台实战,服务树, 日志监控,工作执行,分布式探测

告警的 ql


histogram_quantile(0.99, sum(rate(scheduler_e2e_scheduling_duration_seconds_bucket{job="kube-scheduler"}[5m])) without(instance, pod)) > 3 for 1m
  • 含意:调度耗时超过 3 秒

    追踪这个 histogram 的 metrics

  • 代码版本 v1.20
  • 地位 D:\go_path\src\github.com\kubernetes\kubernetes\pkg\scheduler\metrics\metrics.go
  • 追踪调用方,在 observeScheduleAttemptAndLatency 的封装中,地位 D:\go_path\src\github.com\kubernetes\kubernetes\pkg\scheduler\metrics\profile_metrics.go
  • 这里可看到 调度的三种后果都会记录相干的耗时

追踪调用方

  • 地位 D:\go_path\src\github.com\kubernetes\kubernetes\pkg\scheduler\scheduler.go + 608
  • 在函数 Scheduler.scheduleOne 中,用来记录调度每个 pod 的耗时
  • 能够看到具体的调用点,在异步 bind 函数的底部
  • 由此得出结论 e2e 是统计整个 scheduleOne 的耗时

    go func() {err := sched.bind(bindingCycleCtx, fwk, assumedPod, scheduleResult.SuggestedHost, state)
          if err != nil {metrics.PodScheduleError(fwk.ProfileName(), metrics.SinceInSeconds(start))
              // trigger un-reserve plugins to clean up state associated with the reserved Pod
              fwk.RunReservePluginsUnreserve(bindingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost)
              if err := sched.SchedulerCache.ForgetPod(assumedPod); err != nil {klog.Errorf("scheduler cache ForgetPod failed: %v", err)
              }
              sched.recordSchedulingFailure(fwk, assumedPodInfo, fmt.Errorf("binding rejected: %w", err), SchedulerError, "")
          } else {
              // Calculating nodeResourceString can be heavy. Avoid it if klog verbosity is below 2.
              if klog.V(2).Enabled() {klog.InfoS("Successfully bound pod to node", "pod", klog.KObj(pod), "node", scheduleResult.SuggestedHost, "evaluatedNodes", scheduleResult.EvaluatedNodes, "feasibleNodes", scheduleResult.FeasibleNodes)
              }
              metrics.PodScheduled(fwk.ProfileName(), metrics.SinceInSeconds(start))
              metrics.PodSchedulingAttempts.Observe(float64(podInfo.Attempts))
              metrics.PodSchedulingDuration.WithLabelValues(getAttemptsLabel(podInfo)).Observe(metrics.SinceInSeconds(podInfo.InitialAttemptTimestamp))
    
              // Run "postbind" plugins.
              fwk.RunPostBindPlugins(bindingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost)
          }
    }

scheduleOne 从上到下都蕴含哪几个过程

01 调度算法耗时

  • 实例代码

    // 调用调度算法给出后果
    scheduleResult, err := sched.Algorithm.Schedule(schedulingCycleCtx, fwk, state, pod)
    // 处理错误
    if err != nil{}
    // 记录调度算法耗时
    metrics.SchedulingAlgorithmLatency.Observe(metrics.SinceInSeconds(start}))
  • 从下面能够看出次要分 3 个步骤

    • 调用调度算法给出后果
    • 处理错误
    • 记录调度算法耗时
  • 那么咱们首先应该 算法的耗时,对应的 histogram metrics 为

    histogram_quantile(0.99, sum(rate(scheduler_scheduling_algorithm_duration_seconds_bucket{job="kube-scheduler"}[5m])) by (le))
  • 将 e2e 和 algorithm 99 分位耗时再联合 告警工夫的曲线发现吻合度较高
  • 然而发现 99 分位下 algorithm > e2e,然而依照 e2e 作为兜底来看,应该是 e2e 要更高,所以调整 999 分位发现 2 者差不多
  • 造成上述问题的起因跟 prometheus histogram 线性插值法的误差有关系,具体能够看我的文章 histogram 线性插值法原理
Algorithm.Schedule 具体流程
  • 在 Schedule 中能够看到两个次要的函数调用

    
    feasibleNodes, filteredNodesStatuses, err := g.findNodesThatFitPod(ctx, fwk, state, pod)
    priorityList, err := g.prioritizeNodes(ctx, fwk, state, pod, feasibleNodes)
  • 其中 findNodesThatFitPod 对应的是 filter 流程,对应的 metrics 有 scheduler_framework_extension_point_duration_seconds_bucket

    histogram_quantile(0.999, sum by(extension_point,le) (rate(scheduler_framework_extension_point_duration_seconds_bucket{job="kube-scheduler"}[5m])))
    
  • 相干的截图能够看到
  • prioritizeNodes 对应的是 score 流程,对应的 metrics 有

    histogram_quantile(0.99, sum by(plugin,le) (rate(scheduler_plugin_execution_duration_seconds_bucket{job="kube-scheduler"}[5m])))
  • 相干的截图能够看到
  • 上述具体的算法流程能够和官网文档给出的流程图对得上

02 调度算法耗时

  • 再回过头来看 bind 的过程
  • 其中的外围就在 bind 这里

    err := sched.bind(bindingCycleCtx, fwk, assumedPod, scheduleResult.SuggestedHost, state)
  • 能够看到在 bind 函数外部是独自计时的

    func (sched *Scheduler) bind(ctx context.Context, fwk framework.Framework, assumed *v1.Pod, targetNode string, state *framework.CycleState) (err error) {start := time.Now()
      defer func() {sched.finishBinding(fwk, assumed, targetNode, start, err)
      }()
    
      bound, err := sched.extendersBinding(assumed, targetNode)
      if bound {return err}
      bindStatus := fwk.RunBindPlugins(ctx, state, assumed, targetNode)
      if bindStatus.IsSuccess() {return nil}
      if bindStatus.Code() == framework.Error {return bindStatus.AsError()
      }
      return fmt.Errorf("bind status: %s, %v", bindStatus.Code().String(), bindStatus.Message())
    }
  • 对应的 metric 为

    histogram_quantile(0.999, sum by(le) (rate(scheduler_binding_duration_seconds_bucket{job="kube-scheduler"}[5m])))
  • 这里咱们比照 e2e 和 bind 的 999 分位值
  • 发现相比于 alg,bind 和 e2e 吻合度更高
  • 同时发现 bind 外部次要两个流程 sched.extendersBinding 执行内部 binding 插件
  • fwk.RunBindPlugins 执行外部的绑定插件
外部绑定插件
  • 代码如下,次要流程就是执行绑定插件

    // RunBindPlugins runs the set of configured bind plugins until one returns a non `Skip` status.
    func (f *frameworkImpl) RunBindPlugins(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string) (status *framework.Status) {startTime := time.Now()
      defer func() {metrics.FrameworkExtensionPointDuration.WithLabelValues(bind, status.Code().String(), f.profileName).Observe(metrics.SinceInSeconds(startTime))
      }()
      if len(f.bindPlugins) == 0 {return framework.NewStatus(framework.Skip, "")
      }
      for _, bp := range f.bindPlugins {status = f.runBindPlugin(ctx, bp, state, pod, nodeName)
          if status != nil && status.Code() == framework.Skip {continue}
          if !status.IsSuccess() {err := status.AsError()
              klog.ErrorS(err, "Failed running Bind plugin", "plugin", bp.Name(), "pod", klog.KObj(pod))
              return framework.AsStatus(fmt.Errorf("running Bind plugin %q: %w", bp.Name(), err))
          }
          return status
      }
      return status
    }
  • 那么默认的绑定插件为调用 pod 的 bind 办法绑定到指定的 node 上,binding 是 pods 的子资源

    // Bind binds pods to nodes using the k8s client.
    func (b DefaultBinder) Bind(ctx context.Context, state *framework.CycleState, p *v1.Pod, nodeName string) *framework.Status {klog.V(3).Infof("Attempting to bind %v/%v to %v", p.Namespace, p.Name, nodeName)
      binding := &v1.Binding{ObjectMeta: metav1.ObjectMeta{Namespace: p.Namespace, Name: p.Name, UID: p.UID},
          Target:     v1.ObjectReference{Kind: "Node", Name: nodeName},
      }
      err := b.handle.ClientSet().CoreV1().Pods(binding.Namespace).Bind(ctx, binding, metav1.CreateOptions{})
      if err != nil {return framework.AsStatus(err)
      }
      return nil
    }
    
  • 执行绑定动作也有相干的 metrics 统计耗时,

    histogram_quantile(0.999, sum by(le) (rate(scheduler_plugin_execution_duration_seconds_bucket{extension_point="Bind",plugin="DefaultBinder",job="kube-scheduler"}[5m])))
  • 同时在 RunBindPlugins 中也有 defer func 负责统计耗时

    histogram_quantile(0.9999, sum by(le) (rate(scheduler_framework_extension_point_duration_seconds_bucket{extension_point="Bind",job="kube-scheduler"}[5m])))
  • 从下面两个 metrics 看,外部的插件耗时都很低
extendersBinding 内部插件
  • 代码如下,遍历 Algorithm 的 Extenders,判断是 bind 类型的,而后执行 extender.Bind

    // TODO(#87159): Move this to a Plugin.
    func (sched *Scheduler) extendersBinding(pod *v1.Pod, node string) (bool, error) {for _, extender := range sched.Algorithm.Extenders() {if !extender.IsBinder() || !extender.IsInterested(pod) {continue}
          return true, extender.Bind(&v1.Binding{ObjectMeta: metav1.ObjectMeta{Namespace: pod.Namespace, Name: pod.Name, UID: pod.UID},
              Target:     v1.ObjectReference{Kind: "Node", Name: node},
          })
      }
      return false, nil
    }
    
  • extender.Bind 对应就是通过 http 发往内部的 调度器

    // Bind delegates the action of binding a pod to a node to the extender.
    func (h *HTTPExtender) Bind(binding *v1.Binding) error {
      var result extenderv1.ExtenderBindingResult
      if !h.IsBinder() {
          // This shouldn't happen as this extender wouldn't have become a Binder.
          return fmt.Errorf("unexpected empty bindVerb in extender")
      }
      req := &extenderv1.ExtenderBindingArgs{
          PodName:      binding.Name,
          PodNamespace: binding.Namespace,
          PodUID:       binding.UID,
          Node:         binding.Target.Name,
      }
      if err := h.send(h.bindVerb, req, &result); err != nil {return err}
      if result.Error != "" {return fmt.Errorf(result.Error)
      }
      return nil
    }
  • 很遗憾的是这里并没有相干的 metrics 统计耗时
  • 目前猜想遍历 sched.Algorithm.Extenders 执行的耗时
  • 这里 sched.Algorithm.Extenders 来自于 KubeSchedulerConfiguration 中的配置
  • 也就是编写配置文件,并将其门路传给 kube-scheduler 的命令行参数,定制 kube-scheduler 的行为,目前并没有看到

总结

scheduler 调度过程

  • 单个 pod 的调度次要分为 3 个步骤:

    • 依据 Predict 和 Priority 两个阶段,调用各自的算法插件,抉择最优的 Node
    • Assume 这个 Pod 被调度到对应的 Node,保留到 cache
    • 用 extender 和 plugins 进行验证,如果通过则绑定

    e2e 耗时次要来自 bind

  • 但目前看到 bind 执行耗时并没有很长
  • 待续
退出移动版