kubelet 原理分析

Reference

  • https://atbug.com/kubelet-source-code-analysis/

kubelet 简介

kubernetes 分为控制面和数据面,kubelet 就是数据面最主要的组件,在每个节点上启动,主要负责容器的创建、启停、监控、日志收集等工作。它是一个在每个集群节点上运行的代理,负责确保节点上的容器根据PodSpec(Pod定义文件)正确运行。

Kubelet执行以下几项重要功能:

  • Pod生命周期管理:Kubelet根据从API服务器接收到的PodSpecs创建、启动、终止容器。它负责启动Pod中的容器,并确保它们按预期运行。
  • 节点状态监控:Kubelet定期监控节点和容器的状态,并将状态报告回集群的控制平面。这使得集群中的其他组件能够做出相应的调度决策。
  • 资源管理:Kubelet负责管理分配给每个Pod的资源。这包括CPU、内存和磁盘存储资源。
  • 健康检查:Kubelet可以执行容器健康检查,并根据检查结果决定是否需要重启容器。
  • 与容器运行时的通信:Kubelet与容器运行时(如Docker、containerd等)通信,以管理容器的生命周期。
  • 秘密和配置管理:Kubelet负责将秘密、配置映射等挂载到Pod的容器中,以便应用程序可以访问这些配置。
  • 服务发现和负载均衡:尽管Kubelet本身不直接处理服务发现,但它通过设置网络规则和环境变量来支持容器内的服务发现机制。

kubelet 架构

kubelet 的架构由 N 多的组件组成,下面简单介绍下比较重要的几个:

  • Sync Loop: 这是Kubelet活动的核心,负责同步Pod的状态。同步循环会定期从API服务器获取PodSpecs,并确保容器的当前状态与这些规格相匹配。
  • PodConfig: 负责将各个配置源转换成 PodSpecs,可以选择的配置源包括:Kube-apiserver、本地文件、HTTP。
  • PLEG(Pod Lifecycle Event Generator): 负责监测和缓存Pod生命周期事件,如创建、启动或停止容器,然后将这些事件通知 Sync Loop。
  • PodWorkers: 负责管理 Pod 的生命周期事件处理。当 Pod 生命周期事件 PLEG 检测到新的事件时,PodWorkers 会被调用来处理这些事件,包括启动新的 Pod、更新现有的 Pod、或者停止和清理不再需要的 Pod。
  • PodManager: 存储 Pod 的期望状态,kubelet 服务的不同渠道的 Pod。
  • ContainerRuntime: 顾名思义,容器运行时。与遵循 CRI 规范的高级容器运行时进行交互。
  • StatsProvider: 提供节点和容器的统计信息,有 cAdvisor 和 CRI 两种实现。
  • ProbeManager: 负责执行容器的健康检查,包括 Liveness,Startup 和 Readiness 检查。
  • VolumeManager: 负责管理 Pod 的卷,包括挂载和卸载卷。
  • ImageManager: 负责管理镜像,包括拉取、删除、镜像 GC 等。
  • DeviceManager: 负责管理设备,包括 GPU、RDMA 等。
  • PluginManager: PluginManager 运行一组异步循环,根据此节点确定哪些插件需要注册/取消注册并执行。如 CSI 驱动和设备管理器插件(Device Plugin)。
  • CertificateManager: 处理证书轮换。
  • OOMWatcher: 从系统日志中获取容器的 OOM 日志,将其封装成事件并记录。

流程

首先在 cmd/kubelet 中使用传入命令行参数的方式初始化配置,然后创建 pkg/kubelet 中的 Bootstrap inferface, kubelet struct 实现了这个接口, 然后调用 Run 方法启动 kubelet。

