如何进行kubernetes问题的排障
排障的前置条件
k8s的成熟度很高,伴随着整个项目的扩增,以及新功能和新流程的不断引入,也伴随这产生了一些问题。虽然自动化测试可以排除掉大部分,但是一些复杂流程以及极端情况却很难做到bug的完全覆盖。因此在实际的工作过程中,需要对运行的集群进行故障定位和解决。
当然,进行排障的前提是对于k8s的流程和概念进行掌握,对于源码有一定的掌握能力,才可以更好的进行。待排障的环境和版本和源代码的版本需要进行匹配。版本号可以通过version命令获取,然后从源码进行对照。而且kubectl version还可以展示更为git的commit id。这样更为精准一些。本文以一次排障过程为例,介绍进行kubernetes问题排障的一般思路和方法。
故障背景
在某个压测的集群(集群版本为v1.12.10)内,为了测试极端性能,于是kubelet上配置了单节点可以创建的容器数从110调整为了600。并且进行反复大批量的容器创建和删除。在压测后一段时间,陆续多个节点变为NotReady,直到整个节点全部变为了NotReady。在节点上看到有大量的容器待删除。kubelet虽然仍在运行,但是已经不进行任何的pod生命周期的管理了,已经呆住了。其他组件大都正常。此时停了压测工具,kubelet仍然不能够恢复正常。尝试将一个节点的kubelet重启后,节点恢复正常。
故障分析
日志分析
首先从日志上进行分析。日志是日常排障的最主要的工具。从长期经验来看,我们的主要方式是将日志写入到文件,并配合glogrotate进行日志的回滚。不使用journal的主要原因一个是习惯,另外就是使用效率上也没有文件来的快速。关于日志级别,日志级别太高,日志量会很大;而级别太低,日志信息量又不足。日志级别按照经验我们一般定位4级。
从日志上进行分析,可以看到这样一条日志。
I1105 09:50:27.583544 548093 kubelet.go:1829] skipping pod synchronization - [PLEG is not healthy: pleg was last seen active 58h29m3.043779855s ago; threshold is 3m0s]
也就是PLEG不再健康了。那么这一行是怎么报出来的呢?对照代码,我们可以找到这样的信息。
func (kl *Kubelet) syncLoop(updates <-chan kubetypes.PodUpdate, handler SyncHandler) { ...... for { if rs := kl.runtimeState.runtimeErrors(); len(rs) != 0 { glog.Infof("skipping pod synchronization - %v", rs) // exponential backoff time.Sleep(duration) duration = time.Duration(math.Min(float64(max), factor*float64(duration))) continue } // reset backoff if we have a success duration = base kl.syncLoopMonitor.Store(kl.clock.Now()) if !kl.syncLoopIteration(updates, handler, syncTicker.C, housekeepingTicker.C, plegCh) { break } kl.syncLoopMonitor.Store(kl.clock.Now()) } ......
PLEG不再健康,这里就会直接continue,也就不会走到下面的syncLoopIteration函数。而这个函数通过逐层调用,最终会到syncPod上。这也就解释了为什么节点上kubelet不再处理pod生命周期的原因了。但是为什么PLEG不再健康。那么它的判断标准又是什么呢?继续看代码。
func (g *GenericPLEG) Healthy() (bool, error) { relistTime := g.getRelistTime() elapsed := g.clock.Since(relistTime) if elapsed > relistThreshold { return false, fmt.Errorf("pleg was last seen active %v ago; threshold is %v", elapsed, relistT hreshold) } return true, nil }
同时,这里可以看到PLEG的健康状态是以上次relist的时间来确定的。那么relist的时间又是在哪更新的呢?这个可以通过代码找到func (g *GenericPLEG) relist(){...}
函数,也就是在这个函数里进行relist的时间更新的。那么可以初步判定应该可能在这个函数的某个流程里卡住了,导致的整个问题。但是这个函数有上百行,我们怎么定位呢?好像日志分析能够提供的帮助已经很有限了。那么我们需要一些其他的工具来辅助定位。
pprof
pprof就是这样的一个工具。pprof是什么,怎么用,这里不展开讲,可以去搜具体的资料。kubelet里有一个配置enable-debugging-handlers
,通过配置为true进行开启(默认为true)。开启后,借助于这个工具,我们进行进一步的定位。kubelet的工作默认端口为10250。因为我们的集群中kubelet开启了认证,所以我们这里用curl命令直接把需要的堆栈信息拉取下来。
curl -k --cert admin.pem --key admin-key.pem "https://127.0.0.1:10250/debug/pprof/goroutine?debug=1" > stack.txt
因为卡住了,所以有非常多的goroutine信息。堆栈信息太长,这里就不全部拉取了,这里只截取我们关心的,就是relist卡在了哪里。我们搜索relist,可以看到这样的信息。
goroutine profile: total 2814 ... 1 @ 0x42fcaa 0x42fd5e 0x4066cb 0x406465 0x30d4f3e 0x30d682a 0xad7474 0xad6a0d 0xad693d 0x45d2f1 # 0x30d4f3d k8s.io/kubernetes/pkg/kubelet/pleg.(*GenericPLEG).relist+0x74d /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/pkg/kubelet/pleg/generic.go: