6.3.4 获取异常信息
当我们过滤出处于异常状态的Pod和对应的容器后就可以从中提取出一些有利于分析的信息。例如,在遍历容器状态时可以抽取对应状态的错误码和错误信息,事实上Waiting、Running和Terminated这3个属性都有针对它们的结构体来存储相应的信息,如代码清单6-12所示。
代码清单6-12 获取状态信息:
// ContainerStateWaiting保存了处于等待状态容器的详细信息
type ContainerStateWaiting struct {
// 表示容器还没有运行的原因
Reason string `json:"reason,omitempty" protobuf:"bytes,1,opt,name = reason"`
// 表示容器还没有运行的原因的详细信息
Message string `json:"message,omitempty" protobuf:"bytes,2,opt,name = message"`
}
// ContainerStateRunning保存了处于运行状态容器的详细信息
type ContainerStateRunning struct {
// 容器的启动时间
StartedAt metav1.Time `json:"startedAt,omitempty" protobuf:"bytes,1,opt,name =
startedAt"`
}
// ContainerStateTerminated保存了处于退出状态容器的详细信息
type ContainerStateTerminated struct {
// 容器的退出状态码
ExitCode int32 `json:"exitCode" protobuf:"varint,1,opt,name = exitCode"`
// 容器的退出信号
Signal int32 `json:"signal,omitempty" protobuf:"varint,2,opt,name = signal"`
// 容器退出的简短原因
Reason string `json:"reason,omitempty" protobuf:"bytes,3,opt,name = reason"`
// 容器退出的详细信息
Message string `json:"message,omitempty" protobuf:"bytes,4,opt,name = message"`
// 容器的启动时间
StartedAt metav1.Time `json:"startedAt,omitempty" protobuf:"bytes,5,opt,name =
startedAt"`
// 容器的退出时间
FinishedAt metav1.Time `json:"finishedAt,omitempty" protobuf
:"bytes,6,opt,name = finishedAt"`
// 容器ID
ContainerID string `json:"containerID,omitempty" protobuf
:"bytes,7,opt,name = containerID"`
}
// ContainerState记录容器可能的状态
type ContainerState struct {
// 处于等待状态的容器的信息
Waiting *ContainerStateWaiting `json:"waiting,omitempty" protobuf:
"bytes,1,opt,name = waiting"`
// 处于运行状态的容器的信息
Running *ContainerStateRunning `json:"running,omitempty" protobuf:
"bytes,2,opt,name = running"`
// 处于退出状态的容器的信息
Terminated *ContainerStateTerminated `json:"terminated,omitempty" protobuf:
"bytes,3,opt,name = terminated"`
}
大家可参考代码清单6-12中列出的代码来过滤自己需要的信息。但这些信息只能帮助开发人员对问题进行初步的判断,若想进一步分析仍然需要相关的日志文件。日志的抓取往往并不十分容易,一般来说,集群中会使用类似ELK(Elasticsearch,Logstash,Kibana)的架构来进行日志的收集,日志最终都会保存到Elasticsearch中供项目人员查询,只不过想要精准定位到发生异常的时刻的日志还是十分麻烦的。这里选择使用K8s客户端的日志查询接口来完成这个任务,如代码清单6-13所示。
代码清单6-13 使用日志查询接口:
func (watcher *PodWatcher) getLog(containerName string, podName string)
(map[string]string, error) {
// 抓取容器日志
line := int64(1000) // 定义只抓取最新的1000行日志
opts := &corev1.PodLogOptions{
Container: containerName,
TailLines: &line,
}
containerLog, err := watcher.K8s.CoreV1().Pods(watcher.Namespace).GetLogs
(podName, opts).Stream(context.Background())
if err != nil {
log.Errorf("获取日志失败: %s", err)
return nil, err
}
clog := make(map[string]string)
data, _ := ioutil.ReadAll(containerLog)
clog[containerName] = string(data)
return clog, nil
}
需要注意的是,容器被判断为处于异常状态后,kubelet就会在非常短的时间内将容器销毁并重建,届时将再没有获取日志的机会,所以我们需要在第一时间过滤容器状态并抓取日志。
6.3.5 NPD
前文介绍的监控方法都只能发现Pod级别的异常事件,而在实际的测试场景中,节点故障也会经常发生。虽然节点发生异常会间接导致Pod故障从而被我们的监控工具发现,但这种情况下会有大量的异常Pod进行告警轰炸,并且Pod的异常信息无法帮助测试人员分析节点问题。相信大家已经想到了,可以通过监控Node对象来实现节点级别的监控能力。这个思路是正确的,但与之前可以统一抓取容器日志来分析问题的方法不同,节点可能会由于多种不同的原因出现故障,并不能只通过某个单一的日志文件进行分析。为了增强监控工具的分析能力,这里引入K8s开源项目—NPD来解决这个问题。
NPD是一个守护程序,用于根据内核死锁、OOM、系统线程数压力、系统文件描述符压力、容器运行时是否异常等指标监控和报告节点的健康状况。通常我们把NPD的进程以DaemonSet的形式运行在集群的每个节点中。NPD会为Node对象增加若干个类型的Condition,当它探测到节点异常时就会设置对应的Condition来表明该异常已经发生并且把异常的简介写入Message字段,如图6-2所示。
图6-2 Node中的Condition
在图6-2中的Message字段中会列出监控目标当前的状态,如果监控目标处于非健康状态则会输出错误信息的简介。用户可以根据NPD的开发文档来添加自己系统的监控能力,图6-2中展现的部分监控能力就是我所在团队定制与开发的。具体的内容大家可以阅读官方文档,这里就不详细介绍了。在代码清单6-14中列出一段监控Node对象的代码片段,该片段仅供参考。
代码清单6-14 Watch Node:
func (watcher *NodeWatcher) Watch() {
now := time.Now()
startTime := now
watchNodes := func(k8s *kubernetes.Clientset) (watch.Interface, error) {
return k8s.CoreV1().Nodes().Watch(context.Background(), metav1.ListOptions{})
}
podWatcher, err := watchNodes(watcher.K8s)
if err != nil {
log.Errorf("watch nodes failed, err:%s", err)
watcher.handleK8sErr(err)
}
for {
event, ok := <-podWatcher.ResultChan()
if !ok || event.Object == nil {
log.Info("the channel or Watcher is closed")
podWatcher, err = watchNodes(watcher.K8s)
if err != nil {
watcher.handleK8sErr(err)
time.Sleep(time.Minute * 5)
}
continue
}
// 忽略监控刚开始20秒的Pod事件,防止事前积压的事件传递过来
if time.Now().Before(startTime.Add(time.Second * 20)) {
//log.Debug("忽略监控刚开始20秒的Pod事件, 过滤掉积压事件")
continue
}
node, _ := event.Object.(*corev1.Node)
conditions := node.Status.Conditions
reason := ""
message := ""
for _, c := range conditions {
// 过滤异常,某些情况不需要监控
if c.Type == "KernelDeadlock" ||
c.Type == "ThreadPressure" ||
c.Type == "FDPressure" ||
c.Type == "CustomPIDPressure" ||
c.Type == "NFConntrackPressure" ||
c.Type == "FrequentKubeletRestart" ||
c.Type == "FrequentDockerRestart" ||
c.Type == "FrequentContainerdRestart" ||
c.Type == "DockerdProblem" ||
c.Type == "ContainerdProblem" ||
c.Type == "ReadonlyFilesystem" ||
c.Type == "KubeletProblem" ||
c.Type == "MemoryPressure" ||
c.Type == "DiskPressure" ||
c.Type == "PIDPressure" {
if c.Status == corev1.ConditionTrue {
reason = fmt.Sprintf("%s | %s", reason, c.Reason)
message = fmt.Sprintf("%s | %s", message, c.Message)
}
}
if c.Type == corev1.NodeReady {
if c.Status == corev1.ConditionFalse || c.Status == corev1.ConditionUnknown {
reason = fmt.Sprintf("%s | %s", reason, c.Reason)
message = fmt.Sprintf("%s | %s", message, c.Message)
}
}
}
if reason != "" {
log.Infof(fmt.Sprintf("node %s is not ready, the reason is %s the
message is %s", node.Name, reason, message))
e := &Event{
NodeName: node.Name,
Reason: reason,
Message: message,
Error: nil,
EventType: NodeException,
ErrorTimestamp: time.Now(),
}
watcher.event <- e
}
}
}
6.3.6 小结
用户可以基于List-Watch机制中的watch接口来满足自己的监控需求,除Pod和Node外,完全可以通过监控其他对象来感知集群状态的变化,大家可以在实践的过程中多加探索。在我所经历的项目中,常用的场景除了本章介绍的异常监控,还包括通过监控用户CRD的变化来感知系统特定的事件。总的来说,这是一种十分实用的监控手段,希望大家可以掌握。