func startKubelet(k kubelet.Bootstrap, podCfg *config.PodConfig, kubeCfg *kubeletconfiginternal.KubeletConfiguration, kubeDeps *kubelet.Dependencies, enableServer bool) {
   
   
	// start the kubelet
	go k.Run(podCfg.Updates())

	// start the kubelet server
	if enableServer {
   
   
		go k.ListenAndServe(kubeCfg, kubeDeps.TLSOptions, kubeDeps.Auth, kubeDeps.TracerProvider)
	}
	if kubeCfg.ReadOnlyPort > 0 {
   
   
		go k.ListenAndServeReadOnly(netutils.ParseIPSloppy(kubeCfg.Address), uint(kubeCfg.ReadOnlyPort))
	}
	go k.ListenAndServePodResources()
}

Bootstrap

// Bootstrap is a bootstrapping interface for kubelet, targets the initialization protocol
type Bootstrap interface {
   
   
	GetConfiguration() kubeletconfiginternal.KubeletConfiguration
	BirthCry()
	StartGarbageCollection()
	ListenAndServe(kubeCfg *kubeletconfiginternal.KubeletConfiguration, tlsOptions *server.TLSOptions, auth server.AuthInterface, tp trace.TracerProvider)
	ListenAndServeReadOnly(address net.IP, port uint)
	ListenAndServePodResources()
	Run(<-chan kubetypes.PodUpdate)
	RunOnce(<-chan kubetypes.PodUpdate) ([]RunPodResult, error)
}

type Kubelet struct {
   
   
    // ...
}

方法:

  • GetConfiguration: 获取 kubelet 的配置
  • BirthCry: 打印 kubelet 启动信息
  • StartGarbageCollection: 启动垃圾回收
  • ListenAndServe: 启动 kubelet 服务
  • ListenAndServeReadOnly: 启动只读服务
  • ListenAndServePodResources: 启动 pod 资源服务
  • Run: 启动 kubelet 的同步循环
  • RunOnce: 启动一次同步循环
func (kl *Kubelet) StartGarbageCollection() {
   
   
	loggedContainerGCFailure := false
	go wait.Until(func() {
   
   
		ctx := context.Background()
		if err := kl.containerGC.GarbageCollect(ctx); err != nil {
   
   
			klog.ErrorS(err, "Container garbage collection failed")
			kl.recorder.Eventf(kl.nodeRef, v1.EventTypeWarning, events.ContainerGCFailed, err.Error())
			loggedContainerGCFailure = true
		} else {
   
   
			var vLevel klog.Level = 4
			if loggedContainerGCFailure {
   
   
				vLevel = 1
				loggedContainerGCFailure = false
			}

			klog.V(vLevel).InfoS("Container garbage collection succeeded")
		}
	}, ContainerGCPeriod, wait.NeverStop)

	// when the high threshold is set to 100, stub the image GC manager
	if kl.kubeletConfiguration.ImageGCHighThresholdPercent == 100 {
   
   
		klog.V(2).InfoS("ImageGCHighThresholdPercent is set 100, Disable image GC")
		return
	}

	prevImageGCFailed := false
	go wait.Until(func() {
   
   
		ctx := context.Background()
		if err := kl.imageManager.GarbageCollect(ctx); err != nil {
   
   
			if prevImageGCFailed {
   
   
				klog.ErrorS(err, "Image garbage collection failed multiple times in a row")
				// Only create an event for repeated failures
				kl.recorder.Eventf(kl.nodeRef, v1.EventTypeWarning, events.ImageGCFailed, err.Error())
			} else {
   
   
				klog.ErrorS(err, "Image garbage collection failed once. Stats initialization may not have completed yet")
			}
			prevImageGCFailed = true
		} else {
   
   
			var vLevel klog.Level = 4
			if prevImageGCFailed {
   
   
				vLevel = 1
				prevImageGCFailed = false
			}

			klog.V(vLevel).InfoS("Image garbage collection succeeded")
		}
	}, ImageGCPeriod, wait.NeverStop)
}

大致的流程为使用 containerGC 启动容器垃圾回收,当ImageGCHighThresholdPercent 为100时,不启动镜像垃圾回收,否则使用 imageManager 启动镜像垃圾回收。

