当前位置: 首页 > news >正文

【kubernetes/k8s源码分析】kube-controller-manager之node controller源码分析

NodeLifecycleController主要功能有:
(1)定期检查node的心跳上报,某个node间隔一定时间都没有心跳上报时,更新node的ready condition值为false或unknown,开启了污点驱逐的情况下,给该node添加NoExecute的污点;
(2)当污点驱逐未开启时,当node的ready Condition值为false或unknown且已经持续了一段时间(该时间可配置)时,对该node上的pod做驱逐(删除)操作;
(3)当污点驱逐开启时,node上有NoExecute污点后,立马驱逐(删除)不能容忍污点的pod,对于能容忍该污点的pod,则等待所有污点的容忍时间里最小值后,pod才被驱逐(删除);

0.1 启动节点更新工作协程(节点调和)

  doNodeProcessingPassWorker

   NoSchedule 污点调和(nc.doNoScheduleTaintingPass(ctx, nodeName)):

负责根据节点状态(如 NodeReady/DiskPressure 条件、是否不可调度)自动添加 / 移除 NoSchedule 类型污点(作用是 “禁止新 Pod 调度到该节点,但不影响已运行的 Pod”)。其核心逻辑是 “生成期望污点 → 对比当前污点 → 调和差异”,确保节点污点状态与实际健康 / 配置一致
  • 基于节点状态自动生成污点:根据节点的 .status.conditions(如 DiskPressure 磁盘压力、MemoryPressure 内存压力)和 .spec.unschedulable(是否手动标记为不可调度),生成 “应有的 NoSchedule 污点”;

   节点标签调和(nc.reconcileNodeLabels(ctx, nodeName)

0.2 启动 Pod 更新工作协程(Pod 调和)

  doPodProcessingWorker

核心方法 processPod,负责根据 Pod 所在节点的 Ready 状态,动态调整 Pod 的状态(标记为 NotReady)或处理污点驱逐逻辑,确保 Pod 状态与节点健康状态一致。其核心逻辑是 “节点不健康时标记 Pod 为 NotReady,避免流量调度到不可用的 Pod”,是集群自愈能力的关键环节。
  • 关联 Pod 与节点健康状态:当 Pod 所在节点从 Ready 变为 NotReady/Unknown 时,自动将 Pod 标记为 NotReady(通过更新 Pod 的 .status.conditions[PodReady]),避免 Service 将流量转发到不可用的 Pod;

0.3 启动 NoExecute 污点处理协程(故障驱逐)

  doNoExecuteTaintingPass

节点 Ready 状态→NoExecute 污点 的控制器,核心逻辑是 “当节点不可用(NotReady/Unknown)时,添加 NoExecute 污点触发 Pod 驱逐,同时删除对立污点(避免冲突)”,逻辑更复杂且涉及集群级队列管理:
可以使用命令行为 Node 节点添加 Taints: kubectl taint nodes node1 key=value:NoSchedule
  • NoSchedule:仅影响调度过程,对现存的Pod对象不产生影响;
  • NoExecute:既影响调度过程,也影响显著的Pod对象;不容忍的Pod对象将被驱逐
  • PreferNoSchedule: 表示尽量不调度

1. Run 函数

        Kubernetes Node Controller(节点控制器)的 “主运行入口”Run 方法,负责启动节点控制器的全部核心工作协程(Goroutine),包括事件广播、缓存同步、节点 / Pod 调和、污点驱逐、健康监控等,是节点控制器从 “初始化” 到 “持续运行” 的总调度中心。以下从 函数核心职责逐阶段流程解析关键设计亮点 三方面详细拆解,帮助理解节点控制器的整体运行架构。

一、核心定位与依赖

1. 核心职责

  • 启动异步工作流:通过 go 关键字启动多个独立协程,并行处理不同维度的节点管理任务(如缓存同步、健康监控、污点驱逐)
  • 保障运行稳定性:通过 defer 注册资源清理逻辑(如关闭事件广播、工作队列),确保控制器退出时无资源泄漏;
  • 等待上下文终止:监听 ctx.Done() 信号(如集群关闭、控制器重启),优雅终止所有工作协程,避免强制退出导致的数据不一致。

2. 关键依赖(Controller 结构体核心字段)

二、逐行代码逻辑拆解(按启动流程分阶段)

阶段 1:初始化事件广播器(事件上报基础)

// Start events processing pipeline.
nc.broadcaster.StartStructuredLogging(3)
logger := klog.FromContext(ctx)
logger.Info("Sending events to api server")
nc.broadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: v1core.New(nc.kubeClient.CoreV1().RESTClient()).Events(""),})
defer nc.broadcaster.Shutdown()
  • 核心作用:启动事件广播器,将节点控制器产生的所有事件(如节点注册、状态变化、删除)上报到 API Server,供用户通过 kubectl describe node 或事件监控系统(如 Prometheus Alertmanager)查看。
  • 关键细节:

阶段 2:注册资源清理逻辑(优雅退出)

// Close node update queue to cleanup go routine.
defer nc.nodeUpdateQueue.ShutDown()
defer nc.podUpdateQueue.ShutDown()
  • 核心作用:通过 defer 确保控制器退出时(如收到 SIGTERM 信号),自动关闭 nodeUpdateQueue 和 podUpdateQueue 工作队列,终止正在处理的任务,避免队列中未处理的事件导致协程 “僵死”。

阶段 3:等待本地缓存同步完成(数据准备)

if !cache.WaitForNamedCacheSync("taint", ctx.Done(), nc.leaseInformerSynced, nc.nodeInformerSynced, nc.podInformerSynced, nc.daemonSetInformerSynced) {return
}
  • 核心背景:节点控制器依赖 本地缓存(Informer Cache) 快速查询 Node、Pod、DaemonSet、Lease 等资源(避免频繁调用 API Server)。WaitForNamedCacheSync 会阻塞直到所有指定的缓存同步完成(或上下文终止)。
  • 关键参数解析:
    • "taint":缓存同步的名称标识(用于日志输出,便于定位);
    • ctx.Done():上下文终止信号(如控制器被强制关闭,需退出阻塞);
    • nc.leaseInformerSynced:Lease 资源缓存同步(Lease 用于节点心跳检测,Kubelet 定期更新节点的 Lease 资源);
    • nc.nodeInformerSynced/nc.podInformerSynced:Node/Pod 资源缓存同步;
    • nc.daemonSetInformerSynced:DaemonSet 资源缓存同步(DaemonSet Pod 需特殊处理,默认容忍节点污点)。
  • 退出逻辑:若缓存同步失败(如上下文终止),直接返回,不启动后续工作协程,避免基于未初始化的缓存处理任务导致错误。

