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

62次阅读

共计 9033 个字符,预计需要花费 23 分钟才能阅读完成。

你好呀,是歪歪。

前几天,就在大家还沉迷在期待春节到来的喜悦气氛的时候,在一个外围链路上的外围零碎中,我踩到一个坑的一比的坑,要不是我从容沉着,解决思路忙中有序,解决伎俩雷厉风行,把它给扼杀在萌芽阶段了,那这玩意必定得引发一个比较严重的生产问题。

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

所以写篇文章给大家复盘一下啊,这个案例就是一个纯技术的问题导致的,和业务的相关度其实并不大,所以你拿过来间接添枝加叶,略微改改,往本人的服务上套一下,那就是你的了。

我再说一次: 尽管当初不是你的,然而你看完之后就是你的了,你明确我意思吧?

表象

事件是这样的,我这边有一个服务,你能够把这个服务粗犷的了解为是一个商城一样的服务。有商城必定就有下单嘛。

而后接到上游服务反馈,说调用下单接口偶然有调用超时的状况呈现,断断续续的呈现好几次了,给了几笔流水号,让我看一下啥状况。过后我的第一反馈是不可能是我这边服务的问题,因为这个服务上次上线都至多是一个多月前的事件了,所以不可能是因为近期服务投产导致的。

然而下单接口,你听名字就晓得了,外围链接上的外围性能,不能有一点麻痹大意。

每一个申请都很重要,客户下单体验不好,可能就不买了,造成交易损失。

交易上不去营业额就上不去,营业额上不去利润就上不去,利润上不去年初就上不去。

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

一看日志,的确是我这边接口申请解决慢了,导致的调用方超时。

为什么会慢呢?

于是依照惯例思路先依据日志判断了一下下单接口中调用其余服务的接口相应是否失常,从数据库获取数据的工夫是否失常。

这些判断没问题之后,我转而把眼光放到了 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。

须要留神的是因为实例中的线程池最多包容两个工作,然而这里却有五个工作。我这样写的目标就是为了不便触发回绝策略。

而后在理论的我的项目外面刚刚提到的这一坨逻辑是通过定时工作触发的,所以我这里用一个死循环加手动开启线程来示意:

整个残缺的代码就是这样的,你间接粘过来就能够跑,这个案例就能够齐全复现我在生产上遇到的问题:

public class MainTest {public static void main(String[] args) throws Exception {

        ThreadPoolExecutor productThreadPoolExecutor = new ThreadPoolExecutor(1,
                1,
                1,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(1),
                new MyThreadFactory("product"),
                new MyRejectedPolicy());

        while (true){TimeUnit.SECONDS.sleep(1);
            new Thread(()->{ArrayList<Future<Integer>> futureList = new ArrayList<>();
                // 从数据库获取产品信息
                int productNum = 5;
                for (int i = 0; i < productNum; i++) {
                    try {
                        int finalI = i;
                        Future<Integer> future = productThreadPoolExecutor.submit(() -> {System.out.println("Thread.currentThread().getName() =" + Thread.currentThread().getName());
                            return finalI * 10;
                        });
                        futureList.add(future);
                    } catch (Exception e) {e.printStackTrace();
                    }
                }
                for (Future<Integer> integerFuture : futureList) {
                    try {Integer integer = integerFuture.get();
                        System.out.println(integer);
                        System.out.println("future.get() =" + integer);
                    } catch (Exception e) {e.printStackTrace();
                    }
                }
            }).start();}

    }

    static class MyThreadFactory implements ThreadFactory {private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;
        private final String threadFactoryName;

        public String getThreadFactoryName() {return threadFactoryName;}

        MyThreadFactory(String threadStartName) {SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                    Thread.currentThread().getThreadGroup();
            namePrefix = threadStartName + "-pool-" +
                    poolNumber.getAndIncrement() +
                    "-thread-";
            threadFactoryName = threadStartName;
        }

        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                    namePrefix + threadNumber.getAndIncrement(),
                    0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

    public static class MyRejectedPolicy implements RejectedExecutionHandler {

        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {if (e.getThreadFactory() instanceof MyThreadFactory) {MyThreadFactory myThreadFactory = (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 工夫监控啥的。而后也在公司内造成了“经验教训”文档,被动同步给了其余的共事,以防重复踩坑,巴拉巴拉巴拉 …

反正吧,当前看到本人感觉好的案例,不要看完之后就完了,多想想怎么学一学,包装成本人的货色。

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

求个赞,不过分吧?

正文完
 0