// RunOnce polls from one configuration update and run the associated pods.
func (kl *Kubelet) RunOnce(updates <-chan kubetypes.PodUpdate) ([]RunPodResult, error) {
   
   
	ctx := context.Background()
	// Setup filesystem directories.
	if err := kl.setupDataDirs(); err != nil {
   
   
		return nil, err
	}

	// If the container logs directory does not exist, create it.
	if _, err := os.Stat(ContainerLogsDir); err != nil {
   
   
		if err := kl.os.MkdirAll(ContainerLogsDir, 0755); err != nil {
   
   
			klog.ErrorS(err, "Failed to create directory", "path", ContainerLogsDir)
		}
	}

	select {
   
   
	case u := <-updates:
		klog.InfoS("Processing manifest with pods", "numPods", len(u.Pods))
		result, err := kl.runOnce(ctx, u.Pods, runOnceRetryDelay)
		klog.InfoS("Finished processing pods", "numPods", len(u.Pods))
		return result, err
	case <-time.After(runOnceManifestDelay):
		return nil, fmt.Errorf("no pod manifest update after %v", runOnceManifestDelay)
	}
}

// runOnce runs a given set of pods and returns their status.
func (kl *Kubelet) runOnce(ctx context.Context, pods []*v1.Pod, retryDelay time.Duration) (results []RunPodResult, err error) {
   
   
	ch := make(chan RunPodResult)
	admitted := []*v1.Pod{
   
   }
	for _, pod := range pods {
   
   
		// Check if we can admit the pod.
		if ok, reason, message := kl.canAdmitPod(admitted, pod); !ok {
   
   
			kl.rejectPod(pod, reason, message)
			results = append(results, RunPodResult{
   
   pod, nil})
			continue
		}

		admitted = append(admitted, pod)
		go func(pod *v1.Pod) {
   
   
			err := kl.runPod(ctx, pod, retryDelay)
			ch <- RunPodResult{
   
   pod, err}
		}(pod)
	}

	klog.InfoS("Waiting for pods", "numPods", len(admitted))
	failedPods := []string{
   
   }
	for i := 0; i < len(admitted); i++ {
   
   
		res := <-ch
		results = append(results, res)
		if res.Err != nil {
   
   
			failedContainerName, err := kl.getFailedContainers(ctx, res.Pod)
			if err != nil {
   
   
				klog.InfoS("Unable to get failed containers' names for pod", "pod", klog.KObj(res.Pod), "err", err)
			} else {
   
   
				klog.InfoS("Unable to start pod because container failed", "pod", klog.KObj(res.Pod), "containerName", failedContainerName)
			}
			failedPods = append(failedPods, format.Pod(res.Pod))
		} else {
   
   
			klog.InfoS("Started pod", "pod", klog.KObj(res.Pod))
		}
	}
	if len(failedPods) > 0 {
   
   
		return results, fmt.Errorf("error running pods: %v", failedPods)
	}
	klog.InfoS("Pods started", "numPods", len(pods))
	return results, err
}

大致作用为从 updates 中获取 PodUpdate,然后调用 runOnce 方法,该方法会调用 runPod 方法启动 Pod。

Run

