第三方接口的自动化管理---智能路由

795 阅读15分钟

1、背景介绍

对于很多企业,调用第三方公司提供的接口再平常不过了,但是大家都知道依赖别人的接口并且要求自己的系统稳定、高可用那是多么的困难,很被动,任人宰割。只要依赖别人的服务,系统就很难做到高可用... 为了解决这一问题,笔者也是抓耳挠腮、苦思冥想,最终解决了这一问题,哈哈,现做一分享,希望大家可以参考~

需求场景1:核查用户的实名信息(身份证+姓名 是否匹配)、验证用户的银行卡信息(身份证+姓名+银行卡号)是否一致、人脸识别并去公安后台比对 等等... 需求场景2:给用户发短信

可以发现 这些场景都是需要调用第三方的接口或服务来完成,场景1需要调用征信通道或者公安系统,场景2需要调用运营商来发短信,虽然说别人的服务也很稳定,但实际调用过程中发现其实也没有那么稳定。

准备工作

前期的积累和准备:随着公司业务量不断上升,认证(后面统称业务系统)体系逐渐复杂。为了满足日益增长的业务需求,大量的认证第三方服务逐渐接入,但由于对接的各个第三方系统的稳定性参差不齐,服务故障时有发生,作为我司基础公共服务,要在一系列不稳定的系统之上建立一个可以给公司各业务提供稳定的服务,仅依赖人工维护是远远不够的,所以建立一个完善的第三方接口的自动化管理体系势在必行。

大概意思就是说为了我司的某些业务场景不依赖单个第三方的接口/服务,那就需要针对这些场景对接多个第三方服务提供商,比如:场景1:针对当下的业务场景可以对接多家征信企业的接口/服务,场景2:发短信就可以对接 电信、联通、移动、阿里云这些大的短信平台,这样做的目的就是不局限于固定的一家第三方,至少异常时不至于那么被动。

模块介绍

路由模块:根据调用场景筛选出已配置的某个第三方通道的标识 监控模块:实时计算调用第三方的返回结果,并调整路由模块的通道开关 调用第三方模块:根据路由模块的标识读取通道配置调用第三方接口

2、演进历程

初级阶段

  • 故障处理流程为手动切走,手动切回,异常处理流程如下图:

![在这里插入图片1描述]

处理流程

现阶段的通道故障处理需要手动切走、手动切回,一次第三方接口故障的详情处理流程如下:

1)监控模块(或者运维层面)监控到业务系统异常,发送告警消息给对应业务系统技术负责人;

(2)技术负责人立即登录服务器查看日志,确认故障,并登录路由模块页面修改对应第三方通道状态,将通道置为不可用;

(3)路由模块实时读取通道状态,将故障通道流量全部切走;

(4)同时技术负责人联系第三方负责人告知异常情况,对方排查问题,确认恢复后通知我司技术负责人;

(5)业务负责人登陆路由模块管理页面修改该故障通道状态为可用,路由模块实时读取该第三方配置信息,将线上流量导入该恢复通道;

(6)如果第三方接口恢复,则用户可以正常调用,本次故障结束

(7)如果第三方接口未恢复,大量请求失败,业务系统技术人员将该通道置为不可用,再次去联系第三方认证通道负责人处理,如此往复,直到该第三方所有场景恢复正常,本次故障结束。

存在问题

初级阶段存在的问题 初级阶段的主要目标是扩大业务系统场景的覆盖范围,提高用户业务系统的可用性。随着第三方接口/服务 的不断接入,由于公网环境、第三方服务的不稳定性,导致故障频率升高,故障时间延长。而此时处于初级阶段的监控体系已无法有效保证稳定性:

1)业务告警模块漏报率较高(监控维度单一),小流量时段故障无法及时发现;

(2)第三方通道切换都是人为手动处理,一方面技术的工作量严重增加,另一方面无法保证在处理故障过程中没有任何误操作;

(3)故障解决发给的时间较长,故障对用户造成的影响就更大,同时用户的不断重试对业务系统本身也造成很大的压力;

(4)故障第三方尝试恢复时,只能技术人员手动在开发/测试环境手动用真实数据进行验证,耗时费力。

由于以上原因,痛定思痛,决定优化这块存在问题,所有有了半自动化阶段。

半自动化阶段

系统优化项

1)精确化监控告警:优化监控告警,精确监控告警项,细化告警异常,增加告警监控维度(耗时、返回码、某一错误码频率等);

(2)增加自动关闭功能:如果短时间内某个第三方接口因各种异常而调用失败,一方面修改路由模块通道配置信息,将该通道置为不可用(前提是这个场景有备份的其他第三方通道可用),选择备份通道进行复查重试,另一方面将告警信息发送给技术人员,实现认证通道故障的快速降级。

