调度框架 [1]

本文基于 kubernetes 1.24 进行分析

调度框架(Scheduling Framework)是Kubernetes 的调度器 kube-scheduler 设计的的可插拔架构,将插件(调度算法)嵌入到调度上下文的每个扩展点中,并编译为 kube-scheduler

kube-scheduler 1.22 之后,在 pkg/scheduler/framework/interface.go 中定义了一个 Plugininterface,这个 interface 作为了所有插件的父级。而每个未调度的 Pod,Kubernetes 调度器会根据一组规则尝试在集群中寻找一个节点。

  1. type Plugin interface {
  2. Name() string
  3. }

下面会对每个算法是如何实现的进行分析

在初始化 scheduler 时,会创建一个 profile,profile是关于 scheduler 调度配置相关的定义

  1. func New(client clientset.Interface,
  2. ...
  3. profiles, err := profile.NewMap(options.profiles, registry, recorderFactory, stopCh,
  4. frameworkruntime.WithComponentConfigVersion(options.componentConfigVersion),
  5. frameworkruntime.WithClientSet(client),
  6. frameworkruntime.WithKubeConfig(options.kubeConfig),
  7. frameworkruntime.WithInformerFactory(informerFactory),
  8. frameworkruntime.WithSnapshotSharedLister(snapshot),
  9. frameworkruntime.WithPodNominator(nominator),
  10. frameworkruntime.WithCaptureProfile(frameworkruntime.CaptureProfile(options.frameworkCapturer)),
  11. frameworkruntime.WithClusterEventMap(clusterEventMap),
  12. frameworkruntime.WithParallelism(int(options.parallelism)),
  13. frameworkruntime.WithExtenders(extenders),
  14. )
  15. if err != nil {
  16. return nil, fmt.Errorf("initializing profiles: %v", err)
  17. }
  18. if len(profiles) == 0 {
  19. return nil, errors.New("at least one profile is required")
  20. }
  21. ....
  22. }

关于 profile 的实现,则为 KubeSchedulerProfile,也是作为 yaml生成时传入的配置

  1. // KubeSchedulerProfile 是一个 scheduling profile.
  2. type KubeSchedulerProfile struct {
  3. // SchedulerName 是与此配置文件关联的调度程序的名称。
  4. // 如果 SchedulerName 与 pod “spec.schedulerName”匹配,则使用此配置文件调度 pod。
  5. SchedulerName string
  6. // Plugins指定应该启用或禁用的插件集。
  7. // 启用的插件是除了默认插件之外应该启用的插件。禁用插件应是禁用的任何默认插件。
  8. // 当没有为扩展点指定启用或禁用插件时,将使用该扩展点的默认插件(如果有)。
  9. // 如果指定了 QueueSort 插件,
  10. /// 则必须为所有配置文件指定相同的 QueueSort Plugin 和 PluginConfig。
  11. // 这个Plugins展现的形式则是调度上下文中的所有扩展点(这是抽象),实际中会表现为多个扩展点
  12. Plugins *Plugins
  13. // PluginConfig 是每个插件的一组可选的自定义插件参数。
  14. // 如果省略PluginConfig参数等同于使用该插件的默认配置。
  15. PluginConfig []PluginConfig
  16. }

对于 profile.NewMap 就是根据给定的配置来构建这个framework,因为配置可能是存在多个的。而 Registry 则是所有可用插件的集合,内部构造则是 PluginFactory ,通过函数来构建出对应的 plugin

  1. func NewMap(cfgs []config.KubeSchedulerProfile, r frameworkruntime.Registry, recorderFact RecorderFactory,
  2. stopCh <-chan struct{}, opts ...frameworkruntime.Option) (Map, error) {
  3. m := make(Map)
  4. v := cfgValidator{m: m}
  5. for _, cfg := range cfgs {
  6. p, err := newProfile(cfg, r, recorderFact, stopCh, opts...)
  7. if err != nil {
  8. return nil, fmt.Errorf("creating profile for scheduler name %s: %v", cfg.SchedulerName, err)
  9. }
  10. if err := v.validate(cfg, p); err != nil {
  11. return nil, err
  12. }
  13. m[cfg.SchedulerName] = p
  14. }
  15. return m, nil
  16. }
  17. // newProfile 给的配置构建出一个profile
  18. func newProfile(cfg config.KubeSchedulerProfile, r frameworkruntime.Registry, recorderFact RecorderFactory,
  19. stopCh <-chan struct{}, opts ...frameworkruntime.Option) (framework.Framework, error) {
  20. recorder := recorderFact(cfg.SchedulerName)
  21. opts = append(opts, frameworkruntime.WithEventRecorder(recorder))
  22. return frameworkruntime.NewFramework(r, &cfg, stopCh, opts...)
  23. }

可以看到最终返回的是一个 Framework 。那么来看下这个 Framework

Framework 是一个抽象,管理着调度过程中所使用的所有插件,并在调度上下文中适当的位置去运行对应的插件

  1. type Framework interface {
  2. Handle
  3. // QueueSortFunc 返回对调度队列中的 Pod 进行排序的函数
  4. // 也就是less,在Sort打分阶段的打分函数
  5. QueueSortFunc() LessFunc
  6. // RunPreFilterPlugins 运行配置的一组PreFilter插件。
  7. // 如果这组插件中,任何一个插件失败,则返回 *Status 并设置为non-success。
  8. // 如果返回状态为non-success,则调度周期中止。
  9. // 它还返回一个 PreFilterResult,它可能会影响到要评估下游的节点。
  10. RunPreFilterPlugins(ctx context.Context, state *CycleState, pod *v1.Pod) (*PreFilterResult, *Status)
  11. // RunPostFilterPlugins 运行配置的一组PostFilter插件。
  12. // PostFilter 插件是通知性插件,在这种情况下应配置为先执行并返回 Unschedulable 状态,
  13. // 或者尝试更改集群状态以使 pod 在未来的调度周期中可能会被调度。
  14. RunPostFilterPlugins(ctx context.Context, state *CycleState, pod *v1.Pod, filteredNodeStatusMap NodeToStatusMap) (*PostFilterResult, *Status)
  15. // RunPreBindPlugins 运行配置的一组 PreBind 插件。
  16. // 如果任何一个插件返回错误,则返回 *Status 并且code设置为non-success。
  17. // 如果code为“Unschedulable”,则调度检查失败,
  18. // 则认为是内部错误。在任何一种情况下,Pod都不会被bound。
  19. RunPreBindPlugins(ctx context.Context, state *CycleState, pod *v1.Pod, nodeName string) *Status
  20. // RunPostBindPlugins 运行配置的一组PostBind插件
  21. RunPostBindPlugins(ctx context.Context, state *CycleState, pod *v1.Pod, nodeName string)
  22. // RunReservePluginsReserve运行配置的一组Reserve插件的Reserve方法。
  23. // 如果在这组调用中的任何一个插件返回错误,则不会继续运行剩余调用的插件并返回错误。
  24. // 在这种情况下,pod将不能被调度。
  25. RunReservePluginsReserve(ctx context.Context, state *CycleState, pod *v1.Pod, nodeName string) *Status
  26. // RunReservePluginsUnreserve运行配置的一组Reserve插件的Unreserve方法。
  27. RunReservePluginsUnreserve(ctx context.Context, state *CycleState, pod *v1.Pod, nodeName string)
  28. // RunPermitPlugins运行配置的一组Permit插件。
  29. // 如果这些插件中的任何一个返回“Success”或“Wait”之外的状态,则它不会继续运行其余插件并返回错误。
  30. // 否则,如果任何插件返回 “Wait”,则此函数将创建等待pod并将其添加到当前等待pod的map中,
  31. // 并使用“Wait” code返回状态。 Pod将在Permit插件返回的最短持续时间内保持等待pod。
  32. RunPermitPlugins(ctx context.Context, state *CycleState, pod *v1.Pod, nodeName string) *Status
  33. // 如果pod是waiting pod,WaitOnPermit 将阻塞,直到等待的pod被允许或拒绝。
  34. WaitOnPermit(ctx context.Context, pod *v1.Pod) *Status
  35. // RunBindPlugins运行配置的一组bind插件。 Bind插件可以选择是否处理Pod。
  36. // 如果 Bind 插件选择跳过binding,它应该返回 code=5("skip")状态。
  37. // 否则,它应该返回“Error”或“Success”。
  38. // 如果没有插件处理绑定,则RunBindPlugins返回code=5("skip")的状态。
  39. RunBindPlugins(ctx context.Context, state *CycleState, pod *v1.Pod, nodeName string) *Status
  40. // 如果至少定义了一个filter插件,则HasFilterPlugins返回true
  41. HasFilterPlugins() bool
  42. // 如果至少定义了一个PostFilter插件,则HasPostFilterPlugins返回 true。
  43. HasPostFilterPlugins() bool
  44. // 如果至少定义了一个Score插件,则HasScorePlugins返回 true。
  45. HasScorePlugins() bool
  46. // ListPlugins将返回map。key为扩展点名称,value则是配置的插件列表。
  47. ListPlugins() *config.Plugins
  48. // ProfileName则是与profile name关联的framework
  49. ProfileName() string
  50. }

而实现这个抽象的则是 frameworkImplframeworkImpl 是初始化与运行 scheduler plugins 的组件,并在调度上下文中会运行这些扩展点

  1. type frameworkImpl struct {
  2. registry Registry
  3. snapshotSharedLister framework.SharedLister
  4. waitingPods *waitingPodsMap
  5. scorePluginWeight map[string]int
  6. queueSortPlugins []framework.QueueSortPlugin
  7. preFilterPlugins []framework.PreFilterPlugin
  8. filterPlugins []framework.FilterPlugin
  9. postFilterPlugins []framework.PostFilterPlugin
  10. preScorePlugins []framework.PreScorePlugin
  11. scorePlugins []framework.ScorePlugin
  12. reservePlugins []framework.ReservePlugin
  13. preBindPlugins []framework.PreBindPlugin
  14. bindPlugins []framework.BindPlugin
  15. postBindPlugins []framework.PostBindPlugin
  16. permitPlugins []framework.PermitPlugin
  17. clientSet clientset.Interface
  18. kubeConfig *restclient.Config
  19. eventRecorder events.EventRecorder
  20. informerFactory informers.SharedInformerFactory
  21. metricsRecorder *metricsRecorder
  22. profileName string
  23. extenders []framework.Extender
  24. framework.PodNominator
  25. parallelizer parallelize.Parallelizer
  26. }

那么来看下 Registry ,Registry 是作为一个可用插件的集合。framework 使用 registry 来启用和对插件配置的初始化。在初始化框架之前,所有插件都必须在注册表中。表现形式就是一个 map[]key 是插件的名称,value是 PluginFactory

  1. type Registry map[string]PluginFactory

而在 pkg\scheduler\framework\plugins\registry.go 中会将所有的 in-tree plugin 注册进来。通过 NewInTreeRegistry 。后续如果还有插件要注册,可以通过 WithFrameworkOutOfTreeRegistry 来注册其他的插件。

  1. func NewInTreeRegistry() runtime.Registry {
  2. fts := plfeature.Features{
  3. EnableReadWriteOncePod: feature.DefaultFeatureGate.Enabled(features.ReadWriteOncePod),
  4. EnableVolumeCapacityPriority: feature.DefaultFeatureGate.Enabled(features.VolumeCapacityPriority),
  5. EnableMinDomainsInPodTopologySpread: feature.DefaultFeatureGate.Enabled(features.MinDomainsInPodTopologySpread),
  6. EnableNodeInclusionPolicyInPodTopologySpread: feature.DefaultFeatureGate.Enabled(features.NodeInclusionPolicyInPodTopologySpread),
  7. }
  8. return runtime.Registry{
  9. selectorspread.Name: selectorspread.New,
  10. imagelocality.Name: imagelocality.New,
  11. tainttoleration.Name: tainttoleration.New,
  12. nodename.Name: nodename.New,
  13. nodeports.Name: nodeports.New,
  14. nodeaffinity.Name: nodeaffinity.New,
  15. podtopologyspread.Name: runtime.FactoryAdapter(fts, podtopologyspread.New),
  16. nodeunschedulable.Name: nodeunschedulable.New,
  17. noderesources.Name: runtime.FactoryAdapter(fts, noderesources.NewFit),
  18. noderesources.BalancedAllocationName: runtime.FactoryAdapter(fts, noderesources.NewBalancedAllocation),
  19. volumebinding.Name: runtime.FactoryAdapter(fts, volumebinding.New),
  20. volumerestrictions.Name: runtime.FactoryAdapter(fts, volumerestrictions.New),
  21. volumezone.Name: volumezone.New,
  22. nodevolumelimits.CSIName: runtime.FactoryAdapter(fts, nodevolumelimits.NewCSI),
  23. nodevolumelimits.EBSName: runtime.FactoryAdapter(fts, nodevolumelimits.NewEBS),
  24. nodevolumelimits.GCEPDName: runtime.FactoryAdapter(fts, nodevolumelimits.NewGCEPD),
  25. nodevolumelimits.AzureDiskName: runtime.FactoryAdapter(fts, nodevolumelimits.NewAzureDisk),
  26. nodevolumelimits.CinderName: runtime.FactoryAdapter(fts, nodevolumelimits.NewCinder),
  27. interpodaffinity.Name: interpodaffinity.New,
  28. queuesort.Name: queuesort.New,
  29. defaultbinder.Name: defaultbinder.New,
  30. defaultpreemption.Name: runtime.FactoryAdapter(fts, defaultpreemption.New),
  31. }
  32. }

这里插入一个题外话,关于 in-tree plugin

在这里没有找到关于,kube-scheduler ,只是找到有关的概念,大概可以解释为,in-tree表示为随kubernetes官方提供的二进制构建的 plugin 则为 in-tree,而独立于kubernetes代码库之外的为 out-of-tree [3] 。这种情况下,可以理解为,AA则是 out-of-treePod, DeplymentSet 等是 in-tree

接下来回到初始化 scheduler ,在初始化一个 scheduler 时,会通过NewInTreeRegistry 来初始化

  1. func New(client clientset.Interface,
  2. ....
  3. registry := frameworkplugins.NewInTreeRegistry()
  4. if err := registry.Merge(options.frameworkOutOfTreeRegistry); err != nil {
  5. return nil, err
  6. }
  7. ...
  8. profiles, err := profile.NewMap(options.profiles, registry, recorderFactory, stopCh,
  9. frameworkruntime.WithComponentConfigVersion(options.componentConfigVersion),
  10. frameworkruntime.WithClientSet(client),
  11. frameworkruntime.WithKubeConfig(options.kubeConfig),
  12. frameworkruntime.WithInformerFactory(informerFactory),
  13. frameworkruntime.WithSnapshotSharedLister(snapshot),
  14. frameworkruntime.WithPodNominator(nominator),
  15. frameworkruntime.WithCaptureProfile(frameworkruntime.CaptureProfile(options.frameworkCapturer)),
  16. frameworkruntime.WithClusterEventMap(clusterEventMap),
  17. frameworkruntime.WithParallelism(int(options.parallelism)),
  18. frameworkruntime.WithExtenders(extenders),
  19. )
  20. ...
  21. }

