你好呀,是歪歪。

前几天,就在咱们还沉浸在等待新年到来的喜悦氛围的时分,在一个中心链路上的中心系统中,我踩到一个坑的一比的坑,要不是我沉着冷静,处理思路忙中有序,处理手法雷厉风行,把它给摧残在萌芽阶段了,那这玩意必定得引发一个比较严重的出产问题。

从问题呈现到定位到这个问题的根本原因,我大概是花了两天半的时刻。

所以写篇文章给咱们复盘一下啊,这个事例便是一个纯技能的问题导致的,和事务的相关度其实并不大,所以你拿曩昔直接添枝加叶,略微改改,往自己的服务上套一下,那便是你的了。

我再说一次:尽管现在不是你的,可是你看完之后便是你的了,你理解我意思吧?

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

表象

工作是这样的,我这边有一个服务,你能够把这个服务粗犷的理解为是一个商城相同的服务。有商城必定就有下单嘛。

然后接到上游服务反馈,说调用下单接口偶尔有调用超时的状况呈现,断断续续的呈现好几回了,给了几笔流水号,让我看一下啥状况。其时我的榜首反应是不或许是我这边服务的问题,由于这个服务上次上线都至少是一个多月前的工作了,所以不或许是由于近期服务投产导致的。

可是下单接口,你听名字就知道了,中心链接上的中心功用,不能有一点麻痹大意。

每一个恳求都很重要,客户下单体会不好,或许就不买了,构成买卖丢失。

买卖上不去营业额就上不去,营业额上不去赢利就上不去,赢利上不去年终就上不去。

想到这一层联系之后,我立马就登陆到服务器上,开端定位问题。

一看日志,确实是我这边接口恳求处理慢了,导致的调用方超时。

为什么会慢呢?

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

于是按照惯例思路先根据日志判别了一下下单接口中调用其他服务的接口相应是否正常,从数据库获取数据的时刻是否正常。

这些判别没问题之后,我转而把目光放到了 gc 上,通过监控发现那个时刻点触发了一次耗时挨近 1s 的 full gc,导致呼应慢了。

由于咱们监控只收集服务近一周的 gc 数据,所以我把时刻拉长后发现 full gc 在这一周的时刻内呈现的频率还有点高,尽管我还没定位到问题的根本原因,可是我定位到了问题的表面原因,便是触发了 full gc。

由于是中心链路,中心流程,所以此时不应该急着去定位根本原因,而是先缓解问题。

好在咱们提前准备了各种原因的应急预案,其中就包括这个场景。预案的内容便是扩展应用堆内存,延缓 full gc 的呈现。

所以我当即进行操作报备并联系运维,按照紧迫预案履行,把服务的堆内存由 8G 扩展一倍,提升到 16G。

尽管这个办法简单粗犷,可是既处理了当前的调用超时的问题,也给了我满足的排查问题的时刻。

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

定位原因

其时我其实一点都不慌的,由于问题在萌芽阶段的时分我就把它给干掉了。

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

不便是 full gc 吗,哦,我的老朋友。

先斗胆假定一波:程序里边某个逻辑不小心搞出了大目标,触发了 full gc。

所以我先是双手插兜,带着监控图和日志恳求,闲庭信步的走进项目代码里边,想要凭仗肉眼找出一点蛛丝马迹……

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

没有任何收成,由于下单服务涉及到的逻辑真的是太多了,服务里边 List 和 Map 随处可见,我很难找到究竟哪里是大目标。

可是我仍是一点都不慌,由于这半响都没有再次发生 Full GC,阐明此时留给我的时刻仍是比较充足的,

所以我恳求了场外帮助,让 DBA 帮我导出一下服务的慢查询 SQL,由于我想或许是从数据库里边一次性取的数据太多了,而程序里边也没有做操控导致的。

我之前就踩过相似的坑。

一个根据客户号查询客户有多少订单的内部运用接口,接口的回来是 List<订单>,看起来没啥毛病,对不对?

一般来说一个个人客户就几十上百,多一点的上千,顶天了的上万个订单,一次性拿出来也不是不能够。

可是有一个客户不知道咋回事,特别宠爱咱们的平台,也是咱们平台的老客户了,一个人居然有挨近 10w 的订单。

