获取异常信息——云原生测试实战(6)

发表于:2024-2-20 09:19

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:孙高飞    来源:51Testing软件测试网原创

  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的变化来感知系统特定的事件。总的来说,这是一种十分实用的监控手段,希望大家可以掌握。
查看《云原生测试实战》全部连载章节
版权声明:51Testing软件测试网获得作者授权连载本书部分章节。
任何个人或单位未获得明确的书面许可,不得对本文内容复制、转载或进行镜像,否则将追究法律责
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号