问题现象
kubernetes集群中,发生部分pod应用不可用的告警
问题排查
基于这种情况,根据经验判断,怀疑是对应的某个节点出现问题。
-
找到其中一个pod,kubectl get pod -o wide|grep pod-xxx 查看其在哪个节点上
-
通过kubectl describe node xxxx 查看该node上的信息,发现该节点上的pod都出现上在了不可用的应用清单中。另外还观察到了一条warning信息
- 通过其ip地址,登录服务器,发现已经无法登录了。
- 查看node节点监控,看能否发现一些端倪。监控数据却没有异常。初步确认是底层服务器问题,赶紧反馈给云厂商。
- 排查影响范围,通过kubectl get node -o wide ,发现一个很奇怪的现象,node节点处于ready状态,但是os-image字段显示unknow状态
- 将其unknow状态的节点逐一测试,发现都无法登录。
其实问题通过上面一顿操作,可以得到一个基本结论,出现了unknown状态的node存在问题,导致该节点上的pod都不可用。具体原因还要看云厂商给出结论。从目前得到的信息推断应该是物理机磁盘出现问题,导致文件系统io出现错误。所以后面的分析也是基于这个结论去分析的。
事情到这里,我们还是有些问题要去反思的。
- 服务器上的pod都不可用了,且服务器也无法登录了,而k8s的出问题的node节点为什么状态还是Ready?
- 服务器不可用了,监控为什么还是正常的?
- 问题节点的os-image的信息为什么是unknown的状态?
问题解惑
要弄清上面的问题,我们要先看下kubelet的状态上报机制是怎样的。或许能够解答第一个和第三个问题。
kubelet的状态上报机制
在 v1.13 之前的版本中,节点的心跳只有 NodeStatus,从 v1.13 开始,NodeLease feature 作为 alpha 特性引入。当启用 NodeLease feature 时,每个节点在“kube-node-lease”名称空间中都有一个关联的“Lease”对象,该对象由节点定期更新,NodeStatus 和 NodeLease 都被视为来自节点的心跳。NodeLease 会频繁更新,而只有在 NodeStatus 发生改变或者超过了一定时间(默认值为1分钟,node-monitor-grace-period 的默认值为 40s),才会将 NodeStatus 上报给 master。由于 NodeLease 比 NodeStatus 更轻量级,该特性在集群规模扩展性和性能上有明显提升。
上传信息
状态的信息的心跳我么依靠nodelease来实现,减少apiserver的压力。其上传的信息相对于node的信息少很多,比较轻量级。
// kubernetes/pkg/kubelet/kubelet.go
func (kl *Kubelet) Run(updates <-chan kubetypes.PodUpdate) {
...
if kl.kubeClient != nil {
// Start syncing node status immediately, this may set up things the runtime needs to run.
// 通过node的状态信息
go wait.Until(kl.syncNodeStatus, kl.nodeStatusUpdateFrequency, wait.NeverStop)
// 更新node的缓存信息
go kl.fastStatusUpdateOnce()
// start syncing lease
// 开启同步lease信息
go kl.nodeLeaseController.Run(wait.NeverStop)
}
...
}
NodeLease
通过一下代码我们可以看到,只是上传一个很简单数据。
// retryUpdateLease attempts to update the lease for maxUpdateRetries,
// call this once you're sure the lease has been created
func (c *controller) retryUpdateLease(base *coordinationv1.Lease) error {
// 上传重试机制
for i := 0; i < maxUpdateRetries; i++ {
// 生成新的lease
leaseToUpdate, _ := c.newLease(base)
// 更新apiserver的信息
lease, err := c.leaseClient.Update(context.TODO(), leaseToUpdate, metav1.UpdateOptions{})
if err == nil {
c.latestLease = lease
return nil
}
klog.Errorf("failed to update lease, error: %v", err)
// OptimisticLockError requires getting the newer version of lease to proceed.
if apierrors.IsConflict(err) {
base, _ = c.backoffEnsureLease()
continue
}
if i > 0 && c.onRepeatedHeartbeatFailure != nil {
c.onRepeatedHeartbeatFailure()
}
}
return fmt.Errorf("failed %d attempts to update lease", maxUpdateRetries)
}
// newLease constructs a new lease if base is nil, or returns a copy of base
// with desired state asserted on the copy.
// Note that an error will block lease CREATE, causing the CREATE to be retried in
// the next iteration; but the error won't block lease refresh (UPDATE).
func (c *controller) newLease(base *coordinationv1.Lease) (*coordinationv1.Lease, error) {
// Use the bare minimum set of fields; other fields exist for debugging/legacy,
// but we don't need to make component heartbeats more complicated by using them.
var lease *coordinationv1.Lease
if base == nil {
lease = &coordinationv1.Lease{
ObjectMeta: metav1.ObjectMeta{
Name: c.holderIdentity,
Namespace: c.leaseNamespace,
},
Spec: coordinationv1.LeaseSpec{
HolderIdentity: pointer.StringPtr(c.holderIdentity),
LeaseDurationSeconds: pointer.Int32Ptr(c.leaseDurationSeconds),
},
}
} else {
lease = base.DeepCopy()
}
lease.Spec.RenewTime = &metav1.MicroTime{Time: c.clock.Now()}
if c.newLeasePostProcessFunc != nil {
err := c.newLeasePostProcessFunc(lease)
return lease, err
}
return lease, nil
}
NodeStatus
node的状态信息相对较多,在 NodeStatus 发生改变或者超过了一定时间(默认值为1分钟,node-monitor-grace-period 的默认值为 40s),才会将 NodeStatus 上报给 master。相关的上传的信息可以参考以下代码。
// defaultNodeStatusFuncs is a factory that generates the default set of
// setNodeStatus funcs
func (kl *Kubelet) defaultNodeStatusFuncs() []func(*v1.Node) error {
// if cloud is not nil, we expect the cloud resource sync manager to exist
var nodeAddressesFunc func() ([]v1.NodeAddress, error)
if kl.cloud != nil {
nodeAddressesFunc = kl.cloudResourceSyncManager.NodeAddresses
}
var validateHostFunc func() error
if kl.appArmorValidator != nil {
validateHostFunc = kl.appArmorValidator.ValidateHost
}
var setters []func(n *v1.Node) error
setters = append(setters,
nodestatus.NodeAddress(kl.nodeIPs, kl.nodeIPValidator, kl.hostname, kl.hostnameOverridden, kl.externalCloudProvider, kl.cloud, nodeAddressesFunc),
nodestatus.MachineInfo(string(kl.nodeName), kl.maxPods, kl.podsPerCore, kl.GetCachedMachineInfo, kl.containerManager.GetCapacity,
kl.containerManager.GetDevicePluginResourceCapacity, kl.containerManager.GetNodeAllocatableReservation, kl.recordEvent),
nodestatus.VersionInfo(kl.cadvisor.VersionInfo, kl.containerRuntime.Type, kl.containerRuntime.Version),
nodestatus.DaemonEndpoints(kl.daemonEndpoints),
nodestatus.Images(kl.nodeStatusMaxImages, kl.imageManager.GetImageList),
nodestatus.GoRuntime(),
)
// Volume limits
setters = append(setters, nodestatus.VolumeLimits(kl.volumePluginMgr.ListVolumePluginWithLimits))
setters = append(setters,
nodestatus.MemoryPressureCondition(kl.clock.Now, kl.evictionManager.IsUnderMemoryPressure, kl.recordNodeStatusEvent),
nodestatus.DiskPressureCondition(kl.clock.Now, kl.evictionManager.IsUnderDiskPressure, kl.recordNodeStatusEvent),
nodestatus.PIDPressureCondition(kl.clock.Now, kl.evictionManager.IsUnderPIDPressure, kl.recordNodeStatusEvent),
nodestatus.ReadyCondition(kl.clock.Now, kl.runtimeState.runtimeErrors, kl.runtimeState.networkErrors, kl.runtimeState.storageErrors, validateHostFunc, kl.containerManager.Status, kl.shutdownManager.ShutdownStatus, kl.recordNodeStatusEvent),
nodestatus.VolumesInUse(kl.volumeManager.ReconcilerStatesHasBeenSynced, kl.volumeManager.GetVolumesInUse),
// TODO(mtaufen): I decided not to move this setter for now, since all it does is send an event
// and record state back to the Kubelet runtime object. In the future, I'd like to isolate
// these side-effects by decoupling the decisions to send events and partial status recording
// from the Node setters.
kl.recordNodeSchedulableEvent,
)
return setters
}
kubelet状态上报基本流程
当 Kubernetes 中 Node 节点出现状态异常的情况下,节点上的 Pod 会被重新调度到其他节点上去,但是有的时候我们会发现节点 Down 掉以后,Pod 并不会立即触发重新调度,这实际上就是和 Kubelet 的状态更新机制密切相关的,Kubernetes 提供了一些参数配置来触发重新调度到嗯时间,下面我们来分析下 Kubelet 状态更新的基本流程。
- kubelet 自身会定期更新状态到 apiserver,通过参数
--node-status-update-frequency
指定上报频率,默认是 10s 上报一次。 - kube-controller-manager 会每隔
--node-monitor-period
时间去检查 kubelet 的状态,默认是 5s。 - 当 node 失联一段时间后,kubernetes 判定 node 为
notready
状态,这段时长通过--node-monitor-grace-period
参数配置,默认 40s。 - 当 node 失联一段时间后,kubernetes 判定 node 为
unhealthy
状态,这段时长通过--node-startup-grace-period
参数配置,默认 1m0s。 - 当 node 失联一段时间后,kubernetes 开始删除原 node 上的 pod,这段时长是通过
--pod-eviction-timeout
参数配置,默认 5m0s。
异常时的影响
在 v1.5 之前的版本中 kube-controller-manager 会 force delete pod
然后调度该宿主上的 pods 到其他宿主,在 v1.5 之后的版本中,kube-controller-manager 不会 force delete pod
,pod 会一直处于Terminating
或Unknown
状态直到 node 被从 master 中删除或 kubelet 状态变为 Ready。在 node NotReady 期间,Daemonset 的 Pod 状态变为 Nodelost,Deployment、Statefulset 和 Static Pod 的状态先变为 NodeLost,然后马上变为 Unknown。Deployment 的 pod 会 recreate,Static Pod 和 Statefulset 的 Pod 会一直处于 Unknown 状态。
问题解答
通过以上原理的了解,我们清楚到节点是否处于ready的状态主要是依赖kubelet是否有正常发送心跳信息给apiserver。而此次的情况,问题是文件系统无法读取,但是kubelet发送心跳信息是不依赖文件系统的。所以node的状态信息是ready。
另外为什么os-image未能正确获取到,这里从上面信息可知,kubelet获取操作系统版本是依赖cadvisor来获取版本信息。而cadivsor的版本信息跟踪其代码,其获取方式如下:
// google.cadvisor/manager/manager.go
// 获取操作系统内核版本
func KernelVersion() string {
uname := &unix.Utsname{}
if err := unix.Uname(uname); err != nil {
return "Unknown"
}
return string(uname.Release[:bytes.IndexByte(uname.Release[:], 0)])
}
// 获取内核版本具体信息
func Uname(buf *Utsname) (err error) {
_, _, e1 := RawSyscall(SYS_UNAME, uintptr(unsafe.Pointer(buf)), 0, 0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
可以看出,unknow的错误应该是unix.Uname(uname)这个地方抛出的信息,目前这里猜测应该是跟硬盘有些关联,导致无法获取到操作系统内核信息。
监控数据获取原理
基于第二个问题,其实跟第三个问题有点类似,本质上是应该回答,监控数据的获取原理是怎样的。监控的信息获取一般都可以通过系统调获取,而系统调用又是如何获取到监控数据的呢?实际上系统调用都是通过proc中获取到相关监控数据的。
proc目录详解
/proc 文件系统是一种内核和内核模块用来向进程(process) 发送信息的机制(所以叫做/proc)。这个伪文件系统让你可以和内核内部数据结构进行交互,获取 有关进程的有用信息,在运行中(on the fly) 改变设置(通过改变内核参数)。 与其他文件系统不同,/proc 存在于内存之中而不是硬盘上。
常见的信息获取
- /proc/cpuinfo
- /proc/version
- /proc/meminfo
问题解答
如果只是硬盘不可用,监控采集数据的进程实际上读取的/proc目录,本质上是读取的内存数据,不受此次问题的影响。
如何处理
这次问题中也暴露了个很有意思的问题,貌似传统的监控节点状态的方式,并不能触发告警。那要怎么去解决这个问题呢?实际上kubelet的状态监控只能保证一些最基础的状态判断。我们需要对节点的信息做更多的状态判断,需要怎么做呢?比较好的一种方式可以通过node-problem-detector来实现。
具体可参考:github.com/kubernetes/…
另外还有一些实战文章可以参考: segmentfault.com/a/119000003…
小结
此次问题处理的过程分析,可以总结如下几点:
- 节点状态是kube-controller-manager定时查看kubelet上传的心跳信息判断是否为监控状态
- /proc目录下展示是操作系统内存中的相关数据,监控数据基本通过这个目录进行获取
- 节点状态是否正常,我们应该提供更多维度的指标去判断,可以通过node-problem-detector去实现对节点状态信息的补充。
结束语
此次k8s节点异常问题分析的文章中必然会有一些不严谨的地方,还希望大家包涵,大家吸取精华(如果有的话),去其糟粕。如果大家对我写的文章感兴趣可以关我的公众号:gungunxi。我的微信号:lcomedy2021。