然后这么多订单目标搞到到项目里边,原本呼应就有点慢,上游再建议几回重试,直接触发 Full gc,降低了服务呼应时刻。

所以,通过这个工作,咱们定了一个规矩:用 List、Map 来作为回来目标的时分,必须要考虑一下极点状况下会回来多少数据回去。即使是内部运用,也最好是进行分页查询。

好了,话说回来,我拿到慢查询 SQL 之后,根据几个 Full gc 时刻点,比照之后提取出了几条看起来有点问题的 SQL。

然后拿到数据库履行了一下,发现回来的数据量其实也都不大。

此时我仍是一点都不慌,横竖内存够用,并且针对这类问题,我还有一个场外帮助没有运用呢。

第二天我开端找运维搭档帮我每隔 8 小时 Dump 一次内存文件,然后第三天我开端拿着内存文件渐渐剖析。

可是第二天我也没闲着,根据现有的线索重复剖析、推理或许的原因。

然后在观看 GC 收回内存大小监控的时分,发现了一点点端倪。由于触发 Full GC 之后,发现被收回的堆内存也不是特别多。

其时就想到了除了大目标之外,还有一个现象有或许会导致这个现象:内存走漏。

巧的是在第二天又发生了一次 Full gc,这样我拿到的 Dump 文件就更有剖析的价值了。根据前面的猜想,我剖析的时分直接就冲着内存泄漏的方向去查了。

我拿着 5 个 Dump 文件,剖析了在 5 个 Dump 文件中目标数量一向在添加的目标,这样的目标也不少,可是终究定位到了 FutureTask 目标,便是它:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

找到这玩意了再回去定位对应部分的代码就比较简单。

可是你认为定位了代码就完事了吗?

不是的,到这儿才刚刚开端,朋友。

由于我发现这个代码对应的 Bug 躲藏的仍是比较深的,并且也不是我最开端假象的内存走漏,便是一个朴实的内存溢出。

所以值得拿出来仔细嗦一嗦。

示例代码

为了让你沉浸式体会找 BUG 的进程,我高低得给你整一个可复现的 Demo 出来,你拿曩昔就能够跑的那种。

首要,咱们得搞一个线程池

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

需求阐明一下的是,上面这个线程池的中心线程数、最大线程数和行列长度我都取的 1,仅仅为了便利演示问题,在实践项目中是一个比较合理的值。

然后重点看一下线程池里边有一个自定义的叫做 MyThreadFactory 的线程工厂类和一个自定义的叫做 MyRejectedPolicy 的回绝战略。

在我的服务里边便是有这样一个叫做 product 的线程池,用的也是这个自定义回绝战略。

其中 MyThreadFactory 的代码是这样的:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

它和默认的线程工厂之间仅有的差异便是我加了一个 threadFactoryName 字段,便利给线程池里边的线程取一个合适的名字。

更直观的表明一下差异便是下面这个玩意:

原生:pool-1-thread-1
自定义:product-pool-1-thread-1

接下来看自定义的回绝战略:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

这儿的逻辑很简单,便是当 product 线程池满了,触发了回绝战略的时分打印一行日志,便利后续定位。

然后接着看其他部分的代码:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

标号为 ① 的当地是线程池里边运转的使命,我这儿仅仅一个暗示,所以逻辑十分简单,便是把 i 扩展 10 倍。实践项目中运转的使命事务逻辑,会杂乱一点,可是也是有一个 Future 回来。

标号为 ② 的地便利是把回来的 Future 放到 list 调集中,在标号为 ③ 的当地循环处理这个 list 目标里边的 Future。

需求留意的是由于实例中的线程池最多包容两个使命,可是这儿却有五个使命。我这样写的意图便是为了便利触发回绝战略。

然后在实践的项目里边刚刚说到的这一坨逻辑是通过守时使命触发的,所以我这儿用一个死循环加手动敞开线程来暗示:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

整个完好的代码便是这样的,你直接粘曩昔就能够跑,这个事例就能够彻底复现我在出产上遇到的问题:

