Openshift Master资源不足引发的业务Pod漂移重启排查

背景

容器云出现大量业务接口访问失败告警,观察到批量业务Pod状态变成MatchNodeSelector状态,同时调度生成新的Pod,由于目前未完全推广使用Pod优雅退出方案,在旧pod中的容器被删除,新pod创建起来的过错中就必然会导致交易丢失了。这次事件中我们观察到的现象是:

0、监控发现三个Master节点cpu和内存高使用率告警

1、多个Master节点负载高,一段时间内apiserver出现无法处理请求的现象

2、监控发现大量Sync pod重启

3、大量计算节点Node(kubelet)服务重启

4、监控发现大量业务pod状态变为MatchNodeSelector

问题环境:Openshift 3.11.43(Kubernetes 1.11.0)

 

问题点

出现上述事件之后第一反应是apiserver接收到大量外部请求,因为确实有部分系统会通过sa token来调用我们容器云apiserver的接口(比如获取已部署的服务等),由于我们容器云的管理域名和应用域名都会经过一层haproxy进行转发(其中8443端口的管理流量转发到三个master,80/443端口的业务流量转发到openshift router,使用keepalived做高可用),在这之前已经在prometheus上配置部署好了haproxy的监控,查看grafana监控面板可以看到在故障时间段master具有明显的流量峰值,并且三个master出现了逐渐重启的现象,这里遗憾的是生产环境并没有部署apiServer监控(可以更加清楚的看到是哪些客户端请求apiServer)。

但是这里却带来了很多无法解释的问题:

1、从各种监控信息来看,只有master节点出现了资源紧张的问题,所有计算节点负载都是很正常的,那计算节点上面的业务pod为什么发生MatchNodeSelector现象。

2、为什么会有大量Sync Pod中的container发生restart,前期只知道Sync会同步openshift-node下面的configmap到节点上面,还有什么其他作用吗?

3、master节点在事件期间为什么可以看到apiserver有突发的流量?

4、Node服务在这里发生重启了,重启会影响节点上面的业务Pod吗?

 

排查

我们的排查的目标是把上述各个现象串起来形成一个完整的链路,这样给出对应的优化解决方案也就简单明了了

 

Openshift中openshift-node ns下sync pod的作用描述

该pod直接运行一段脚本,可直接在github openshift-tools sync 查看,改脚本逻辑比较简单,可以简单描述为如下过程:

1、检查/etc/origin/node/node-config.yam即node服务配置文件是否存在,是则计算md5值写到/tmp/.old
2、检查/etc/sysconfig/atomic-openshift-node文件中是否配置BOOTSTRAP_CONFIG_NAME参数,该参数表示当前节点在openshift-node ns下的configmap名称(ansible安装集群时指定)
3、执行一段后台脚本不断检测2中的参数是否发生变化,是则exit 0退出自己以让pod中container重启
4、访问apiserver取出2中指定的conigmap内容,写入/etc/origin/node/tmp/node-config.yaml,并计算md5值写入/tmp/.new
5、如果/tmp/.new中的内容和/tmp/.old中的内容不一致,则把/etc/origin/node/tmp/node-config.yaml文件覆盖到/etc/origin/node/node-config.yaml,并执行6
6、提取/etc/origin/node/node-config.yaml中的node-labels参数并其重新强制刷新到etcd(oc label),如果成功则kill kubelet进程以Node服务重启
7、使用oc annotate为node对象添加一个annotations为新配置文件的md5值
8、使用cp命令覆盖/tmp/.new到/tmp/.old
9、重复执行4、5、6、7、8

我们发现上面脚本在configmap中的内容与节点本地配置不一致时,确实是会重启kubelet进程,但是生产环境不应该存在configmap和节点本地配置被修改的可能。再次分析脚本,发现脚本中存在两个潜在的问题:

1、md5sum命令对文件求md5值结果是带文件名称的,根据上述过程,/tmp/.old和/tmp/.new在首次比较时,内容分别如下

[root@k8s-master ~]# md5sum /etc/origin/node/node-config.yaml 
6de9439834c9147569741d3c9c9fc010  /etc/origin/node/node-config.yaml
[root@k8s-master ~]# md5sum /etc/origin/node/tmp/node-config.yaml 
6de9439834c9147569741d3c9c9fc010  /etc/origin/node/tmp/node-config.yaml

if [[ "$( cat /tmp/.old )" != "$( cat /tmp/.new )" ]]; then
  mv /etc/origin/node/tmp/node-config.yaml /etc/origin/node/node-config.yaml
  echo "info: Configuration changed, restarting kubelet" 2>&1
  if ! pkill -U 0 -f '(^|/)hyperkube kubelet '; then
    echo "error: Unable to restart Kubelet" 2>&1
    sleep 10 &
    wait $!
    continue
  fi
fi

这样直接比较,即使配置内容是一致的,也会重启kubelet服务了,由于每次都会把/tmp/.new覆盖到/tmp/.old,之后的比较则不会判断为配置不同,但这里带来的问题就是sync pod在启动之后,必然会重启Node服务即kubelet服务一次,这样就能把我们的现象2和现象3联系起来了。

2、脚本在开头位置设置了set -euo pipefail属性,其中

