一、事件经过
-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的方式,将上传失败的数据保留和标记。