publicclassMainTest{
publicstaticvoidmain(String[]args)throwsException{
ThreadPoolExecutorproductThreadPoolExecutor=newThreadPoolExecutor(1,
1,
1,
TimeUnit.SECONDS,
newLinkedBlockingQueue<>(1),
newMyThreadFactory("product"),
newMyRejectedPolicy());
while(true){
TimeUnit.SECONDS.sleep(1);
newThread(()->{
ArrayList<Future<Integer>>futureList=newArrayList<>();
//从数据库获取产品信息
intproductNum=5;
for(inti=0;i<productNum;i++){
try{
intfinalI=i;
Future<Integer>future=productThreadPoolExecutor.submit(()->{
System.out.println("Thread.currentThread().getName()="+Thread.currentThread().getName());
returnfinalI*10;
});
futureList.add(future);
}catch(Exceptione){
e.printStackTrace();
}
}
for(Future<Integer>integerFuture:futureList){
try{
Integerinteger=integerFuture.get();
System.out.println(integer);
System.out.println("future.get()="+integer);
}catch(Exceptione){
e.printStackTrace();
}
}
}).start();
}
}
staticclassMyThreadFactoryimplementsThreadFactory{
privatestaticfinalAtomicIntegerpoolNumber=newAtomicInteger(1);
privatefinalThreadGroupgroup;
privatefinalAtomicIntegerthreadNumber=newAtomicInteger(1);
privatefinalStringnamePrefix;
privatefinalStringthreadFactoryName;
publicStringgetThreadFactoryName(){
returnthreadFactoryName;
}
MyThreadFactory(StringthreadStartName){
SecurityManagers=System.getSecurityManager();
group=(s!=null)?s.getThreadGroup():
Thread.currentThread().getThreadGroup();
namePrefix=threadStartName+"-pool-"+
poolNumber.getAndIncrement()+
"-thread-";
threadFactoryName=threadStartName;
}
publicThreadnewThread(Runnabler){
Threadt=newThread(group,r,
namePrefix+threadNumber.getAndIncrement(),
0);
if(t.isDaemon())
t.setDaemon(false);
if(t.getPriority()!=Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
returnt;
}
}
publicstaticclassMyRejectedPolicyimplementsRejectedExecutionHandler{
@Override
publicvoidrejectedExecution(Runnabler,ThreadPoolExecutore){
if(e.getThreadFactory()instanceofMyThreadFactory){
MyThreadFactorymyThreadFactory=(MyThreadFactory)e.getThreadFactory();
if("product".equals(myThreadFactory.getThreadFactoryName())){
System.out.println(THREAD_FACTORY_NAME_PRODUCT+"线程池有使命被回绝了,请重视");
}
}
}
}
}

你跑的时分能够把堆内存设置的小一点,比方我设置为 10m:

-Xmx10m -Xms10m

然后用 jconsole 监控,你会发现内存走势图是这样的:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

哦,我的老天爷啊,这个该死的图,也是我的老伙计了,一个缓慢的继续上升的内存趋势图, 最终疯狂的触发 gc,可是并没有内存被收回,最终程序直接崩掉:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

这绝大概率便是内存泄漏了啊。

可是在出产上的内存走势图彻底看不出来这个趋势,我前面说了,首要由于 GC 状况的数据只会保存一周时刻,所以就算把整个图放出来也不是那么直观。

其次不是由于我牛逼嘛,萌芽阶段就干掉了这个问题,所以没有遇到最终频频触发 gc,可是没啥收回的,导致 OOM 的状况。

所以我再带着你看看另外一个视角,这是我实在定位到问题的视角。便是剖析内存 Dump 文件。

剖析内存 Dump 文件的东西以及相关的文章十分的多,我就不赘述了,你随意找个东西玩一玩就行。我这儿首要是共享一个思路,所以就直接运用 idea 里边的 Profiler 插件了,便利。

我用上面的代码,启动起来之后在四个时刻点分别 Dump 之后,调查内存文件。内存走漏的思路便是找文件里边哪个目标的个数和占用空间是在继续上升嘛,特别是中心还发生过 full gc,这个进程其实是一个比较枯燥且杂乱的进程,在出产项目中或许会剖析出很多个这样的目标,然后都要到代码里边去定位相关逻辑。

可是我这儿极大的简化了程序,所以很简单就会发现这个 FutureTask 目标特别的抢眼,数量在继续添加,并且仍是独占鳌头的:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

然后这个东西还能够看目标占用大小,大概是这个意思:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

所以我还能够看看在这几个文件中 FutureTask 目标大小的改变,也是继续添加:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

就它了,准没错。

好,问题现已能复现了,GC 图和内存 Dump 的图也都给你看了。

到这儿,假如有人现已看出来问题的原因了,能够直接拉到文末点个赞,感谢大佬阅读我的文章。

假如你还没看出端倪来,那么我先给你说问题的根本原因:

问题的根本原因就出在 MyRejectedPolicy 这个自定义回绝战略上。

在带你细嗦这个问题之前,我先问一个问题:

JDK 自带的线程池回绝战略有哪些?

这玩意,老八股文了,存在的时刻比我从业的时刻都长,得张口就来:

  • AbortPolicy:丢掉使命并抛出 RejectedExecutionException 反常,这是默认的战略。
  • DiscardOldestPolicy:丢掉行列最前面的使命,履行后面的使命
  • CallerRunsPolicy:由调用线程处理该使命
  • DiscardPolicy:也是丢掉使命,可是不抛出反常,相当于静默处理。

然后你再回头看看我的自定义回绝战略,是不是和 DiscardPolicy 十分像,也没有抛出反常。仅仅比它更高级一点,打印了一点日志。

当咱们运用默认的战略的时分:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

或许咱们把框起来这行代码粘到咱们的 MyRejectedPolicy 战略里边:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

再次运转,不管是调查 gc 状况,仍是 Dump 内存,你会发现程序正常了,没毛病了。

下面这个走势图便是在回绝战略中是否抛出反常对应的内存走势比照图:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

在回绝战略中抛出反常就没毛病了,为啥?

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

探索

首要,咱们来看一下没有抛出反常的时分,发生了什么工作。

没有抛出反常时,咱们前面剖析了,呈现了十分多的 FutureTask 目标,所以咱们就找程序里边这个目标是哪里出来的,定位到这个当地:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

future 没有被收回,阐明 futureList 目标没有被收回,而这两个目标对应的 GC Root 都是new 出来的这个线程,由于一个活泼线程是 GC Root。

进一步阐明对应 new 出来的线程没有被收回。

所以我给你看一下前面两个事例对应的线程数比照图:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

没有在回绝战略中抛出反常的线程十分的多,看起来每一个都没有被收回,这个当地必定便是有问题的。

然后随机选一个检查详情,能够看到线程在第 39 行卡着的:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

也便是这样一行代码:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

这个办法咱们应该熟悉,由于也没有给等待时刻嘛,所以假如等不到 Future 的成果,线程就会在这儿死等。

也就导致线程不会运转结束,所以不会被收回。

对应着源码说便是有 Future 的 state 字段,即状况不正确,导致线程堵塞在这个 if 里边:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

if 里边的 awaitDone 逻辑略微有一点点杂乱,这个当地其实还有一个 BUG,在 JDK 9 进行了修复,这一点我在之前的文章中写过,所以就不赘述了,你有爱好能够去看看:《Doug Lea在J.U.C包里边写的BUG又被网友发现了。》

总之,在咱们的事例下,终究会走到我框起来的代码:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

也便是当前线程会在这儿堵塞住,比及唤醒。

那么问题就来了,谁来唤醒它呢?

巧了,这个问题我之前也写过,在这篇文章中,有这样一句话:《关于多线程中抛反常的这个面试题我再说最终一次!》

假如子线程捕获了反常,该反常不会被封装到 Future 里边。是通过 FutureTask 的 run 办法里边的 setException 和 set 办法完成的。在这两个办法里边完成了 FutureTask 里边的 outcome 变量的设置,一起完成了从 NEW 到 NORMAL 或许 EXCEPTIONAL 状况的流通。

带你看一眼 FutureTask 的 run 办法:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

也便是说 FutureTask 状况改变的逻辑是被封装到它的 run 办法里边的。

知道了它在哪里等待,在哪里唤醒,揭晓答案之前,还得带你去看一下它在哪里诞生。

它的出生地,便是线程池的 submit 办法:

java.util.concurrent.AbstractExecutorService#submit

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

可是,朋友,留意,我要说可是了。

首要,咱们看一下当线程池的 execute 办法,当线程池满了之后,再次提交使命会触发 reject 办法,而当前的使命并不会被放到行列里边去:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

也便是说当 submit 办法不抛出反常就会把正常回来的这个状况为 NEW 的 future 放到 futureList 里边去,即下面编号为 ① 的当地。然后被标号为 ② 的循环办法处理:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

那么问题就来了:被回绝了的使命,还会被线程池触发 run 办法吗?

必定是不会的,都被回绝了,还触发个毛线啊。

不会被触发 run 办法,那么这个 future 的状况就不会从 NEW 改变到 EXCEPTION 或许 NORMAL。

所以调用 Future.get() 办法就必定一向堵塞。又由于是守时使命触发的逻辑,所以导致 Future 目标越来越多,构成一种内存走漏。

submit 办法假如抛出反常则会被标号为 ② 的当地捕获到反常。

不会履行标号为 ① 的当地,也就不会导致内存走漏:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

道理便是这么一个道理。

处理方案

知道问题的根本原因了,处理方案也很简单。

定位到这个问题之后,我发现项目中的线程池参数配置的并不合理,每次守时使命触发之后,由于数据库里边的数据较多,所以都会触发回绝战略。

所以首要是调整了线程池的参数,让它更加的合理。其时假如你要用这个事例,这个当地你也能够包装一下,动态线程池,巨大上,对吧,以前讲过。

然后是调用 Future.get() 办法的时分,给一个超时时刻,这样至少能帮咱们兜个底。资源能及时开释,比死等好。

最终便是一个经验:自定义线程池回绝战略的时分,必定必定记得要考虑到这个场景。

比方我前面抛出反常的自定义回绝战略其实仍是有问题的,我故意留下了一个坑:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

抛出反常的条件是要满足最开端的 if 条件:

e.getThreadFactory() instanceof MyThreadFactory

假如他人误用了这个回绝战略,导致这个 if 条件不成立的话,那么这个回绝战略仍是有问题。

所以,应该把抛出反常的逻辑移到 if 之外。

一起在排查问题的进程中,在项目里边看到了相似这样的写法:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

不要这样写,好吗?

一个是由于 submit 是有回来值的,你要是不必回来值,直接用 execute 办法不香吗?

另外一个是由于你这样写,假如线程池里边的使命履行的时分出反常了,会把反常封装到 Future 里边去,而你又不关怀 Future,相当于把反常给吞了,排查问题的时分你就哭去吧。

这些都是编码进程中的一些小坑和小留意点。

反转

这一末节的标题为什么要叫反转?

由于以上的内容,除了技能原理是真的,我铺垫的一切和布景相关的东西,全部都是假的。

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

整篇文章从第二句开端便是假的,我根本就没有遇到过这样的一个出产问题,也谈不上摧残在摇篮里,更谈不上是我去处理的了。

可是我在开端的时分说了这样一句话,也是全文仅有一句加粗的话:

尽管现在不是你的,可是你看完之后便是你的了,你理解我意思吧?

所以这个布景其实我前几天看到了“严选技能”发布的这篇文章《严选库存稳定性治理系列:一个线程池回绝战略引发的血案》。

看完他们的这篇文章之后,我想起了我之前写过的这篇文章:《看起来是线程池的BUG,可是我认为是源码规划不合理。》

我写的这篇便是单纯从技能角度去解析的这个问题,而“严选技能”则是从实在场景动身,层层剥茧,抵达了问题的中心。

可是这两篇文章遇到的问题的中心原因其实是一模相同的。

我在我的文章中的最终就有这样一段话:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

巧了,这不是和“严选技能”里边这句话遥遥相对起来了吗:

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

在我重复阅读了他们的文章,了解到了布景和原因之后,我润饰了一下,写了这篇文章来“骗”你。

假如你有那么几个瞬间被我“骗”到了,那么我问你一个问题:假定你是面试官,你问我工作中有没有遇到过比较棘手的问题?

而我是一个只有三年工作经验的求职者。

我用这篇文章中我假想出来的出产问题处理进程,并辅以技能细节,你能看出来这是我“包装”的吗?

然后在描绘完工作之后,再表现一下对于工作的复盘,能够说一下根据这个工作,后面自己对监控层面进行了丰富,比方接口超时率监控、GC 导致的 STW 时刻监控啥的。然后也在公司内构成了“经验经验”文档,主动同步给了其他的搭档,以防重复踩坑,巴拉巴拉巴拉…

横竖吧,以后看到自己觉得好的事例,不要看完之后就完了,多想想怎样学一学,包装成自己的东西。

这波包装,归于手摸手教学了吧?

求个赞,不过分吧?