func (kl *Kubelet) Run(updates <-chan kubetypes.PodUpdate) {
   
   
	ctx := context.Background()
	if kl.logServer == nil {
   
   
		file := http.FileServer(http.Dir(nodeLogDir))
		if utilfeature.DefaultFeatureGate.Enabled(features.NodeLogQuery) && kl.kubeletConfiguration.EnableSystemLogQuery {
   
   
			kl.logServer = http.StripPrefix("/logs/", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
   
   
				if nlq, errs := newNodeLogQuery(req.URL.Query()); len(errs) > 0 {
   
   
					http.Error(w, errs.ToAggregate().Error(), http.StatusBadRequest)
					return
				} else if nlq != nil {
   
   
					if req.URL.Path != "/" && req.URL.Path != "" {
   
   
						http.Error(w, "path not allowed in query mode", http.StatusNotAcceptable)
						return
					}
					if errs := nlq.validate(); len(errs) > 0 {
   
   
						http.Error(w, errs.ToAggregate().Error(), http.StatusNotAcceptable)
						return
					}
					// Validation ensures that the request does not query services and files at the same time
					if len(nlq.Services) > 0 {
   
   
						journal.ServeHTTP(w, req)
						return
					}
					// Validation ensures that the request does not explicitly query multiple files at the same time
					if len(nlq.Files) == 1 {
   
   
						// Account for the \ being used on Windows clients
						req.URL.Path = filepath.ToSlash(nlq.Files[0])
					}
				}
				// Fall back in case the caller is directly trying to query a file
				// Example: kubectl get --raw /api/v1/nodes/$name/proxy/logs/foo.log
				file.ServeHTTP(w, req)
			}))
		} else {
   
   
			kl.logServer = http.StripPrefix("/logs/", file)
		}
	}
	if kl.kubeClient == nil {
   
   
		klog.InfoS("No API server defined - no node status update will be sent")
	}

	// Start the cloud provider sync manager
	if kl.cloudResourceSyncManager != nil {
   
   
		go kl.cloudResourceSyncManager.Run(wait.NeverStop)
	}

	if err := kl.initializeModules(); err != nil {
   
   
		kl.recorder.Eventf(kl.nodeRef, v1.EventTypeWarning, events.KubeletSetupFailed, err.Error())
		klog.ErrorS(err, "Failed to initialize internal modules")
		os.Exit(1)
	}

	// Start volume manager
	go kl.volumeManager.Run(kl.sourcesReady, wait.NeverStop)

	if kl.kubeClient != nil {
   
   
		// Start two go-routines to update the status.
		//
		// The first will report to the apiserver every nodeStatusUpdateFrequency and is aimed to provide regular status intervals,
		// while the second is used to provide a more timely status update during initialization and runs an one-shot update to the apiserver
		// once the node becomes ready, then exits afterwards.
		//
		// Introduce some small jittering to ensure that over time the requests won't start
		// accumulating at approximately the same time from the set of nodes due to priority and
		// fairness effect.
		go wait.JitterUntil(kl.syncNodeStatus, kl.nodeStatusUpdateFrequency, 0.04, true, wait.NeverStop)
		go kl.fastStatusUpdateOnce()

		// start syncing lease
		go kl.nodeLeaseController.Run(context.Background())
	}
	go wait.Until(kl.updateRuntimeUp, 5*time.Second, wait.NeverStop)

	// Set up iptables util rules
	if kl.makeIPTablesUtilChains {
   
   
		kl.initNetworkUtil()
	}

	// Start component sync loops.
	kl.statusManager.Start()

	// Start syncing RuntimeClasses if enabled.
	if kl.runtimeClassManager != nil {
   
   
		kl.runtimeClassManager.Start(wait.NeverStop)
	}

	// Start the pod lifecycle event generator.
	kl.pleg.Start()

	// Start eventedPLEG only if EventedPLEG feature gate is enabled.
	if utilfeature.DefaultFeatureGate.Enabled(features.EventedPLEG) {
   
   
		kl.eventedPleg.Start()
	}

	kl.syncLoop(ctx, updates, kl)
}

代码的流程为:

  1. 检查是否需要创建日志服务器 如果需要则创建
  2. 启动云提供商同步管理器
  3. 初始化模块,如果出错则打印日志并退出
  4. 启动卷管理器
  5. 启动两个 goroutine 来更新状态,一个是定时更新,一个是在初始化时更新
  6. 启动同步租约的goroutine
  7. 定期更新RuntimeUp状态的goroutine
  8. 设置iptables规则
  9. 启动组件同步循环
  10. 如果启用了RuntimeClasses,则启动RuntimeClasses同步循环
  11. 启动Pod Lifecycle Event Generator
  12. 如果启用了EventedPLEG特性,则启动EventedPLEG
  13. 启动 syncloop

