1。概要:
1.1ソースコード環境
バージョン情報は次のとおり
です。a。kubernetesクラスター:v1.15.0
1.2ノードの安定性を維持するための2つの防御線
Linuxオペレーティングシステムの場合、CPUタイムスライス、メモリ、ディスク容量、iノード、PIDなどはすべてシステムリソースです。
システムリソースの分類:
a、圧縮性リソース(CPU)
b、非圧縮性リソース(メモリ、ディスク容量、iノード、PID)
ノードエージェントとして、kubeletは当然、サーバーリソースが使い果たされないようにするための特定のメカニズムを必要とします。圧縮可能なリソースが十分でない場合、ポッド(コンテナーまたはプロセス)は不足しますが、オペレーティングシステムとkubeletによって強制的に終了されることはありません。非圧縮性リソース(メモリなど)が十分でない場合、kubeletレイヤーは事前に一部のサーバーリソースを再利用できます(未使用のイメージの削除、既存のコンテナーと障害のあるポッドのクリーンアップ、実行中のポッドの強制終了)。最後の防衛線であるKILLERには、ユーザーモードプロセスを直接強制終了する機会もあります。
kubeletは実行中のポッドを強制終了します。このプロセスはエビクションと呼ばれます。Kubeletは、未使用のイメージを削除し、廃止されたコンテナーと障害が発生したポッドをクリーンアップします。このプロセスは、ノードレベルのリソースのリサイクルと呼ばれます。
したがって、ノードの安定性を維持するために2つの防御線があります。最初の防御線はユーザープロセスkubeletであり、2番目の防御線はLinux OOMKILLERです。
1.3ノードの安定性を維持するためのkubeletのメカニズム
非圧縮性リソース(メモリ、ディスク容量、iノード、PID)が十分に使用されていない場合、ノードの安定性に深刻な影響を及ぼします。このため、Linuxユーザーモードプロセスおよびk8sクラスターのノードエージェントとしてのkubeletは、ノードの安定性を維持するための特定のメカニズムを備えている必要があります。本質は、イメージを削除してコンテナープロセスを停止することで目標を達成することです。
1.3.1リソースのしきい値、ユーザーが最終決定権を持つ
そのkubeletは、ノードリソースの全体的な情報を定期的に取得する必要がありますが、これは避けられません。
非圧縮性リソースが「十分ではない」と見なされるためにサーバーに残っているリソースの数はいくつですか?この「不十分な」基準は、ユーザーがkubelet構成ファイルで指定します。
kubeletがノードリソースの容量、実際の使用量(レート)、およびユーザーが設定したしきい値を取得している限り、「条件が満たされている」と感じて、イメージの削除を開始し、コンテナープロセスを停止できます。
1.3.2ソフトおよびハードの立ち退き
「条件が満たされました」、ゼロ秒待ってイメージの削除を開始し、コンテナプロセスを停止します。これは、ハードエビクションです。
「条件が満たされました」、N(N> 0)秒待って、イメージを削除し、コンテナプロセスを停止する操作を開始します。これはソフトエビクションです。数値Nは、kubeletのパラメーター-eviction-soft-grace-periodで設定できます。さらに、ソフトエビクションの場合、-eviction-max-pod-grace-periodパラメーターは、エビクションマネージャーがkubeletがポッドをクリーンアップするのを待機する時間に影響します(実際の待機時間はeviction-max-pod-grace-period +( eviction-max-pod- grace-period / 2))。
1.3.3ポッドの作成をブロックする
ノードの非圧縮性リソースが「十分でない」場合、kubeletには、追放マネージャーのAdmit(...)メソッドに関連するポッドの作成をブロックするメカニズムも必要です。
1.3.4LinuxのOOMKILLERに影響を与える
Kubeletは、サーバーの安定性を維持するための最初の防衛線です。それがわからない場合は、2番目の防衛線であるLinux OOMKILLERが必然的に最下位になります。最初の防衛線と2番目の防衛線の相関関係はoom_score_adjです。
ポッドのQosは異なり、kubeletはoom_score_adjに異なるスコアを構成し、プロセスのoom_score_adjは、Linux OOMKILLERによるユーザーモードプロセスの強制終了に影響を与える要因です。
その他の手順:
1)ポッド内のコンテナーのプロセスのoom_score_adjスコアを表示するコマンドテンプレート:
kubectl exec demo-qos-pod cat / proc / 1 / oom_score_adj
1.4 Linux OOMKILLERの概要
1)Linuxは、メモリアプリケーションに対するほとんどの要求に「はい」と応答するため、より多くのより大きなプログラムを実行できます。メモリを申請した後、メモリはすぐには使用されないためです。この手法はオーバーコミットと呼ばれます。
2)Linuxがメモリ不足を検出すると、OOMキラー(OOM =メモリ不足)が発生します。このとき、メモリを解放するために一部のプロセス(カーネルスレッドではなくユーザーモードプロセス)を強制終了することを選択します。 。
3)oom-killerが発生した場合、Linuxはどのプロセスを強制終了することを選択しますか?Linuxは、各プロセスのポイント数(0〜1000)を計算します。ポイントの数が多いほど、このプロセスが強制終了される可能性が高くなります。
oomポイントの式:
进程OOM点数 = oom_score + oom_score_adj
#oom_scoreは、プロセスによって消費されるメモリに関連しています。
#oom_score_adjは構成可能であり、値の範囲は-1000が最小で、1000が最大です。
4)oomログは、カーネルログに属する/ var / log / messagesファイルにあります。カーネルログは、grep kernel / var / log / messagesコマンドで表示できます。
2 kubelet追放ソースコード分析:
2.1主な方法
|-> kl.cadvisor.Start() // 启动cadvisor,cadvisor能获取节点信息和容器信息
|
//启动 //初始化额外模块,只会运行一次 |
Kubelet -> Run() -> updateRuntimeUp() -> initializeRuntimeDependentModules -->|-> kl.containerManager.Start() //containerManager需要cAdvisor提供的文件系统信息
|
|
| //evictionManager需要通过cadvisor得知容器运行时是否具备一个独立专用的imagefs
|
|-> kl.evictionManager.Start() -> 定时任务 ->| -> m.synchronize() -> | -> m.reclaimNodeLevelResources(...) //清理节点级别的资源(镜像、已退出的容器)
| |
| | -> m.evictPod(...) //清理pod对象
|
| -> m.waitForPodsCleanup(...) //如果m.synchronize()驱逐了pod,执行本方法进行等待pod被清理.
2.2ポッドの削除をトリガーできるサポートされている信号
メモリ、ディスク容量、ディスクiノード番号、およびpid番号が不十分な場合、kubeletをトリガーしてイメージとコンテナーをクリーンアップし、ノードリソースを再利用する目的を達成できます。
// Signal defines a signal that can trigger eviction of pods on a node.
type Signal string
/*
分类:
1)内存
2)磁盘容量
3)磁盘inode数量
4)pid数量
*/
const (
// SignalMemoryAvailable is memory available (i.e. capacity - workingSet), in bytes.
SignalMemoryAvailable Signal = "memory.available"
// SignalNodeFsAvailable is amount of storage available on filesystem that kubelet uses for volumes, daemon logs, etc.
// nodefs是kubelet使用的卷、进程日志所在的文件系统
SignalNodeFsAvailable Signal = "nodefs.available"
// SignalNodeFsInodesFree is amount of inodes available on filesystem that kubelet uses for volumes, daemon logs, etc.
SignalNodeFsInodesFree Signal = "nodefs.inodesFree"
// SignalImageFsAvailable is amount of storage available on filesystem that container runtime uses for storing images and container writable layers.
// imagefs是容器运行时(例如docker)的镜像层、读写层所在的文件系统
SignalImageFsAvailable Signal = "imagefs.available"
// SignalImageFsInodesFree is amount of inodes available on filesystem that container runtime uses for storing images and container writable layers.
SignalImageFsInodesFree Signal = "imagefs.inodesFree"
// SignalAllocatableMemoryAvailable is amount of memory available for pod allocation (i.e. allocatable - workingSet (of pods), in bytes.
SignalAllocatableMemoryAvailable Signal = "allocatableMemory.available"
// SignalPIDAvailable is amount of PID available for pod allocation
SignalPIDAvailable Signal = "pid.available"
)
2.3全体的なコード
//kubelet的启动入口
func (kl *Kubelet) Run(updates <-chan kubetypes.PodUpdate) {
/*
其他代码
*/
go wait.Until(kl.updateRuntimeUp, 5*time.Second, wait.NeverStop)
}
// 当容器运行时第一次运行,初始化一些它依赖的模块
func (kl *Kubelet) updateRuntimeUp() {
/*
其他代码
*/
kl.oneTimeInitializer.Do(kl.initializeRuntimeDependentModules)
}
//启动cadvisor、containerManager和本文的核心研究对象evictionManager
func (kl *Kubelet) initializeRuntimeDependentModules() {
// 启动cadvisor
if err := kl.cadvisor.Start(); err != nil {
}
/*
其他代码
*/
// containerManager必须在cAdvisor之后启动,因为它需要cAdvisor提供的文件系统信息
if err := kl.containerManager.Start(node, kl.GetActivePods, kl.sourcesReady, kl.statusManager, kl.runtimeService); err != nil {
}
//evictionManager是本文的核心
// evictionManager必须在cAdvisor之后启动,因为它需要知道容器运行时是否具备一个独立专用的imagefs
kl.evictionManager.Start(kl.StatsProvider, kl.GetActivePods, kl.podResourcesAreReclaimed, evictionMonitoringPeriod)
/*
其他代码
*/
}
エビクションマネージャーの構造の主な属性
// managerImpl implements Manager
type managerImpl struct {
//kubelet的配置文件中和驱逐相关的字段会赋值到本属性
config Config
//杀死Pod的方法
//--eviction-max-pod-grace-period参数和此方法相关
killPodFunc KillPodFunc
// the interface that knows how to do image gc
imageGC ImageGC
// the interface that knows how to do container gc
containerGC ContainerGC
// protects access to internal state
// 达到阈值的不可压缩资源,就会出现在本属性
thresholdsMet []evictionapi.Threshold
// 对Pod进行排序的方法保存在此属性
signalToRankFunc map[evictionapi.Signal]rankFunc
// signalToNodeReclaimFuncs maps a resource to an ordered list of functions that know how to reclaim that resource.
thresholdNotifiers []ThresholdNotifier
}
2.4エビクションマネージャーの起動方法
func (m *managerImpl) Start(diskInfoProvider DiskInfoProvider, podFunc ActivePodsFunc, podCleanedUpFunc PodCleanedUpFunc, monitoringInterval time.Duration) {
/*
其他代码
*/
//通过for循环和睡眠来定时执行m.synchronize()方法
go func() {
for {
//m.synchronize()方法内部有清理 节点资源 和 Pod 的逻辑
if evictedPods := m.synchronize(diskInfoProvider, podFunc); evictedPods != nil {
m.waitForPodsCleanup(podCleanedUpFunc, evictedPods)
} else {
//睡眠
time.Sleep(monitoringInterval)
}
}
}()
}
2.4リソースを再利用するための追放マネージャーの主なロジック
func (m *managerImpl) synchronize(diskInfoProvider DiskInfoProvider, podFunc ActivePodsFunc) []*v1.Pod {
/*
填充m.signalToRankFunc的值,也就是pod排序的排序函数,整个过程只执行一次;
*/
// 获取节点上运行中的pod
activePods := podFunc()
// 从cadvisor获取节点的详细信息,并进行统计
summary, err := m.summaryProvider.Get(true)
//再从统计数据中获得节点资源的使用情况observations
observations, statsFunc := makeSignalObservations(summary)
// 将资源实际使用量和资源容量进行比较,最终得到阈值结构体对象的列表
thresholds = thresholdsMet(thresholds, observations, false)
thresholdsFirstObservedAt := thresholdsFirstObservedAt(thresholds, m.thresholdsFirstObservedAt, now)
//此时的thresholds是即将触发回收资源的thresholds,因为这些thresholds已经过了平滑时间
thresholds = thresholdsMetGracePeriod(thresholdsFirstObservedAt, now)
/*
获取nodeConditions;
nodeConditions是一个字符串切片;
打印一些和nodeConditions相关的日志信息;
*/
/*
更新驱逐管理器(即本方法的接收者)的一些成员变量的值
m.thresholdsMet = thresholds
//m.nodeConditions过了--eviction-pressure-transition-period指定的时间,也会更新到kubelet本地的node对象中,而node对象最终也会被kubelet同步到kube-apiserver中
//m.nodeConditions也和m.Admit(...)方法相关,m.Admit(...)方法决定是否允许在当前节点上创建或运行目标pod
m.nodeConditions = nodeConditions
*/
// 本地临时存储导致的驱逐(一定是驱逐pod),如果发生,后续的回收资源(清理节点资源和运行中的pod)的操作不会执行,直接返回
if utilfeature.DefaultFeatureGate.Enabled(features.LocalStorageCapacityIsolation) {
if evictedPods := m.localStorageEviction(summary, activePods); len(evictedPods) > 0 {
return evictedPods
}
}
// 节点资源使用量没有到达用户配置的阈值,不需要回收节点资源,因此直接返回
if len(thresholds) == 0 {
klog.V(3).Infof("eviction manager: no resources are starved")
return nil
}
// 回收节点级的资源,如果回收的资源足够的话,直接返回,不需要驱逐正在运行中的pod
if m.reclaimNodeLevelResources(thresholdToReclaim.Signal, resourceToReclaim) {
klog.Infof("eviction manager: able to reduce %v pressure without evicting pods.", resourceToReclaim)
return nil
}
// 根据资源类型,从一个map中拿到一个排序方法,使用该排序方法对运行中的pod进行排序
rank, ok := m.signalToRankFunc[thresholdToReclaim.Signal]
rank(activePods, statsFunc)
// 此时activePods已经按照一定规则进行排序
klog.Infof("eviction manager: pods ranked for eviction: %s", format.Pods(activePods))
// 开始对运行中的pod实施驱逐操作
for i := range activePods {
//如果驱逐了一个pod,则立马返回,因为一个周期最多驱逐一个pod
if m.evictPod(pod, gracePeriodOverride, message, annotations) {
return []*v1.Pod{pod}
}
}
// 到达此处,说明本周期试图驱逐pod,但连一个pod都没驱逐成功
klog.Infof("eviction manager: unable to evict any pods from the node")
return nil
}
2.4エビクションマネージャーのsignalToRankFuncプロパティ
ポッドをソートするための一連のメソッドを保持します。
func (m *managerImpl) synchronize(diskInfoProvider DiskInfoProvider, podFunc ActivePodsFunc) []*v1.Pod {
/*
其他代码
*/
if m.dedicatedImageFs == nil {
hasImageFs, ok := diskInfoProvider.HasDedicatedImageFs()
if ok != nil {
return nil
}
//dedicatedImageFs被赋值后,不会再进入当前整个if语句
m.dedicatedImageFs = &hasImageFs
//把pod的排序方法作为map传给驱逐管理器的signalToRankFunc属性
m.signalToRankFunc = buildSignalToRankFunc(hasImageFs)
}
/*
其他代码
*/
}
func buildSignalToRankFunc(withImageFs bool) map[evictionapi.Signal]rankFunc {
//不管入参是true还是false,内存、pid这两种资源的排序方法是不变的
signalToRankFunc := map[evictionapi.Signal]rankFunc{
evictionapi.SignalMemoryAvailable: rankMemoryPressure,
evictionapi.SignalAllocatableMemoryAvailable: rankMemoryPressure,
evictionapi.SignalPIDAvailable: rankPIDPressure,
}
//虽然都是rankDiskPressureFunc,但方法的入参不同。
if withImageFs {
// 使用独立的imagefs,那么nodefs的排序方法只包含日志和本地卷
signalToRankFunc[evictionapi.SignalNodeFsAvailable] = rankDiskPressureFunc([]fsStatsType{fsStatsLogs, fsStatsLocalVolumeSource}, v1.ResourceEphemeralStorage)
signalToRankFunc[evictionapi.SignalNodeFsInodesFree] = rankDiskPressureFunc([]fsStatsType{fsStatsLogs, fsStatsLocalVolumeSource}, resourceInodes)
// 使用独立的imagefs,那么imagefs的排序方法只包含rootfs
signalToRankFunc[evictionapi.SignalImageFsAvailable] = rankDiskPressureFunc([]fsStatsType{fsStatsRoot}, v1.ResourceEphemeralStorage)
signalToRankFunc[evictionapi.SignalImageFsInodesFree] = rankDiskPressureFunc([]fsStatsType{fsStatsRoot}, resourceInodes)
} else {
// 不使用独立的imagefs,nodefs的排序方法是包含所有文件系统,即fsStatsRoot, fsStatsLogs, fsStatsLocalVolumeSource
// 此时imagefs和nodefs在使用同一个块设备,因此它们的排序方法都是一样的。
signalToRankFunc[evictionapi.SignalNodeFsAvailable] = rankDiskPressureFunc([]fsStatsType{fsStatsRoot, fsStatsLogs, fsStatsLocalVolumeSource}, v1.ResourceEphemeralStorage)
signalToRankFunc[evictionapi.SignalNodeFsInodesFree] = rankDiskPressureFunc([]fsStatsType{fsStatsRoot, fsStatsLogs, fsStatsLocalVolumeSource}, resourceInodes)
signalToRankFunc[evictionapi.SignalImageFsAvailable] = rankDiskPressureFunc([]fsStatsType{fsStatsRoot, fsStatsLogs, fsStatsLocalVolumeSource}, v1.ResourceEphemeralStorage)
signalToRankFunc[evictionapi.SignalImageFsInodesFree] = rankDiskPressureFunc([]fsStatsType{fsStatsRoot, fsStatsLogs, fsStatsLocalVolumeSource}, resourceInodes)
}
return signalToRankFunc
}
2.4ポッドの並べ替え方法
追放マネージャーがポッドを追放すると、実行中のポッドが並べ替えられ、キューの一番上にあるポッドが最初にクリーンアップされます。
Qosが保証されているポッドはキューの最後です。これは、通常の操作では、使用量とリクエストの差が負の数である必要があり、もちろんキューの最後にあるためです。このタイプのポッドのリソース使用量がリクエストに達すると、つまり制限に達すると、Linuxは(cgroupファイル内の)制限によって設定された値を検出して比較し、ポッド内のプロセスに強制終了信号を送信します現時点では、実行中のポッドではありません。
また、Qosをベストエフォートとして使用するポッドが最上位です。リクエストが設定されていないため、使用量とリクエストの差は正の数である必要があり、キューの最上位である必要があります。
2.4.1メモリが不足しているときにポッドを並べ替える方法
/*
当内存使用量达到阈值时,对入参pods进行排序。
首先看使用量是否超过pod的request字段
接着看pod的QOS类型
最后看使用量和request之间的差值
*/
func rankMemoryPressure(pods []*v1.Pod, stats statsFunc) {
orderedBy(
exceedMemoryRequests(stats), //使用量是否超过pod的request字段
priority, //pod的QOS类型
memory(stats) //使用量和request之间的差值
).Sort(pods)
}
2.4.2Pidが神経質なときにポッドを並べ替える方法
/*
当pid用量达到阈值时,根据pod的QOS类型,对入参pods进行排序。
*/
func rankPIDPressure(pods []*v1.Pod, stats statsFunc) {
orderedBy(priority).Sort(pods)
}
2.4.3ディスクがタイトなときにポッドを並べ替える方法
/*
当磁盘使用量达到阈值时,对入参pods进行排序。
首先看使用量是否超过pod的request字段
接着看pod的QOS类型
最后看使用量和request之间的差值
*/
func rankDiskPressureFunc(fsStatsToMeasure []fsStatsType, diskResource v1.ResourceName) rankFunc {
return func(pods []*v1.Pod, stats statsFunc) {
orderedBy(
exceedDiskRequests(stats, fsStatsToMeasure, diskResource), //使用量是否超过pod的request字段
priority, //pod的QOS类型
disk(stats, fsStatsToMeasure, diskResource) //使用量和request之间的差值
).Sort(pods)
}
}
2.4その他の方法
2.4.1 func(m * managerImpl)Admit(…)
エビクションマネージャーのAdmit(...)メソッドは、ポッドの作成を許可するかどうかに影響します。
/*
1)入参attrs对象中包含一个pod对象
2)m.nodeConditions字段通过本方法和kubelet的其他流程有关联:
2.1)HandlePodAdditions(...)方法中会间接调用本方法,从而达到不创建目标pod的效果。
2.2)syncPod(...)方法中会间接调用本方法,从而到达目标pod(如果存在的话)会被删除的效果。
*/
func (m *managerImpl) Admit(attrs *lifecycle.PodAdmitAttributes) lifecycle.PodAdmitResult {
m.RLock()
defer m.RUnlock()
//节点无资源压力,则允许创建任意pod
if len(m.nodeConditions) == 0 {
return lifecycle.PodAdmitResult{Admit: true}
}
//Critical pods也能被允许创建
if kubelettypes.IsCriticalPod(attrs.Pod) {
return lifecycle.PodAdmitResult{Admit: true}
}
if hasNodeCondition(m.nodeConditions, v1.NodeMemoryPressure) {
notBestEffort := v1.PodQOSBestEffort != v1qos.GetPodQOS(attrs.Pod)
// 节点处于内存压力状态,best-effort类型的pod不能被允许创建,其余类型的pod可以
if notBestEffort {
return lifecycle.PodAdmitResult{Admit: true}
}
// 节点处于内存压力状态并且开启TaintNodesByCondition特性,pod能容忍内存压力污点的话,它也能被允许创建
if utilfeature.DefaultFeatureGate.Enabled(features.TaintNodesByCondition) &&
v1helper.TolerationsTolerateTaint(attrs.Pod.Spec.Tolerations, &v1.Taint{
Key: schedulerapi.TaintNodeMemoryPressure,
Effect: v1.TaintEffectNoSchedule,
}) {
return lifecycle.PodAdmitResult{Admit: true}
}
}
// 来到此处,说明节点是处于磁盘压力状态,或者处于内存压力状态但入参的pod是bestEffort类型,此时不允许创建pod
klog.Warningf("Failed to admit pod %s - node has conditions: %v", format.Pod(attrs.Pod), m.nodeConditions)
return lifecycle.PodAdmitResult{
Admit: false,
Reason: Reason,
Message: fmt.Sprintf(nodeConditionMessageFmt, m.nodeConditions),
}
}
3ポッドが追い出された経験
3.1 dsおよびstsコントローラーのポッドが削除された後、コントローラーは新しいポッドを作成しません。
排出されたポッドはまだレコードを占有しており、stsコントローラーとdsコントローラーのメインループロジックは新しいポッドを作成しません。
したがって、マスターノードでcrontabタスク(kubectl delete pod --all-namespaces --field-selector = 'status.phase == Failed')を実行し、失敗したポッド(排出されたポッドを含む)を定期的に削除することをお勧めします。 )。
スクリーンショットの2つのdsポッドが削除された後、コントローラーが新しいポッドを作成しなかったため、ノードのコンテナー通信の問題が発生し、削除されたdsおよびstsサービスは自動的に修復されません。
スクリーンショットのcorednsポッドは、デプロイメントコントローラーによって管理されるポッドです。古いポッドが削除された後、コントローラーは新しいポッドを作成するため、クラスター内のdnsサービスは自己修復します。
4まとめ
ノードリソースは、圧縮性リソースと非圧縮性リソースに分けることができると認識されています。非圧縮性リソースが不足している場合、このような不足はノードの不安定性につながります。
ノードエージェントとして、ユーザープロセスkubeletは、ノードの安定性を維持するための最初の防衛線であり、ノードリソースのステータス、ポッドによって使用される実際のリソースのステータス、およびによって構成されたリソースのしきい値を定期的にチェックします。最後に、イメージとコンテナをクリーニングして維持する操作を実行します。ノードの安定性は、結局のところ、ノードの安定性はコンテナの安定性よりも大きくなります。
2番目の防衛線はLinuxOOM KILLERであり、これは防衛の最下線でもあります。
最初の防衛線と2番目の防衛線であるoom_score_adjの間には一種のコラボレーションがあります。kubeletがコンテナーを作成するときに、oom_score_adjを設定して、Linux OOMKILLERの強制終了プロセスに影響を与えることができます。