【问题复盘】第三方接口变慢导致服务崩溃

一、事件经过

-1、一个不在公司的下午,接到客户投诉,说平台不能访问了。

0、介入调查,发现服务器http请求无法访问,https请求却可以正常访问,一时有些无法理解;(后来发现,http和https协议是两个不同的线程池。)

1、排查发现Tomcat的线程数达到maxThreads设定的值,于是选择调大maxThreads,原以为问题会这样就被解决了,但在重启服务后,线程数飙升,不一会儿线程数又达到最大值;

Linux查看Tomcat线程命令 (可用top命令查看进程ID)

ps -T -p <Tomcat进程ID> | wc -l

详解tomcat的连接数与线程池 - 编程迷思 - 博客园 (cnblogs.com)

2、开始陷入迷惘,因为最近的代码只是简单修复了一些bug,不应该会造成线程数剧增。 为了进一步确认是否是代码造成的问题,将代码回滚到之前正常的版本,结果线程数同样剧增,直至设定的最大值。

3、困惑加深,难道不是代码的问题? 陷入毫无头绪之中,于是选择以日志作为突破口,有一行WARN日志引起了注意。 这行WARN日志会反复出现,而且出现的同时伴随着不断增加的线程数,由此断定,这行日志就是问题的关键。

4、柳暗花明。 但这行日志看不懂,于是开始了面向百度解决问题。去网上找各种关于这个日志的博客,尝试了博客里的多种方法,也试过了GPT提供的方法,但始终无法确定日志产生的原因,这行WARN日志依旧一直存在。

5、或许,一开始方向就错了。 解决警告日志的问题,就应该先定位到,具体是哪一行代码产生的警告日志。或许是夜太深了,连排查问题的基本思路都迷糊了。

6、突然,在网上看到一篇说明这个报警日志的博客,里面提到了一句,产生这个报警日志的原因在于调用了第三方接口,问题是出现在第三方平台。

关键文章

7、起初,这篇博客没有引起我的注意,因为印象中好像平台基本没有调用第三方接口。但当试了各种方法都没有用以后,想起了这篇博客说的,再试试或许能行呢? 刚好也想到最近确实有调用一个上传记录的第三方接口,于是选择将那部分代码注释了,然后进行测试。

8、果然,一注释掉那行代码,线程数就立刻不增加了。再测试一下那个三方接口,发现请求一次居然要花费5秒钟,之前那个接口调用只需要1-2秒,某些神秘原因导致接口变慢。而设备访问自己平台的频次是2秒一次,2秒没有结果后,就会重新再次发起请求。相当于因为请求超时,然后设备一直不停的访问。(在排查过程中,有那么几次怀疑服务器是被人攻击了,因为在设备配置的是ip+端口号,有心人想要攻击实在太容易了)

9、注释三方接口代码重新部署后,服务又恢复了正常。

10、悬着的心终于放下了,看看外面,天空已经露出了一丝丝鱼肚白。。。

二、问题代码优化

  • 代码业务逻辑

设备上传数据到平台,平台再把数据上传到第三方平台。

  • 初始代码

初始逻辑:在controller层,拿到数据后处理后,调用postDataToAPI方法上传数据。

@Autowired
private RestTemplate restTemplate;
/**
 * 发送POST请求
 * @param url
 * @param requestBody
 * @return
 */
public  boolean postDataToAPI(String url, String requestBody) {
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_JSON);
    HttpEntity<String> entity = new HttpEntity<>(requestBody, headers);
    ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
    String bodyStr = response.getBody().toString();
    JSONObject responseBodyObject = JSONObject.parseObject(bodyStr);
    String code = responseBodyObject.getString("ResultCode");
    if (!StringUtils.isEmpty(code) && "0".equals(code)) {
        return true;
    }
    return false;
}
  • 改进后的代码

改进逻辑:

controller层拿到数据后,不调用postDataToAPI方法,而是将数据保存到数据库,然后将成功结果返回。

调用三方接口上传数据的过程,单独启用一个定时任务执行。在执行的过程中,使用FixedThreadPool线程池来多线程执行,增加上传数据的效率。 如果数据上传成功,则删除数据库数据,失败则保留至下一轮尝试再次上传。

// 固定线程数的线程池
private final ExecutorService executorService = Executors.newFixedThreadPool(5);
@Scheduled(fixedRate = 60000)
public void timedUpload(){
    // 获取第一页数据
    List<TemptData> list1 = getDataByPage(0, 8);
    // 获取第二页数据
    List<TemptData> list2 = getDataByPage(8, 8);
    // 获取第三页数据
    List<TemptData> list3 = getDataByPage(16, 8);
    // 获取第四页数据
    List<TemptData> list4 = getDataByPage(24, 8);
    // 获取第五页数据
    List<TemptData> list5 = getDataByPage(32, 8);
    // 提交任务给线程池执行
    executorService.submit(() -> executeUpload(list1));
    executorService.submit(() -> executeUpload(list2));
    executorService.submit(() -> executeUpload(list3));
    executorService.submit(() -> executeUpload(list4));
    executorService.submit(() -> executeUpload(list5));
}

// 查询数据
private List<TemptData> getDataByPage(int start, int pageSize) {
    return temptDataMapper.getDataList(start, pageSize);
}

// 上传数据
public void executeUpload(List<TemptData> list) {
    if (!list.isEmpty()){
        for (TemptData temptData : list) {
            sendToDongshun(temptData);
        }
    }
}

注意:

  • getDataByPage获取数据时,需要考虑重复消费的问题。因为可能在60秒内,线程还没有执行完,然后下一轮又开始拿到相同的数据执行了。
  • 需要考虑到异常导致数据上传失败的问题,可以采用try catch finally的方式,将上传失败的数据保留和标记。

  • 35
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值