syncLoop

// syncLoop is the main loop for processing changes. It watches for changes from
// three channels (file, apiserver, and http) and creates a union of them. For
// an
<think>我们正在处理一个关于Kubernetes节点状态的问题。用户报告了“kubelethas nodisk pressure”的消息,这通常是一个正常状态,表示节点磁盘空间充足。但用户可能是在确认状态或遇到了相关困惑。我们需要解释这个状态的含义,并可能讨论如何监控或处理DiskPressure情况。 分析: -“kubelethas nodisk pressure”是节点状态的一部分,表明kubelet检测到该节点没有磁盘压力,即磁盘空间在安全阈值内。 -这可能出现在使用`kubectldescribe node`命令时,在Conditions部分会显示DiskPressure的状态(False表示没有压力)。 回答结构: 1.解释该消息的含义:表示节点磁盘空间充足,kubelet不会因此驱逐Pod。 2.说明如何查看节点状态(包括DiskPressure)。3.讨论如何配置磁盘压力阈值以及相关的最佳实践。4.提出相关问题。注意:用户的问题看起来是陈述一个状态,而不是报错。因此,回答应侧重于解释和预防性维护。步骤: 1.解释消息含义。2.提供查看节点状态的命令。3.简要说明如何配置阈值(如果需要)。 4.提出相关问题。 注意格式:最后必须输出“§§相关问题§§”并列出3-5个问题。</think>### Kubernetes 节点 "kubelet has no disk pressure" 状态解析 当 Kubernetes 节点报告 **"kubelet has no disk pressure"** 状态时,表示该节点的磁盘资源处于健康状态。这是 kubelet 监控的节点条件之一,具体含义如下: #### 状态含义与监控机制 1. **健康状态指示** - 表示节点磁盘使用率低于 Kubelet 设置的驱逐阈值 - 节点可正常调度新 Pod,不会触发 Pod 驱逐 - 可通过命令验证: ```bash kubectl describe node <NODE_NAME> | grep -A 5 "Conditions" # 输出示例:DiskPressure False ... kubelet has no disk pressure ``` 2. **监控阈值默认值** | 监控指标 | 默认阈值 | 检测位置 | |----------|----------|----------| | `nodefs.available` | <10% | 节点文件系统(/var/lib/kubelet) | | `imagefs.available` | <15% | 容器镜像存储(/var/lib/docker) | | `nodefs.inodesFree` | <5% | inode 可用率 | 3. **状态转换原理** ```mermaid graph LR A[磁盘使用检测] -->|低于阈值| B[NoDiskPressure] A -->|超过阈值| C[DiskPressure] C -->|触发| D[Pod驱逐] B -->|允许| E[新Pod调度] ``` #### 运维建议 1. **主动监控配置** ```bash # 查看当前磁盘压力阈值 kubectl get node <NODE_NAME> -o jsonpath='{.status.allocatable}' | jq # 设置自定义阈值(在kubelet配置中) evictionHard: nodefs.available: "12%" imagefs.available: "18%" ``` 2. **预防性维护策略** - 部署节点磁盘监控(Prometheus + Node Exporter) - 配置日志轮转策略(限制容器日志大小) - 定期清理未使用的镜像: ```bash crictl rmi --prune # Containerd docker system prune -af # Docker ``` 3. **存储优化实践** - 为 Pod 设置临时存储限制: ```yaml resources: limits: ephemeral-storage: 2Gi ``` - 使用 EmptyDir 时添加大小限制: ```yaml volumes: - name: temp-data emptyDir: sizeLimit: 500Mi ``` > **注意**:该状态是动态变化的,建议通过 `kubectl top node` 持续监控节点资源使用情况,并设置磁盘使用率超过 70% 的预警通知。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值