多线程异常处理:挖掘页面空窗背后的原因
作为一名应用开发,大家是否有遇到以下现象,为什么一套非常优秀的兜底机制还是会出现页面空窗现象?本文将会通过实例和大家分享,作者在线程池使用过程中遇到的问题:异常处理,以及下线程池的参数设置经验。
什么现象
先解释下什么是空窗,就是数据缺失导致某块或整页出现空白的现象。
事情有点早了,刚接聚划算,还没来得及看逻辑,就被告知,压测时页面出现了空窗,像这样:
原因是什么
其实就是对应的接口超时或者数据处理异常,导致该块儿数据没有返回。
我们的代码是运行在阿拉丁容器里的,阿拉丁本身是有兜底机制的,并且有两层:
如果接口发生异常,阿拉丁会从tair里取缓存的数据返回给前端做兜底
如果阿拉丁也没有兜住,前端接收到错误的code,会自动从cdn取对应接口的数据做兜底
这套机制还是非常优秀的,但为什么还是出现了空窗了。
翻看代码发现,是我们把对应的异常给吃掉了,没有抛给阿拉丁容器,代码是这样的:
try {executorService.invokeAll(callableHashSet);
} catch (Exception e) {throw new RuntimeException(e);
}
初看,是不是以为把try catch拿掉就没问题了,然而不是,我们看看java.util.concurrent.ExecutorService#invokeAll的实现,先看我们最常用的ThreadPoolExecutor,它的invokeAll方法在父类AbstractExecutorService里实现:
这里变量ignore的命名非常漂亮,想都不用想,它被忽略了,为什么要看这个ExecutionException,是因为线程里发生的异常都被包装成了ExecutionException,我们跟着AbstractExecutorService##invokeAll看下,上图有个newTaskFor,看下实现:
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {return new FutureTask<T>(callable);
}
看看FutureTask#get方法:
public V get() throws InterruptedException, ExecutionException {int s = state;if (s <= COMPLETING)s = awaitDone(false, 0L);return report(s);
}
最终在report方法里实现:
private V report(int s) throws ExecutionException {Object x = outcome;if (s == NORMAL)return (V)x;if (s >= CANCELLED)throw new CancellationException();throw new ExecutionException((Throwable)x);
}
可以看到,如果线程里抛出了异常,都被包装成了ExecutionException,而ThreadPoolExecutor#invokeAll方法里忽略了这个异常,导致我们根本捕捉不到异常。
上边说的是ThreadPoolExecutor,我们再看另一个常用的ExecutorService的实现类ForkJoinPool:
看到了吧,方法命名就告诉你了,我不会抛异常给你,进去看看ForkJoinTask#quietlyJoin:
注释说得很清楚,不抛异常。
怎么解决
首先,根据上边的分析,要慎用invokeAll,解决也很简单,可以有以下几种方式:
1. 能让主线程感知到异常,并向外抛,就可以触发阿拉丁的兜底
2. 模块内部做数据缓存,捕捉到异常以后取缓存数据做兜底
▐ 第一回合
因为对整体的逻辑没摸透,不敢直接替换掉invokeAll,会影响整个聚划算首页,时间又比较急,就先缩小改动范围,选取方法二:
在对应模块内容做数据缓存,为了兼顾时效性(聚划算商品有上团和下团时间,所以时效性很强),做了1分钟的缓存和5分钟的缓存,如果发生异常,按优先级取缓存,优先1分钟的缓存。
为了减轻写压力,只针对一定比例的请求写缓存
效果
上线之后没问题,然而第二次全链路压测,半夜又收到消息说空窗了。
第一回合失败。
▐ 第二回合
经过分析,可能有多个原因:
1. 应该是压测状态下,下游服务持续压力大,导致缓存数据过期,
2. 写入缓存的数据也没有做好校验,可能写入不合法的数据
继续做调整:
1. 严格校验写入缓存的数据,保持缓存数据的合法性
2. 既然是兜底数据,可以直接缓存在内存,这样就不用关心写比例,直接100%缓存合法数据,并且不设置失效时间,这样保证兜底时总能取到最新的合法数据
3. 把该组件的Callable从invokeAll里拎出来,增加预案,可以触发整页兜底,作为最后的保命手段,如下:
效果
后续压测和日常没再出现过空窗,就这个模块来说,应该没问题了。
这样就好了吗
其实不应该结束,上述方案都是在时间紧张的情况下做的临时补救措施,代码里到处是特判逻辑,我们应该有更系统的设计方案:
1. 模块异常都外抛,触发阿拉丁的兜底,但阿拉丁的兜底是接口级别的,我们一个接口里边通常包含多个模块,如果因为次要模块导致用户看到的主要模块也是兜底的数据,用户体验不好
2. 针对每一个模块做独立的兜底,但像上述方法一样,一个模块一个模块来改,太累,也容易遗漏。我们应该有一个框架性设计,让以后的开发只需要关心业务逻辑,而不用关心这些非功能性问题,这点我准备在EasyWidget里边来实现,基础设施已经具备,只需要在模板方法里加几行就能实现。
总结一下
这里边遇到的主要问题是没有正确处理线程池的异常和兜底设计不完善导致,兜底的设计上边提到了思路,我们再看下处理子线程内部异常的常用方式:
▐ 通过原子变量
AtomicBoolean exception = new AtomicBoolean(false);Callable<Void> qwbkt = () -> {try {qwbktSections.add(qwbktManager.query(context, null));} catch (Throwable t) {context.getLogger().error("qwbkt exception:", t);exception.set(true);}return null;
};//...if (exception.get()) {throw new RuntimeException("queryError");
}
▐ 以code形式返回
Callable<String> task = new Callable<String>() {@Overridepublic String call() throws Exception {Result<String> result = new Result<>();try {//..} catch (Exception e) {result.setCode("500");}return result;}
};
▐ 老老实实future#get
try {String s = future.get();
} catch (InterruptedException e) {//..
} catch (ExecutionException e) {//todo: 这里处理线程内部异常
}
再说说线程池的其它问题
▐ 线程池设置不合理
看到很多应用里的线程池参数不合理,尤其是很多新同学,分不清前台应用和后台任务需要的线程数和拒绝策略怎么设置。
很多同学从教程里边或者某些框架源码里边看到线程池的线程数尽量跟机器核数保持一致,就一直保持这个设置。
还有看到前台应用了设置了少量的线程,队列长度是10000。这种情况在遇到突发流量的情况下很容易把自己拖垮,之所以一直没触发问题,一种原因可能是没有遇到过大流量,另一种可能是被限流保护了,一旦限流没有设置好,就可能遇到致命问题。
这里简单说下自己的经验:
搞清楚核心线程数、最大线程数、任务队列的工作原理,核心线程用完了是先放任务队列,队列满了才会继续增加线程数至最大线程数
前台应用队列长度一定不能太大,根据线程数、接口RT、客户端所能接受的RT来计算队列长度
分清我们的应用是CPU密集型还是IO密集型,大多数情况我们的业务应用都是IO密集型的,这种情况下不必拘泥于线程数跟核数保持一致
用Runtime.getRuntime().availableProcessors()设置线程数的时候,你以为取到的是虚拟机的线程数,但很可能取到的是物理机的线程数,要注意这个坑
前台应用的线程数必须通过压测不断调整,才能获得合理的线程数,但一旦依赖接口的RT等情况发生变化,线程数就可能不再合理,所以合理的线程数很难保持
后台应用如果不关心响应的及时性,可以设置较大的队列,但要关注机器内存,也要主要机器重启时的任务丢失问题
▐ 线程池的关闭
任务不能丢失的时候一定要在jvm关闭的时候通过钩子关闭线程池。
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {@Overridepublic void run()
{threadPool.shutdown();}
}));
上述方法只在jvm正常关闭的时候有效,如果强杀或断电等情况还是有问题,就要做更强有力的保障,如先发消息队列,再处理。
✿ 拓展阅读
作者|朱天富(海培)
编辑|橙子君
出品|阿里巴巴新零售淘系技术
多线程异常处理:挖掘页面空窗背后的原因相关推荐
- 异常处理器详解 Java多线程异常处理机制 多线程中篇(四)
在Thread中有异常处理器相关的方法 在ThreadGroup中也有相关的异常处理方法 示例 未检查异常 对于未检查异常,将会直接宕掉,主线程则继续运行,程序会继续运行 在主线程中能不能捕获呢? 我 ...
- java多线程 异常处理_Java8多线程ForkJoinPool:处理异常
java多线程 异常处理 引入Java8 lambda背后的主要动机之一是能够尽可能轻松地使用多核的能力(请参阅精通Lambdas:多核世界中的Java编程 ). 只需将代码从collection.s ...
- 异步多线程(五)多线程异常处理
异步多线程(五)多线程异常处理 参考文章: (1)异步多线程(五)多线程异常处理 (2)https://www.cnblogs.com/JohnTang/p/11010357.html (3)http ...
- Java 异常处理(标准抛异常、异常处理、多异常、Finally、多线程异常处理、获取异常的堆栈信息、链试异常、自定义异常)
使用 catch 处理异常(标准抛异常) public class Main {public static void main (String args[]) {int array[]={20,20, ...
- 设计灵感|App中的页面空状态应该如何表现?
什么是空状态? 空状态:顾名思义就是空白的状态,空状态是对没有数据页面的补充,一种对零数据的设计形式(如下图所示:无网络.订单为空.没有关注的人以及无法连接网络等等这些页面就是所谓的"空状态 ...
- .NET异步和多线程系列(四)- 多线程异常处理、线程取消、多线程的临时变量问题、线程安全和锁lock
本文是.NET异步和多线程系列第四章,主要介绍的是多线程异常处理.线程取消.多线程的临时变量问题.线程安全和锁lock等. 一.多线程异常处理 多线程里面抛出的异常,会终结当前线程,但是不会影响别的线 ...
- 空窗期要抓紧了.....
下面是一名程序员的求职遭遇: 刚得知被HR挂了的原因是离职超过3个月,当时快过年了就给自己放了个假,没想到赶上了"铜三铁四". 楼主问大家:招聘时是否有一条不成文的规定,离职超过几 ...
- 《大数据时代》读书笔记——知道“是什么”就够了,没必要知道“为什么”。我们不必非得知道现象背后的原因,而是要让数据自己“发声”
引言--一场生活.工作与思维的大变革 今天,一种可能的方式,亦是本书采取的方式,认为大数据是人们在大规模数据的基础上可以做到的事情,而这些事情在小规模数据的基础上是无法完成的.大数据是人们获得新的认知 ...
- 出现并发问题背后的原因
出现并发问题背后的原因 众所周知,CPU.内存.I/O设备的速度差异特别的大,而根据木桶理论,程序整体的性能取决于最慢的操作--读写I/O设备,为了合理利用计算机的高性能,平衡三者的速度差异,做了以下 ...
最新文章
- python实现数据库查询_通过Python实现mysql查询数据库实例
- 一个popup弹窗实现思路--(基于mintui分析)
- 英文句子改写在线软件_试完这些英文论文写作辅助神器,你会发现新大陆der~...
- 张小龙宣布微信小程序可直接从桌面进入
- 【linux】Shell脚本中调用另外一个脚本的方法
- android 布局管理器,【Android开发】布局管理器-表格布局
- Macaca上手体验
- LABJS的使用教程
- 单肩包属于什么类目_Lazada时尚类目成为优势类目?
- 测试有道:微软测试技术心得
- 二维数组遍历_布尔值数组的状态压缩
- 刷题记录 kuangbin带你飞专题一:简单搜索
- RS485电路原理以及设计
- 一个无穷积分方程的求解
- ADNI数据集相关概念初步整理
- 2019前端书籍推荐,前端PDF书籍,前端书籍下载
- web版Project简介
- 《程序员》12月精彩内容:双11技术决战
- SDLC开发过程:基于DevSecOps理念的解决方案
- JavaCV音视频开发宝典:JavaCV使用gdigrab方式实现windows录屏(windows屏幕画面抓取/采集,可实现高帧率屏幕截屏、录屏功能)