阶段 4:启动污点驱逐控制器(可选)

if !utilfeature.DefaultFeatureGate.Enabled(features.SeparateTaintEvictionController) {logger.Info("Starting", "controller", taintEvictionController)go nc.taintManager.Run(ctx)
}
  • 核心逻辑:根据 Kubernetes 特性门控(SeparateTaintEvictionController)判断是否启动内置的污点驱逐控制器:
    • 特性门控 未启用(默认情况,旧版本 Kubernetes):启动 nc.taintManager.Run(ctx),由 Node Controller 内置的污点管理器处理污点添加和 Pod 驱逐
    • 特性门控 已启用(新版本 Kubernetes,如 1.24+):污点驱逐逻辑由独立的 taint-eviction-controller 处理,Node Controller 不再启动内置污点管理器,实现 “职责解耦”。
  • 作用:污点驱逐是节点故障处理的核心(如节点失联时添加 unreachable 污点,触发 Pod 驱逐),确保故障节点上的 Pod 能迁移到健康节点。

阶段 5:启动节点更新工作协程(节点调和)

// Start workers to reconcile labels and/or update NoSchedule taint for nodes.
for i := 0; i < nodeUpdateWorkerSize; i++ {// Thanks to "workqueue", each worker just need to get item from queue, because// the item is flagged when got from queue: if new event come, the new item will// be re-queued until "Done", so no more than one worker handle the same item and// no event missed.go wait.UntilWithContext(ctx, nc.doNodeProcessingPassWorker, time.Second)
}
  • 核心作用:启动 nodeUpdateWorkerSize 个并行协程(默认数倍于 CPU 核心,如 4 核 CPU 启动 8 个协程),处理 nodeUpdateQueue 中的节点更新事件(如节点标签变化、污点更新)。
  • 关键细节:

阶段 6:启动 Pod 更新工作协程(Pod 调和)

for i := 0; i < podUpdateWorkerSize; i++ {go wait.UntilWithContext(ctx, nc.doPodProcessingWorker, time.Second)
}
  • 核心作用:启动 podUpdateWorkerSize 个并行协程,处理 podUpdateQueue 中的 Pod 更新事件(如 Pod 调度到节点、Pod 状态变化),确保 Pod 状态与节点状态一致。
  • nc.doPodProcessingWorker 核心逻辑(未展示源码):

阶段 7:启动 NoExecute 污点处理协程(故障驱逐)

// Handling taint based evictions. Because we don't want a dedicated logic in TaintManager for NC-originated
// taints and we normally don't rate limit evictions caused by taints, we need to rate limit adding taints.
go wait.UntilWithContext(ctx, nc.doNoExecuteTaintingPass, scheduler.NodeEvictionPeriod)
  • 核心作用:定期执行 nc.doNoExecuteTaintingPass 方法(周期为 scheduler.NodeEvictionPeriod,默认 10 秒),处理 NoExecute 类型的污点(最严格的污点,会驱逐已调度的 Pod),避免因节点故障导致的 Pod 长期不可用。
  • 关键背景:
    • NoExecute 污点(如 node.kubernetes.io/unreachable)会触发 Pod 驱逐,但为避免 “短时间内大量节点故障导致的集群级 Pod 驱逐风暴”,该协程会对污点添加逻辑做 速率限制(如每秒最多处理 N 个节点的污点添加);
    • nc.doNoExecuteTaintingPass 会遍历所有节点,检查是否需要添加 / 移除 NoExecute 污点,确保污点状态与节点健康状态一致。

阶段 8:启动节点健康监控协程(核心健康检查)

// Incorporate the results of node health signal pushed from kubelet to master.
go wait.UntilWithContext(ctx, func(ctx context.Context) {if err := nc.monitorNodeHealth(ctx); err != nil {logger.Error(err, "Error monitoring node health")}
}, nc.nodeMonitorPeriod)
  • 核心作用:按 nc.nodeMonitorPeriod(默认几秒,如 5 秒)定期执行 nc.monitorNodeHealth 方法(前序文章解析过的核心方法),实现节点健康状态的持续监控:
  • 错误处理:若 monitorNodeHealth 执行失败(如缓存查询错误),仅记录错误日志,不终止协程(下一个周期会重试),确保健康监控的连续性。

1.1 doNodeProcessingPassWorker

        kubernetes Node Controller(节点控制器)中 “节点更新工作协程” 的核心实现(doNodeProcessingPassWorker),负责从 nodeUpdateQueue 工作队列中读取待处理的节点任务,执行 NoSchedule 污点管理 和 节点标签调和 两大核心逻辑,是节点控制器 “异步处理节点配置变化” 的关键组件。以下从 函数职责、逐行逻辑拆解、核心子逻辑解析、设计亮点 四方面详细说明。

1. 归属与调用时机

  • 归属:Controller 结构体(Node Controller 核心结构体)的方法,在 Run 方法中通过循环启动 nodeUpdateWorkerSize 个并行协程(如 8 个协程),每个协程独立执行 doNodeProcessingPassWorker
  • 核心目标:异步处理节点的 “配置变更任务”(如节点标签更新、污点需求变化),避免同步处理阻塞主流程,提升节点控制器的吞吐能力;
  • 触发场景:当节点发生需要调和的事件(如节点标签被手动修改、控制平面需为节点添加 NoSchedule 污点)时,任务会被加入 nodeUpdateQueue,由该协程消费处理。

2. 关键依赖(Controller 结构体字段)

func (nc *Controller) doNodeProcessingPassWorker(ctx context.Context) {logger := klog.FromContext(ctx)// 1. 无限循环:持续从工作队列中获取任务,直到队列关闭for {// 2. 从 nodeUpdateQueue 中获取待处理的节点任务(阻塞直到有任务或队列关闭)obj, shutdown := nc.nodeUpdateQueue.Get()// 3. 若队列已关闭(如控制器退出),终止协程if shutdown {return}// 4. 将获取的任务转为节点名称(队列中存储的是节点名称字符串)nodeName := obj// 5. 执行 NoSchedule 污点调和(核心逻辑1)if err := nc.doNoScheduleTaintingPass(ctx, nodeName); err != nil {logger.Error(err, "Failed to taint NoSchedule on node, requeue it", "node", klog.KRef("", nodeName))// TODO(k82cn): Add nodeName back to the queue  // 待实现:失败时将节点重新加入队列重试}// 6. 执行节点标签调和(核心逻辑2,1.19+ 版本保留的兼容逻辑)// TODO: re-evaluate whether there are any labels that need to be reconcile in 1.19. Remove this function if it's no longer necessary.if err := nc.reconcileNodeLabels(ctx, nodeName); err != nil {logger.Error(err, "Failed to reconcile labels for node, requeue it", "node", klog.KRef("", nodeName))// TODO(yujuhong): Add nodeName back to the queue  // 待实现:失败时重新入队重试}// 7. 标记任务处理完成(通知队列该节点任务已处理,避免重复消费)nc.nodeUpdateQueue.Done(nodeName)}
}