接下来在调度上下文 scheduleOneschedulePod 时,会通过 framework 调用对应的插件来处理这个扩展点工作。具体的体现在,pkg\scheduler\schedule_one.go 中的预选阶段

  1. func (sched *Scheduler) schedulePod(ctx context.Context, fwk framework.Framework, state *framework.CycleState, pod *v1.Pod) (result ScheduleResult, err error) {
  2. trace := utiltrace.New("Scheduling", utiltrace.Field{Key: "namespace", Value: pod.Namespace}, utiltrace.Field{Key: "name", Value: pod.Name})
  3. defer trace.LogIfLong(100 * time.Millisecond)
  4. if err := sched.Cache.UpdateSnapshot(sched.nodeInfoSnapshot); err != nil {
  5. return result, err
  6. }
  7. trace.Step("Snapshotting scheduler cache and node infos done")
  8. if sched.nodeInfoSnapshot.NumNodes() == 0 {
  9. return result, ErrNoNodesAvailable
  10. }
  11. feasibleNodes, diagnosis, err := sched.findNodesThatFitPod(ctx, fwk, state, pod)
  12. if err != nil {
  13. return result, err
  14. }
  15. trace.Step("Computing predicates done")

与其他扩展点部分,在调度上下文 scheduleOne 中可以很好的看出,功能都是 framework 提供的。

  1. func (sched *Scheduler) scheduleOne(ctx context.Context) {
  2. ...
  3. scheduleResult, err := sched.SchedulePod(schedulingCycleCtx, fwk, state, pod)
  4. ...
  5. // Run the Reserve method of reserve plugins.
  6. if sts := fwk.RunReservePluginsReserve(schedulingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost); !sts.IsSuccess() {
  7. }
  8. ...
  9. // Run "permit" plugins.
  10. runPermitStatus := fwk.RunPermitPlugins(schedulingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost)
  11. // One of the plugins returned status different than success or wait.
  12. fwk.RunReservePluginsUnreserve(schedulingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost)
  13. ...
  14. // bind the pod to its host asynchronously (we can do this b/c of the assumption step above).
  15. go func() {
  16. ...
  17. waitOnPermitStatus := fwk.WaitOnPermit(bindingCycleCtx, assumedPod)
  18. if !waitOnPermitStatus.IsSuccess() {
  19. ...
  20. // trigger un-reserve plugins to clean up state associated with the reserved Pod
  21. fwk.RunReservePluginsUnreserve(bindingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost)
  22. }
  23. // Run "prebind" plugins.
  24. preBindStatus := fwk.RunPreBindPlugins(bindingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost)
  25. ...
  26. // trigger un-reserve plugins to clean up state associated with the reserved Pod
  27. fwk.RunReservePluginsUnreserve(bindingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost)
  28. ...
  29. ...
  30. // trigger un-reserve plugins to clean up state associated with the reserved Pod
  31. fwk.RunReservePluginsUnreserve(bindingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost)
  32. ...
  33. // Run "postbind" plugins.
  34. fwk.RunPostBindPlugins(bindingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost)
  35. ...
  36. }

插件 [4]

插件(Plugins)(也可以算是调度策略)在 kube-scheduler 中的实现为 framework plugin,插件API的实现分为两个步骤:registerconfigured,然后都实现了其父方法 Plugin。然后可以通过配置(kube-scheduler --config 提供)启动或禁用插件;除了默认插件外,还可以实现自定义调度插件与默认插件进行绑定。

  1. type Plugin interface {
  2. Name() string
  3. }
  4. // sort扩展点
  5. type QueueSortPlugin interface {
  6. Plugin
  7. Less(*v1.pod, *v1.pod) bool
  8. }
  9. // PreFilter扩展点
  10. type PreFilterPlugin interface {
  11. Plugin
  12. PreFilter(context.Context, *framework.CycleState, *v1.pod) error
  13. }

插件的载入过程

scheduler 被启动时,会 scheduler.New(cc.Client.. 这个时候会传入 profiles,整个的流如下:

NewScheduler

我们了解如何 New 一个 scheduler 即为 Setup 中去配置这些参数,

  1. func Setup(ctx context.Context, opts *options.Options, outOfTreeRegistryOptions ...Option) (*schedulerserverconfig.CompletedConfig, *scheduler.Scheduler, error) {
  2. ...
  3. // Create the scheduler.
  4. sched, err := scheduler.New(cc.Client,
  5. cc.InformerFactory,
  6. cc.DynInformerFactory,
  7. recorderFactory,
  8. ctx.Done(),
  9. scheduler.WithComponentConfigVersion(cc.ComponentConfig.TypeMeta.APIVersion),
  10. scheduler.WithKubeConfig(cc.KubeConfig),
  11. scheduler.WithProfiles(cc.ComponentConfig.Profiles...),
  12. scheduler.WithPercentageOfNodesToScore(cc.ComponentConfig.PercentageOfNodesToScore),
  13. scheduler.WithFrameworkOutOfTreeRegistry(outOfTreeRegistry),
  14. scheduler.WithPodMaxBackoffSeconds(cc.ComponentConfig.PodMaxBackoffSeconds),
  15. scheduler.WithPodInitialBackoffSeconds(cc.ComponentConfig.PodInitialBackoffSeconds),
  16. scheduler.WithPodMaxInUnschedulablePodsDuration(cc.PodMaxInUnschedulablePodsDuration),
  17. scheduler.WithExtenders(cc.ComponentConfig.Extenders...),
  18. scheduler.WithParallelism(cc.ComponentConfig.Parallelism),
  19. scheduler.WithBuildFrameworkCapturer(func(profile kubeschedulerconfig.KubeSchedulerProfile) {
  20. // Profiles are processed during Framework instantiation to set default plugins and configurations. Capturing them for logging
  21. completedProfiles = append(completedProfiles, profile)
  22. }),
  23. )
  24. ...
  25. }

profile.NewMap

scheduler.New 中,会根据配置生成profile,而 profile.NewMap 会完成这一步

  1. func New(client clientset.Interface,
  2. ...
  3. clusterEventMap := make(map[framework.ClusterEvent]sets.String)
  4. profiles, err := profile.NewMap(options.profiles, registry, recorderFactory, stopCh,
  5. frameworkruntime.WithComponentConfigVersion(options.componentConfigVersion),
  6. frameworkruntime.WithClientSet(client),
  7. frameworkruntime.WithKubeConfig(options.kubeConfig),
  8. frameworkruntime.WithInformerFactory(informerFactory),
  9. frameworkruntime.WithSnapshotSharedLister(snapshot),
  10. frameworkruntime.WithPodNominator(nominator),
  11. frameworkruntime.WithCaptureProfile(frameworkruntime.CaptureProfile(options.frameworkCapturer)),
  12. frameworkruntime.WithClusterEventMap(clusterEventMap),
  13. frameworkruntime.WithParallelism(int(options.parallelism)),
  14. frameworkruntime.WithExtenders(extenders),
  15. )
  16. ...
  17. }

NewFramework

newProfile 返回的则是一个创建好的 framework

  1. func newProfile(cfg config.KubeSchedulerProfile, r frameworkruntime.Registry, recorderFact RecorderFactory,
  2. stopCh <-chan struct{}, opts ...frameworkruntime.Option) (framework.Framework, error) {
  3. recorder := recorderFact(cfg.SchedulerName)
  4. opts = append(opts, frameworkruntime.WithEventRecorder(recorder))
  5. return frameworkruntime.NewFramework(r, &cfg, stopCh, opts...)
  6. }

最终会走到 pluginsNeeded,这里会根据配置中开启的插件而返回一个插件集,这个就是最终在每个扩展点中药执行的插件。

  1. func (f *frameworkImpl) pluginsNeeded(plugins *config.Plugins) sets.String {
  2. pgSet := sets.String{}
  3. if plugins == nil {
  4. return pgSet
  5. }
  6. find := func(pgs *config.PluginSet) {
  7. for _, pg := range pgs.Enabled {
  8. pgSet.Insert(pg.Name)
  9. }
  10. }
  11. // 获取到所有的扩展点,找到为Enabled的插件加入到pgSet
  12. for _, e := range f.getExtensionPoints(plugins) {
  13. find(e.plugins)
  14. }
  15. // Parse MultiPoint separately since they are not returned by f.getExtensionPoints()
  16. find(&plugins.MultiPoint)
  17. return pgSet
  18. }

插件的执行

在对插件源码部分分析,会找几个典型的插件进行分析,而不会对全部的进行分析,因为总的来说是大同小异,分析的插件有 NodePortsNodeResourcesFitpodtopologyspread

NodePorts

这里以一个简单的插件来分析;NodePorts 插件用于检查Pod请求的端口,在节点上是否为空闲端口。

NodePorts 实现了 FilterPluginPreFilterPlugin

PreFilter 将会被 frameworkPreFilter 扩展点被调用。

  1. func (pl *NodePorts) PreFilter(ctx context.Context, cycleState *framework.CycleState, pod *v1.Pod) (*framework.PreFilterResult, *framework.Status) {
  2. s := getContainerPorts(pod) // 或得Pod得端口
  3. // 写入状态
  4. cycleState.Write(preFilterStateKey, preFilterState(s))
  5. return nil, nil
  6. }

Filter 将会被 frameworkFilter 扩展点被调用。

  1. // Filter invoked at the filter extension point.
  2. func (pl *NodePorts) Filter(ctx context.Context, cycleState *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo) *framework.Status {
  3. wantPorts, err := getPreFilterState(cycleState)
  4. if err != nil {
  5. return framework.AsStatus(err)
  6. }
  7. fits := fitsPorts(wantPorts, nodeInfo)
  8. if !fits {
  9. return framework.NewStatus(framework.Unschedulable, ErrReason)
  10. }
  11. return nil
  12. }
  13. func fitsPorts(wantPorts []*v1.ContainerPort, nodeInfo *framework.NodeInfo) bool {
  14. // 对比existingPorts 和 wantPorts是否冲突,冲突则调度失败
  15. existingPorts := nodeInfo.UsedPorts
  16. for _, cp := range wantPorts {
  17. if existingPorts.CheckConflict(cp.HostIP, string(cp.Protocol), cp.HostPort) {
  18. return false
  19. }
  20. }
  21. return true
  22. }

New ,初始化新插件,在 register 中注册得

  1. func New(_ runtime.Object, _ framework.Handle) (framework.Plugin, error) {
  2. return &NodePorts{}, nil
  3. }

在调用中,如果有任何一个插件返回错误,则跳过该扩展点注册得其他插件,返回失败。

  1. func (f *frameworkImpl) RunFilterPlugins(
  2. ctx context.Context,
  3. state *framework.CycleState,
  4. pod *v1.Pod,
  5. nodeInfo *framework.NodeInfo,
  6. ) framework.PluginToStatus {
  7. statuses := make(framework.PluginToStatus)
  8. for _, pl := range f.filterPlugins {
  9. pluginStatus := f.runFilterPlugin(ctx, pl, state, pod, nodeInfo)
  10. if !pluginStatus.IsSuccess() {
  11. if !pluginStatus.IsUnschedulable()
  12. errStatus := framework.AsStatus(fmt.Errorf("running %q filter plugin: %w", pl.Name(), pluginStatus.AsError())).WithFailedPlugin(pl.Name())
  13. return map[string]*framework.Status{pl.Name(): errStatus}
  14. }
  15. pluginStatus.SetFailedPlugin(pl.Name())
  16. statuses[pl.Name()] = pluginStatus
  17. }
  18. }
  19. return statuses
  20. }

返回得状态是一个 Status 结构体,该结构体表示了插件运行的结果。由 Codereasons、(可选)errfailedPlugin (失败的那个插件名)组成。当 code 不是 Success 时,应说明原因。而且,当 codeSuccess 时,其他所有字段都应为空。nil 状态也被视为成功。

  1. type Status struct {
  2. code Code
  3. reasons []string
  4. err error
  5. // failedPlugin is an optional field that records the plugin name a Pod failed by.
  6. // It's set by the framework when code is Error, Unschedulable or UnschedulableAndUnresolvable.
  7. failedPlugin string
  8. }

NodeResourcesFit [5]

NodeResourcesFit 扩展检查节点是否拥有 Pod 请求的所有资源。分数可以使用以下三种策略之一,扩展点为:preFilterfilterscore

  • LeastAllocated (默认)
  • MostAllocated
  • RequestedToCapacityRatio

Fit

NodeResourcesFit PreFilter 可以看到调用得 computePodResourceRequest

  1. // PreFilter invoked at the prefilter extension point.
  2. func (f *Fit) PreFilter(ctx context.Context, cycleState *framework.CycleState, pod *v1.Pod) (*framework.PreFilterResult, *framework.Status) {
  3. cycleState.Write(preFilterStateKey, computePodResourceRequest(pod))
  4. return nil, nil
  5. }

computePodResourceRequest 这里有一个注释,总体解释起来是这样得:computePodResourceRequest ,返回值( framework.Resource)覆盖了每一个维度中资源的最大宽度。因为将按照 init-containers , containers 得顺序运行,会通过迭代方式收集每个维度中的最大值。计算时会对常规容器的资源向量求和,因为containers 运行会同时运行多个容器。计算示例为:

  1. Pod:
  2. InitContainers
  3. IC1:
  4. CPU: 2
  5. Memory: 1G
  6. IC2:
  7. CPU: 2
  8. Memory: 3G
  9. Containers
  10. C1:
  11. CPU: 2
  12. Memory: 1G
  13. C2:
  14. CPU: 1
  15. Memory: 1G

在维度1中(InitContainers)所需资源最大值时,CPU=2, Memory=3G;而维度2(Containers)所需资源最大值为:CPU=2, Memory=1G;那么最终结果为 CPU=3, Memory=3G,因为在维度1,最大资源时Memory=3G;而维度2最大资源是CPU=1+2, Memory=1+1,取每个维度中最大资源最大宽度即为 CPU=3, Memory=3G。

下面则看下代码得实现

  1. func computePodResourceRequest(pod *v1.Pod) *preFilterState {
  2. result := &preFilterState{}
  3. for _, container := range pod.Spec.Containers {
  4. result.Add(container.Resources.Requests)
  5. }
  6. // 取最大得资源
  7. for _, container := range pod.Spec.InitContainers {
  8. result.SetMaxResource(container.Resources.Requests)
  9. }
  10. // 如果Overhead正在使用,需要将其计算到总资源中
  11. if pod.Spec.Overhead != nil {
  12. result.Add(pod.Spec.Overhead)
  13. }
  14. return result
  15. }
  16. // SetMaxResource 是比较ResourceList并为每个资源取最大值。
  17. func (r *Resource) SetMaxResource(rl v1.ResourceList) {
  18. if r == nil {
  19. return
  20. }
  21. for rName, rQuantity := range rl {
  22. switch rName {
  23. case v1.ResourceMemory:
  24. r.Memory = max(r.Memory, rQuantity.Value())
  25. case v1.ResourceCPU:
  26. r.MilliCPU = max(r.MilliCPU, rQuantity.MilliValue())
  27. case v1.ResourceEphemeralStorage:
  28. if utilfeature.DefaultFeatureGate.Enabled(features.LocalStorageCapacityIsolation) {
  29. r.EphemeralStorage = max(r.EphemeralStorage, rQuantity.Value())
  30. }
  31. default:
  32. if schedutil.IsScalarResourceName(rName) {
  33. r.SetScalar(rName, max(r.ScalarResources[rName], rQuantity.Value()))
  34. }
  35. }
  36. }
  37. }

leastAllocate

LeastAllocated 是 NodeResourcesFit 的打分策略 ,LeastAllocated 打分的标准是更偏向于请求资源较少的Node。将会先计算出Node上调度的pod请求的内存、CPU与其他资源的百分比,然后并根据请求的比例与容量的平均值的最小值进行优先级排序。

计算公式是这样的:\(\frac{\frac{cpu((capacity-requested) \times MaxNodeScore \times cpuWeight)}{capacity} + \frac{memory((capacity-requested) \times MaxNodeScore \times memoryWeight}{capacity}) + ...}{weightSum}\)

下面来看下实现

  1. func leastResourceScorer(resToWeightMap resourceToWeightMap) func(resourceToValueMap, resourceToValueMap) int64 {
  2. return func(requested, allocable resourceToValueMap) int64 {
  3. var nodeScore, weightSum int64
  4. for resource := range requested {
  5. weight := resToWeightMap[resource]
  6. // 计算出的资源分数乘weight
  7. resourceScore := leastRequestedScore(requested[resource], allocable[resource])
  8. nodeScore += resourceScore * weight
  9. weightSum += weight
  10. }
  11. if weightSum == 0 {
  12. return 0
  13. }
  14. // 最终除weightSum
  15. return nodeScore / weightSum
  16. }
  17. }

leastRequestedScore 计算标准为未使用容量的计算范围为 0~MaxNodeScore,0 为最低优先级,MaxNodeScore 为最高优先级。未使用的资源越多,得分越高。

  1. func leastRequestedScore(requested, capacity int64) int64 {
  2. if capacity == 0 {
  3. return 0
  4. }
  5. if requested > capacity {
  6. return 0
  7. }
  8. // 容量 - 请求的 x 预期值(100)/ 容量
  9. return ((capacity - requested) * int64(framework.MaxNodeScore)) / capacity
  10. }

Topology [6]

Concept

在对 podtopologyspread 插件进行分析前,先需要掌握Pod拓扑的概念。

Pod拓扑(Pod Topology)是Kubernetes Pod调度机制,可以将Pod分布在集群中不同 Zone ,以及用户自定义的各种拓扑域 (topology domains)。当有了拓扑域后,用户可以更高效的利用集群资源。

如何来解释拓扑域,首先需要提及为什么需要拓扑域,在集群有3个节点,并且当Pod副本数为2时,又不希望两个Pod在同一个Node上运行。在随着扩大Pod的规模,副本数扩展到到15个时,这时候最理想的方式是每个Node运行5个Pod,在这种背景下,用户希望对集群中Zone的安排为相似的副本数量,并且在集群存在部分问题时可以更好的自愈(也是按照相似的副本数量均匀的分布在Node上)。在这种情况下Kubernetes 提供了Pod 拓扑约束来解决这个问题。

定义一个Topology

  1. apiVersion: v1
  2. kind: Pod
  3. metadata:
  4. name: example-pod
  5. spec:
  6. # Configure a topology spread constraint
  7. topologySpreadConstraints:
  8. - maxSkew: <integer> #
  9. minDomains: <integer> # optional; alpha since v1.24
  10. topologyKey: <string>
  11. whenUnsatisfiable: <string>
  12. labelSelector: <object>

参数的描述

  • maxSkew:Required,Pod分布不均的程度,并且数字必须大于零

    • whenUnsatisfiable: DoNotSchedule,则定义目标拓扑中匹配 pod 的数量与 全局最小值拓扑域中的标签选择器匹配的 pod 的最小数量maxSkew之间的最大允许差异。例如有 3 个 Zone,分别具有 2、4 和 5 个匹配的 pod,则全局最小值为 2
    • whenUnsatisfiable: ScheduleAnywayscheduler 会为减少倾斜的拓扑提供更高的优先级。
  • minDomains:optional,符合条件的域的最小数量。
    • 如果不指定该选项 minDomains,则约束的行为 minDomains: 1
    • minDomains必须大于 0。minDomainswhenUnsatisfiable 一起时为whenUnsatisfiable: DoNotSchedule
  • topologyKey:Node label的key,如果多个Node都使用了这个lable key那么 scheduler 将这些 Node 看作为相同的拓扑域。
  • whenUnsatisfiable:当 Pod 不满足分布的约束时,怎么去处理
    • DoNotSchedule(默认)不要调度。
    • ScheduleAnyway仍然调度它,同时优先考虑最小化倾斜节点
  • labelSelector:查找匹配的 Pod label选择器的node进行技术,以计算Pod如何分布在拓扑域中

对于拓扑域的理解

对于拓扑域,官方是这么说明的,假设有一个带有以下lable的 4 节点集群:

  1. NAME STATUS ROLES AGE VERSION LABELS
  2. node1 Ready <none> 4m26s v1.16.0 node=node1,zone=zoneA
  3. node2 Ready <none> 3m58s v1.16.0 node=node2,zone=zoneA
  4. node3 Ready <none> 3m17s v1.16.0 node=node3,zone=zoneB
  5. node4 Ready <none> 2m43s v1.16.0 node=node4,zone=zoneB

那么集群拓扑如图:

图1:集群拓扑图
Source:https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/

假设一个 4 节点集群,其中 3个label被标记为foo: bar的 Pod 分别位于Node1、Node2 和 Node3:

图2:集群拓扑图
Source:https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/

这种情况下,新部署一个Pod,并希望新Pod与现有Pod跨 Zone均匀分布,资源清单文件如下:

  1. kind: Pod
  2. apiVersion: v1
  3. metadata:
  4. name: mypod
  5. labels:
  6. foo: bar
  7. spec:
  8. topologySpreadConstraints:
  9. - maxSkew: 1
  10. topologyKey: zone
  11. whenUnsatisfiable: DoNotSchedule
  12. labelSelector:
  13. matchLabels:
  14. foo: bar
  15. containers:
  16. - name: pause
  17. image: k8s.gcr.io/pause:3.1

这个清单对于拓扑域来说,topologyKey: zone 表示对Pod均匀分布仅应用于已标记的节点(如 foo: bar),将会跳过没有标签的节点(如zone: <any value>)。如果 scheduler 找不到满足约束的方法,whenUnsatisfiable: DoNotSchedule 设置的策略则是 scheduler 对新部署的Pod保持 Pendding

如果此时 scheduler 将新Pod 调度至 \(Zone_A\),此时Pod分布在拓扑域间为 \([3,1]\) ,而 maxSkew 配置的值是1。此时倾斜值为 \(Zone_A - Zone_B = 3-1=2\),不满足 maxSkew=1,故这个Pod只能被调度到 \(Zone_B\)。

此时Pod调度拓扑图为图3或图4

图3:集群拓扑图
Source:https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/

图4:集群拓扑图
Source:https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/

如果需要将Pod调度到 \(Zone_A\) ,可以按照如下方式进行:

  • 修改 maxSkew=2
  • 修改 topologyKey: node 而不是 Zone ,这种模式下可以将 Pod 均匀分布在Node而不是Zone之间。
  • 修改 whenUnsatisfiable: DoNotSchedulewhenUnsatisfiable: ScheduleAnyway 确保新的Pod始终可被调度

下面再通过一个例子增强对拓扑域了解

多拓扑约束

设拥有一个 4 节点集群,其中 3 个现有 Pod 标记 foo: bar 分别位于 node1node2node3

图5:集群拓扑图
Source:https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/

部署的资源清单如下:可以看出拓扑分布约束配置了多个

  1. kind: Pod
  2. apiVersion: v1
  3. metadata:
  4. name: mypod
  5. labels:
  6. foo: bar
  7. spec:
  8. topologySpreadConstraints:
  9. - maxSkew: 1
  10. topologyKey: zone
  11. whenUnsatisfiable: DoNotSchedule
  12. labelSelector:
  13. matchLabels:
  14. foo: bar
  15. - maxSkew: 1
  16. topologyKey: node
  17. whenUnsatisfiable: DoNotSchedule
  18. labelSelector:
  19. matchLabels:
  20. foo: bar
  21. containers:
  22. - name: pause
  23. image: k8s.gcr.io/pause:3.1

在这种情况下,为了匹配第一个约束条件,新Pod 只能放置在 \(Zone_B\) ;而就第二个约束条件,新Pod只能调度到 node4。在这种配置多约束条件下, scheduler 只考虑满足所有约束的值,因此唯一有效的是 node4

如何为集群设置一个默认拓扑域约束

默认情况下,拓扑域约束也作 scheduler 的为 scheduler configurtion 中的一部分参数,这也意味着,可以通过profile为整个集群级别指定一个默认的拓扑域调度约束,

  1. apiVersion: kubescheduler.config.k8s.io/v1beta3
  2. kind: KubeSchedulerConfiguration
  3. profiles:
  4. - schedulerName: default-scheduler
  5. pluginConfig:
  6. - name: PodTopologySpread
  7. args:
  8. defaultConstraints:
  9. - maxSkew: 1
  10. topologyKey: topology.kubernetes.io/zone
  11. whenUnsatisfiable: ScheduleAnyway
  12. defaultingType: List

默认约束策略

如果在没有配置集群级别的约束策略时,kube-scheduler 内部 topologyspread 插件提供了一个默认的拓扑约束策略,大致上如下列清单所示

  1. defaultConstraints:
  2. - maxSkew: 3
  3. topologyKey: "kubernetes.io/hostname"
  4. whenUnsatisfiable: ScheduleAnyway
  5. - maxSkew: 5
  6. topologyKey: "topology.kubernetes.io/zone"
  7. whenUnsatisfiable: ScheduleAnyway

上述清单中内容可以在 pkg\scheduler\framework\plugins\podtopologyspread\plugin.go

  1. var systemDefaultConstraints = []v1.TopologySpreadConstraint{
  2. {
  3. TopologyKey: v1.LabelHostname,
  4. WhenUnsatisfiable: v1.ScheduleAnyway,
  5. MaxSkew: 3,
  6. },
  7. {
  8. TopologyKey: v1.LabelTopologyZone,
  9. WhenUnsatisfiable: v1.ScheduleAnyway,
  10. MaxSkew: 5,
  11. },
  12. }

可以通过在配置文件中留空,来禁用默认配置

  • defaultConstraints: []
  • defaultingType: List
  1. apiVersion: kubescheduler.config.k8s.io/v1beta3
  2. kind: KubeSchedulerConfiguration
  3. profiles:
  4. - schedulerName: default-scheduler
  5. pluginConfig:
  6. - name: PodTopologySpread
  7. args:
  8. defaultConstraints: []
  9. defaultingType: List

通过源码学习Topology

podtopologyspread 实现了4种扩展点方法,包含 filterscore

PreFilter

可以看到 PreFilter 的核心为 calPreFilterState

  1. func (pl *PodTopologySpread) PreFilter(ctx context.Context, cycleState *framework.CycleState, pod *v1.Pod) (*framework.PreFilterResult, *framework.Status) {
  2. s, err := pl.calPreFilterState(ctx, pod)
  3. if err != nil {
  4. return nil, framework.AsStatus(err)
  5. }
  6. cycleState.Write(preFilterStateKey, s)
  7. return nil, nil
  8. }

calPreFilterState 主要功能是用在计算如何在拓扑域中分布Pod,首先看段代码时,需要掌握下属几个概念

  1. func (pl *PodTopologySpread) calPreFilterState(ctx context.Context, pod *v1.Pod) (*preFilterState, error) {
  2. // 获取Node
  3. allNodes, err := pl.sharedLister.NodeInfos().List()
  4. if err != nil {
  5. return nil, fmt.Errorf("listing NodeInfos: %w", err)
  6. }
  7. var constraints []topologySpreadConstraint
  8. if len(pod.Spec.TopologySpreadConstraints) > 0 {
  9. // 这里会构建出TopologySpreadConstraints,因为约束是不确定的
  10. constraints, err = filterTopologySpreadConstraints(
  11. pod.Spec.TopologySpreadConstraints,
  12. v1.DoNotSchedule,
  13. pl.enableMinDomainsInPodTopologySpread,
  14. pl.enableNodeInclusionPolicyInPodTopologySpread,
  15. )
  16. if err != nil {
  17. return nil, fmt.Errorf("obtaining pod's hard topology spread constraints: %w", err)
  18. }
  19. } else {
  20. // buildDefaultConstraints使用".DefaultConstraints"与pod匹配的
  21. // service、replication controllers、replica sets
  22. // 和stateful sets的选择器为pod构建一个约束。
  23. constraints, err = pl.buildDefaultConstraints(pod, v1.DoNotSchedule)
  24. if err != nil {
  25. return nil, fmt.Errorf("setting default hard topology spread constraints: %w", err)
  26. }
  27. }
  28. if len(constraints) == 0 { // 如果是空的,则返回空preFilterState
  29. return &preFilterState{}, nil
  30. }
  31. // 初始化一个 preFilterState 状态
  32. s := preFilterState{
  33. Constraints: constraints,
  34. TpKeyToCriticalPaths: make(map[string]*criticalPaths, len(constraints)),
  35. TpPairToMatchNum: make(map[topologyPair]int, sizeHeuristic(len(allNodes), constraints)),
  36. }
  37. // 根据node统计拓扑域数量
  38. tpCountsByNode := make([]map[topologyPair]int, len(allNodes))
  39. // 获取pod亲和度配置
  40. requiredNodeAffinity := nodeaffinity.GetRequiredNodeAffinity(pod)
  41. processNode := func(i int) {
  42. nodeInfo := allNodes[i]
  43. node := nodeInfo.Node()
  44. if node == nil {
  45. klog.ErrorS(nil, "Node not found")
  46. return
  47. }
  48. // 通过spreading去过滤node以用作filters,错误解析以向后兼容
  49. if !pl.enableNodeInclusionPolicyInPodTopologySpread {
  50. if match, _ := requiredNodeAffinity.Match(node); !match {
  51. return
  52. }
  53. }
  54. // 确保node的lable 包含topologyKeys定义的值
  55. if !nodeLabelsMatchSpreadConstraints(node.Labels, constraints) {
  56. return
  57. }
  58. tpCounts := make(map[topologyPair]int, len(constraints))
  59. for _, c := range constraints { // 对应的约束列表
  60. if pl.enableNodeInclusionPolicyInPodTopologySpread &&
  61. !c.matchNodeInclusionPolicies(pod, node, requiredNodeAffinity) {
  62. continue
  63. }
  64. // 构建出 topologyPair 以key value形式,
  65. // 通常情况下TopologyKey属于什么类型的拓扑
  66. // node.Labels[c.TopologyKey] 则是属于这个拓扑中那个子域
  67. pair := topologyPair{key: c.TopologyKey, value: node.Labels[c.TopologyKey]}
  68. // 计算与标签选择器相匹配的pod有多少个
  69. count := countPodsMatchSelector(nodeInfo.Pods, c.Selector, pod.Namespace)
  70. tpCounts[pair] = count
  71. }
  72. tpCountsByNode[i] = tpCounts // 最终形成的拓扑结构
  73. }
  74. // 执行上面的定义的processNode,执行的数量就是node的数量
  75. pl.parallelizer.Until(ctx, len(allNodes), processNode)
  76. // 最后构建出 TpPairToMatchNum
  77. // 表示每个拓扑域中的每个子域各分布多少Pod,如图6所示
  78. for _, tpCounts := range tpCountsByNode {
  79. for tp, count := range tpCounts {
  80. s.TpPairToMatchNum[tp] += count
  81. }
  82. }
  83. if pl.enableMinDomainsInPodTopologySpread {
  84. // 根据状态进行构建 preFilterState
  85. s.TpKeyToDomainsNum = make(map[string]int, len(constraints))
  86. for tp := range s.TpPairToMatchNum {
  87. s.TpKeyToDomainsNum[tp.key]++
  88. }
  89. }
  90. // 计算最小匹配出的拓扑对
  91. for i := 0; i < len(constraints); i++ {
  92. key := constraints[i].TopologyKey
  93. s.TpKeyToCriticalPaths[key] = newCriticalPaths()
  94. }
  95. for pair, num := range s.TpPairToMatchNum {
  96. s.TpKeyToCriticalPaths[pair.key].update(pair.value, num)
  97. }
  98. return &s, nil // 返回的值则包含最小的分布
  99. }

preFilterState

  1. // preFilterState 是在PreFilter处计算并在Filter处使用。
  2. // 它结合了 “TpKeyToCriticalPaths” 和 “TpPairToMatchNum” 来表示:
  3. //(1)在每个分布约束上匹配最少pod的criticalPaths。
  4. // (2) 在每个分布约束上匹配的pod的数量。
  5. // “nil preFilterState” 则表示没有设置(在PreFilter阶段);
  6. // empty “preFilterState”对象则表示它是一个合法的状态,并在PreFilter阶段设置。
  7. type preFilterState struct {
  8. Constraints []topologySpreadConstraint
  9. // 这里记录2条关键路径而不是所有关键路径。
  10. // criticalPaths[0].MatchNum 始终保存最小匹配数。
  11. // criticalPaths[1].MatchNum 总是大于或等于criticalPaths[0].MatchNum,但不能保证是第二个最小匹配数。
  12. TpKeyToCriticalPaths map[string]*criticalPaths
  13. // TpKeyToDomainsNum 以 “topologyKey” 作为key ,并以zone的数量作为值。
  14. TpKeyToDomainsNum map[string]int
  15. // TpPairToMatchNum 以 “topologyPair作为key” ,并以匹配到pod的数量作为value。
  16. TpPairToMatchNum map[topologyPair]int
  17. }

criticalPaths

  1. // [2]criticalPath能够工作的原因是基于当前抢占算法的实现,特别是以下两个事实
  2. // 事实 1:只抢占同一节点上的Pod,而不是多个节点上的 Pod。
  3. // 事实 2:每个节点在其抢占周期期间在“preFilterState”的单独副本上进行评估。如果我们计划转向更复杂的算法,例如“多个节点上的任意pod”时则需要重新考虑这种结构。
  4. type criticalPaths [2]struct {
  5. // TopologyValue代表映射到拓扑键的拓扑值。
  6. TopologyValue string
  7. // MatchNum代表匹配到的pod数量
  8. MatchNum int
  9. }

单元测试中的测试案例,具有两个约束条件的场景,通过表格来解析如下:

Node列表与标签如下表:

Node Name ️Lable-zone ️Lable-node
node-a zone1 node-a
node-b zone1 node-b
node-x zone2 node-x
node-y zone2 node-y

Pod列表与标签如下表:

Pod Name Node ️Label
p-a1 node-a foo:
p-a2 node-a foo:
p-b1 node-b foo:
p-y1 node-y foo:
p-y2 node-y foo:
p-y3 node-y foo:
p-y4 node-y foo:

对应的拓扑约束

  1. spec:
  2. topologySpreadConstraints:
  3. - MaxSkew: 1
  4. TopologyKey: zone
  5. labelSelector:
  6. matchLabels:
  7. foo: bar
  8. MinDomains: 1
  9. NodeAffinityPolicy: Honor
  10. NodeTaintsPolicy: Ignore
  11. - MaxSkew: 1
  12. TopologyKey: node
  13. labelSelector:
  14. matchLabels:
  15. foo: bar
  16. MinDomains: 1
  17. NodeAffinityPolicy: Honor
  18. NodeTaintsPolicy: Ignore

那么整个分布如下:

图6:具有两个场景的分布图

实现的测试代码如下

  1. {
  2. name: "normal case with two spreadConstraints",
  3. pod: st.MakePod().Name("p").Label("foo", "").
  4. SpreadConstraint(1, "zone", v1.DoNotSchedule, fooSelector, nil, nil, nil).
  5. SpreadConstraint(1, "node", v1.DoNotSchedule, fooSelector, nil, nil, nil).
  6. Obj(),
  7. nodes: []*v1.Node{
  8. st.MakeNode().Name("node-a").Label("zone", "zone1").Label("node", "node-a").Obj(),
  9. st.MakeNode().Name("node-b").Label("zone", "zone1").Label("node", "node-b").Obj(),
  10. st.MakeNode().Name("node-x").Label("zone", "zone2").Label("node", "node-x").Obj(),
  11. st.MakeNode().Name("node-y").Label("zone", "zone2").Label("node", "node-y").Obj(),
  12. },
  13. existingPods: []*v1.Pod{
  14. st.MakePod().Name("p-a1").Node("node-a").Label("foo", "").Obj(),
  15. st.MakePod().Name("p-a2").Node("node-a").Label("foo", "").Obj(),
  16. st.MakePod().Name("p-b1").Node("node-b").Label("foo", "").Obj(),
  17. st.MakePod().Name("p-y1").Node("node-y").Label("foo", "").Obj(),
  18. st.MakePod().Name("p-y2").Node("node-y").Label("foo", "").Obj(),
  19. st.MakePod().Name("p-y3").Node("node-y").Label("foo", "").Obj(),
  20. st.MakePod().Name("p-y4").Node("node-y").Label("foo", "").Obj(),
  21. },
  22. want: &preFilterState{
  23. Constraints: []topologySpreadConstraint{
  24. {
  25. MaxSkew: 1,
  26. TopologyKey: "zone",
  27. Selector: mustConvertLabelSelectorAsSelector(t, fooSelector),
  28. MinDomains: 1,
  29. NodeAffinityPolicy: v1.NodeInclusionPolicyHonor,
  30. NodeTaintsPolicy: v1.NodeInclusionPolicyIgnore,
  31. },
  32. {
  33. MaxSkew: 1,
  34. TopologyKey: "node",
  35. Selector: mustConvertLabelSelectorAsSelector(t, fooSelector),
  36. MinDomains: 1,
  37. NodeAffinityPolicy: v1.NodeInclusionPolicyHonor,
  38. NodeTaintsPolicy: v1.NodeInclusionPolicyIgnore,
  39. },
  40. },
  41. TpKeyToCriticalPaths: map[string]*criticalPaths{
  42. "zone": {{"zone1", 3}, {"zone2", 4}},
  43. "node": {{"node-x", 0}, {"node-b", 1}},
  44. },
  45. for pair, num := range s.TpPairToMatchNum {
  46. s.TpKeyToCriticalPaths[pair.key].update(pair.value, num)
  47. }
  48. TpPairToMatchNum: map[topologyPair]int{
  49. {key: "zone", value: "zone1"}: 3,
  50. {key: "zone", value: "zone2"}: 4,
  51. {key: "node", value: "node-a"}: 2,
  52. {key: "node", value: "node-b"}: 1,
  53. {key: "node", value: "node-x"}: 0,
  54. {key: "node", value: "node-y"}: 4,
  55. },
  56. },
  57. },

update

update 函数实际上时用于计算 criticalPaths 中的第一位始终保持为是一个最小Pod匹配值

  1. func (p *criticalPaths) update(tpVal string, num int) {
  2. // first verify if `tpVal` exists or not
  3. i := -1
  4. if tpVal == p[0].TopologyValue {
  5. i = 0
  6. } else if tpVal == p[1].TopologyValue {
  7. i = 1
  8. }
  9. if i >= 0 {
  10. // `tpVal` 表示已经存在
  11. p[i].MatchNum = num
  12. if p[0].MatchNum > p[1].MatchNum {
  13. // swap paths[0] and paths[1]
  14. p[0], p[1] = p[1], p[0]
  15. }
  16. } else {
  17. // `tpVal` 表示不存在,如一个新初始化的值
  18. // num对应子域分布的pod
  19. // 说明第一个元素不是最小的,则作为交换
  20. if num < p[0].MatchNum {
  21. // update paths[1] with paths[0]
  22. p[1] = p[0]
  23. // update paths[0]
  24. p[0].TopologyValue, p[0].MatchNum = tpVal, num
  25. } else if num < p[1].MatchNum {
  26. // 如果小于 paths[1],则更新它,永远保证元素0是最小,1是次小的
  27. p[1].TopologyValue, p[1].MatchNum = tpVal, num
  28. }
  29. }
  30. }

综合来讲 Prefilter 主要做的工作是。循环所有的节点,先根据 NodeAffinity 或者 NodeSelector 进行过滤,然后根据约束中定义的 topologyKeys (拓扑划分的依据) 来选择节点。

接下来会计算出每个拓扑域下的拓扑对(可以理解为子域)匹配的 Pod 数量,存入 TpPairToMatchNum 中,最后就是要把所有约束中匹配的 Pod 数量最小(第二小)匹配出来的路径(代码是这么定义的,理解上可以看作是分布图)放入 TpKeyToCriticalPaths 中保存起来。整个 preFilterState 保存下来传递到后续的 filter 插件中使用。

Filter

preFilter 中 最后的计算结果会保存在 CycleState

  1. cycleState.Write(preFilterStateKey, s)

Filter 主要是从 PreFilter 处理的过程中拿到状态 preFilterState,然后看下每个拓扑约束中的 MaxSkew 是否合法,具体的计算公式为:\(matchNum + selfMatchNum - minMatchNum\)

  • matchNum:Prefilter 中计算出的对应的拓扑分布数量,可以在Prefilter中参考对应的内容

    • if tpCount, ok := s.TpPairToMatchNum[pair]; ok {
  • selfMatchNum:匹配到label的数量,匹配到则是1,否则为0
  • minMatchNum:获的 Prefilter 中计算出来的最小匹配的值
  1. func (pl *PodTopologySpread) Filter(ctx context.Context, cycleState *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo) *framework.Status {
  2. node := nodeInfo.Node()
  3. if node == nil {
  4. return framework.AsStatus(fmt.Errorf("node not found"))
  5. }
  6. // 拿到 prefilter处理的s,即preFilterState
  7. s, err := getPreFilterState(cycleState)
  8. if err != nil {
  9. return framework.AsStatus(err)
  10. }
  11. // 一个 空类型的 preFilterState是合法的,这种情况下将容忍每一个被调度的 Pod
  12. if len(s.Constraints) == 0 {
  13. return nil
  14. }
  15. podLabelSet := labels.Set(pod.Labels) // 设置标签
  16. for _, c := range s.Constraints { // 因为拓扑约束允许多个所以
  17. tpKey := c.TopologyKey
  18. tpVal, ok := node.Labels[c.TopologyKey]
  19. if !ok {
  20. klog.V(5).InfoS("Node doesn't have required label", "node", klog.KObj(node), "label", tpKey)
  21. return framework.NewStatus(framework.UnschedulableAndUnresolvable, ErrReasonNodeLabelNotMatch)
  22. }
  23. // 判断标准
  24. // 现有的匹配数量 + 子匹配(1|0) - 全局minimum <= maxSkew
  25. minMatchNum, err := s.minMatchNum(tpKey, c.MinDomains, pl.enableMinDomainsInPodTopologySpread)
  26. if err != nil {
  27. klog.ErrorS(err, "Internal error occurred while retrieving value precalculated in PreFilter", "topologyKey", tpKey, "paths", s.TpKeyToCriticalPaths)
  28. continue
  29. }
  30. selfMatchNum := 0
  31. if c.Selector.Matches(podLabelSet) {
  32. selfMatchNum = 1
  33. }
  34. pair := topologyPair{key: tpKey, value: tpVal}
  35. matchNum := 0
  36. if tpCount, ok := s.TpPairToMatchNum[pair]; ok {
  37. matchNum = tpCount
  38. }
  39. skew := matchNum + selfMatchNum - minMatchNum
  40. if skew > int(c.MaxSkew) {
  41. klog.V(5).InfoS("Node failed spreadConstraint: matchNum + selfMatchNum - minMatchNum > maxSkew", "node", klog.KObj(node), "topologyKey", tpKey, "matchNum", matchNum, "selfMatchNum", selfMatchNum, "minMatchNum", minMatchNum, "maxSkew", c.MaxSkew)
  42. return framework.NewStatus(framework.Unschedulable, ErrReasonConstraintsNotMatch)
  43. }
  44. }
  45. return nil
  46. }

minMatchNum

  1. // minMatchNum用于计算 倾斜的全局最小值,同时考虑 MinDomains。
  2. func (s *preFilterState) minMatchNum(tpKey string, minDomains int32, enableMinDomainsInPodTopologySpread bool) (int, error) {
  3. paths, ok := s.TpKeyToCriticalPaths[tpKey]
  4. if !ok {
  5. return 0, fmt.Errorf("failed to retrieve path by topology key")
  6. }
  7. // 通常来说最小值是第一个
  8. minMatchNum := paths[0].MatchNum
  9. if !enableMinDomainsInPodTopologySpread { // 就是plugin的配置的 enableMinDomainsInPodTopologySpread
  10. return minMatchNum, nil
  11. }
  12. domainsNum, ok := s.TpKeyToDomainsNum[tpKey]
  13. if !ok {
  14. return 0, fmt.Errorf("failed to retrieve the number of domains by topology key")
  15. }
  16. if domainsNum < int(minDomains) {
  17. // 当有匹配拓扑键的符合条件的域的数量小于 配置的"minDomains"(每个约束条件的这个配置) 时,
  18. //它将全局“minimum” 设置为0。
  19. // 因为minimum默认就为1,如果他小于1,就让他为0
  20. minMatchNum = 0
  21. }
  22. return minMatchNum, nil
  23. }

PreScore

与 Filter 类似, PreScore 也是类似 PreFilter 的构成。 initPreScoreState 来完成过滤。

有了 PreFilter 基础后,对于 Score 来说大同小异

  1. func (pl *PodTopologySpread) PreScore(
  2. ctx context.Context,
  3. cycleState *framework.CycleState,
  4. pod *v1.Pod,
  5. filteredNodes []*v1.Node,
  6. ) *framework.Status {
  7. allNodes, err := pl.sharedLister.NodeInfos().List()
  8. if err != nil {
  9. return framework.AsStatus(fmt.Errorf("getting all nodes: %w", err))
  10. }
  11. if len(filteredNodes) == 0 || len(allNodes) == 0 {
  12. // No nodes to score.
  13. return nil
  14. }
  15. state := &preScoreState{
  16. IgnoredNodes: sets.NewString(),
  17. TopologyPairToPodCounts: make(map[topologyPair]*int64),
  18. }
  19. // Only require that nodes have all the topology labels if using
  20. // non-system-default spreading rules. This allows nodes that don't have a
  21. // zone label to still have hostname spreading.
  22. // 如果使用非系统默认分布规则,则仅要求节点具有所有拓扑标签。
  23. // 这将允许没有zone标签的节点仍然具有hostname分布。
  24. requireAllTopologies := len(pod.Spec.TopologySpreadConstraints) > 0 || !pl.systemDefaulted
  25. err = pl.initPreScoreState(state, pod, filteredNodes, requireAllTopologies)
  26. if err != nil {
  27. return framework.AsStatus(fmt.Errorf("calculating preScoreState: %w", err))
  28. }
  29. // return if incoming pod doesn't have soft topology spread Constraints.
  30. if len(state.Constraints) == 0 {
  31. cycleState.Write(preScoreStateKey, state)
  32. return nil
  33. }
  34. // Ignore parsing errors for backwards compatibility.
  35. requiredNodeAffinity := nodeaffinity.GetRequiredNodeAffinity(pod)
  36. processAllNode := func(i int) {
  37. nodeInfo := allNodes[i]
  38. node := nodeInfo.Node()
  39. if node == nil {
  40. return
  41. }
  42. if !pl.enableNodeInclusionPolicyInPodTopologySpread {
  43. // `node` should satisfy incoming pod's NodeSelector/NodeAffinity
  44. if match, _ := requiredNodeAffinity.Match(node); !match {
  45. return
  46. }
  47. }
  48. // All topologyKeys need to be present in `node`
  49. if requireAllTopologies && !nodeLabelsMatchSpreadConstraints(node.Labels, state.Constraints) {
  50. return
  51. }
  52. for _, c := range state.Constraints {
  53. if pl.enableNodeInclusionPolicyInPodTopologySpread &&
  54. !c.matchNodeInclusionPolicies(pod, node, requiredNodeAffinity) {
  55. continue
  56. }
  57. pair := topologyPair{key: c.TopologyKey, value: node.Labels[c.TopologyKey]}
  58. // If current topology pair is not associated with any candidate node,
  59. // continue to avoid unnecessary calculation.
  60. // Per-node counts are also skipped, as they are done during Score.
  61. tpCount := state.TopologyPairToPodCounts[pair]
  62. if tpCount == nil {
  63. continue
  64. }
  65. count := countPodsMatchSelector(nodeInfo.Pods, c.Selector, pod.Namespace)
  66. atomic.AddInt64(tpCount, int64(count))
  67. }
  68. }
  69. pl.parallelizer.Until(ctx, len(allNodes), processAllNode)
  70. // 保存状态给后面sorce调用
  71. cycleState.Write(preScoreStateKey, state)
  72. return nil
  73. }

与Filter中Update使用的函数一样,这里也会到这一步,这里会构建出TopologySpreadConstraints,因为约束是不确定的

  1. func filterTopologySpreadConstraints(constraints []v1.TopologySpreadConstraint, action v1.UnsatisfiableConstraintAction, enableMinDomainsInPodTopologySpread, enableNodeInclusionPolicyInPodTopologySpread bool) ([]topologySpreadConstraint, error) {
  2. var result []topologySpreadConstraint
  3. for _, c := range constraints {
  4. if c.WhenUnsatisfiable == action { // 始终调度时
  5. selector, err := metav1.LabelSelectorAsSelector(c.LabelSelector)
  6. if err != nil {
  7. return nil, err
  8. }
  9. tsc := topologySpreadConstraint{
  10. MaxSkew: c.MaxSkew,
  11. TopologyKey: c.TopologyKey,
  12. Selector: selector,
  13. MinDomains: 1, // If MinDomains is nil, we treat MinDomains as 1.
  14. NodeAffinityPolicy: v1.NodeInclusionPolicyHonor, // If NodeAffinityPolicy is nil, we treat NodeAffinityPolicy as "Honor".
  15. NodeTaintsPolicy: v1.NodeInclusionPolicyIgnore, // If NodeTaintsPolicy is nil, we treat NodeTaintsPolicy as "Ignore".
  16. }
  17. if enableMinDomainsInPodTopologySpread && c.MinDomains != nil {
  18. tsc.MinDomains = *c.MinDomains
  19. }
  20. if enableNodeInclusionPolicyInPodTopologySpread {
  21. if c.NodeAffinityPolicy != nil {
  22. tsc.NodeAffinityPolicy = *c.NodeAffinityPolicy
  23. }
  24. if c.NodeTaintsPolicy != nil {
  25. tsc.NodeTaintsPolicy = *c.NodeTaintsPolicy
  26. }
  27. }
  28. result = append(result, tsc)
  29. }
  30. }
  31. return result, nil
  32. }

Score

  1. // 在分数扩展点调用分数。该函数返回的“score”是 `nodeName` 上匹配的 pod 数量,稍后会进行归一化。
  2. func (pl *PodTopologySpread) Score(ctx context.Context, cycleState *framework.CycleState, pod *v1.Pod, nodeName string) (int64, *framework.Status) {
  3. nodeInfo, err := pl.sharedLister.NodeInfos().Get(nodeName)
  4. if err != nil {
  5. return 0, framework.AsStatus(fmt.Errorf("getting node %q from Snapshot: %w", nodeName, err))
  6. }
  7. node := nodeInfo.Node()
  8. s, err := getPreScoreState(cycleState)
  9. if err != nil {
  10. return 0, framework.AsStatus(err)
  11. }
  12. // Return if the node is not qualified.
  13. if s.IgnoredNodes.Has(node.Name) {
  14. return 0, nil
  15. }
  16. // 对于每个当前的 <pair>,当前节点获得 <matchSum> 的信用分。
  17. // 计算 <matchSum>总和 并将其作为该节点的分数返回。
  18. var score float64
  19. for i, c := range s.Constraints {
  20. if tpVal, ok := node.Labels[c.TopologyKey]; ok {
  21. var cnt int64
  22. if c.TopologyKey == v1.LabelHostname {
  23. cnt = int64(countPodsMatchSelector(nodeInfo.Pods, c.Selector, pod.Namespace))
  24. } else {
  25. pair := topologyPair{key: c.TopologyKey, value: tpVal}
  26. cnt = *s.TopologyPairToPodCounts[pair]
  27. }
  28. score += scoreForCount(cnt, c.MaxSkew, s.TopologyNormalizingWeight[i])
  29. }
  30. }
  31. return int64(math.Round(score)), nil
  32. }

Framework 中会运行 ScoreExtension ,即 NormalizeScore

  1. // Run NormalizeScore method for each ScorePlugin in parallel.
  2. f.Parallelizer().Until(ctx, len(f.scorePlugins), func(index int) {
  3. pl := f.scorePlugins[index]
  4. nodeScoreList := pluginToNodeScores[pl.Name()]
  5. if pl.ScoreExtensions() == nil {
  6. return
  7. }
  8. status := f.runScoreExtension(ctx, pl, state, pod, nodeScoreList)
  9. if !status.IsSuccess() {
  10. err := fmt.Errorf("plugin %q failed with: %w", pl.Name(), status.AsError())
  11. errCh.SendErrorWithCancel(err, cancel)
  12. return
  13. }
  14. })
  15. if err := errCh.ReceiveError(); err != nil {
  16. return nil, framework.AsStatus(fmt.Errorf("running Normalize on Score plugins: %w", err))
  17. }

NormalizeScore 会为所有的node根据之前计算出的权重进行打分

  1. func (pl *PodTopologySpread) NormalizeScore(ctx context.Context, cycleState *framework.CycleState, pod *v1.Pod, scores framework.NodeScoreList) *framework.Status {
  2. s, err := getPreScoreState(cycleState)
  3. if err != nil {
  4. return framework.AsStatus(err)
  5. }
  6. if s == nil {
  7. return nil
  8. }
  9. // 计算 <minScore> 和 <maxScore>
  10. var minScore int64 = math.MaxInt64
  11. var maxScore int64
  12. for i, score := range scores {
  13. // it's mandatory to check if <score.Name> is present in m.IgnoredNodes
  14. if s.IgnoredNodes.Has(score.Name) {
  15. scores[i].Score = invalidScore
  16. continue
  17. }
  18. if score.Score < minScore {
  19. minScore = score.Score
  20. }
  21. if score.Score > maxScore {
  22. maxScore = score.Score
  23. }
  24. }
  25. for i := range scores {
  26. if scores[i].Score == invalidScore {
  27. scores[i].Score = 0
  28. continue
  29. }
  30. if maxScore == 0 {
  31. scores[i].Score = framework.MaxNodeScore
  32. continue
  33. }
  34. s := scores[i].Score
  35. scores[i].Score = framework.MaxNodeScore * (maxScore + minScore - s) / maxScore
  36. }
  37. return nil
  38. }

到此,对于pod拓扑插件功能大概可以明了了,

  • Filter 部分(PreFilterFilter)完成拓扑对(Topology Pair)划分
  • Score部分(PreScore, Score , NormalizeScore )主要是对拓扑对(可以理解为拓扑结构划分)来选择一个最适合的pod的节点(即分数最优的节点)

而在 scoring_test.go 给了很多用例,可以更深入的了解这部分算法

Reference

[1] scheduling code hierarchy

[2] scheduler algorithm

[3] in tree VS out of tree volume plugins

[4] scheduler_framework_plugins

[5] scheduling config

[6] topology spread constraints

彻底搞懂kubernetes调度框架与插件的更多相关文章

  1. 夯实Java基础系列19:一文搞懂Java集合类框架,以及常见面试题

    本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial 喜欢的话麻烦点下 ...

  2. Hadoop系列番外篇之一文搞懂Hadoop RPC框架及细节实现

    @ 目录 Hadoop RPC 框架解析 1.Hadoop RPC框架概述 1.1 RPC框架特点 1.2 Hadoop RPC框架 2.Java基础知识回顾 2.1 Java反射机制与动态代理 2. ...

  3. kubernetes 调度器

    调度器 kube-scheduler 是 kubernetes 的核心组件之一,主要负责整个集群资源的调度功能,根据特定的调度算法和策略,将 Pod 调度到最优的工作节点上面去,从而更加合理.更加充分 ...

  4. # kubernetes调度之nodeName与NodeSelector

    系列目录 Kubernetes的调度有简单,有复杂,指定NodeName和使用NodeSelector调度是最简单的,可以将Pod调度到期望的节点上. 本文主要介绍kubernetes调度框架中的No ...

  5. 搞懂 XML 解析,徒手造 WEB 框架

    恕我斗胆直言,对开源的 WEB 框架了解多少,有没有尝试写过框架呢?XML 的解析方式有哪些?能答出来吗?! 心中没有答案也没关系,因为通过今天的分享,能让你轻松 get 如下几点,绝对收获满满. a ...

  6. React16源码解读:开篇带你搞懂几个面试考点

    引言 如今,主流的前端框架React,Vue和Angular在前端领域已成三足鼎立之势,基于前端技术栈的发展现状,大大小小的公司或多或少也会使用其中某一项或者多项技术栈,那么掌握并熟练使用其中至少一种 ...

  7. 搞懂分布式技术10:LVS实现负载均衡的原理与实践

    搞懂分布式技术10:LVS实现负载均衡的原理与实践 浅析负载均衡及LVS实现 原创: fireflyc 写程序的康德 2017-09-19 负载均衡 负载均衡(Load Balance,缩写LB)是一 ...

  8. 搞懂ELK并不是一件特别难的事(ELK)

    本篇文章主要介绍elk的一些框架组成,原理和实践,采用的ELK本版为7.7.0版本 一.ELK介绍 1.1.ELK简介 ELK是Elasticsearch.Logstash.Kibana三大开源框架首 ...

  9. k8s调度器介绍(调度框架版本)

    从一个pod的创建开始 由kubectl解析创建pod的yaml,发送创建pod请求到APIServer. APIServer首先做权限认证,然后检查信息并把数据存储到ETCD里,创建deployme ...

随机推荐

  1. zabbix 1.1

    1.zabbix监控平台 2.zabbix的三部分组件:      Zabbix server 是 Zabbix软件的核心组件,agent 向其报告可用性.系统完整性信息和统计信息.server也是存 ...

  2. Blazor和Vue对比学习(基础1.9):表单输入绑定和验证,VeeValidate和EditFrom

    这是基础部分的最后一章,内容比较简单,算是为基础部分来个HappyEnding.我们分三个部分来学习: 表单输入绑定 Vue的表单验证:VeeValidate Blazor的表单验证:EditForm ...

  3. pymysql.err.OperationalError: (1054, "Unknown column 'aa' in 'field list'")(已解决)

    错误描述: 今天使用python连接mysql数据库进行数据添加时,出现报错"pymysql.err.OperationalError: (1054, "Unknown colum ...

  4. 安装PostgreSQL到CentOS(YUM)

    运行环境 系统版本:CentOS Linux release 7.6.1810 (Core) 软件版本:postgresql-12 硬件要求:无 安装过程 1.安装YUM-PostgreSQL存储库 ...

  5. 关于我学git这档子事(4)

    ------------恢复内容开始------------ 当本地分支(main/dev)比远程仓库分支(main/dev)落后几次提交时 先: git pull 更新本地仓库 再 git push ...

  6. 【Unity Shader】syntax error: unexpected token 'struct' at line x 错误解决办法

    以下代码处出现了syntax error: unexpected token 'struct' at line 33的错误 struct a2v { float4 vertex_position : ...

  7. 什么是Netty编解码,Netty编解码器有哪些?Protostuff怎么使用?

    哈喽!大家好,我是小奇,一位热爱分享的程序员 小奇打算以轻松幽默的对话方式来分享一些技术,如果你觉得通过小奇的文章学到了东西,那就给小奇一个赞吧 文章持续更新 一.前言 书接上回,昨天下雨没怎么上街上 ...

  8. 关于p命名空间和c命名空间 外加一个context

    P命名空间注入 : 需要在头文件中加入约束文件 导入约束 : xmlns:p="http://www.springframework.org/schema/p" 如 xmlns=& ...

  9. 1. 时序练习(广告渠道vs销量预测)

    用散点图来看下sales销量与哪一维度更相关. 和目标销量的关系的话,那么这就是多元线性回归问题了. 上面把所有的200个数据集都用来训练了,现在把数据集拆分一下,分成训练集合测试集,再进行训练. 可 ...

  10. Charles如何抓取https请求-移动端+PC端

    Charles安装完成,默认只能抓取到http请求,如果查看https请求,会显示unkonw或其它之类的响应.所以需要先进行一些配置,才能抓取到完整的https请求信息.下面针对PC端和手机端抓包的 ...