作者:树洞君
链接:https://juejin.cn/post/7064376361334358046
事故描述
从6点32分开始少量用户访问app时会出现首页访问异常,到7点20分首页服务大规模不可用,7点36分问题解决。
整体经过
6:58 发现报警,同时发现群里反馈首页出现网络繁忙,考虑到前几日晚上门店列表服务上线发布过,所以考虑回滚代码紧急处理问题。
7:07 开始先后联系XXX查看解决问题。
7:36 代码回滚完,服务恢复正常。
事故根本原因-事故代码模拟
public static void test() throws InterruptedException, ExecutionException {
Executor executor = Executors.newFixedThreadPool(3);
CompletionService<String> service = new ExecutorCompletionService<>(executor);
service.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return "HelloWorld--" + Thread.currentThread().getName();
}
});
}
根源就在于ExecutorCompletionService结果没 调用take,poll方法。
正确的写法如下所示:
public static void test() throws InterruptedException, ExecutionException {
Executor executor = Executors.newFixedThreadPool(3);
CompletionService<String> service = new ExecutorCompletionService<>(executor);
service.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return "HelloWorld--" + Thread.currentThread().getName();
}
});
service.take().get();
}
一行代码引发的血案,而且不容易被发现,因为oom是一个内存缓慢增长的过程,稍微粗心大意就会忽略,如果是这个代码块的调用量少的话,很可能几天甚至几个月后暴雷。
操作人回滚or重启服务器确实是最快的方式,但是如果不是事后快速分析出oom的代码,而且不巧回滚的版本也是带oom代码的,就比较悲催了,如刚才所说,流量小了,回滚或者重启都可以释放内存;但是流量大的情况下,除非回滚到正常的版本,否则GG。
推荐一个开源免费的 Spring Boot 实战项目:https://github.com/javastacks/spring-boot-best-practice
探询问题的根源
为了更好的理解ExecutorCompletionService的 “套路” 我们用 ExecutorService来作为对比,可以让我们更好的清楚,什么场景下用ExecutorCompletionService。
先看ExecutorService代码(建议down下来跑一跑)
public static void test1() throws Exception{
ExecutorService executorService = Executors.newCachedThreadPool();
ArrayList<Future<String>> futureArrayList = new ArrayList<>();
System.out.println("公司让你通知大家聚餐 你开车去接人");
Future<String> future10 = executorService.submit(() -> {
System.out.println("总裁:我在家上大号 我最近拉肚子比较慢 要蹲1个小时才能出来 你等会来接我吧");
TimeUnit.SECONDS.sleep(10);
System.out.println("总裁:1小时了 我上完大号了。你来接吧");
return "总裁上完大号了";
});
futureArrayList.add(future10);
Future<String> future3 = executorService.submit(() -> {
System.out.println("研发:我在家上大号 我比较快 要蹲3分钟就可以出来 你等会来接我吧");
TimeUnit.SECONDS.sleep(3);
System.out.println("研发:3分钟 我上完大号了。你来接吧");
return "研发上完大号了";
});
futureArrayList.add(future3);
Future<String> future6 = executorService.submit(() -> {
System.out.println("中层管理:我在家上大号 要蹲10分钟就可以出来 你等会来接我吧");
TimeUnit.SECONDS.sleep(6);
System.out.println("中层管理:10分钟 我上完大号了。你来接吧");
return "中层管理上完大号了";
});
futureArrayList.add(future6);
TimeUnit.SECONDS.sleep(1);
System.out.println("都通知完了,等着接吧。");
try {
for (Future<String> future : futureArrayList) {
String returnStr = future.get();
System.out.println(returnStr + ",你去接他");
}
Thread.currentThread().join();
} catch (Exception e) {
e.printStackTrace();
}
}
三个任务,每个任务执行时间分别是 10s、3s、6s 。通过JDK线程池的 submit 提交这三个 Callable类型的任务。
- step1 主线程把三个任务提交到线程池里面去,把对应返回的 Future 放到 List 里面存起来,然后执行“都通知完了,等着接吧。”这行输出语句。
- step2在循环里面执行 future.get() 操作,阻塞等待。 最后结果如下:
先通知到总裁,也是先接总裁 足足等了1个小时,接到总裁后再去