鏈接:https://blog.csdn.net/jettery/article/details/78891733
概述
Kubelet組件運行在Node節點上,維持運行中的Pods以及提供kuberntes運行時環境,主要完成以下使命:
1.監視分配給該Node節點的pods
2.掛載pod所需要的volumes
3.下載pod的secret
4.通過docker/rkt來運行pod中的容器
5.周期的執行pod中為容器定義的liveness探針
6.上報pod的狀態給系統的其他組件
7.上報Node的狀態
整個kubelet可以按照上圖所示的模塊進行划分,模塊之間相互配合完成Kubelet的所有功能.下面對上圖中的模塊進行簡要的介紹.
Kubelet對外暴露的端口,通過該端口可以獲取到kubelet的狀態
10250 kubelet API –kublet暴露出來的端口,通過該端口可以訪問獲取node資源以及狀態,另外可以配合kubelet的啟動參數contention-profiling enable-debugging-handlers來提供了用於調試和profiling的api
4194 cAdvisor –kublet通過該端口可以獲取到本node節點的環境信息以及node上運行的容器的狀態等內容
10255 readonly API –kubelet暴露出來的只讀端口,訪問該端口不需要認證和鑒權,該http server提供查詢資源以及狀態的能力.注冊的消息處理函數定義src/k8s.io/kubernetes/pkg/kubelet/server/server.go:149
10248 /healthz –kubelet健康檢查,通過訪問該url可以判斷Kubelet是否正常work, 通過kubelet的啟動參數–healthz-port –healthz-bind-address來指定監聽的地址和端口.默認值定義在pkg/kubelet/apis/kubeletconfig/v1alpha1/defaults.go
核心功能模塊
PLEG
PLEG全稱為PodLifecycleEvent,PLEG會一直調用container runtime獲取本節點的pods,之后比較本模塊中之前緩存的pods信息,比較最新的pods中的容器的狀態是否發生改變,當狀態發生切換的時候,生成一個eventRecord事件,輸出到eventChannel中. syncPod模塊會接收到eventChannel中的event事件,來觸發pod同步處理過程,調用contaiener runtime來重建pod,保證pod工作正常.
cAdvisor
cAdvisor集成在kubelet中,起到收集本Node的節點和啟動的容器的監控的信息,啟動一個Http Server服務器,對外接收rest api請求.cAvisor模塊對外提供了interface接口,可以通過interface接口獲取到node節點信息,本地文件系統的狀態等信息,該接口被imageManager,OOMWatcher,containerManager等所使用
cAdvisor相關的內容詳細可參考github.com/google/cadvisor
GPUManager
對於Node上可使用的GPU的管理,當前版本需要在kubelet啟動參數中指定feature-gates中添加Accelerators=true,並且需要才用runtime=Docker的情況下才能支持使用GPU,並且當前只支持NvidiaGPU,GPUManager主要需要實現interface定義的Start()/Capacity()/AllocateGPU()三個函數
OOMWatcher
系統OOM的監聽器,將會與cadvisor模塊之間建立SystemOOM,通過Watch方式從cadvisor那里收到的OOM信號,並發生相關事件
ProbeManager
探針管理,依賴於statusManager,livenessManager,containerRefManager,實現Pod的健康檢查的功能.當前支持兩種類型的探針:LivenessProbe和ReadinessProbe,
LivenessProbe:用於判斷容器是否存活,如果探測到容器不健康,則kubelet將殺掉該容器,並根據容器的重啟策略做相應的處理
ReadinessProbe: 用於判斷容器是否啟動完成
探針有三種實現方式
execprobe:在容器內部執行一個命令,如果命令返回碼為0,則表明容器健康
tcprobe:通過容器的IP地址和端口號執行TCP檢查,如果能夠建立TCP連接,則表明容器健康
httprobe:通過容器的IP地址,端口號以及路徑調用http Get方法,如果響應status>=200 && status<=400的時候,則認為容器狀態健康
StatusManager
該模塊負責pod里面的容器的狀態,接受從其它模塊發送過來的pod狀態改變的事件,進行處理,並更新到kube-apiserver中.
Container/RefManager
容器引用的管理,相對簡單的Manager,通過定義map來實現了containerID與v1.ObjectReferece容器引用的映射.
EvictionManager
evictManager當node的節點資源不足的時候,達到了配置的evict的策略,將會從node上驅趕pod,來保證node節點的穩定性.可以通過kubelet啟動參數來決定evict的策略.另外當node的內存以及disk資源達到evict的策略的時候會生成對應的node狀態記錄.
ImageGC
imageGC負責Node節點的鏡像回收,當本地的存放鏡像的本地磁盤空間達到某閾值的時候,會觸發鏡像的回收,刪除掉不被pod所使用的鏡像.回收鏡像的閾值可以通過kubelet的啟動參數來設置.
ContainerGC
containerGC負責NOde節點上的dead狀態的container,自動清理掉node上殘留的容器.具體的GC操作由runtime來實現.
ImageManager
調用kubecontainer.ImageService提供的PullImage/GetImageRef/ListImages/RemoveImage/ImageStates的方法來保證pod運行所需要的鏡像,主要是為了kubelet支持cni.
VolumeManager
負責node節點上pod所使用的volume的管理.主要涉及如下功能
Volume狀態的同步,模塊中會啟動gorountine去獲取當前node上volume的狀態信息以及期望的volume的狀態信息,會去周期性的sync volume的狀態,另外volume與pod的生命周期關聯,pod的創建刪除過程中volume的attach/detach流程.更重要的是kubernetes支持多種存儲的插件,kubelet如何調用這些存儲插件提供的interface.涉及的內容較多,更加詳細的信息可以看kubernetes中volume相關的代碼和文檔.
containerManager
負責node節點上運行的容器的cgroup配置信息,kubelet啟動參數如果指定–cgroupPerQos的時候,kubelet會啟動gorountie來周期性的更新pod的cgroup信息,維持其正確.實現了pod的Guaranteed/BestEffort/Burstable三種級別的Qos,通過配置kubelet可以有效的保證了當有大量pod在node上運行時,保證node節點的穩定性.該模塊中涉及的struct主要包括
runtimeManager
containerRuntime負責kubelet與不同的runtime實現進行對接,實現對於底層container的操作,初始化之后得到的runtime實例將會被之前描述的組件所使用.
當前可以通過kubelet的啟動參數–container-runtime來定義是使用docker還是rkt.runtime需要實現的接口定義在src/k8s.io/kubernetes/pkg/kubelet/apis/cri/services.go文件里面
podManager
podManager提供了接口來存儲和訪問pod的信息,維持static pod和mirror pods的關系,提供的接口如下所示
跟其他Manager之間的關系,podManager會被statusManager/volumeManager/runtimeManager所調用,並且podManager的接口處理流程里面同樣會調用secretManager以及configMapManager.
上面所說的模塊可能只是kubelet所有模塊中的一部分,更多的需要大家一起看探索.
深入分析
下面進一步的深入分析kubelet的代碼
Kubelet負責pod的創建,pod的來源kubelet當前支持三種類型的podSource
- FileSource: 通過kubelet的啟動參數–pod-manifest-path來指定pod manifest文件所在的路徑或者文件都可以.Kubelet會讀取文件里面定義的pod進行創建.常常我們使用來定義kubelet管理的static pod
- HTTPSource: 通過kubelet的啟動參數–manifest-url –manifest-url-header來定義manifest url. 通過http Get該manifest url獲取到pod的定義
- ApiserverSource: 通過定義跟kube-apiserver進行通過的kubeclient, 從kube-apiserver中獲取需要本節點創建的pod的信息.
Kubelet如何同時處理這三種podSource里面定義的pod進行處理的.在src/k8s.io/kubernetes/pkg/kubelet/kubelet.go:254的makePodSourceConfig中分別是處理三種podSource的啟動參數.
三種source的實現類似,分別啟動goroutinue,周期性的查看是否有新的數據來源,如果發現獲取到新的數據,生成PodUpdate對象,輸出到對應的channel里面.
會針對每種類型創建對應的Channel.
cfg.Channel(kubetypes.FileSource)
cfg.Channel(kubetypes.HTTPSource)
cfg.Channel(kubetypes.ApiserverSource)
Channel存儲在PodConfig.mux.sources里面
其中channel中傳遞的對象定義如下
type PodUpdate struct {
Pods []*v1.Pod
Op PodOperation
Source string
}
Op為kubetypes.SET Source表示pod的來源,可能的值為HTTPSource|FileSource|ApiserverSource,
進一步的分析代碼,發現定義chanel的時候,同時也定義gorountine用來watch該channel的變化
go wait.Until(func() { m.listen(source, newChannel) }, 0, wait.NeverStop)
func (m *Mux) listen(source string, listenChannel <-chan interface{}) {
for update := range listenChannel {
m.merger.Merge(source, update)
}
}
原來是將3個channel的對象merge到podConfig.updates中.這個地方merge會對podUpdate進行預處理,處理流程可以仔細看podStorage.merge().會將事件中包含的pods與本地內存中存儲pods信息進行分析,將podUpdate分成adds.updates,deletes,removes,reconsiles五類,並分別更新kubetypes.PodUpdate.Op的操作.
對於podSource中生成的podUpdate,如果初次進入該流程,一開始podUpdate.Op=kubetypes.SET, 將會podSource中定義的pod將會addPods里面,podUpdate.Op=kubetype.ADD.
定義的podConfig保存在kubeDeps.PodConfig中.
進一步跟進kubelet源碼,自然想到的是誰會從channel中獲取PodUpdate進行處理,進行pod同步.
Kubelet的主流程里面會啟動gorountiue執行如下代碼
func (kl *Kubelet) syncLoop(updates <-chan kubetypes.PodUpdate, handler SyncHandler) {
glog.Info("Starting kubelet main sync loop.")
// The resyncTicker wakes up kubelet to checks if there are any pod workers
// that need to be sync'd. A one-second period is sufficient because the
// sync interval is defaulted to 10s.
syncTicker := time.NewTicker(time.Second)
defer syncTicker.Stop()
housekeepingTicker := time.NewTicker(housekeepingPeriod)
defer housekeepingTicker.Stop()
plegCh := kl.pleg.Watch()
for {
if rs := kl.runtimeState.runtimeErrors(); len(rs) != 0 {
glog.Infof("skipping pod synchronization - %v", rs)
time.Sleep(5 * time.Second)
continue
}
kl.syncLoopMonitor.Store(kl.clock.Now())
if !kl.syncLoopIteration(updates, handler, syncTicker.C, housekeepingTicker.C, plegCh) {
break
}
kl.syncLoopMonitor.Store(kl.clock.Now())
}
}
其中updates就是podConfig.updates中定義的channel. 同時kl.pleg.Watch()是pleg模塊定義的事件channel,
SyncHandler定義
type SyncHandler interface {
HandlePodAdditions(pods []*v1.Pod)
HandlePodUpdates(pods []*v1.Pod)
HandlePodRemoves(pods []*v1.Pod)
HandlePodReconcile(pods []*v1.Pod)
HandlePodSyncs(pods []*v1.Pod)
HandlePodCleanups() error
}
syncLoopIteration方法是一個channel中事件處理中心,處理從跟pod生命周期創建相關的channel中獲取事件,之后進行轉發到對應的處理函數中.這個對於理解kubelet對於pod的管理至關重要.
觸發同步的事件channel主要包括
1.configCh pod的配置改變
2.PLEG模塊中狀態更新事件
3.1s為周期的同步時鍾 syncPod
4.2s為周期的執行全局清理任務的始終 CleanupPod
啟動了goroutinue循環調用PodCfg.Updates()中的Channel中獲取PodUpdate.進入消息中專分支流程.
1.<-configCh && u.Op=kubetype.ADD
a)執行handler.HandlePodAdditions, handler.HandlePodAdditions()的實現在kubelt.HandlePodAdditions(pods []*v1.Pod)
在Kubelt.HandlePodAdditions中再一次分發podUpdate並將kl.probeManager(AddPod(pod)),執行dispatchWork. 之后將updatePodOption初始化,添加到podWorkers.podUpdates的channel中.
dispatchWork中將updatePodOptions定義成如下結構體
type UpdatePodOptions struct {
// pod to update
Pod *v1.Pod
// the mirror pod for the pod to update, if it is a static pod
MirrorPod *v1.Pod
// the type of update (create, update, sync, kill)
UpdateType kubetypes.SyncPodType
// optional callback function when operation completes
// this callback is not guaranteed to be completed since a pod worker may
// drop update requests if it was fulfilling a previous request. this is
// only guaranteed to be invoked in response to a kill pod request which is
// always delivered.
OnCompleteFunc OnCompleteFunc
// if update type is kill, use the specified options to kill the pod.
KillPodOptions *KillPodOptions
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
同時啟動goroutinue用於處理managePodLoop,在managePodLoop將會依次處理channel中的podUpdate.最終將會調用kubelet中定義的func (kl *Kubelet) syncPod(o syncPodOptions).在kubelet的syncPod中實現了調用底層其他模塊來完成pod狀態的同步.
1.kubelet.syncPod記錄podWorkerStartLatency監控指標,該指標用來統計pod被node所創建延遲的時間
2.執行kubelet中定義的podSyncHandler.ShouldEvict(),當前podSyncHandler定義有activeDeadlineHandler,該handler對應pod的spec.activeDeadlineSeconds定義進行處理,如果pod中定義該字段,則要求pod在該字段定義的時間內完成創建過程.
3.generateAPIPodStatus根據pod的信息來生成PodStatus結構體
4.kubelet.canRunPod檢查是否本節點可以運行該pod,檢查通過softAdmitHandler進行定義,另外對於pod是否具有allowPrivileged的權限.其中softAdmitHandler的定義在kubelet的啟動流程中,分別為
lifecycle.NewPredicateAdmitHandler
lifecycle.NewAppArmorAdmitHandler
lifecycle.NewNoNewPrivsAdmitHandler
1
2
3
5.statusManager.SetPodStatus更新緩存pod的狀態信息,觸發狀態更新
6.觸發kl.containerManager.updateQOSCgroups()更新pod的cgroup設置.其中kl.containerManager的定義為cm.NewContainerManager()中創建的type containerManagerImpl struct實現的interface. 繼續分析代碼最終調用的函數是qosContainerManagerImpl.UpdateCgroups(),配置頂層的QosCgroup的相關內容.
7.調用kl.containerManager.NewPodContainerManager().EnsureExists(),配置pod的cgroup的信息. 只有在kubelet的啟動參數–cgroups-per-qos為true的時候,會執行podContainerManagerImpl.EnsureExists()來創建將pod中設置對於資源的限制去對應的cgroup.詳細的創建是由cgroupManager來完成.
8.創建pod所使用的podDir,podVolumesDir以及podPluginDir目錄
9.volumeManager.WaitForAttachAndMount(pod) volumeManager將會把volume掛載到pod運行的宿主機上面.
10.調用kl.getPullSecretsForPod(),如果pod中定義了spec.ImagePullSecrets,則獲取資源對象中定義的內容.
11.最后調用kl.containerRuntime.SyncPod(),kl.containerRuntime的定義可以參考kubelet的啟動流程.如果runtime是docker,那么SyncPod()的定義為kubeGenericRuntimeManager, src/k8s.io/kubernetes/pkg/kubelet/kuberuntime/kuberuntime_manager.go:568,在該流程中分別是創建sandbox,之后創建create init/containers.
Node資源管理
Node節點運行運用的pod以及系統基礎組件,這里的資源主要指的是node節點上的cpu,memory,storage. 用戶的pod存在資源使用隨着時間變大的情況,可能會影響到其他正常工作的pod以及node節點上的其他系統組件等,如何在該場景下提高node節點的穩定性是一個需要探索的問題.
首先,先Kubernetes中相關的概念
Cgroups
Cgroups是control groups的縮寫,是Linux內核提供的一種可以限制、記錄、隔離進程組(process groups)所使用的物理資源(如:cpu,memory,IO等等)的機制。
跟容器相關的功能主要涉及以下
· 限制進程組可以使用的資源數量(Resource limiting )。比如:memory子系統可以為進程組設定一個memory使用上限,一旦進程組使用的內存達到限額再申請內存,就會出發OOM(out of memory)。
· 進程組的優先級控制(Prioritization )。比如:可以使用cpu子系統為某個進程組分配特定cpu share。
· 記錄進程組使用的資源數量(Accounting )。比如:可以使用cpuacct子系統記錄某個進程組使用的cpu時間。
· 進程組隔離(Isolation)。比如:使用ns子系統可以使不同的進程組使用不同的namespace,以達到隔離的目的,不同的進程組有各自的進程、網絡、文件系統掛載空間。
Limits/request
Request: 容器使用的最小資源需求,作為容器調度時資源分配的判斷依賴。只有當節點上可分配資源量>=容器資源請求數時才允許將容器調度到該節點。但Request參數不限制容器的最大可使用資源。
Limit: 容器能使用資源的資源的最大值,設置為0表示使用資源無上限。
當前可以設置的有memory/cpu, kubernetes版本增加了localStorage在1.8版本中的策略.
Pod Qos
Qos服務質量分成三個級別
BestEffort:POD中的所有容器都沒有指定CPU和內存的requests和limits,默認為0,不限制資源的使用量,那么這個POD的QoS就是BestEffort級別
Burstable:POD中只要有一個容器,這個容器requests和limits的設置同其他容器設置的不一致,requests < limits, 那么這個POD的QoS就是Burstable級別
Guaranteed:POD中所有容器都必須統一設置了limits,並且設置參數都一致,如果有一個容器要設置requests,那么所有容器都要設置,並設置參數同limits一致,requests = limits. 那么這個POD的QoS就是Guaranteed級別.
Kuberntes管理的node資源中cpu為可壓縮資源,當node上運行的pod cpu負載較高的時候,出現資源搶占,會根據設置的limit值來分配時間片.
Kubernetes管理的node資源memory/disk是不可壓縮資源,當出現資源搶占的時候,會killer方式來釋放pod所占用的內存以及disk資源.
當非可壓縮資源出現不足的時候,kill掉pods的Qos優先級比較低的pods.通過OOM score來實現,Guaranteed pod,OOM_ADJ設置為-998, BestEffort 設置的OOM_ADJ為1000, Burstable級別的POD設置為2-999.
Evict策略
當系統資源不足的時候,kubelet會通過evict策略來驅逐本節點的pod來使得node節點的負載不是很高, 保證系統組件的穩定性.
當前支持的驅逐信號Eviction Signal為
memory.available
nodefs.available
nodefs.inodesFree
imagefs.available
imagefs.inodesFree
kubelet將支持軟硬驅逐門檻, 操作管理員通過設置Kubelet的啟動參數–eviction-soft –eviction-hard 來指定, 硬驅逐閾值沒有寬限期,如果觀察到,kubelet將立即采取行動來回收相關的飢餓資源。 如果滿足硬驅逐閾值,那么kubelet會立即殺死pods,沒有優雅的終止。軟驅逐閾值將驅逐閾值與所需的管理員指定的寬限期配對。kubelet不采取任何措施來回收與驅逐信號相關的資源,直到超過寬限期。
以內存導致驅逐的場景來詳細說明
讓我們假設操作員使用以下命令啟動kubelet:
–eviction-hard=”memory.available<100Mi”
–eviction-soft=”memory.available<300Mi”
–eviction-soft-grace-period=”memory.available=30s”
kubelet將運行一個同步循環,通過計算(capacity-workingSet)從cAdvisor報告,查看節點上的可用內存。 如果觀察到可用內存降至100Mi以下,那么kubelet將立即啟動驅逐。 如果觀察到可用內存低於300Mi,則會在高速緩存中內部觀察到該信號時記錄。 如果在下一次同步時,該條件不再滿足,則該信號將清除緩存。 如果該信號被視為滿足長於指定時間段,則kubelet將啟動驅逐以嘗試回收已滿足其逐出閾值的資源。
Pods的驅逐策略
如果已經達到逐出閾值,那么kubelet將啟動逐出pods的過程,直到觀察到信號已經低於其定義的閾值。
驅逐順序如下:
1. 對於每個監測間隔,如果已經達到逐出閾值
2. 找候選pod
3. 失敗pod
4. 阻止直到pod在節點上終止
kubelet將實施圍繞pod質量服務類定義的默認驅逐策略。
它將針對相對於其調度請求的飢餓計算資源的最大消費者的pod。它按照以下順序對服務質量等級進行排序。
消費最多的飢餓資源的BestEffort pods首先失敗。
消耗最大數量的飢餓資源相對於他們對該資源的請求的Burstable pod首先被殺死。如果沒有pod超出其要求,該策略將針對飢餓資源的最大消費者。
相對於他們的請求消費最多的飢餓資源的Guaranteed pod首先被殺死。如果沒有pod超出其要求,該策略將針對飢餓資源的最大消費者。
關於imagefs/nodefs導致的資源的驅逐詳細參考:
https://github.com/kubernetes/community/blob/master/contributors/design-proposals/node/kubelet-eviction.md#enforce-node-allocatable
實踐經驗
將系統資源進行划分, 預留資源
Node Capacity - 已經作為NodeStatus.Capacity提供,這是從節點實例讀取的總容量,並假定為常量。
System-Reserved(提議) - 為未由Kubernetes管理的流程預留計算資源。目前,這涵蓋了/系統原始容器中集中的所有進程。
Kubelet Allocatable - 計算可用於調度的資源(包括計划和非計划資源)。這個價值是這個提案的重點。請參閱下面的更多細節。
Kube-Reserved(提出) - 為諸如docker守護進程,kubelet,kube代理等的Kubernetes組件預留的計算資源。
可分配的資源數據將由Kubelet計算並報告給API服務器。它被定義為:
[Allocatable] = [Node Capacity] - [Kube-Reserved] – [System-Reserved] – [Hard-Eviction-Threshold]
調度pod時,調度程序將使用Allocatable代替Capacity,Kubelet將在執行准入檢查Admission checks時使用它。
一個kubelet從來不希望驅逐從DaemonSet導出的pod,因為pod將立即重新創建並重新安排回到同一個節點。此時,kubelet無法區分從DaemonSet創建的pod與任何其他對象。 如果/當該信息可用時,kubelet可以主動地從提供給驅逐策略的候選pod集合中過濾這些pod。一般來說,強烈建議DaemonSet不要創建BestEffort pod,以避免被識別為候選pods被驅逐。 相反,DaemonSet應該理想地包括僅Guaranteed pod。
靜態static pod
靜態POD直接由某個節點上的kubelet程序進行管理,不需要api server介入,靜態POD也不需要關聯任何RC,完全是由kubelet程序來監控,當kubelet發現靜態POD停止掉的時候,重新啟動靜態POD。
EvictManager模塊在系統資源緊張的時候, 根據pod的Qos以及pod使用的資源會選擇本節點的pod killer,來釋放資源, 但是靜態pod被killer之后,並不會發生重啟的現象, 設置pod的yaml中定義加入如下內容,並且kubelet的啟動參數打開feature gateway(–feature-gates=ExperimentalCriticalPodAnnotation=true).
annotations:
scheduler.alpha.kubernetes.io/critical-pod: ''
參考
https://speakerdeck.com/luxas/kubernetes-architecture-fundamentals
https://kubernetes.io/docs