NCE线上关键业务监控实践总结及心得

为什么需要线上业务监控

我们知道一个产品上线后,可用率非常重要,尤其是业务关键流程必须百分之百的保证可用。线上报警确实可以起到一定作用,假如服务进程有问题或者日志中报出error级别的日志则会触发短信、泡泡甚至电话等报警。开发看到报警后就开始救火,查清楚是什么原因导致的然后快速修复。 但是想想如果这种报警都是由真实用户触发,那么这种救火的操作就犹如亡羊补牢。已经带来了一部分的真实或者信誉上的损失。 那么如何防患于未然呢。定时的线上业务监控就可以一定程度缓解这种局面。

线上业务监控,是指的使用自动化手段模拟真实用户关键操作,并通过自动化检查来确认操作是否成功的一种手段。

线上业务监控实施前提

是不是所有项目都适合做线上业务定时运行(下面统一称为线上批跑)呢。请检查你的项目是否符合这么几个要点:

1、项目或产品提供出去的接口或者API可以直接被用户或者UI层调用(如果过于底层的项目也可以批跑,但是需要注意这种批跑不完全能够模拟用户的真实行为,需要评估)。

2、被测对象提供的接口或者API是具备原子性的,测试前后对环境无侵入、无资源残留。(例如创建的资源都可以被删除,注册逻辑显然不太适用于业务批跑)

3、接口需要有安全保证,需要有认证授权以及流控等流程,避免自动化代码中误操作导致线上资源误删除,或者操作过于频繁导致线上服务压力太大影响可用性。

拿网易云基础服务的NCE项目举例。NCE项目提供两种对外的方式,openAPI是提供给开发者使用token来发起定制化的API请求,是Restful API风格的接口。采用Https进行安全认证。WebAPI是提供给UI层调用的方式,授权采用cookie认证的方式。后端提供login接口来支持UI获取用户的cookie,然后在webAPI的请求头部加入cookie来验证用户信息,并访问接口。 这两种形式都可以模拟用户真实操作,符合第一点行为。另外关键业务流程例如服务创建、删除、设置等都是跟服务生命周期一一对应的。测试前后可以确保无侵入,无资源残留。 安全方面,cookie和token授权以及API流控可以满足要求。

线上业务监控实施步骤

1、编写自动化代码调用线上接口。 自动化框架可以任选,如果http接口有很多自动化框架可以选择,nce这边沿用testng+maven+httpclient的模式去编写自动化代码。 建议框架大概可以参照下图的模式,http接口尽量封装、数据隔离、配置文件独立的原则,便于维护和跟踪:

2、编写用例原则:关键业务、系统无侵入、租户隔离、异常分支以及错误后报警、错误日志清晰。 下面具体列举了一个服务创建、删除的部分代码示例。简单粗暴抛代码出来:

  /**
     * 创建2个云盘,创建1个有状态服务,等待成功,删除服务,
     *
     * @throws Exception 
     */
    @Test
    public void MicroServiceTest_state() {
           log.info("==========Test Case: create state service=============");
           long startTime = System.currentTimeMillis();
            if (productId != null)
            { 
                try {
                CreateMicroServiceInput input_state = CreateMicroServiceInput.getMicroServicePostInput();
                  input_state.serviceName = "apitest-sta" + System.currentTimeMillis();
                  input_state.stateStateless = "1";
                  input_state.statePublicNet = "{\"used\":false,\"type\":\"bandwidth\",\"bandwidth\":1}";
                  input_state.diskType = "0";
                  input_state.productId = productId;
                      log.info("==========Test Case :create volume=============");
                      NceHttpPub.createVolumeUseBill(input_state.serviceName+"v1-10", "10");
                      NceHttpPub.createVolumeUseBill(input_state.serviceName+"v1-20", "20");
                    Thread.sleep(2000);
                      ArrayList<Integer> volumeIdList = NceHttpPub.getVolumeIdList();
                      volumeId1 = volumeIdList.get(0);
                      volumeId2 = volumeIdList.get(1);
                      NceHttpPub.waitForValumeState(volumeId1.toString(),"", "creating","create_succ");
                      NceHttpPub.waitForValumeState(volumeId2.toString(),"","creating","create_succ");
                      input_state.volumeInfo="{\""+volumeId1+"\":\"/mnt/\",\""+volumeId2+"\":\"/mnt2/\"}";
                      NceHttpPub.CreateMicroServiceUseBill(input_state);
                      //计费回调nce创建微服务有延时,sleep 10s通过serviceName从微服务列表里获取msId
                      Thread.sleep(10 * 1000L);
                      msId_state = NceHttpPub.getMSIdByName(productId,input_state.serviceName)
                  if (msId_state != null)
                      NceHttpPub.waitForMicroServiceCreate(msId_state, "creating","creating", "create_succ");
                  log.info("==========Test Case: create state service end!=============");
                  long endTime = System.currentTimeMillis();
                long costTime = endTime - startTime;
                if (costTime > 50 * 60 * 1000L)
                    {
                        alert.sendAdminCriticalMsg(uniqueId+" "+account+" "+"MicroServiceTest_state cost:"+costTime+" ms");
                        Assert.fail(uniqueId+" "+"state MicroService Creating slowly,cost:"+costTime+" ms");
                    }
                Thread.sleep(50 * 1000L);
                  log.info("==========Test Case: begin delete state service!=============");
                  if (msId_state!=null && !msId_state.isEmpty()) {
                        //String responsestr = NceHttpPub.delMicroServiceById(msId_state);
                         NceHttpPub.delMicroServiceByIdOpenAPI(msId_state);
                        log.info("delete state MicroService ok ");
                    }
                Thread.sleep(60 * 1000L);
                 log.info("==========Test Case: delete volume=============");
                    NceHttpPub.waitForValumeState(volumeId1.toString(),"mount_succ", "unmounting","unmounted");
                    NceHttpPub.waitForValumeState(volumeId2.toString(),"mount_succ","unmounting","unmounted");
                    String responsestr1 = NceHttpPub.delVolumebyId(volumeId1.toString());
                    log.info("delete volume1"+volumeId1+"return : "+ responsestr1);
                    String responsestr2 = NceHttpPub.delVolumebyId(volumeId2.toString());
                    log.info("delete volume2 return"+volumeId2+"return : "+ responsestr2);
                } catch (Exception e) {
                    // TODO Auto-generated catch block
                         alert.sendAdminCriticalMsg(uniqueId+" "+account+"run MicroServiceTest_state case fail"+e.getMessage());
                        String responsestr1 = NceHttpPub.delVolumebyId(volumeId1.toString());
                        log.info("delete volume1"+volumeId1+"return : "+ responsestr1);
                        String responsestr2 = NceHttpPub.delVolumebyId(volumeId2.toString());
                        log.info("delete volume2 return"+volumeId2+"return : "+ responsestr2);
                        log.info("==========tearDownClass : deleteNamespace=============");
                        String responsestr = NceHttpPub.delNamespaceById(productId);
                        log.info("tearDownClass deleteNamespace return + " + responsestr);
                    Assert.fail("MicroServiceTest_state fail");
                    e.printStackTrace();
                }                
            }
        }