-e表示在脚本中某个独立的命令出现错误(exit)时马上退出,后续命令不再执行(默认继续执行)

-u表示所有未定义的变量被认为是错误(默认是视为空值)

-o pipefail表示多个命令通过管道连接时,所有命令都正常(exit 0)才认为最后结果是正常(默认是最后一个命令的退出码作为整体退出码)

上述sync pod执行过程描述7中使用如下命令请求apiserver为node资源对象打上注解annotation,这个命令是独立的一个shell语句,如果这个时候apiserver不可用,那么这个命令将会以非零状态码退出,由于set -e的存在,脚本作为容器主进程将退出,也即容器会发生容器,这样就能把我们的现象1和现象2联系起来了。

#If this command failed, sync pod will restart.           
oc annotate --config=/etc/origin/node/node.kubeconfig "node/${NODE_NAME}" \
              node.openshift.io/md5sum="$( cat /tmp/.new | cut -d' ' -f1 )" --overwrite

 

上述关于sync pod脚本存在的两个潜在问题相结合会导致apiserver出问题时kubelet服务重启(我这边认为是可以避免且没有什么益处的,于是提了redhat的问题case并等待其回复确认是否为bug),而大量kubelet服务重启之后会向apiserver做List Pod的操作,似乎能解释为什么故障期间apiserver有流量峰值的情况。

 

20200907:查询到openshift v3.11.154中的sync pod脚本是已经修复了启动会重启Node服务的问题

function md5()
{
// 将命令执行结果用()括号括起来表示一个数组,下面echo数组第一个元素 local md5result
=($(md5sum $1)) echo md5result } md5 {file}

 

 

 

Kubelet的Admit机制

关于kubelet的admit机制,可以参考另一文档:https://www.cnblogs.com/orchidzjl/p/14801278.html

在kubernetes项目的issue中Podstatus becomes MatchNodeSelector after restart kubelet关于MatchNodeSelector的讨论跟我们的场景基本是一致的,大概过程为kubelet服务的重启之后将会请求apiServer节点上面所有的Pod列表(新调度到节点上和正在节点上运行的pod),对列表中的每一个Pod进行Admit操作,这个Admit过程将会执行一系列类似scheduler中的预选策略,来判断这些pod是否真正适合跑在我这个节点,其中有一个策略就是从apiserver获取node资源对象,并判断从node中的标签是否满足pod亲和性,如果不满足则该Pod会变成MatchNodeSelector,如果这个Pod是之前已经运行在当前节点上,那么这个Pod会被停止并重新生成调度。那为什么node标签会不满足pod的亲和性呢,因为在kubelet向apiServer获取最新的node对象时,如果apiServer不可用导致获取失败时,那么kubelet会通过本地配置文件直接生成一个initNode对象,这个node对象的标签只有kubelet的--node-labels参数指定的标签,那些通过api添加的标签都不会出现在这个initNode对象上(我们的生产环境都是通过api额外添加的标签作为pod的亲和性配置),这样就能把我们的现象3和现象4联系起来了。

# 从Apiserver中获取Node信息,如果失败,则构造一个initNode
# pkg/kubelet/kubelet_getters.go
// getNodeAnyWay() must return a *v1.Node which is required by RunGeneralPredicates().
// The *v1.Node is obtained as follows:
// Return kubelet's nodeInfo for this node, except on error or if in standalone mode,
// in which case return a manufactured nodeInfo representing a node with no pods,
// zero capacity, and the default labels.
func (kl *Kubelet) getNodeAnyWay() (*v1.Node, error) {
    if kl.kubeClient != nil {
        if n, err := kl.nodeInfo.GetNodeInfo(string(kl.nodeName)); err == nil {
            return n, nil
        }
    }
    return kl.initialNode()
}

 

 

模拟MatchNodeSelector Pod的生成:通过restAPI(kc、oc等命令)给节点打上自定义标签,给Pod所属的deploy加上spec.nodeAffinity通过前面的标签亲和到节点,创建该deploy,pod成功部署到节点上。再把节点上那个标签删除,pod正常running在节点上,重启节点的kubelet服务,pod变成MatchNodeSelector状态并重新调度新pod。

 

Master节点资源配置

 参考openshift官方文档关于master节点的资源配置需求以及相关的基准测试数据 scaling-performance-capacity-host-practices-master,扩容三个master的虚拟机配置,部署容器云apiserver、etcd、controller-manager prometheus监控,根据监控信息优化各组件配置。

 

结论

这是一个由于master节点资源使用率高导致的apiserver不可用,进而导致openshift sync pod重启,进而导致节点上的kubelet服务重启,进而导致节点上面的业务pod发生MatchNodeSelector的现象。

 

优化

修改sync pod中shell的逻辑,让sync pod只有在真正检查到配置文件修改时才重启节点上的kubelet服务

提高三个master节点的cpu和memory资源配置,调整apisever所能接受请求的并发数限制,观察一段时间内apiserver的资源使用情况

部署容器云apiserver、etcd、controller-manager prometheus监控,观察相关的各种性能指标

posted @ 2020-08-19 12:46  JL_Zhou  阅读(1169)  评论(0编辑  收藏  举报