(3)增加重试策略:若某次请求失败后,触发重试策略完成用户本次请求(用户无感知,耗时久一点)

优化后的异常处理流程如下图:

![在这里插入图片2描述]

优化后的处理流程

优化后的故障处理流程是自动切走,手动切回,一次通道故障的详细处理流程如下:

1)实时监控模块检测到通道故障后,将该通道置为不可用,同时发送告警信息给相关负责人;

(2)业务系统触发重试策略,根据路由模块返回的备份通道请求备份通道,完成本次用户请求;

(3)图像认证技术人员立刻定位问题并联系第三方通道负责人,对方确认问题和恢复情况后反馈到图像认证负责人;

(4)图像认证技术将通道置为可用,刷新认证路由缓存接口,将线上流量放入该通道;

(5)如果通道恢复,则用户可以正常认证,本次故障结束;

(6)如果认证通道未恢复,再次去联系第三方认证通道负责人处理,如此往复,直到该通道所有认证场景恢复正常,本次故障结束。

主要完成的改进点

1)细化监控告警维度,做到精确、实时监控告警,保证第三方通道故障的快速发现;

(2)优化处理流程,请求A 通道失败后继续请求备份通道,增加通道复查策略,提高一次请求的成功率;

(3)大幅降低处理第三方通道故障的人力成本。

半自动化阶段存在的问题

半自动化阶段已将故障处理大幅度简化,但此时的系统还存在以下问题:

1)第三方通道恢复依赖于第三方通道技术人员的反馈,导致通道恢复耗时较久;

(2)一次第三方通道故障涉及到的系统和人员较多,人工无法保证准确性和及时的处理;

(3)虽然故障时支持自动切走,但是恢复后需要手动切回已恢复的第三方通道,人力没有完全解放出来;

由于半自动化阶段还是存在以上问题,没有达到理想状态,所以智能(全自动)路由诞生了.....

智能路由阶段

主要思想

针对线上请求流量进行监控和结果实时统计,一旦触发阈值则自动切走线上流量(将该故障第三方通道路由状态置为关闭状态),刷新路由模块缓存机制,完成关闭故障通道操作。 同时,自动化阶段也同样支持手动一键切走,并且考虑周末在外不方便,可考虑支持移动端一键切走故障通道功能(比如发送是否切换告警短信,回复1切换等思路)。

认证通道故障时,智能路由阶段处理流程如下:

![在这里插入图片3描述]

智能路由处理流程

故障通道智能化阶段,故障处理自动切走,自动切回,一次第三方通道故障的处理流程如下:

1)实时监控模块检测到某个第三方成功率异常时,发送告警信息给业务系统技术人员,同时自动将故障通道置为不可用;

(2)同时刷新路由缓存机制,路由模块实时读取通道缓存信息从而将故障通道线上流量全部切走;

(3)监控模块在将故障通道置为不可用一段时间后(时间可配置),尝试对故障通道放部分流量进来用以检测通道是否恢复正常;

(4)如果放进来的这部分量的成功率正常,监控则继续放2倍的量,以此类推,直到通道全量,监控将通道置为可用;

(5)如果放进来的这部分量成功率异常(并且识别的请求会触发重试策略,用户无感知),则继续将该通道直接置为不可用,监控隔一段时间(时间递增)后再继续放量,直到通道恢复为可用;

(6)业务系统技术人员发现通道故障后,可以向第三方通道询问故障原因、并记录,留作日后分析(阈值分析)使用。

3、实现思路

实时监控

借助阿里Sentinel组件完成实时统计

阿里Sentinel我就不用多介绍了,不了解的建议看看,绝对实用到爆炸,哈哈。。。 Sentinel官网:github.com/alibaba/Sen… Sentinel主要特性: ![在这里插入图片4描述]

下面我就介绍怎么通过Sentinel完成我们的实时统计(监控)功能:

  1. 首先需要梳理出业务系统有哪些场景/服务/接口,在sentinel中被定义为资源,其实sentinel中所说的资源可以是任何对象,可以是几行代码块、一个方法、或者一个类都可以。
  2. 我们把需要控制流量的资源用sentinel保护起来,也就是用 Sentinel API SphU.entry("资源名称") 和 entry.exit() 包围起来即可。 代码示例:
public static void main(String[] args) {
    initFlowRules();
    while (true) {
        Entry entry = null;
        try {
	    entry = SphU.entry("HelloWorld");
            /*您的业务逻辑 - 开始*/
            System.out.println("hello world");
            /*您的业务逻辑 - 结束*/
	} catch (BlockException e1) {
            /*流控逻辑处理 - 开始*/
	    System.out.println("block!");
            /*流控逻辑处理 - 结束*/
	} finally {
	   if (entry != null) {
	       entry.exit();
	   }
	}
    }
}