1.1.1 NoSchedule 污点调和(nc.doNoScheduleTaintingPass(ctx, nodeName)

Kubernetes Node Controller 中 “管理 NoSchedule 污点” 的核心方法 doNoScheduleTaintingPass负责根据节点状态(如 NodeReady/
DiskPressure 条件、是否不可调度)自动添加 / 移除 NoSchedule 类型污点(作用是 “禁止新 Pod 调度到该节点,但不影响已运行的 Pod”)。其核心逻辑是 “生成期望污点 → 对比当前污点 → 调和差异”,确保节点污点状态与实际健康 / 配置一致。以下从 函数职责、逐行逻辑拆解、核心机制、典型场景 四方面详细解析。

一、核心定位与依赖

1. 核心目标

  • 基于节点状态自动生成污点:根据节点的 .status.conditions(如 DiskPressure 磁盘压力、MemoryPressure 内存压力)和 .spec.unschedulable(是否手动标记为不可调度),生成 “应有的 NoSchedule 污点”;
  • 调和污点差异:对比节点当前的 NoSchedule 污点与 “期望污点”,自动添加缺失的污点、删除多余的污点,确保二者一致;
  • 保障调度安全:通过 NoSchedule 污点阻止新 Pod 调度到 “不健康” 或 “维护中” 的节点,避免资源竞争或服务异常。

2. 关键依赖

二、逐行代码逻辑拆解

func (nc *Controller) doNoScheduleTaintingPass(ctx context.Context, nodeName string) error {// 1. 从本地缓存获取节点信息node, err := nc.nodeLister.Get(nodeName)if err != nil {// 若节点不存在(如已被删除),直接返回(无需处理)if apierrors.IsNotFound(err) {return nil}// 其他错误(如缓存查询失败)返回错误,触发重试return err}// 2. 步骤1:根据节点状态条件(.status.conditions)生成期望的 NoSchedule 污点var taints []v1.Taintfor _, condition := range node.Status.Conditions {// 检查该节点条件是否有对应的“污点键-状态”映射(如 DiskPressure → node.kubernetes.io/disk-pressure)if taintMap, found := nodeConditionToTaintKeyStatusMap[condition.Type]; found {// 根据条件状态(True/False/Unknown)获取对应的污点键(仅特定状态需生成污点)if taintKey, found := taintMap[condition.Status]; found {// 生成 NoSchedule 污点,添加到期望列表taints = append(taints, v1.Taint{Key:    taintKey,    // 污点键(如 node.kubernetes.io/disk-pressure)Effect: v1.TaintEffectNoSchedule,  // 污点效果(禁止新 Pod 调度)})}}}// 3. 步骤2:若节点被标记为“不可调度”(.spec.unschedulable=true),添加 unschedulable 污点if node.Spec.Unschedulable {taints = append(taints, v1.Taint{Key:    v1.TaintNodeUnschedulable,  // 预定义污点键:node.kubernetes.io/unschedulableEffect: v1.TaintEffectNoSchedule,})}// 4. 步骤3:过滤节点当前的 NoSchedule 污点(仅保留与本次调和相关的污点)nodeTaints := taintutils.TaintSetFilter(node.Spec.Taints, func(t *v1.Taint) bool {// 条件1:仅保留 NoSchedule 类型的污点(排除 NoExecute/PreferNoSchedule 等其他类型)if t.Effect != v1.TaintEffectNoSchedule {return false}// 条件2:保留“不可调度污点”(v1.TaintNodeUnschedulable)或“与节点状态条件相关的污点”(如 disk-pressure)if t.Key == v1.TaintNodeUnschedulable {return true}// 检查污点键是否在“污点→节点条件”的反向映射中(即是否是由节点状态生成的污点)_, found := taintKeyToNodeConditionMap[t.Key]return found})// 5. 步骤4:计算期望污点与当前污点的差异(需添加的污点、需删除的污点)taintsToAdd, taintsToDel := taintutils.TaintSetDiff(taints, nodeTaints)// 6. 若无差异(无需添加/删除污点),直接返回成功if len(taintsToAdd) == 0 && len(taintsToDel) == 0 {return nil}// 7. 步骤5:调用 API 调和污点差异(添加缺失的、删除多余的)if !controllerutil.SwapNodeControllerTaint(ctx, nc.kubeClient, taintsToAdd, taintsToDel, node) {return fmt.Errorf("failed to swap taints of node %+v", node)}// 调和成功,返回 nilreturn nil
}

三、核心机制深度解析

1. 关键映射表:nodeConditionToTaintKeyStatusMap

该全局映射表是 “节点状态 → 污点” 的核心关联逻辑,硬编码在 Kubernetes 源码中,定义了哪些节点状态需要生成 
NoSchedule 污点。以 Kubernetes 1.21 为例,其核心映射关系如下(简化版):
var nodeConditionToTaintKeyStatusMap = map[v1.NodeConditionType]map[v1.ConditionStatus]string{// 磁盘压力:当条件状态为 True 时,生成 disk-pressure 污点v1.NodeDiskPressure: {v1.ConditionTrue: "node.kubernetes.io/disk-pressure",},// 内存压力:当条件状态为 True 时,生成 memory-pressure 污点v1.NodeMemoryPressure: {v1.ConditionTrue: "node.kubernetes.io/memory-pressure",},// PID 压力:当条件状态为 True 时,生成 pid-pressure 污点v1.NodePIDPressure: {v1.ConditionTrue: "node.kubernetes.io/pid-pressure",},// 网络不可用:当条件状态为 True 时,生成 network-unavailable 污点v1.NodeNetworkUnavailable: {v1.ConditionTrue: "node.kubernetes.io/network-unavailable",},
}
  • 逻辑规则:仅当节点条件的 状态为 True(如 DiskPressure: True 表示节点确实存在磁盘压力)时,才会生成对应的 NoSchedule 污点;
  • 作用:通过该映射,Node Controller 可自动将 “节点健康问题” 转化为 “调度限制”,无需人工干预。

2. 污点过滤:taintutils.TaintSetFilter

该函数用于从节点当前的所有污点中,筛选出 “与本次调和相关的 NoSchedule 污点”,避免干扰其他类型的污点(如用户手动添加的 
app=web:NoSchedule 污点)。筛选逻辑如下:
  • 保留 Effect = NoSchedule 的污点(排除 NoExecute(驱逐已运行 Pod)、PreferNoSchedule(优先不调度)等其他类型);
  • 保留 两类关键污点
示例:若节点当前有以下污点,筛选后仅保留前 2 个:
  • node.kubernetes.io/disk-pressure:NoSchedule(相关)
  • node.kubernetes.io/unschedulable:NoSchedule(相关)
  • app=web:NoSchedule(用户自定义,不相关,过滤)
  • node.kubernetes.io/unreachable:NoExecute(类型为 NoExecute,过滤)

3. 污点差异计算:taintutils.TaintSetDiff

该函数对比 “期望污点列表(taints)” 和 “当前筛选后的污点列表(nodeTaints)”,返回两个结果:
  • taintsToAdd:期望有但当前没有的污点(需添加);
  • taintsToDel:当前有但期望没有的污点(需删除)。
示例
  • 期望污点:[disk-pressure:NoSchedule, unschedulable:NoSchedule]
  • 当前污点:[unschedulable:NoSchedule, memory-pressure:NoSchedule]
  • 计算结果:
    • taintsToAdd:[disk-pressure:NoSchedule](期望有,当前无)
    • taintsToDel:[memory-pressure:NoSchedule](当前有,期望无)

4. 污点调和执行:controllerutil.SwapNodeControllerTaint

该函数是 “添加 / 删除污点” 的最终执行逻辑,封装了 API 调用细节,核心功能包括:
  1. 生成 Patch 请求:构造 Kubernetes API 的 Patch 请求(JSON Merge Patch),描述 “添加哪些污点、删除哪些污点”;
  2. 处理并发冲突:若同时有其他组件(如用户手动修改)修改节点污点,导致 Patch 请求返回 Conflict(409)错误,函数会自动重试(默认重试几次),避免因并发导致调和失败;
  3. 事件记录:若污点调和成功,记录事件(如 “Added taint [disk-pressure:NoSchedule] to node xxx”),便于通过 kubectl describe node 追溯;
  4. 返回结果:调和成功返回 true,失败(如重试多次仍冲突、API Server 不可用)返回 false

四、典型场景示例

场景 1:节点出现磁盘压力(DiskPressure: True

  1. 节点 Kubelet 检测到磁盘使用率超过阈值(如 90%),将 .status.conditions[NodeDiskPressure] 设为 True
  2. 任务被加入 nodeUpdateQueuedoNodeProcessingPassWorker 协程调用 doNoScheduleTaintingPass
  3. 生成期望污点:根据 nodeConditionToTaintKeyStatusMap,添加 disk-pressure:NoSchedule 污点;
  4. 对比当前污点:若当前无该污点,taintsToAdd 包含 disk-pressure:NoSchedule
  5. 执行调和:调用 API 添加该污点,新 Pod 因无法容忍该污点,不再调度到该节点。

场景 2:节点从不可调度恢复为可调度(kubectl uncordon

  1. 管理员执行 kubectl uncordon ,节点的 .spec.unschedulable 设为 false
  2. 协程调用 doNoScheduleTaintingPass,生成期望污点时,不再添加 unschedulable:NoSchedule 污点;
  3. 对比当前污点:若节点仍有该污点,taintsToDel 包含 unschedulable:NoSchedule
  4. 执行调和:调用 API 删除该污点,节点恢复正常调度,新 Pod 可调度到该节点。

场景 3:节点磁盘压力消失(DiskPressure: False

  1. 节点清理磁盘空间后,Kubelet 将 .status.conditions[NodeDiskPressure] 设为 False
  2. 协程调用 doNoScheduleTaintingPass,生成期望污点时,不再包含 disk-pressure:NoSchedule(因映射表仅 True 状态生成污点);
  3. 对比当前污点:若节点仍有该污点,taintsToDel 包含 disk-pressure:NoSchedule
  4. 执行调和:删除该污点,节点恢复正常调度。

总结

        doNoScheduleTaintingPass 是 Node Controller 实现 “基于节点状态自动限制调度” 的核心逻辑,其本质是 “状态→污点” 的映射与调和。通过该方法,Kubernetes 实现了 “节点不健康时自动阻止新 Pod 调度” 的自愈能力,无需人工干预,大幅提升集群稳定性。理解其逻辑,可帮助运维人员快速定位 “调度限制异常” 问题,如 “节点明明有压力却能调度 Pod”“节点已恢复却无法调度” 等,是深入掌握 Kubernetes 调度机制的关键。

1.1.2 节点标签调和(nc.reconcileNodeLabels(ctx, nodeName)

        Kubernetes Node Controller 中 “节点标签调和” 的核心方法 reconcileNodeLabels,负责维护节点的 “主 - 从标签(Primary-Secondary Label)一致性”—— 即确保 “从标签”(Secondary Label)的取值与 “主标签”(Primary Label)完全一致,或在需要时自动创建 “从标签”。其核心目标是解决 “标签版本兼容” 和 “外部修改导致的标签不一致” 问题,保障依赖标签的组件(如调度器、监控系统)正常工作。以下从 函数职责、逐行逻辑拆解、核心机制、典型场景 四方面详细解析。

1. 核心目标

  • 标签一致性维护:针对 Kubernetes 版本迭代中引入的 “标签别名”(如 node-role.kubernetes.io/master 与 node-role.kubernetes.io/control-plane),确保 “从标签” 与 “主标签” 取值一致,避免因标签不一致导致组件功能异常;
  • 自动修复标签:若 “从标签” 被误删除或修改(如人工操作、外部工具干预),自动基于 “主标签” 重建或修正 “从标签”;
  • 版本兼容保障:在标签名称迭代过程中(如从 master 角色标签迁移到 control-plane 角色标签),通过调和逻辑确保新旧组件都能识别节点角色,避免兼容性问题。

1.2 doPodProcessingWorker

        Kubernetes Node Controller(节点控制器)中 “处理 Pod 与节点状态关联” 的核心方法 processPod,负责根据 Pod 所在节点的 Ready 状态,动态调整 Pod 的状态(标记为 
NotReady)或处理污点驱逐逻辑,确保 Pod 状态与节点健康状态一致。其核心逻辑是 “节点不健康时标记 Pod 为 NotReady,避免流量调度到不可用的 Pod”,是集群自愈能力的关键环节。以下从 函数职责、逐行逻辑拆解、核心机制、典型场景 四方面详细解析。

一、核心定位与背景

1. 核心目标

  • 关联 Pod 与节点健康状态:当 Pod 所在节点从 Ready 变为 NotReady/Unknown 时,自动将 Pod 标记为 NotReady(通过更新 Pod 的 .status.conditions[PodReady]),避免 Service 将流量转发到不可用的 Pod;
  • 处理节点不存在场景:若 Pod 绑定的节点在缓存中不存在(如节点已被删除),跳过处理,避免无效操作;
  • 失败重试保障:若标记 Pod 状态失败(如 API 冲突、网络异常),通过 podUpdateQueue 进行速率限制重试,确保最终处理成功。

2. 关键依赖

二、逐行代码逻辑拆解

// processPod 处理 Pod 与节点的关联事件,核心是根据节点 Ready 状态调整 Pod 状态
func (nc *Controller) processPod(ctx context.Context, podItem podUpdateItem) {// 1. 任务处理完成后,标记队列任务为“已处理”(无论成功/失败)defer nc.podUpdateQueue.Done(podItem)// 2. 从本地缓存获取 Pod 的最新信息(podItem 包含 Pod 的 namespace 和 name)pod, err := nc.podLister.Pods(podItem.namespace).Get(podItem.name)logger := klog.FromContext(ctx)if err != nil {// 2.1 若 Pod 不存在(如已被删除),无需处理,直接返回if apierrors.IsNotFound(err) {return}// 2.2 其他错误(如缓存查询失败),记录日志并将任务重新加入队列(速率限制重试)logger.Info("Failed to read pod", "pod", klog.KRef(podItem.namespace, podItem.name), "err", err)nc.podUpdateQueue.AddRateLimited(podItem)return}// 3. 获取 Pod 绑定的节点名称(Pod 调度后,.spec.NodeName 会被设置为目标节点名)nodeName := pod.Spec.NodeName// 4. 从节点健康缓存中获取该节点的最新健康状态(深拷贝,避免修改原缓存)nodeHealth := nc.nodeHealthMap.getDeepCopy(nodeName)if nodeHealth == nil {// 4.1 若节点健康缓存不存在(如节点刚加入、缓存未初始化或节点已删除),跳过处理return}// 5. 再次从本地缓存确认节点是否存在(双重校验,避免节点已被删除但缓存未更新)_, err = nc.nodeLister.Get(nodeName)if err != nil {// 5.1 节点不存在(如已删除),记录日志并重试(可能节点重建后需要重新处理)logger.Info("Failed to read node", "node", klog.KRef("", nodeName), "err", err)nc.podUpdateQueue.AddRateLimited(podItem)return}// 6. 从节点健康状态中提取 NodeReady 条件(核心判断依据)_, currentReadyCondition := controllerutil.GetNodeCondition(nodeHealth.status, v1.NodeReady)if currentReadyCondition == nil {// 6.1 若节点缺少 NodeReady 条件(异常场景,如节点刚创建或条件被恶意删除),跳过处理// 后续节点更新事件会重新触发处理,确保最终正确处理return}// 7. 核心逻辑:根据节点 NodeReady 状态调整 Pod 状态pods := []*v1.Pod{pod}  // 封装为切片,适配 MarkPodsNotReady 函数的参数格式if currentReadyCondition.Status != v1.ConditionTrue {// 7.1 节点状态为 NotReady/Unknown → 将 Pod 标记为 NotReadyif err := controllerutil.MarkPodsNotReady(ctx, nc.kubeClient, nc.recorder, pods, nodeName); err != nil {// 7.2 标记失败(如 API 冲突、Pod 已被修改),记录日志并重试logger.Info("Unable to mark pod NotReady on node", "pod", klog.KRef(podItem.namespace, podItem.name), "node", klog.KRef("", nodeName), "err", err)nc.podUpdateQueue.AddRateLimited(podItem)}}// 7.3 节点状态为 Ready → 无需处理(Pod 状态由 Kubelet 维护为 Ready)
}

三、核心机制深度解析

1. 输入参数:podUpdateItem

        podUpdateItem 是 Pod 任务的 “元数据”,包含 Pod 的 namespace 和 name,用于从缓存中查询完整的 Pod 信息。其定义通常如下(简化版):
type podUpdateItem struct {namespace stringname      string
}
  • 触发场景:当 Pod 发生以下事件时,任务会被加入 podUpdateQueue,触发 processPod 处理:
a.Pod 被调度到节点(.spec.NodeName 从空变为非空);b.Pod 所在节点的 NodeReady 状态变更(从 Ready 变为 NotReady,或反之);c.Pod 本身状态变更(如从 Pending 变为 Running)。

2. 节点健康缓存:nc.nodeHealthMap

        nodeHealthMap 是 Node Controller 维护的 节点健康状态本地缓存,用于高效存储和查询节点的关键健康信息(如 NodeReady 条件、污点状态),避免频繁从 
nodeLister 解析完整节点对象(提升性能)。
  • 更新时机:当节点状态更新(如 Kubelet 上报 NodeReady 变化)时,Node Controller 会同步更新 nodeHealthMap
  • 深拷贝获取:通过 getDeepCopy(nodeName) 获取节点健康状态,避免处理过程中修改原缓存(线程安全)。

3. 核心工具函数:controllerutil.MarkPodsNotReady

该函数是 “将 Pod 标记为 NotReady” 的核心实现,封装了 API 调用和事件记录,逻辑如下:
  1. 构造 Pod 状态更新请求:
  1. 调用 API 更新 Pod:通过 nc.kubeClient.CoreV1().Pods(pod.Namespace).Patch(...) 发送请求,处理可能的并发冲突(如其他组件同时修改 Pod 状态)。
  2. 记录事件:若更新成功,通过 nc.recorder 记录 Type: WarningReason: PodNotReady 的事件,描述 “Pod 因节点不健康被标记为 NotReady”,便于通过 kubectl describe pod  追溯。
  3. 返回结果:成功返回 nil,失败返回错误(如 API 不可用、Pod 已删除)。

4. 失败重试机制:nc.podUpdateQueue.AddRateLimited

当处理 Pod 时遇到错误(如缓存查询失败、API 冲突、标记 NotReady 失败),通过 AddRateLimited(podItem) 将任务重新加入队列,实现 “速率限制重试”:
  • 速率限制:重试间隔随失败次数递增(如 1s、2s、4s... 指数退避),避免因频繁重试导致 API Server 负载过高;
  • 重试上限:队列默认设置最大重试次数(如 5 次),超过后将任务加入 “死信队列”(需人工干预),避免无限重试。

四、典型场景示例

场景 1:节点从 Ready 变为 NotReady(如磁盘压力)

  1. 触发条件:节点因磁盘压力(DiskPressure: True)被 Kubelet 上报为 NodeReady: FalseNode Controller 更新 nodeHealthMap 并将该节点上的所有 Pod 任务加入 podUpdateQueue
  2. 处理流程:
  1. 最终效果:Service 检测到 Pod 为 NotReady,自动从 endpoints 中移除该 Pod,停止转发流量,避免请求失败。

场景 2:Pod 调度到不存在的节点(异常场景)

  1. 触发条件:Pod 调度后,目标节点因故障被删除(.spec.NodeName 仍为已删除的节点名),任务被加入 podUpdateQueue
  2. 处理流程:
  1. 最终效果:若节点未重建,重试多次后任务进入死信队列;若节点重建,重试时会检测到节点状态,正常处理 Pod 状态。

场景 3:标记 Pod NotReady 失败(API 冲突)

  1. 触发条件:MarkPodsNotReady 调用 API 时,Pod 正被其他组件(如 kube-scheduler)修改,返回 Conflict(409)错误;
  2. 处理流程:
  1. 最终效果:重试时若冲突解除,成功标记 Pod 为 NotReady;若冲突持续,超过重试上限后进入死信队列,需人工排查冲突原因。

总结

        processPod 是 Node Controller 实现 “Pod 状态与节点健康状态联动” 的核心逻辑,其本质是 “节点不健康时自动隔离 Pod”,避免不可用的 Pod 接收流量,保障集群服务可用性。通过本地缓存、失败重试、事件记录等机制,该方法实现了高效、可靠的 Pod 状态管理,是 Kubernetes 自愈能力的重要组成部分。理解其逻辑,可帮助运维人员快速定位 “Pod 状态与节点状态不一致” 的问题,如 “节点已下线但 Pod 仍在接收流量”“Pod 被误标记为 NotReady” 等,提升集群运维效率。

1.3 doNoExecuteTaintingPass

Kubernetes Node Controller(节点控制器)中 管理 NoSchedule 污点 的 doNoScheduleTaintingPass 和 管理 NoExecute 污点 的 doNoExecuteTaintingPass 方法。二者均通过 “生成期望污点→对比当前污点→调和差异” 的逻辑维护节点污点状态,但针对的污点类型、触发场景和作用效果完全不同(NoSchedule 限制新 Pod 调度,
NoExecute 驱逐已运行 Pod)。以下从 函数职责对比、核心逻辑拆解、关键差异分析、典型场景 四方面详细解析,帮助理解两种污点的管理机制。
        核心职责与定位对比首先明确两种污点的本质区别(Kubernetes 污点核心特性),这是理解两段代码的基础:

基于此,两段代码的核心职责差异如下:

        doNoExecuteTaintingPass,负责在节点 Ready 状态异常(False/Unknown)时,通过 “区域级速率限制队列” 异步添加 NoExecute 污点(触发 Pod 驱逐),同时保证 not-ready 与 unreachable 两类污点互斥,避免冲突。其核心设计目标是 “安全、有序地处理节点故障后的 Pod 驱逐,防止集群级震荡”。以下从 核心职责、关键组件、逐段逻辑拆解、核心机制、典型场景 五方面详细解析:

一、核心职责与设计背景

1. 核心目标

  • NoExecute 污点管理:当节点 NodeReady 状态为 False(明确不可用)或 Unknown(失联)时,自动添加对应的 NoExecute 污点(node.kubernetes.io/not-ready 或 node.kubernetes.io/unreachable),触发节点上 “无法容忍该污点” 的 Pod 驱逐;
  • 污点互斥保障:确保 not-ready 和 unreachable 污点不会同时存在(二者互斥),避免依赖污点的组件(如 kubelet、调度器)逻辑混乱;
  • 区域级速率控制:通过 “按节点所在区域(Zone)划分队列”,限制同一区域内同时处理的节点任务数量,避免大量节点故障时 Pod 集中驱逐,导致集群资源耗尽或服务中断;
  • 异步安全处理:基于 RateLimitedTimedQueue 实现失败重试(如缓存查询失败、API 冲突),确保污点调和最终成功,同时避免阻塞主流程。

2. 关键依赖组件

二、逐段代码逻辑拆解

(一)第一步:安全获取所有区域的队列键(避免锁持有过久)

// 1. 定义存储区域键的切片(后续遍历区域队列用)
var zoneNoExecuteTainterKeys []string// 2. 加锁获取所有区域键(仅持有锁到获取键完成,避免后续遍历队列时长期占锁)
func() {nc.evictorLock.Lock()defer nc.evictorLock.Unlock()// 初始化切片,容量为映射长度(避免动态扩容)zoneNoExecuteTainterKeys = make([]string, 0, len(nc.zoneNoExecuteTainter))// 遍历映射,收集所有区域键(如 "cn-beijing-a"、"cn-shanghai-b")for k := range nc.zoneNoExecuteTainter {zoneNoExecuteTainterKeys = append(zoneNoExecuteTainterKeys, k)}
}()
  • 核心设计:通过 “匿名函数 + 局部锁”,将锁的持有时间压缩到 “获取区域键” 这一短操作,避免后续遍历队列、处理任务时长期占用锁,导致其他协程无法修改 zoneNoExecuteTainter(如添加新区域的队列),提升并发性能。

(二)第二步:遍历每个区域,获取对应的速率限制队列

logger := klog.FromContext(ctx)
// 3. 遍历所有区域键,处理每个区域的队列任务
for _, k := range zoneNoExecuteTainterKeys {var zoneNoExecuteTainterWorker *scheduler.RateLimitedTimedQueue// 4. 再次加锁,获取当前区域对应的速率限制队列func() {nc.evictorLock.Lock()defer nc.evictorLock.Unlock()// 注释说明:区域不会被删除,因此队列也不会被删除,仅会新增,无需检查键是否存在zoneNoExecuteTainterWorker = nc.zoneNoExecuteTainter[k]}()// 5. 处理当前区域队列中的任务(核心逻辑,见下文)zoneNoExecuteTainterWorker.Try(logger, func(value scheduler.TimedValue) (bool, time.Duration) {// ... 任务处理逻辑 ...})
}
  • 关键逻辑:每个区域对应一个独立的 RateLimitedTimedQueue,确保不同区域的任务处理互不干扰;再次加锁获取队列,是因为 zoneNoExecuteTainter 可能被其他协程并发修改(如新增区域队列),需通过锁保证数据一致性。

(三)第三步:处理队列中的节点任务(Try 方法与匿名函数)

        zoneNoExecuteTainterWorker.Try(...) 是队列的核心消费方法,接收一个 “任务处理函数”,返回值 (bool, time.Duration) 定义 “任务是否成功” 和 “失败后重试延迟”。以下是任务处理的详细逻辑:
1. 从缓存获取节点信息(失败重试)
// value.Value 存储的是待处理的节点名(string 类型)
node, err := nc.nodeLister.Get(value.Value)
// 情况1:节点不存在(如已被删除)→ 任务成功,无需重试
if apierrors.IsNotFound(err) {logger.Info("Node no longer present in nodeLister", "node", klog.KRef("", value.Value))return true, 0
}
// 情况2:缓存查询失败(如缓存未同步)→ 任务失败,50ms 后重试
else if err != nil {logger.Info("Failed to get Node from the nodeLister", "node", klog.KRef("", value.Value), "err", err)return false, 50 * time.Millisecond
}
  • 重试策略:缓存查询失败时,选择短延迟(50ms)重试,平衡 “快速恢复” 和 “避免频繁查询压垮缓存”。
2. 检查节点 NodeReady 条件(缺失重试)
// 从节点状态中提取 NodeReady 条件(第二个返回值为条件对象,第一个为索引,此处无用)
_, condition := controllerutil.GetNodeCondition(&node.Status, v1.NodeReady)
// 若 NodeReady 条件缺失(异常场景,如节点刚创建、条件被恶意删除)→ 任务失败,50ms 后重试
if condition == nil {logger.Info("Failed to get NodeCondition from the node status", "node", klog.KRef("", value.Value))return false, 50 * time.Millisecond
}
  • 合理性说明:NodeReady 是节点最核心的健康条件,正常节点必然存在该条件;缺失时重试,等待 Kubelet 上报完整状态。
3. 生成互斥的 NoExecute 污点(核心逻辑)
// 初始化“待添加污点”和“需删除的对立污点”
taintToAdd := v1.Taint{}
oppositeTaint := v1.Taint{}// 根据 NodeReady 状态,选择对应的污点(二选一,互斥)
switch condition.Status {
case v1.ConditionFalse: // 节点明确不可用(如磁盘压力、内存压力导致 NotReady)→ 添加 not-ready 污点,删除 unreachable 污点taintToAdd = *NotReadyTaintTemplateoppositeTaint = *UnreachableTaintTemplate
case v1.ConditionUnknown: // 节点失联(如网络断开,无法确认状态)→ 添加 unreachable 污点,删除    // 节点失联(如网络断开,无法确认状态)→ 添加 unreachable 污点,删除 not-ready 污点taintToAdd = *UnreachableTaintTemplateoppositeTaint = *NotReadyTaintTemplate
default: // 节点恢复 Ready(如网络恢复、压力解除)→ 无需添加污点,任务成功logger.V(4).Info("Node was in a taint queue, but it's ready now. Ignoring taint request", "node", klog.KRef("", value.Value))return true, 0
}
  • 污点互斥设计:not-ready 和 unreachable 分别对应 “明确不可用” 和 “失联” 两种状态,逻辑上互斥(一个节点不可能同时 “明确不可用” 和 “失联”);通过 “添加目标污点 + 删除对立污点”,确保节点上始终只有一种(或零种)NoExecute 污点,避免组件逻辑混乱。
4. 调和污点并记录监控指标
// 调用工具函数,原子化添加目标污点、删除对立污点(返回 true 表示调和成功)
result := controllerutil.SwapNodeControllerTaint(ctx, nc.kubeClient, []*v1.Taint{&taintToAdd},  // 需添加的 NoExecute 污点[]*v1.Taint{&oppositeTaint}, // 需删除的对立污点node
)// 若调和成功,更新该区域的 Pod 驱逐计数(监控用)
if result {// 获取节点所在区域(如通过节点标签 topology.kubernetes.io/zone 解析)zone := nodetopology.GetZoneKey(node)// 按区域累加驱逐次数(Prometheus 指标,可用于 Grafana 面板监控)evictionsTotal.WithLabelValues(zone).Inc()
}// 返回调和结果:成功→true(无需重试),失败→false(队列默认重试)
return result, 0
  • 原子化调和:SwapNodeControllerTaint 封装了 Kubernetes API 的 Patch 请求,确保 “添加” 和 “删除” 操作原子化(要么都成功,要么都失败),避免部分操作失败导致污点状态不一致;
  • 监控价值:evictionsTotal 指标按区域统计驱逐次数,可帮助运维人员快速定位 “哪个区域节点故障集中”,及时排查问题(如某区域网络中断导致大量节点 unreachable)。

三、核心机制深度解析

1. 区域级速率限制:RateLimitedTimedQueue

        zoneNoExecuteTainter 按节点所在区域划分队列,每个队列是 scheduler.RateLimitedTimedQueue,核心特性是 “控制单位时间内处理的任务数量”,避免集群震荡:
  • 速率限制:队列可配置 “每秒处理 N 个任务”,若同一区域内大量节点故障(如 100 个节点同时 unreachable),队列会按速率依次处理,避免 100 个节点同时添加 NoExecute 污点,导致 1000 个 Pod 集中驱逐,引发集群资源(如 CPU、内存)耗尽;
  • 定时重试:任务处理失败时,队列会按返回的延迟时间(如 50ms)重新加入任务,避免立即重试导致的资源浪费;
  • 任务去重:若同一节点多次被加入队列(如节点 Ready 状态反复波动),队列会合并重复任务,仅处理最新一次,避免无效操作。

2. 锁粒度控制:evictorLock 的合理使用

        代码中两次加锁,均采用 “短持有时间” 设计,避免锁竞争:
  • 第一次加锁:仅用于 “收集区域键”,操作耗时极短,不影响其他协程修改 zoneNoExecuteTainter
  • 第二次加锁:仅用于 “获取当前区域的队列”,同样是短操作,避免后续处理任务时长期占锁;
  • 设计优势:若全程持有锁,当某区域队列处理耗时较长(如 API 重试),其他区域的队列会被阻塞,导致跨区域任务处理延迟;“短持有锁” 设计最大化了并发性能。

3. 污点模板:NotReadyTaintTemplate 与 UnreachableTaintTemplate这两个全局模板定义了 

// NotReadyTaintTemplate:节点不可用(NotReady)的 NoExecute 污点模板
var NotReadyTaintTemplate = &v1.Taint{Key:       "node.kubernetes.io/not-ready",Effect:    v1.TaintEffectNoExecute,TimeAdded: func() *metav1.Time {now := metav1.Now()return &now}(), // 动态设置污点添加时间,便于追溯
}// UnreachableTaintTemplate:节点失联(Unknown)的 NoExecute 污点模板
var UnreachableTaintTemplate = &v1.Taint{Key:       "node.kubernetes.io/unreachable",Effect:    v1.TaintEffectNoExecute,TimeAdded: func() *metav1.Time {now := metav1.Now()return &now}(),
}
  • 标准化价值:所有节点的 NoExecute 污点格式统一,确保依赖污点的组件(如 kubelet 驱逐逻辑、调度器污点匹配)能正确识别,避免因 Key 拼写错误(如 node.not-ready)导致功能失效。

四、典型场景示例

场景 1:节点因网络断开变为 Unknown

  1. 触发条件:节点网络中断,Kubelet 无法向 API Server 上报状态,Node Controller 将节点 NodeReady 条件设为 Unknown,并将节点名加入其所在区域(如 cn-beijing-a)的 zoneNoExecuteTainter 队列;
  2. 任务处理:

      - doNoExecuteTaintingPass 遍历到 cn-beijing-a 区域,获取对应的队列;
- 队列 Try 方法处理该节点任务,从缓存获取节点信息,确认 NodeReady: Unknown;
- 生成 unreachable 污点(taintToAdd)和 not-ready 对立污点(oppositeTaint);
- 调用 SwapNodeControllerTaint 添加 unreachable:NoExecute 污点,删除 not-ready 污点(若存在);

     最终效果:

     - 节点上 “无法容忍 unreachable 污点” 的 Pod 被 kubelet 驱逐;
- evictionsTotal{zone="cn-beijing-a"} 指标加 1,监控面板显示该区域驱逐次数增加;
- 若网络恢复,节点 NodeReady 变为 True,后续任务会忽略污点添加,Pod 可重新调度到该节点。

场景 2:节点 Ready 状态反复波动(False ↔ True

  1. 触发条件:节点因临时磁盘压力变为 NodeReady: False,被加入队列;处理前压力解除,节点恢复 Ready: True
  2. 任务处理:

      - 队列处理该节点时,获取 NodeReady: True;
- 进入 default 分支,日志提示 “节点已恢复,忽略污点请求”,返回 true, 0;

      最终效果:不添加任何污点,避免 “节点已恢复但仍被驱逐 Pod” 的误操作,保障服务稳定性。

五、常见问题与排查

1. 节点 Unknown 但未添加 unreachable 污点

  • 可能原因:

       a.节点所在区域的队列未被创建(如 zoneNoExecuteTainter 中无该区域键);
b.队列任务处理失败(如缓存查询失败后重试次数耗尽);
c.SwapNodeControllerTaint 调和失败(如 API Server 返回 Conflict 且重试失败)。

  • 排查步骤:

      a.查看 zoneNoExecuteTainter 状态:通过日志(grep "zoneNoExecuteTainter" kube-controller-manager.log)确认节点所在区域是否有队列;
b.检查任务处理日志:kubectl -n kube-system logs kube-controller-manager-master | grep -i "Failed to get Node" | grep <node-name>,查看是否有缓存失败日志;
c.验证 API 权限:确保 kube-controller-manager 的 ServiceAccount 有 nodes/update 权限(通过 kubectl describe clusterrole system:kube-controller-manager 查看)。

总结

  • doNoScheduleTaintingPass:是 “节点健康状态→调度限制” 的桥梁,通过 NoSchedule 污点 “挡新不赶旧”,适合节点临时故障(如磁盘压力);
  • doNoExecuteTaintingPass:是 “节点失联 / 不可用→Pod 驱逐” 的核心,通过 NoExecute 污点 “赶旧且挡新”,适合节点严重故障(如网络断开);
  • 二者协同确保节点故障时 “先限制调度,再驱逐 Pod”,避免故障扩散,是 Kubernetes 集群自愈能力的关键实现。理解其差异,可帮助运维人员快速定位 “调度限制异常”“Pod 驱逐失败” 等问题,提升集群稳定性。
http://www.dtcms.com/a/464818.html

相关文章:

  • SMOTE 算法详解:解决不平衡数据问题的有效工具
  • HGDB集群(安全版)repmgr手动切换主备库
  • 三维GIS数据转换指南:SHAPE文件到3DTiles的高效实现方案
  • K8S(三)—— 基于kubeadm 1.20版本部署Kubernetes集群与Harbor私有仓库实战
  • 宁波外贸网站制作公司手机网站建设哪家公司好
  • 【C语言实战(8)】C语言循环结构(do-while):解锁编程新境界
  • 面向Qt/C++开发工程师的Ai提示词(附Trae示例)
  • sqlite 使用: 01-源码编译与使用
  • Django视图进阶:快捷函数、装饰器与请求响应
  • 企业营销网站的建设网站开发响应式
  • 掌握DMA基于GD32F407VE的天空星的配置
  • 基于腾讯云的物联网导盲助手设计与实现(论文+源码)
  • Vue3打造高效前端埋点系统
  • 框架--Maven
  • 【Java集合】
  • 停止Conda开机自动运行方法
  • 湘潭市高新建设局施工报建网站wordpress 宕机
  • 复杂结构数据挖掘(二)关联规则挖掘 Association rule mining
  • Windows 上安装 PostgreSQL
  • 基于JETSON/x86+FPGA+AI的5G远程驾驶座舱时延验证方案
  • 支持向量机(SVM)完全解读
  • 单片机学习日记
  • 重庆网站制作多少钱app设计开发哪家好
  • AI大模型学习(17)python-flask AI大模型和图片处理工具的从一张图到多平台适配的简单方法
  • 如何通过 7 种解决方案将文件从PC无线传输到Android
  • Word 为每一页设置不同页边距(VBA 宏)
  • wordpiece、unigram、sentencepiece基本原理
  • css word-spacing属性
  • 使用 python-docx 库操作 word 文档(2):在word文档中插入各种内容
  • 中企动力销售工作内容白城网站seo