Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 三):CycleState —— 调度循环内的"黑板"机制
Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 三):CycleState —— 调度循环内的"黑板"机制
调度专题第二篇搭建了 6 大内置插件的全景图。这一篇我们下沉一层,聊一聊所有插件共享的一个核心数据结构:CycleState。它是调度循环内的"黑板"——所有插件在同一个 Pod 的同一个调度周期内,把中间结果写到这里面,后续插件再读出来。理解了它就理解了 PreFilter → Filter → PreScore → Score 的数据流到底是怎么串起来的。
读完本篇,你应该能回答三个问题:CycleState 在 调度循环里扮演什么角色?为什么它内部用 sync.Map 而不是普通 map?一个插件该什么时候写、什么时候读、什么时候清?
Kubernetes Scheduler CycleState 黑板模式 sync.Map Go k8s v1.36.1
学习重点提示 — 建议先通读全文,再重点回顾标注内容
重点掌握(必须)
- CycleState 的源码位置:pkg/scheduler/framework/cycle_state.go:28(struct 定义)、:52(NewCycleState)、:152(Read)、:162(Write)、:126(Clone)
- 核心字段:storage sync.Map(黑板本体,cycle_state.go:30)、skipFilterPlugins / skipScorePlugins / skipPreBindPlugins(三组跳过集合,:34-38)、recordPluginMetrics(指标开关,:32)
- "写一次读多次"的访问模式:sync.Map 不是因为并发读多,而是因为写少读多 + 写后不变的语义最匹配 (cycle_state.go:26)
- 读写 API:Write(key, val) / Read(key),key 必须是 StateKey 接口实现(cycle_state.go:152-167)
次重点(了解即可)
- Clone 的语义:CycleState.Clone()(cycle_state.go:126)做的是浅拷贝,黑板内容按 key-value 复制,但 value 本身还是共享的
- PodGroup 嵌套状态:podGroupCycleState 字段(:48)只在 GenericWorkload 特性开启时非 nil,用于 PodGroup 协同调度
- 指标记录的开关:recordPluginMetrics(:32)由 framework 在调度循环入口根据 pluginMetricsSamplePercent 配置设入
文章目录
一、Why:为什么调度循环需要一个共享的"黑板"
每个 Pod 的调度并不是单步完成的:PreFilter 要计算资源请求合并、Filter 要逐节点检查、PreScore 要预计算一份 Pod 的特征向量、Score 要给所有可行节点打分。这些步骤里,前一步算出来的东西,后面几步还要用。
如果每一步都重新计算,调度性能会非常差。举个例子:InterPodAffinity 在 PreFilter 阶段要把 Pod 的所有 preferredDuringSchedulingIgnoredDuringExecution 亲和项解析成已合并的亲和术语义(pkg/scheduler/framework/plugins/interpodaffinity/filtering.go:185),这些解析结果在 Filter 阶段要重复用 N 次(每个候选节点一次),如果每次都重新解析,复杂度会乘以节点数。
k8s 给出的方案是黑板模式(Blackboard Pattern):每次调度一个 Pod 时,framework 给这个 Pod 分配一个 *CycleState,每个插件可以往里面写中间结果,后面的插件可以读这些结果。这个 *CycleState 在调度循环结束后就销毁,下一个 Pod 来时再 new 一个全新的。
设计精髓
CycleState 的生命周期 = 一次调度循环。这意味着它里面的数据天然适合"用一次就扔"的场景:跨调度周期的复用交给 Informer / Snapshot,跨扩展点的复用交给 CycleState。两个层次的复用严格分层,互不污染。这种短生命周期 + 键值存储的组合,是 CycleState 设计的核心张力。
二、What:CycleState 的接口与数据结构
CycleState 在源码里同时扮演两个角色:接口(k8s.io/kube-scheduler/framework.CycleState,外部抽象)和实现(pkg/scheduler/framework.CycleState,具体结构体)。下面分别看:
2.1 接口层(kube-scheduler/framework/cycle_state.go)
这是插件开发面向的接口,所有插件只依赖这个接口、不直接依赖实现:
// k8s.io/kube-scheduler/framework.CycleState(接口层)
type CycleState interface {
Read(key StateKey) (StateData, error)
Write(key StateKey, val StateData)
Delete(key StateKey)
// 克隆当前 CycleState
Clone() CycleState
// 指标录制相关
ShouldRecordPluginMetrics() bool
SetRecordPluginMetrics(flag bool)
// 各阶段跳过插件集合(framework 内部控制)
SetSkipFilterPlugins(plugins sets.Set[string])
GetSkipFilterPlugins() sets.Set[string]
SetSkipScorePlugins(plugins sets.Set[string])
GetSkipScorePlugins() sets.Set[string]
SetSkipPreBindPlugins(plugins sets.Set[string])
GetSkipPreBindPlugins() sets.Set[string]
SetParallelPreBindPlugins(plugins sets.Set[string])
GetParallelPreBindPlugins() sets.Set[string]
SetSkipAllPostFilterPlugins(flag bool)
ShouldSkipAllPostFilterPlugins() bool
// PodGroup 协同调度(GenericWorkload 特性)
IsPodGroupSchedulingCycle() bool
SetPodGroupSchedulingCycle(podGroupCycleState PodGroupCycleState)
GetPodGroupSchedulingCycle() PodGroupCycleState
}
2.2 实现层(pkg/scheduler/framework/cycle_state.go)
接口的实现非常薄——核心只有一个 sync.Map,外加几组 framework 内部用的跳过集合:
// pkg/scheduler/framework/cycle_state.go:28-49 (k8s v1.36.1)
type CycleState struct {
// 核心:键值存储,写少读多
storage sync.Map
// 是否记录插件执行指标(由 framework 根据 sampling 决定)
recordPluginMetrics bool
// 各扩展点的"跳过集合"——framework 内部用,插件一般不动
skipFilterPlugins sets.Set[string]
skipScorePlugins sets.Set[string]
skipPreBindPlugins sets.Set[string]
skipAllPostFilterPlugins bool
parallelPreBindPlugins sets.Set[string]
// PodGroup 嵌套调度状态(GenericWorkload 特性开启时非 nil)
podGroupCycleState fwk.PodGroupCycleState
}
小贴士 — 关于 sync.Map 的选择
cycle_state.go:26-27 注释明确写了选型理由:"write once and read many times"。每个 key 只写一次(PreFilter/PreScore),但会被读 N 次(Filter/Score 对所有节点跑)。sync.Map 内部为这种"读远多于写"的场景做了读写分离优化:read-only 部分用 atomic.Value,读开销接近普通 map。普通 map + RWMutex 在"读 N 次写 1 次"场景下,每次读都要加 RLock——sync.Map 更便宜。
三、How:CycleState 在调度循环里如何流转
一个 Pod 的调度循环里,CycleState 的生命周期大致如下:
┌────────────────────────────────────────────────────────────────────┐
│ ScheduleOne(ctx) pkg/scheduler/schedule_one.go:67 │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ ① state := framework.NewCycleState() cycle_state.go:52 │ │
│ │ └── 新建一个空的 CycleState(黑板还是白板) │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ ② PreFilter 扩展点 │ │
│ │ plugins[i].PreFilter(ctx, state, pod) │ │
│ │ └─ NodeResourcesFit: state.Write("NodeResourcesFitKey", …) │ │
│ │ └─ InterPodAffinity: state.Write("InterPodAffinityKey", …) │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ ③ Filter 扩展点(并发,对所有节点) │ │
│ │ for node := range feasibleNodes { │ │
│ │ plugins[i].Filter(ctx, state, pod, nodeInfo) │ │
│ │ └─ InterPodAffinity: state.Read("InterPodAffinityKey") │ │
│ │ ↑ 这里就是"写一次读多次"中"读多次"的部分 │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ ④ PreScore 扩展点(可选) │ │
│ │ plugins[i].PreScore(ctx, state, pod, nodes) │ │
│ │ └─ NodeResourcesFit: 预计算资源利用率 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ ⑤ Score 扩展点(并发,对所有 feasible 节点) │ │
│ │ for node := range feasibleNodes { │ │
│ │ plugins[i].Score(ctx, state, pod, nodeInfo) │ │
│ │ └─ NodeResourcesFit: state.Read("NodeResourcesFitKey") │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ ⑥ Reserve / Permit / PreBind / Bind / PostBind │ │
│ │ └─ 大多数插件不写 CycleState(这一步主要是"占座+绑定") │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ ⑦ state 离开作用域,被 GC 回收 │ │
│ │ └─ 黑板上的内容随 Pod 调度完成而消失 │ │
│ └──────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────┘
注意
CycleState 是在 schedulingCycle 入口创建的(pkg/scheduler/schedule_one.go),但 waitingCycle 会Clone() 一份新的(cycle_state.go:126)。原因是 Permit 阶段的异步等待:waiting 期间原来的 CycleState 可能已经被 framework 修改或释放,必须 Clone 一份只读的快照给等待线程用。Clone 做的是浅拷贝——storage map 整体复制,但 value 本身共享指针。
四、Detail:源码逐行精读(pkg/scheduler/framework/cycle_state.go)
4.1 struct 定义(第 28-49 行)
职责:定义 CycleState 的所有字段。存储用 sync.Map,控制字段(skip 系列)由 framework 写、插件读。
// pkg/scheduler/framework/cycle_state.go (行 28-49, k8s v1.36.1)
// Note: CycleState uses a sync.Map to back the storage, because it is thread safe.
// It's aimed to optimize for the "write once and read many times" scenarios.
// It is the recommended pattern used in all in-tree plugins - plugin-specific state
// is written once in PreFilter/PreScore and afterward read many times in Filter/Score.
type CycleState struct {
storage sync.Map // 黑板本体
recordPluginMetrics bool // 指标开关
skipFilterPlugins sets.Set[string] // Filter 跳过集合
skipScorePlugins sets.Set[string] // Score 跳过集合
skipPreBindPlugins sets.Set[string] // PreBind 跳过集合
skipAllPostFilterPlugins bool // PostFilter 整体跳过
parallelPreBindPlugins sets.Set[string] // 可并行 PreBind 插件
podGroupCycleState fwk.PodGroupCycleState // PodGroup 嵌套状态
}
4.2 NewCycleState(第 52 行)
职责:构造一个空的 CycleState。极简:只返回 &CycleState{},所有字段走 Go 零值。
// pkg/scheduler/framework/cycle_state.go:52 (k8s v1.36.1)
func NewCycleState() *CycleState {
return &CycleState{}
}
小贴士 — 关于 sync.Map 的零值
sync.Map 的零值是可用的,不需要额外的初始化。直接 &CycleState{} 就能用——这是 Go 标准库 sync 包的常见约定(参考 sync.Mutex、sync.WaitGroup)。
4.3 Read / Write(第 152 / 162 行)
职责:CycleState 的唯一对外读写 API。key 必须是 StateKey 接口(用于类型安全的 key 命名空间)。
// pkg/scheduler/framework/cycle_state.go (行 152-167, k8s v1.36.1)
func (c *CycleState) Read(key fwk.StateKey) (fwk.StateData, error) {
if v, ok := c.storage.Load(key); ok {
return v.(fwk.StateData), nil
}
return nil, fmt.Errorf("no data for key %T", key)
}
func (c *CycleState) Write(key fwk.StateKey, val fwk.StateData) {
c.storage.Store(key, val)
}
func (c *CycleState) Delete(key fwk.StateKey) {
c.storage.Delete(key)
}
设计精髓
key 必须是 StateKey 接口实现而不是 string,原因是为了避免 key 命名冲突。每个插件定义自己的 XxxStateKey 空 struct(type NodeResourcesFitStateKey struct{}),由于 interface{} 比较的是类型 + 值,两个插件就算都定义 "empty struct key",也不会冲突。这种 key 模式是 Kubernetes 内部的惯用法(同样模式见 informers 的 cache.SplitKey、scheduler 的 plugin.Name)。
4.4 Clone(第 126 行)
职责:做 CycleState 的浅拷贝,给 waitingCycle 等需要"快照"的场景用。
// pkg/scheduler/framework/cycle_state.go:126 (k8s v1.36.1)
func (c *CycleState) Clone() fwk.CycleState {
if c == nil {
return nil
}
copy := &CycleState{
recordPluginMetrics: c.recordPluginMetrics,
skipAllPostFilterPlugins: c.skipAllPostFilterPlugins,
}
// 拷贝 storage(浅拷贝:value 仍共享指针)
c.storage.Range(func(k, v interface{}) bool {
copy.storage.Store(k, v)
return true
})
// 拷贝 skip 集合
if c.skipFilterPlugins != nil {
copy.skipFilterPlugins = sets.New(c.skipFilterPlugins.UnsortedList()...)
}
// ... 其他 set 同样处理
return copy
}
注意
Clone 是浅拷贝——storage map 整体复制,但 value 本身还是共享指针。不要在 Clone 之后修改 value 指向的对象,否则会污染原 CycleState。如果你需要修改 value,先 deepCopy 再改。CycleState 内的 value 通常是只读语义(插件只在 PreFilter/PreScore 写一次,后面只读),所以浅拷贝是安全的。
4.5 Skip 系列方法(第 72-100 行)
职责:framework 内部用,控制哪些插件在某个扩展点被跳过(用于重调度、PodGroup 协调等场景)。
// pkg/scheduler/framework/cycle_state.go (行 72-100, k8s v1.36.1)
func (c *CycleState) SetSkipFilterPlugins(plugins sets.Set[string]) {
c.skipFilterPlugins = plugins
}
func (c *CycleState) GetSkipFilterPlugins() sets.Set[string] {
return c.skipFilterPlugins
}
// ... skipScorePlugins / skipPreBindPlugins 同样模式
func (c *CycleState) SetSkipAllPostFilterPlugins(flag bool) {
c.skipAllPostFilterPlugins = flag
}
小贴士 — 关于 skip 集合的使用场景
Skip 集合最常用于preemption 后重调度:抢占成功后,新 Pod 进入 schedulingCycle 时 framework 会把一些"已经判定为不可行"的插件跳过,避免重复 Filter。比如 NodeResourcesFit 已经判断节点资源不够,重调度时直接跳过它。另一个场景是 PodGroup(GenericWorkload 特性开启时):父 Pod 已经做完 Filter,子 Pod 复用结果。
五、Compare:CycleState vs Snapshot vs NodeInfo
调度专题里会出现几个相似的"共享数据"概念,容易混淆。下表对比:
| 概念 | 生命周期 | 存储位置 | 典型用法 |
|---|---|---|---|
| CycleState | 一次调度循环(毫秒级) | 调度器内存(goroutine 栈变量) | 插件之间共享中间计算结果 |
| Snapshot | 一轮调度周期(秒级) | scheduler cache(共享) | 某时刻所有 NodeInfo 的全量快照 |
| NodeInfo | 随 Snapshot 变更 | scheduler cache / Snapshot | 单个节点的资源/Pod/镜像信息 |
| Informer Cache | 控制器整个生命周期 | apiserver local cache | 所有 watch 到的 Pod/Node 资源 |
小贴士 — 关于三个概念的"层级"关系
从粒度看:Informer Cache > Snapshot > NodeInfo > CycleState。Informer Cache 是最宽的视图(所有 watch 到的资源);Snapshot 是某个时刻的全量切片;NodeInfo 是 Snapshot 里单个节点的信息;CycleState 是单次调度的临时工作台。它们层层递进,每一层只关心自己作用域内的数据。
六、Pitfall:踩坑实录
6.1 把对象放进 CycleState 后修改其字段
问题现象:写插件时图省事,在 PreFilter 阶段 state.Write(key, &myState{...}),然后在 Filter 阶段不重新 Read,而是直接通过闭包引用修改了 myState 的字段。结果下一个节点调度时,Filter 阶段读到的数据是被前一个节点污染的——某次并发 Score 出来诡异分数。
根本原因:CycleState 内的 value 是共享指针。所有节点、所有 goroutine 读到的都是同一个指针,谁改谁污染。
正确做法:CycleState 内的 value 一律视为只读。要修改就先 state.Read(key) 拿到 value → deepCopy → 修改 → 不写回(除非跨扩展点)。如果只是 Score 阶段需要临时计算,写到 CycleState 之外(闭包/局部变量),不要污染黑板。
6.2 跨调度周期读 CycleState(信息泄露)
问题现象:自定义插件在某次调度时往 CycleState 写了一个 map,下一次调度另一个 Pod 时,用 state.Read(sameKey) 居然读到了旧数据——明明 CycleState 应该是新的。
根本原因:framework 在 waitingCycle 中会 Clone() 旧的 CycleState(cycle_state.go:126),如果新 Pod 进入 schedulingCycle 之前没正确清理,仍然可能引用到 Clone 出来的对象。
正确做法:永远不要假设 state.Read(key) 一定返回 nil 或 err,显式处理两种情况。或者干脆不在 PreFilter 之外写 CycleState,从源头杜绝。
6.3 自定义 StateKey 不实现 StateKey 接口
问题现象:写了 type MyKey struct{ name string } 然后 state.Write(MyKey{}, val),编译报错 cannot use MyKey{} as fw.StateKey。
根本原因:StateKey 是接口,需要 String() string 方法(用于日志/debug 打印)。
// ✅ 正确:自定义 StateKey 实现 StateKey 接口
type MyPluginStateKey struct {
Name string
}
func (k MyPluginStateKey) String() string { return "MyPlugin/" + k.Name }
注意:String() 只用于日志输出,不影响 key 的唯一性。key 唯一性由 Go 的 interface{} 类型比较保证(类型 + 值都相同才算相等)。
七、FAQ:常见疑问
Question 1:CycleState 和 informers 的本地 cache 有什么区别?
Answer:CycleState 是单次调度的临时工作台,生命周期只覆盖一次 ScheduleOne 调用;informers 的 cache 是跨调度的长期缓存,覆盖控制器的整个运行周期。前者存的是"这次调度算出来的中间值",后者存的是"集群当前的真实状态"。两者严格分层:插件想读"集群里有哪些 Node"应该查 informers cache,想读"我刚才算的亲和术语义"应该读 CycleState。
Question 2:为什么 CycleState 用 sync.Map 而不是 map + sync.RWMutex?
Answer:见 cycle_state.go:26-27 注释:"write once and read many times"。sync.Map 内部为"读多写少"场景做了读写分离:已写入且未被修改的 entry 存在 atomic.Value 保护的 read-only map 中,读操作完全无锁;只有写和"读到 dirty"时才加锁。普通 map + RWMutex 即使读,也需要 RLock()/RUnlock(),高频读场景下 atomic 操作比锁便宜得多。
Question 3:CycleState 的 Read 返回 error,为什么不用 (StateData, bool)?
Answer:语义上 Read 是读一个 key 必须存在的操作——如果 key 不存在,那是插件 bug(忘了 PreFilter 先写)而不是正常分支。用 error 能让插件代码更显式地处理这个 case:要么 panic 要么补默认值。返回 bool 会让插件默认行为变成"读不到就算了",掩盖 bug。
Question 4:Clone 出来的 CycleState 改 value,会影响原 state 吗?
Answer:会,Clone 是浅拷贝。如果你改的是 value 指向的对象(如 copyState.Read(key).(*MyStruct).Field = x),原 state 的同 key 也会变(因为是同一个指针)。如果改的是 value 本身(如 copyState.Write(key, newVal)),不会影响原 state(因为是 storage map 的覆盖,不是 value 的修改)。CycleState 内的 value 一律视为只读。
Question 5:能跨 Pod 复用 CycleState 吗?比如预计算某些通用结果?
Answer:不能。CycleState 是per-pod 的,每次 NewCycleState() 都是全新对象(cycle_state.go:52)。跨 Pod 复用应该放在scheduler cache / Snapshot 里,CycleState 只承担"单次调度"的职责。强行跨 Pod 复用会破坏 framework 的并发模型(多个 Pod 的调度 goroutine 会同时读写)。
Question 6:什么时候需要 SkipFilterPlugins 而不是直接返回 Unschedulable?
Answer:SkipFilterPlugins 用于framework 内部的"已经知道这个插件一定失败,不需要再调"的场景,比如 preemption 后重调度(之前已经 Filter 失败了)。如果插件自己判断"我这个 Pod 不需要某个扩展点",应该让插件的 Filter 方法返回 Success(不报错)而不是去动 SkipFilterPlugins——后者是framework 的 API,插件应该只通过接口返回值影响调度结果。
Question 7:PodGroupCycleState 在生产中应该用吗?
Answer:podGroupCycleState 字段(cycle_state.go:48)只在 GenericWorkload 特性门开启时才非 nil,对应的是 PodGroup 协同调度(类似 Volcano 的 gang scheduling)。如果你用的是原生 kube-scheduler,没装 Volcano / scheduler-plugins,这个字段永远是 nil,不需要关心。如果你要写 PodGroup 调度插件,看 pkg/scheduler/framework/plugins/podgroup(scheduler-plugins 仓库)。
Question 8:CycleState 里的 value 应该存指针还是值?
Answer:几乎都是指针(*MyState)。原因有二:(1) CycleState 内的 value 是 interface{},存值类型会有 boxing 开销;(2) 插件的 state 结构通常比较大(动辄几十个字段),拷贝不值。存指针同时也提醒插件:value 是只读的,要改请 deepCopy 后改本地副本。
Question 9:CycleState 能不能跨扩展点存一些"配置"?
Answer:不推荐。CycleState 是 per-pod per-cycle 的,配置应该放在插件的 struct 字段里(Plugin 在 New 时就有,从 KubeSchedulerConfiguration 解析)。CycleState 适合存"这次调度算出来的中间结果",不适合存"插件的全局配置"。后者会让 CycleState 越来越臃肿,违背"短生命周期"的初衷。
Question 10:调度失败时 CycleState 会被清理吗?
Answer:会。state := framework.NewCycleState() 是 schedule_one.go 里的局部变量,调度失败(Unschedulable / Error)也会走 defer 或函数返回,局部变量离开作用域,Go GC 会回收 *CycleState 及其内部 sync.Map。这就是"用一次就扔"的物理保证。
Question 11:自定义插件读 CycleState 时,如何判断某个 key 是不是我写的?
Answer:用自定义空 struct当 key。Go 的 interface{} 比较规则是"类型相同 + 值相同"。两个插件即使都定义 type EmptyKey struct{},因为类型不同(pluginA.EmptyKey vs pluginB.EmptyKey),互相读不到对方的 key。这就是 CycleState 用 StateKey 接口而不是 string 当 key 的核心原因。
Question 12:CycleState 是线程安全的吗?Filter 阶段多个 goroutine 同时读会出问题吗?
Answer:线程安全,Filter 阶段多个 goroutine 并发读不会出问题。sync.Map 内部对所有读写操作加了保护,sync.Map.Load 是无锁的 atomic 读(命中 read-only map 时),并发安全。但要注意:value 是共享指针,多个 goroutine 读同一个 value 时,不要并发修改 value 的字段——这不在 CycleState 的保护范围内,需要插件自己保证。
八、Roadmap:调度专题后续预告
本篇把 CycleState 的数据结构、生命周期、读写 API全部讲完了。下一步我们会从 CycleState 向上走,进入 framework.Handle 和 runtime.Registry 内部,看 framework 是如何调度所有插件的——也就是 pkg/scheduler/framework/runtime/framework.go 里那 2000 行的"调度骨架"。
- 调度专题 · 四:framework.Handle 接口 — 插件看到的"framework 视角"
- 调度专题 · 五:runtime.Registry — 插件如何注册、framework 如何组装
- 调度专题 · 六:Parallelizer — Filter/Score 阶段的并发模型(errgroup + worker pool)
- 调度专题 · 七:Permit 扩展点 — 异步批准机制与 waitingCycle 的协作
- 调度专题 · 八:Preemption 抢占 — 怎么"挤掉"别人、为什么要 Clone CycleState
- Out-of-Tree 插件实战:用 scheduler.RegisterPluginBuilder 注册一个自定义插件,并发布到 Helm chart
调度专题的目标读者是资深运维开发:能读 Go 源码、有集群运维经验、对 k8s 整体架构已有认知。下一篇我会从 framework.Handle 切入,把插件看到的"framework 视角"补全。
本文参考与源码链接:
• cycle_state.go · CycleState struct 定义
• cycle_state.go:52 · NewCycleState
• cycle_state.go:126 · Clone
• cycle_state.go:152 · Read
• cycle_state.go:162 · Write
• schedule_one.go · ScheduleOne 入口
• filtering.go · InterPodAffinity 读取 CycleState
• kube-scheduler/framework · CycleState 接口定义
• Scheduling, Preemption and Eviction · 官方文档

浙公网安备 33010602011771号