完成以上两步后,代码端的改造就完成了。当然,我们也提供了 注解支持模块,可以以低侵入性的方式定义资源。

  1. 当接口资源被保护起来,用户请求场景接口后,我们可以在日志:

~/logs/csp/${appName}-metrics.log.xxx

里看到下面的输出:

|--timestamp-|------date time----|-resource-|p |block|s |e|rt
1529998904000|2018-06-26 15:41:44|HelloWorld|20|0    |20|0|0
1529998905000|2018-06-26 15:41:45|HelloWorld|20|5579 |20|0|728
1529998906000|2018-06-26 15:41:46|HelloWorld|20|15698|20|0|0
1529998907000|2018-06-26 15:41:47|HelloWorld|20|19262|20|0|0
1529998908000|2018-06-26 15:41:48|HelloWorld|20|19502|20|0|0
1529998909000|2018-06-26 15:41:49|HelloWorld|20|18386|20|0|0

其中resource代表资源名称, p 代表通过的请求, block 代表被阻止的请求, s 代表成功执行完成的请求个数, e 代表用户自定义的异常, rt 代表平均响应时长。 可以看到,这个程序每秒稳定输出 "hello world" 20 次,这个和控制台规则中预先设定的阈值是一样的。

  1. 有了上面的文件名后缀为 -metrics.log.xxx 的持久化调用记录日志后,我们可以调用Sentinel提供的实时监控接口获取持久化调用记录,并通过分类汇总得出 每个第三方下的场景(接口)的总数、异常数、耗时等信息,并通过计算得数成功率、失败率等信息,用于实时监控模块关闭通道的依据。

实时查询 相关 API: GET /metric

curl http://localhost:8719/metric?startTime=XXXX&endTime=XXXX&maxLines=XXXX   // 查询当前系统所有资源信息,智能路由使用
curl http://localhost:8719/metric?identity=XXX&startTime=XXXX&endTime=XXXX&maxLines=XXXX  


需指定以下 URL 参数:

identity:资源名称 startTime:开始时间(时间戳) endTime:结束时间 maxLines:监控数据最大行数

返回和 资源的秒级日志 格式一样的内容。例如:

1529998904000|2018-06-26 15:41:44|interface1|100|0|0|0|0
1529998905000|2018-06-26 15:41:45|interface2|4|5579|104|16|728
1529998906000|2018-06-26 15:41:46|interface3|0|15698|0|0|0
1529998907000|2018-06-26 15:41:47|interface1|0|19262|0|0|0
1529998908000|2018-06-26 15:41:48|interface1|0|19502|0|0|0
1529998909000|2018-06-26 15:41:49|interface2|0|18386|0|0|0
1529998910000|2018-06-26 15:41:50|interface4|0|19189|0|0|0
1529998911000|2018-06-26 15:41:51|interface1|0|16543|0|0|0
1529998912000|2018-06-26 15:41:52|interface1|0|18471|0|0|0
1529998913000|2018-06-26 15:41:53|interface1|0|19405|0|0|0

  1. 查出来后利用Java进行分类汇总统计,实时统计方法如下:
  private static final String EX = "?";
  private static final String SPLIT = "\\|";
