1. Visão Geral:
1.1 Ambiente de código-fonte
As informações da versão são as seguintes:
a. Cluster kubernetes: v1.15.0
1.2 Duas linhas de defesa para manter a estabilidade do nó
Para o sistema operacional Linux, fatia de tempo da CPU, memória, capacidade do disco, inode, PID, etc. são todos recursos do sistema.
Uma classificação dos recursos do sistema:
a, recursos compressíveis (CPU)
b, recursos incompressíveis (memória, capacidade do disco e inode, PID)
Como um agente de nó, o kubelet naturalmente precisa de um certo mecanismo para garantir que os recursos do servidor não se esgotem. Quando os recursos compactáveis não são suficientes, o pod (contêiner ou processo) ficará sem energia, mas não será forçado a sair pelo sistema operacional e pelo kubelet. Quando recursos incompressíveis (como memória) não são suficientes, a camada kubelet pode recuperar alguns recursos do servidor com antecedência (excluir imagens não utilizadas, limpar contêineres saídos e pods com falha, eliminar pods em execução) e o OOM do sistema operacional Linux como o última linha de defesa, KILLER também tem a oportunidade de matar diretamente o processo do modo de usuário.
O kubelet mata o Pod em execução. Esse processo é chamado de despejo. O Kubelet exclui imagens não utilizadas, limpa contêineres desativados e pods com falha. Esse processo é chamado de reciclagem de recursos no nível do nó.
Portanto, existem duas linhas de defesa para manter a estabilidade do nó: a primeira linha de defesa é o kubelet de processo do usuário e a segunda linha de defesa é o Linux OOM KILLER.
1.3 mecanismo do kubelet para manter a estabilidade do nó
Quando recursos incompressíveis (memória, capacidade de disco e inode, PID) não são usados o suficiente, isso afetará seriamente a estabilidade do nó. Por esse motivo, o kubelet, como o agente do nó do processo do modo de usuário do Linux e do cluster k8s, deve ter um certo mecanismo para manter a estabilidade do nó. A essência é atingir o objetivo excluindo a imagem e interrompendo o processo do contêiner.
1.3.1 Limite de recursos, os usuários têm a palavra final
Esse kubelet precisa obter as informações gerais dos recursos do nó regularmente, o que é inevitável.
Quantos recursos restam no servidor para serem considerados recursos incompressíveis "não é suficiente"? Essa base "insuficiente" é especificada pelo usuário no arquivo de configuração kubelet.
Contanto que o kubelet obtenha a capacidade dos recursos do nó, o uso real (taxa) e o limite configurado pelo usuário, ele sente que as "condições foram atendidas" podem começar a excluir a imagem e interromper o processo do contêiner.
1.3.2 Remoção leve e pesada
"As condições foram atendidas", aguarde zero segundos para iniciar a exclusão da imagem e interromper o processo do contêiner, este é um despejo forçado.
"As condições são atendidas", aguarde N (N> 0) segundos para iniciar a operação de exclusão da imagem e interromper o processo do contêiner, que é o despejo suave. O número N pode ser definido pelo parâmetro -eviction-soft-grace-period do kubelet. Além disso, para o despejo suave, o parâmetro --eviction-max-pod-grace-period afeta o tempo que o gerente de despejo espera que o kubelet limpe o Pod (o tempo real de espera é eviction-max-pod-grace-period + ( eviction-max-pod- grace-period / 2)).
1.3.3 Criação de pod de bloco
Quando os recursos incompressíveis do nó "não bastam", o kubelet também deve ter um mecanismo para bloquear a criação do Pod, que está relacionado ao método Admitir (...) do gerenciador de expulsões.
1.3.4 Afeta o Linux OOM KILLER
Kubelet é a primeira linha de defesa para manter a estabilidade do servidor. Se você não conseguir descobrir, a segunda linha de defesa, Linux OOM KILLER, inevitavelmente chegará ao fim. A correlação entre a primeira linha de defesa e a segunda linha de defesa é oom_score_adj.
O Qos do pod é diferente, o kubelet configura pontuações diferentes para oom_score_adj e o oom_score_adj do processo é um fator que afeta a eliminação dos processos de modo de usuário do Linux OOM KILLER.
Outras instruções:
1) Modelo de comando para visualizar a pontuação oom_score_adj do processo do contêiner no Pod:
kubectl exec demo-qos-pod cat / proc / 1 / oom_score_adj
1.4 Visão geral do Linux OOM KILLER
1) O Linux responde "sim" à maioria dos pedidos de aplicação de memória, de forma que mais e maiores programas possam ser executados. Porque depois de solicitar a memória, a memória não será usada imediatamente. Essa técnica é chamada de Overcommit.
2) Quando o Linux descobrir que a memória é insuficiente, o OOM killer (OOM = out-of-memory) ocorrerá. Neste momento, ele escolherá matar alguns processos (processos de modo de usuário, não threads de kernel) para liberar memória .
3) Quando ocorre o oom-killer, quais processos o Linux escolherá para matar? O Linux calculará o número de pontos (0 ~ 1000) para cada processo. Quanto maior o número de pontos, maior a probabilidade de esse processo ser eliminado.
A fórmula dos pontos oom:
进程OOM点数 = oom_score + oom_score_adj
#oom_score está relacionado à memória consumida pelo processo.
#oom_score_adj é configurável e o intervalo de valores é -1000 é o mais baixo e 1000 é o mais alto.
4) O log oom está no arquivo / var / log / messages, que pertence ao log do kernel.Você pode ver o log do kernel através do comando grep kernel / var / log / messages.
Análise de código-fonte de expulsão de 2 kubelet:
2.1 Principais métodos
|-> 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 Sinais suportados que podem desencadear o despejo do Pod
Quando a memória, a capacidade do disco, o número do inode do disco e o número do pid são insuficientes, o kubelet pode ser acionado para limpar imagens e contêineres para atingir o objetivo de recuperar os recursos do nó.
// 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 Código geral
//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)
/*
其他代码
*/
}
Os principais atributos da estrutura do administrador de despejo
// 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 Como iniciar o gerenciador de despejo
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 A lógica principal do gerente de expulsão para recuperar recursos
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 A propriedade signalToRankFunc do gerenciador de despejo
Ele contém um conjunto de métodos para classificar pods.
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 Método de classificação de pod
Quando o gerenciador de expulsão expulsa o pod, ele classifica os pods em execução, e o pod no topo da fila é limpo primeiro.
O pod cujo Qos é garantido é o último da fila, porque durante a operação normal, a diferença entre o uso e a solicitação deve ser um número negativo e, claro, está no final da fila. Quando o uso de recursos deste tipo de Pod atinge a solicitação, ou seja, quando o limite é atingido, o linux detecta e compara o valor definido pelo limite (no arquivo cgroup), e envia um sinal kill para o processo no Pod . Não é um pod em execução no momento.
E o pod com Qos como Best-Effort é o topo, porque a solicitação não está definida, então a diferença entre o uso e a solicitação deve ser um número positivo, portanto, deve ser a parte superior da fila.
2.4.1 Como classificar pods quando a memória está apertada
/*
当内存使用量达到阈值时,对入参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.2 Como classificar pods quando Pid está nervoso
/*
当pid用量达到阈值时,根据pod的QOS类型,对入参pods进行排序。
*/
func rankPIDPressure(pods []*v1.Pod, stats statsFunc) {
orderedBy(priority).Sort(pods)
}
2.4.3 Como classificar pods quando o disco está apertado
/*
当磁盘使用量达到阈值时,对入参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 Outros métodos
2.4.1 func (m * managerImpl) Admit (...)
O método Admit (...) do gerenciador de despejo afetará se o Pod pode ser criado.
/*
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 A experiência do Pod sendo expulso
3.1 Depois que os pods dos controladores ds e sts forem expulsos, o controlador não criará novos pods.
O pod expulso ainda ocupa um registro, e a lógica do loop principal do controlador sts e controlador ds não criará um novo pod.
Portanto, é melhor executar uma tarefa crontab (kubectl delete pod --all-namespaces --field-selector = 'status.phase == Failed') no nó mestre e excluir periodicamente os pods com falha (incluindo os pods expulsos )
Depois que os dois pods ds na captura de tela foram expulsos, porque o controlador não criou um novo pod, o problema de comunicação do contêiner do nó foi causado, então os serviços ds e sts expulsos não serão reparados.
O pod coredns na captura de tela é um pod gerenciado pelo controlador de implantação. Depois que o pod antigo for removido, o controlador criará um novo pod, para que o serviço dns dentro do cluster seja autocorrigido.
4 resumo
É reconhecido que os recursos do nó podem ser divididos em recursos compressíveis e recursos incompressíveis.Quando os recursos incompressíveis estão em estado de escassez, este tipo de escassez levará à instabilidade do nó.
Como um agente de nó, o processo do usuário kubelet é a primeira linha de defesa para manter a estabilidade do nó. Ele verifica periodicamente o status do recurso do nó, o status do recurso real usado pelo pod e o limite de recurso configurado pelo usuário, e por fim realizar a operação de limpeza da imagem e do contêiner para manter a estabilidade do nó, afinal, a estabilidade do nó é maior que a estabilidade do contêiner.
A segunda linha de defesa é o Linux OOM KILLER, que também é a linha de fundo da defesa.
Haverá uma espécie de colaboração entre a primeira linha de defesa e a segunda linha de defesa, que é oom_score_adj. Quando o kubelet cria um contêiner, oom_score_adj pode ser definido para afetar o processo de eliminação do Linux OOM KILLER.