值得注意的是各种异常分支中需要清理case中可能产生的资源,另外报警不止要加到case失败的流程中还要加到异常分支中。 当然代码经过辗转多人之手,可能看起来有些不优雅,也欢迎各方大神指正其中需要改进之处。 由于创建以后,服务列表获取有一定延迟,因此里面加了不太优雅的但是很有效的sleep操作。

3、将定时触发用例执行结合到jenkins的job上。 用例定时触发由jenkins的job去做再合适不过,可以适当调整定时触发的频率,以确保既能满足关键业务及时运行并发现问题的需要,又不至于太过频繁导致线上批跑占用过多资源或者导致线上压力过大。按nce的频率调整为1个小时触发1次微服务创建逻辑、镜像仓库构建逻辑、持续集成逻辑。并且根据不同的用户隔离式单独触发,确保不同的集群中的用户服务可用性都为100%。

4、运行一段时间后及时调整策略,减少因为环境、租户原因导致的线上批跑失败问题。 NCE的线上批跑历史性比较久远,从开始到现在大概有一年多的时间,由单个场景也丰富至四五个场景。同时也踩了不少坑。 线上批跑健康度非常重要,如果长时间由于环境、租户原因等因素导致失败,那么就无法起到应有的作用。之前最常见的错误原因就是线上租户代金券不足导致资源创建失败。这个通过定时检查可以一定程度解决。另外就是开启安全验证的租户虽然创建服务没有问题,但是删除会触发验证流程导致删除失败,从而删不掉的问题。还有测试人员用同一个租户做回归操作与线上批跑步骤冲突导致失败。这个通过测试回归账号与线上批跑账号分离的策略来规避。 还有另外踩到的一些坑,例如持续集成节点机网络原因导致的失败,这种环境问题就需要见招拆招了。

5、 根据上线任务及时调整自动化代码,避免由于上线影响的接口变动原因造成线上批跑失败。 提供给UI或者用户的接口不是一成不变的,由于需求等各方面原因,接口的输入输出或者流程都可能会有变更,在上线之前qa应该识别到这些变更,评估影响,及时修改自动化代码,待上线之后调试自动化代码并且重新开启定时批跑。因此线上批跑的自动化代码这部分,qa的及时定位跟踪、维护更新也非常重要。

总结

不同的项目适用的线上业务监控手段有所不同,但是目的都是一致的,就是有效保证系统可用率。如果大家在做线上业务自动化编写过程中遇到了什么坑也欢迎抛出来讨论解决。编写自动化并不难,难的是如何把自动化合理的编写成可以达到保证系统可用率的目的。好的自动化批跑可以达到这样的目的,可以使得事故或者线上bug提前发掘。差的自动化批跑只能增加qa的负担,而不能起到应有的作用。不断地优化线上批跑,达到应有的目的,是我们需要一直思考的事情。

本文来自网易实践者社区,经作者崔晓晴授权发布。