服务可用时间——云原生测试实战(8)

发表于:2024-2-26 09:44

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

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

  6.4.2 服务可用时间
  在第4章介绍过生产环境的SLA指标结果是不可能在测试环境中被精确地计算出来的,因为世界上没有 100%的高可用,所以SLA指标的表现很大程度上取决于环境中故障发生的频率和故障的类别。因此,在测试环节,项目成员往往更倾向于使用RTO和RPO这样的指标来衡量服务从故障中恢复的能力。但RTO和RPO只能衡量服务从故障中恢复的能力,如果一个服务在没有故障的情况下由于自身的缺陷导致频繁重启,这必然也会影响到生产环境的SLA指标的表现。另外,RTO和RPO也无法衡量服务自身的稳定性,所以一些团队倾向于通过在没有故障注入的测试环境中统计每个服务的SLA指标来衡量服务的稳定性,并通过这个指标配合RTO和RPO来完善高可用能力的评估手段。所以,在稳定性测试中,统计SLA指标是常用的测量方式。
  注意
  除了用无故障下SLA指标的统计来评估产品自身的稳定性,测试人员往往还会通过这种手段来评估公司的测试环境是否稳定,是否常常因为开发人员或运维人员的问题导致测试环境无法使用。管理人员喜欢收集类似的数据来判定测试团队在日常工作中所遇到的阻碍,从而推动过程管理,提升工作效率。
  一般来说,在一个复杂的系统内统计每个服务的SLA指标是比较困难的,在每个服务的探活策略中需要考虑的东西比较多,但在K8s中统计SLA指标则简单了很多。在第3章讲解K8s基础时我们提到Service作为接管Pod网络的主要对象,会维护一个名为EndPoints的对象来保存和管理Pod的IP地址列表。当某个Pod出现异常(例如readiness探针探测失败)后,EndPoints对象就会把该Pod的IP地址从可用IP地址列表中移除,所以测试人员只需要监控EndPoints对象变化就可以得知服务什么时间可用、什么时间不可用,进而计算出每个服务的SLA指标。EndPoints对象的代码片段如代码清单6-20所示。
  代码清单6-20  EndPoint对象的代码片段: 
  type Endpoints struct {
      metav1.TypeMeta `json:",inline"`
      metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name = metadata"`
      Subsets []EndpointSubset `json:"subsets,omitempty" protobuf:"bytes,2,rep,name 
                                       = subsets"`
  }

  // EndpointSubset保存了一组IP地址和端口号
  // 举例如下
  //   {
  //     Addresses: [{"ip": "10.10.1.1"}, {"ip": "10.10.2.2"}],
  //     Ports:     [{"name": "a", "port": 8675}, {"name": "b", "port": 309}]
  //   }
  // 上面的记录可以被翻译为以下信息
  //     a: [ 10.10.1.1:8675, 10.10.2.2:8675 ],
  //     b: [ 10.10.1.1:309, 10.10.2.2:309 ]
  type EndpointSubset struct {
      // 记录处于Ready状态的Pod的地址信息
      Addresses []EndpointAddress `json:"addresses,omitempty" protobuf:"bytes,1,
                                                        rep,name = addresses"`
      // 记录处于异常状态的Pod的地址信息,通常Pod处于异常状态都是由探针的异常引起的
      NotReadyAddresses []EndpointAddress `json:"notReadyAddresses,omitempty"
                             protobuf:"bytes,2,rep,name = notReadyAddresses"`
      // 记录与地址信息对应的端口号
      // +optional
      Ports []EndpointPort `json:"ports,omitempty" protobuf:"bytes,3,rep,name = ports"`
  }
  需要注意的是,在EndPoints对象中使用Addresses和NotReadyAddresses两个属性来保存该服务的可用IP地址列表和不可用IP地址列表,用户可以通过这两个字段的变化来计算不同维度的SLA指标。例如,一个Service往往对应着多个Pod,用户可以选择只要其中一个Pod异常就开始计算服务的不可用时间,也可以当所有Pod都异常后才开始计算,这取决于用户统计SLA指标的目的。统计SLA指标的部分代码片段如代码清单6-21所示。
  代码清单6-21  统计SLA指标:
  func (watcher *SLAWatcher) Watch() {
      now := time.Now()

      watchEndpoints := func(namespace string, k8s *kubernetes.Clientset) 
                                 (watch.Interface, error) {
          return k8s.CoreV1().Endpoints(namespace).Watch(context.Background(), 
                                                            metav1.ListOptions{})
      }

      endpointsWatcher, err := watchEndpoints(watcher.Namespace, watcher.K8s)
      if err != nil {
          log.Errorf("watch EndPoints of namespace %s failed, err:", watcher.Namespace,
                          err.Error())
          watcher.handleK8sErr(err)
      }

      for {
          event, ok := <-endpointsWatcher.ResultChan()

          if !ok || event.Object == nil {
              log.Info("the channel or Watcher is closed")
              endpointsWatcher, err = watchEndpoints(watcher.Namespace, watcher.K8s)
              if err != nil {
                  watcher.handleK8sErr(err)
                  time.Sleep(time.Minute * 5)
              }
              continue
          }

          // 忽略监控刚开始20秒的Pod事件,防止事前积压的事件传递过来
      if time.Now().Before(now.Add(time.Second * 20)) {
          continue
      }

      endPoints, _ := event.Object.(*corev1.Endpoints)
      
      if endPoints.DeletionTimestamp != nil {
          log.Warnf("the event is a deletion event. endpoints:%s namespace:%s, skip", 
                       endPoints.Name, watcher.Namespace)
          continue
      }

      // 为了统计SLA指标,需要把一个可用IP地址都没有的EndPoints过滤出来
      for _, sub := range endPoints.Subsets {
        if len(sub.Addresses) == 0 && len(sub.NotReadyAddresses) != 0 {
            if !checkWhiteList(endPoints.Name, endPoints.Namespace, watcher.WhiteList) {
                log.Info(endPoints.Name + "没有命中白名单,不予监控")
                break
            }
            log.Infof("服务:%s namespace:%s 目前没有任何可用Pod,判断服务异常
                           ", endPoints.Name, endPoints.Namespace)
            key := endPoints.Name + endPoints.Namespace
            if e, ok := slaMap[key]; !ok {
                event := &ServiceEvent{
                    Namespace:       endPoints.Namespace,
                    ServiceName:     endPoints.Name,
                    ServiceType:     endPoints.Kind,
                    EventType:       FlowNoneException,
                    UnAvailableTime: time.Now(),
                    EnvName:         envName,
                    Reason:          sql.NullString{String: "NetworkTrafficNone", Valid: true},
                }
                id, err := InsertEventToDB(event)
                if err != nil {
                    log.Errorf("SLANone信息存储数据库失败,endpoints:%s namespace 
                                 :%s err:%s",endPoints.Name, event.Namespace, err)
                    continue
                }
                event.ID = id
                slaMap[key] = event
            } else {
                e.RecoverTime = sql.NullTime{Time: time.Time{}}
            }
          }
      }
    }
  }
  代码清单6-21中只列举了过滤出处于异常状态的IP地址的相关代码,由于统计SLA指标的方法各不相同,而且需要存储设备的辅助,这里就不过多展示了。统计SLA指标的工作一般也是在稳定性测试过程中进行的,属于持续性观测的一部分。
  6.4.3 业务巡检与Pushgateway
  持续性观测的最后一个内容是业务巡检,有些团队称之为业务拨测。增加业务巡检的原因是通用的监控系统不带有业务属性,无法探知业务异常,毕竟很多时候虽然从系统层面看一切是正常的,但业务逻辑已经无法正常运作;或者即便监控系统检查出了系统异常,但是它无法很好地表达异常影响的业务范围,所以近些年不少团队选择让测试人员以接口测试的形式对系统各个模块进行周期性的巡检。
  利用以接口测试的形式进行巡检的方式对系统进行监控需要注意以下两点。
  ·由于在巡检过程中需要执行正常的业务流程,必然会产生相关的数据,因此对这些数据进行清理和隔离是比较重要的。
  · 巡检结果需对接产品已有的监控系统,保证监控和告警的统一性。这一点比较好理解,如果业务巡检的监控告警机制是完全独立的,就会造成产品中有两套监控系统存在。这样不论在用户操作上还是资源成本上都是不可接受的。
  上述第一点需要根据每个产品和团队的特点进行针对性设计,可以选择利用账号系统与用户数据进行隔离,也可以选择在每个巡检逻辑结束后主动清理数据,这里不详细讲解,而是关注如何将巡检结果对接到Prometheus中。也许大家会想到把业务巡检服务开发成一个Exporter就能完成任务,但在实际的业务中,几乎没有团队会选择这种方案,最大的原因是业务巡检的周期一般会比较长,它不需要实时地对整个系统进行业务上的检测,如果把业务巡检做成一个持续运行的服务会造成较多的资源浪费,并且实现起来也会更复杂。所以,大多数团队倾向于利用现有的定时任务系统(如K8s CronJob)设定周期性的任务来完成巡检工作。
  在第5章提到Prometheus是典型的Pull架构,它的主服务需要周期性地访问Exporter暴露的接口来采集监控数据,而业务巡检需要的是Push架构,它需要主动上报自身的数据,所以Prometheus推出了Pushgateway和Prometheus提供的客户端。Pushgateway可以理解为一种特别的Exporter,Prometheus根据配置周期性地抓取Pushgateway中的数据。只不过Pushgateway本身并不监控数据,它的数据都来自使用Prometheus提供的客户端开发的程序。这些程序不用像Exporter一样是持续运行的HTTP服务,它们可以以任何形式运行,只要它们按自己的逻辑收集到数据后,通过主动推送的方式将数据发送给Pushgateway即可。关于Pushgateway的架构,大家可以回顾图5-2所示的内容。
  使用客户端开发的程序向Pushgateway推送数据比较简单,如代码清单6-22所示。
  代码清单6-22  向Pushgateway推送数据:
  from prometheus_client import CollectorRegistry, Gauge, push_to_gateway

  if __name__ == '__main__':
      registry = CollectorRegistry()
      labels = ['req_status', 'req_method', 'req_url']
      g_one = Gauge('requests_total', requests_total ', labels, registry = registry)
      g_two = Gauge('avg_response_time_seconds', ' avg_response_time_seconds ', 
                         labels, registry = registry)
      g_one.labels('200','GET', '/test/url').set(1) 
      g_two.labels('200','GET', '/test/api/url/').set(10) 
      push_to_gateway('http://192.128.0.110:9091', job = 'DemoMetrics', registry 
                                                           = registry)
  如果巡检使用的仍然是自动化测试框架,那么推荐用户实现一个带有参数的装饰器,在装饰器中通过判断测试用例的执行结果向Pushgateway推送对应的数据。
  6.4.4 小结
  持续性观测的核心仍然是监控体系的建设。测试人员参与监控体系的建设在业界已经是很常见的实践方法了。毕竟不同人员的关注点是不同的,开发人员和运维人员在搭建监控系统时很难考虑到产品方方面面的需求。
查看《云原生测试实战》全部连载章节
版权声明:51Testing软件测试网获得作者授权连载本书部分章节。
任何个人或单位未获得明确的书面许可,不得对本文内容复制、转载或进行镜像,否则将追究法律责
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

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

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

沪ICP备05003035号

沪公网安备 31010102002173号