【问题描述】
APP首页接口响应慢,加载不出来楼层。
【影响范围】
APP首页
【事故级别】
P0
【处理过程】
9:14 群里反馈app响应慢,首页楼层加载不出来 9:20 定位昨天首页上过线,紧急重启,重启后依然无效; 9:30 评估后进行版本回滚,回滚后首页加载正常; 10:00 根据监控定位首页接口响应变慢; 10:30 review代码,发现新版本上了几个新楼层,首页网关设置了多线程超时的时间,但是并未生效; 10:50 定位到原因,修改代码上线;
【故障原因】
网关多线程批量调用下游接口,每个楼层都设置了超时时间,如果有的楼层超时则默认抛弃不展示,但是实际场景中,超时时间并未生效,而是等所有楼层接口执行加载后才结束,问题代码如下所示:
超时时间设置的是 380ms,执行第一次 for 循环时,假如任务执行的时间是 300ms,则可以正常展示,接着下一个任务执行时间是 500ms,这个楼层仍然能够显示出来,因为它是在第一个任务等待 300ms 的基础之上又等待 380ms 超时,两个时间加起来是 680ms,500ms 的任务是可以拿到结果的,依次类推,越往后的任务能够允许等待的超时时间会特别久。
我们通过代码进行复现:
public class FuatureTaskDemo {
static ExecutorService mExecutor = Executors.newFixedThreadPool(3);
private class QuoteTask implements Callable<Long> {
public final Long time;
public QuoteTask(Long time) {
this.time = time;
}
@Override
public Long call() throws Exception {
Thread.sleep(time);
return time;
}
}
/**
* @return
*/
public void getWorker() throws Exception {
List<FutureTask> features = new ArrayList<>();
FutureTask<Long> task = new FutureTask<>(new QuoteTask(300L));
FutureTask<Long> task1 = new FutureTask<>(new QuoteTask(680L));
FutureTask<Long> task2 = new FutureTask<>(new QuoteTask(800L));
features.add(task);
features.add(task1);
features.add(task2);
features.forEach(futureTask -> {
mExecutor.execute(futureTask);
});
int timeOut = 380;
long start = System.currentTimeMillis();
for (FutureTask future : features) {
try {
System.out.println("执行任务的结果:" + (future.get(timeOut, TimeUnit.MILLISECONDS)));
// Long excuteTime = System.currentTimeMillis() - start;
// timeOut = timeOut - excuteTime.intValue();
// if(timeOut<=0){
// timeOut=1;
// }
} catch (CancellationException e) {
System.out.println("任务超时异常:" + e.getMessage());
} catch (Exception e) {
System.out.println("任务异常:" + e.getMessage());
}
}
long end = System.currentTimeMillis();
System.out.println("总共执行任务的时间:" + (end - start));
mExecutor.shutdown();
}
public static void main(String[] args) {
FuatureTaskDemo it = new FuatureTaskDemo();
try {
it.getWorker();
} catch (Exception e) {
e.printStackTrace();
}
}
}
我们把task1设置为680ms,执行结果如下所示:
我们把task1设置为690ms,执行结果如下所示:
如何解决?
验证了我们上述的事故原因。那么我们如何解决这个问题呢?
方式一:
累减的方式来定义每个线程的超时时间,最终线程时间归1;(上述代码里的注释去掉) 我们可以依次动态设置每个任务的超时时间,拿设置的允许任务执行的最大超时时间,减去获取任务结果已经执行的时间,作为下一个任务获取结果超时时间(最小超时时间设置为 1)即可;
方式二:
使用 invokeAll 实现设置所有任务的超时时间,改造代码如下
public class FuatureTaskDemo {
static ExecutorService mExecutor = Executors.newFixedThreadPool(3);
private static class QuoteTask implements Callable<Long> {
public final Long time;
public QuoteTask(Long time, ConcurrentHashMap<Long, Long> map) {
this.time = time;
map.put(time,time);
}
@Override
public Long call() throws Exception {
Thread.sleep(time);
return time;
}
}
/**
* @return
*/
public void getWorker() throws Exception {
int timeOut = 380;
long start = System.currentTimeMillis();
ConcurrentHashMap<Long, Long> map = new ConcurrentHashMap<>();
List<QuoteTask> lists = new ArrayList<>();
lists.add(new QuoteTask(300L,map));
lists.add(new QuoteTask(500L,map));
lists.add(new QuoteTask(800L,map));
List<Future<Long>> futuresList = mExecutor.invokeAll(lists, timeOut, TimeUnit.MILLISECONDS);
for (Future<Long> future : futuresList) {
try {
System.out.println("执行任务的结果:" + (future.get(timeOut, TimeUnit.MILLISECONDS)));
} catch (CancellationException e) {
System.out.println("任务超时异常:" + e.getMessage());
} catch (Exception e) {
System.out.println("任务异常:" + e.getMessage());
}
}
long end = System.currentTimeMillis();
System.out.println("执行map结果" + JSON.toJSONString(map));
System.out.println("总共执行任务的时间:" + (end - start));
mExecutor.shutdown();
}
}
invokeAll 底层也是按任务的获取超时时间递减实现
总结:
- 我们在使用FutureTask多线程开发的时候,一定注意子线程的超时时间问题
- 在业务场景下一定要考虑是否整体设置一个超时时间
- 如果业务核心的话,也可以考虑采用线程编排的方式,隔离线程池的方式 进行并行。