HAMi 源码阅读笔记 09:/bind 路由入口如何接收 kube-scheduler 的绑定请求 一、/bind 在 Kubernetes 调度流程中的位置在 Kubernetes 官方调度框架中一次 Pod 调度大致可以分成两个阶段Scheduling Cycle选择 Node Binding Cycle把调度结果应用到集群Kubernetes 官方文档中明确说明Scheduling Cycle 负责为 Pod 选择节点而 Binding Cycle 负责把这个决策应用到集群中并且 Scheduling Cycle 是串行执行的而 Binding Cycle 可以并发执行。对于 HAMi 来说/bind并不是一开始就执行的。它是在 kube-scheduler 已经确定目标节点之后才会被调用。也就是说到了/bind阶段kube-scheduler 发给 HAMi 的信息已经非常明确了我要把哪个 Pod 绑定到哪个 Node这个阶段不再是“筛选节点”也不是“计算节点分数”而是“执行绑定”。二、为什么 kube-scheduler 会调用 HAMi 的 /bindHAMi 是通过 Kubernetes Scheduler Extender 机制接入 kube-scheduler 调度流程的。Kubernetes 的 kube-scheduler 配置里可以配置 extender。extender 可以参与 Filter、Prioritize、Bind 等阶段。managedResources 用来声明这个 extender 管理哪些扩展资源。官方文档说明如果 managedResources 非空那么只有当 Pod 请求了其中至少一种扩展资源时kube-scheduler 才会在 Filter、Prioritize、Bind 阶段把该 Pod 发送给 extender如果 managedResources 为空或未配置则所有 Pod 都会发送给这个 extender。简化理解就是extenders: - urlPrefix: https://127.0.0.1:443 filterVerb: filter bindVerb: bind managedResources: - name: nvidia.com/gpu ignoredByScheduler: true如果配置了urlPrefix: https://127.0.0.1:443 bindVerb: bind那么 kube-scheduler 在 Bind 阶段就会请求https://127.0.0.1:443/bindHAMi 的 scheduler 进程启动时HTTP server 使用httprouter.New()创建路由器然后注册了/filter、/bind、/webhook、/healthz、/readyz等接口其中/bind对应的 handler 就是routes.Bind(sher)。对应源码router : httprouter.New() router.POST(/filter, routes.PredicateRoute(sher)) router.POST(/bind, routes.Bind(sher)) router.POST(/webhook, routes.WebHookRoute()) router.GET(/healthz, routes.HealthzRoute()) router.GET(/readyz, routes.ReadyzRoute(sher))所以完整链路可以理解为kube-scheduler 进入 Bind 阶段 ↓ 根据 extender 配置拼出 /bind 请求 ↓ 请求 HAMi scheduler 的 /bind 接口 ↓ 进入 routes.Bind(sher) 返回的 HTTP Handler ↓ 调用真正的 s.Bind(args)三、/bind 路由入口在哪里/bind的入口在 HAMi 源码的这个文件中pkg/scheduler/routes/route.go核心方法是func Bind(s *scheduler.Scheduler) httprouter.HandleBind(s *scheduler.Scheduler)返回的是一个httprouter.Handle也就是一个真正处理 HTTP 请求的函数。这个 handler 内部会读取请求体、解析成ExtenderBindingArgs调用s.Bind(extenderBindingArgs)最后把ExtenderBindingResult序列化成 JSON 返回。四、/bind 路由入口源码解析4.1 方法签名为什么 Bind 返回的是 httprouter.Handle先看方法签名func Bind(s *scheduler.Scheduler) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { ... } }这个写法说明Bind()本身不是每次 HTTP 请求进来时直接执行的最终业务逻辑而是一个路由处理函数的构造器。它接收一个*scheduler.Scheduler对象然后返回一个httprouter.Handle。httprouter.Handle的函数签名大致是func(http.ResponseWriter, *http.Request, httprouter.Params)所以 HAMi 在main.go中注册路由时会这样写router.POST(/bind, routes.Bind(sher))这行代码的含义是当有 POST /bind 请求进来时 交给 routes.Bind(sher) 返回的那个匿名函数处理。这里的sher是 HAMi scheduler 的核心对象后面调用真正绑定逻辑时需要用到它。可以把这一层理解成HTTP 路由层routes.Bind(sher) 核心业务层sher.Bind(args)4.2 进入 Handler一次 /bind 请求真正执行的代码Bind()方法返回的内部匿名函数如下return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { klog.V(5).Infoln(Entering Bind handler) ... }这里有三个参数w http.ResponseWriter表示 HTTP 响应对象用来给 kube-scheduler 写回响应。r *http.Request表示 kube-scheduler 发来的 HTTP 请求请求体中携带了 PodName、PodNamespace、PodUID、Node 等信息。ps httprouter.Params表示路由参数。当前/bind路由没有使用路径变量所以这个参数在方法体中没有实际使用。4.3 限制请求体大小io.LimitReader 的作用进入 handler 后HAMi 先创建了一个 buffer然后对请求体做了限制读取var buf bytes.Buffer limitedReader : io.LimitReader(r.Body, maxRequestSize) body : io.TeeReader(limitedReader, buf)其中maxRequestSize在同一个文件中定义为const maxRequestSize 1024 * 1024 // 1MB limit也就是最多读取 1MB 请求体。这一步的作用是防止异常请求体过大导致服务端消耗过多资源。limitedReader : io.LimitReader(r.Body, maxRequestSize)表示从r.Body中读取数据但最多读取maxRequestSize个字节。body : io.TeeReader(limitedReader, buf)表示后续从body读取数据时读到的数据会同步写入buf。也就是说数据流大致是r.Body ↓ io.LimitReader限制最大读取 1MB ↓ io.TeeReader一边给 JSON Decoder 读一边复制到 buf ↓ json.NewDecoder(body).Decode(...)4.4 定义 ExtenderBindingArgs 和 ExtenderBindingResult接下来HAMi 定义了两个变量var extenderBindingArgs extenderv1.ExtenderBindingArgs var extenderBindingResult *extenderv1.ExtenderBindingResult这两个结构体来自 Kubernetes scheduler extender 包extenderv1 k8s.io/kube-scheduler/extender/v1ExtenderBindingArgs是 kube-scheduler 调用 extender bind 接口时发送给 extender 的参数。Kubernetes 源码中定义了四个字段PodName、PodNamespace、PodUID、Node。结构体可以简化理解为type ExtenderBindingArgs struct { PodName string PodNamespace string PodUID types.UID Node string }也就是说到了/bind阶段kube-scheduler 发给 HAMi 的不是完整候选节点列表而是PodName要绑定的 Pod 名称 PodNamespacePod 所在命名空间 PodUIDPod 的 UID Nodekube-scheduler 选中的目标节点举个例子请求体大致可以理解成{ PodName: gpu-test, PodNamespace: default, PodUID: xxxxxx, Node: gpu-node-01 }ExtenderBindingResult是 HAMi 返回给 kube-scheduler 的结果。它只有一个核心字段type ExtenderBindingResult struct { Error string }这个字段表示绑定过程中的错误信息。所以判断成功或失败非常简单Error 表示绑定成功 Error ! 表示绑定失败4.5 JSON Decode把 HTTP 请求体解析成 ExtenderBindingArgs接下来是请求体解析逻辑if err : json.NewDecoder(body).Decode(extenderBindingArgs); err ! nil { klog.ErrorS(err, Failed to decode extender binding arguments) extenderBindingResult extenderv1.ExtenderBindingResult{ Error: err.Error(), } } else { extenderBindingResult, err s.Bind(extenderBindingArgs) ... }这段代码的逻辑很清晰尝试把 HTTP body 解析成 ExtenderBindingArgs ↓ 如果解析失败 记录错误日志 构造 ExtenderBindingResult 把错误信息放到 Error 字段 ↓ 如果解析成功 调用 s.Bind(extenderBindingArgs)当JSON decode 失败时HAMi 不会继续调用s.Bind()。也就是说如果 kube-scheduler 发来的请求体不是合法 JSON或者字段格式无法正确解析那么这个请求会直接返回错误结果。4.6 调用 s.Bind路由层把请求交给真正的绑定逻辑如果 JSON 解析成功代码会进入else分支extenderBindingResult, err s.Bind(extenderBindingArgs) if err ! nil { klog.ErrorS(err, Bind error for pod, pod, extenderBindingArgs.PodName) extenderBindingResult extenderv1.ExtenderBindingResult{ Error: err.Error(), } }这一行是本文最关键的分界点extenderBindingResult, err s.Bind(extenderBindingArgs)从这里开始routes.Bind()的 HTTP 路由职责已经完成大半。它把 kube-scheduler 发来的ExtenderBindingArgs交给了Scheduler.Bind()。也就是说routes.Bind() 负责 HTTP 请求处理 Scheduler.Bind() 负责真正执行 Pod 绑定逻辑如果s.Bind()返回 Go error则把 error 信息写入ExtenderBindingResult.Error。4.7 处理 s.Bind 返回的 errorScheduler.Bind()返回两个值(*extenderv1.ExtenderBindingResult, error)所以调用之后路由层会检查第二个返回值if err ! nil { klog.ErrorS(err, Bind error for pod, pod, extenderBindingArgs.PodName) extenderBindingResult extenderv1.ExtenderBindingResult{ Error: err.Error(), } }在 HAMi 的Scheduler.Bind()里很多业务失败并不一定通过第二个error返回而是会通过ExtenderBindingResult.Error返回。例如下一篇会分析到Scheduler.Bind()某些失败路径会返回return extenderv1.ExtenderBindingResult{Error: err.Error()}, nil也就是说Go error nil 但 ExtenderBindingResult.Error ! 这种情况下HTTP 路由层不会进入if err ! nil但是最终返回给 kube-scheduler 的 JSON 里仍然包含 Error 字段。所以这里要区分两个错误层次第一层Go 方法调用错误也就是 err ! nil 第二层Extender 业务结果错误也就是 ExtenderBindingResult.Error ! 对于 kube-scheduler 来说HTTP 调用本身必须成功并且响应状态码必须是 200在 HTTP 调用和 JSON 解码都成功的前提下kube-scheduler 会继续检查 ExtenderBindingResult.Error。如果 Error 不为空kube-scheduler 会把它视为 extender bind 失败。4.8 JSON Marshal把绑定结果序列化返回给 kube-scheduler无论前面是 decode 失败还是s.Bind()执行成功或失败最后都会走到响应返回逻辑if response, err : json.Marshal(extenderBindingResult); err ! nil { ... } else { klog.V(5).InfoS(Returning bind response, result, extenderBindingResult) w.Header().Set(Content-Type, application/json) w.WriteHeader(http.StatusOK) w.Write(response) }这段代码做了三件事1. 把 extenderBindingResult 序列化成 JSON 2. 设置响应头 Content-Type 为 application/json 3. 返回 HTTP 200 和 JSON 响应体成功情况下响应大致是{ error: }失败情况下响应大致是{ error: 具体错误信息 }这里要注意只要json.Marshal(extenderBindingResult)成功HTTP 状态码就是http.StatusOK也就是 200。所以/bind的业务成功或失败主要不是靠 HTTP 状态码判断而是靠响应体中的Error字段判断。如果json.Marshal()自己失败才会返回http.StatusInternalServerError也就是 500。五、/bind 路由入口整体流程图routes.Bind()的执行流程如下所示六、总结routes.Bind()的职责可以总结为一句话它是 HAMi scheduler 暴露给 kube-scheduler 的/bindHTTP 入口负责把 kube-scheduler 发来的绑定请求解析成ExtenderBindingArgs调用真正的Scheduler.Bind()再把ExtenderBindingResult以 JSON 形式返回给 kube-scheduler。它本身不负责真正绑定 Pod。真正绑定 Pod 的动作发生在下一层func (s *Scheduler) Bind(args extenderv1.ExtenderBindingArgs) (*extenderv1.ExtenderBindingResult, error)