/**
   * 一定时间间隔统计调用流水情况
   * @param timeSpan 时间间隔(以秒为单位)
   * @return
   * @throws ParseException
   */
  public  Map<String,Map<String,Long>> statisticBlockCount(int timeSpan) throws ParseException {
    String result = requestSentinel(timeSpan, Calendar.SECOND,null);
    if (null == result || StringUtils.isEmpty(result)) {
      log.info("实时统计异常,统计结果为空,请确认线上是否有调用流水!!!");
      return null;
    }
    String[] resultList = result.split("\n");
    String[] copyOfResultList = Arrays.copyOf(resultList, (resultList.length - 1));
    List<SentinelStatistic> recodes = Arrays.stream(copyOfResultList).map(resp -> {
      String[] rpt = resp.split(SPLIT);
      SentinelStatistic record = new SentinelStatistic();
      record.setResourceName(rpt[1]);
      record.setBlockCount(Integer.parseInt(rpt[3]));
      record.setSuccessCount(Integer.parseInt(rpt[2]) - Integer.parseInt(rpt[5]));
      record.setPassCount(Integer.parseInt(rpt[2]));
      record.setExceptionCount(Integer.parseInt(rpt[5]));
      record.setTotalCount(Integer.parseInt(rpt[2] + Integer.parseInt(rpt[3])));
      return record;
    }).collect(Collectors.toList());
    Map<String, Long> successResultMap = new HashMap<>(3 << 1);
    Map<String, Long> blockResultMap = new HashMap<>(3 << 1);
    Map<String, Long> exceptionResultMap = new HashMap<>(3 << 1);
    Map<String, Long> totalResultMap = new HashMap<>(3 << 1);
    Map<String, Long> passResultMap = new HashMap<>(3<<1);
    // 分组
    Map<String, List<SentinelStatistic>> countMap = recodes.stream().collect(
            Collectors.groupingBy(SentinelStatistic::getResourceName));
    Set<String> set = countMap.keySet();
    // 遍历统计存到缓存中
    for (String resourceName : set) {
      List<SentinelStatistic> list = countMap.get(resourceName);
      long successSum = 0;
      long blockSum = 0;
      long exceptionSum = 0;
      long totalSum = 0;
      long passSum = 0;
      for (int i = 0; i < list.size(); i++) {
        successSum = successSum + list.get(i).getSuccessCount();
        blockSum = blockSum + list.get(i).getBlockCount();
        exceptionSum = exceptionSum + list.get(i).getExceptionCount();
        totalSum = totalSum + list.get(i).getTotalCount();
        passSum = passSum+list.get(i).getPassCount();
      }
      blockResultMap.put(resourceName,blockSum);
      successResultMap.put(resourceName,successSum);
      exceptionResultMap.put(resourceName,exceptionSum);
      totalResultMap.put(resourceName,totalSum);
      passResultMap.put(resourceName,passSum);
    }
    Map<String, Map<String,Long>> combineResultMap = new HashMap<>(3<<1);
    // blockResultMap 代表被阻止的请求资源名及总数
    combineResultMap.put("blockResultMap",blockResultMap);
    // blockResultMap 代表成功的资源名及总数
    combineResultMap.put("successResultMap",successResultMap);
    // exceptionResultMap 代表异常资源名及总数
    combineResultMap.put("exceptionResultMap",exceptionResultMap);
    // totalResultMap 代表总的请求总数=blockResultMap+successResultMap+exceptionResultMap+passResultMap
    combineResultMap.put("totalResultMap",totalResultMap);
    // passResultMap 代表通过的
    combineResultMap.put("passResultMap",passResultMap);
    return combineResultMap;
  }

解析后的结构数据如下:

{
    "successResultMap":{
        "interface1":100,
        "interface2":104,
        "st-interface3":14,
        "st-interface4":2,
        "ali-interface5":1
    },
    "exceptionResultMap":{
        "interface1":0,
        "interface2":16,
        "interface3":0,
        "interface4":0,
        "interface5":0
    },
    "totalResultMap":{
        "interface1":40,
        "interface2":5703,
        "interface3":140,
        "interface4":20,
        "interface5":10
    },
    "passResultMap":{
        "interface1":4,
        "interface2":4,
        "interface3":14,
        "interface4":2,
        "interface5":1
    },
    "blockResultMap":{
        "interface1":0,
        "interface2":5579,
        "interface3":0,
        "interface4":0,
        "interface5":0
    }
}

自动切走

实时监控模块后续逻辑可根据上述统计结果和预先设置的阈值进行对比,再决定是否关闭某个第三方下某个场景/接口,前提是这个场景有其他通道的备份可用

自动切回

监控自动回切的主要思想是对故障的第三方通道进行小幅放量,通过检测放量后的第三方接口返回值的成功率判断通道是否恢复正常。如果小幅放量的认证成功率正常则继续放量,反之则将通道切回故障,隔一段时间再重新开始进行放量测试,直到将通道置为正常为止。 算法思想及自动回切状态机制如下: ![在这里插入图片5描述]

此过程的关键点是通道放量节奏的控制,通道放量节奏的影响要素有三个:首次放量的大小、两次放量时间间隔、通道放量速度,放量节奏太快则易造成二次故障,太慢则通道恢复过慢,无法达到缩短故障影响时间要求。

某段时间间隔内(5min*2^n),随机从生产场景中引流请求5条(可配置的),调用路由的指定通道接口,进行沙箱验证,搭配TimerTask抽象类完成
计算验证结果的成功率,判断是否开启开关(服务的健康状况 = 请求成功数 / 请求总数)
           if  成功率  >  预先设置该通道的成功率  ,开启路由开关

           if  成功率  <= 预先设置成功率,开关继续关闭,并且 n+1

 记录每次通道状态的变更,并通知到系统对应负责人

4、总结

后续优化方向主要是针对每个第三方实现根据日常数据的学习加人工干预,对每个第三方接口评分作为后续是否关闭通道的阈值。 生产环境验证效果如下图: ![在这里插入图片6描述]