线程说:不是我想爆炸,只怪你Nd4j没用好

1、项目介绍

web_rec_comm_ctr

背景:

去年接手了一个排序服务,用于播单、声音、主播排序。接手以来处理过内存溢出问题,后面也没再出现过其余情况。可是最近该项目用于离线任务计算后,出现了问题。而且问题发生时间是在计算量扩量以后。html

项目背景:

  1. 该项目与算法的配合方式:项目提供接口规范,涉及:排序算法加载、自动更新、模型调用、输入参数解析、告知模型所需特征数据(包括特征表、表字段等)。
  2. 项目须要作的事:加载算法–>解析请求数据–>获取特征数据–>调用模型排序–>解析排序结果–>结果拼装返回。

2、问题背景

一、发现项目的k8s容器会出现重启现象。
发生问题时,容器配置:CPU:4个:排序计算须要; 内存:堆内6G:w2v模型本地加载; 堆外3G:各类算法包计算使用。java

3、问题结论:

Nd4j计算框架在作计算时(使用了OpenMP库:OpenMP是一个开源的并行编程API,支持C/C++/Fortran语言。ND4j使用以C++编写的后端,所以咱们用OpenMP来改善CPU的并行计算性能。),库里面直接调用pthread_create进行线程建立,多线程并行计算。因为对该领域的包不是很了解,就不深挖该计算框架的优化。直接摒弃该库采用其余方式作计算。linux

4、问题排查流程

查看监控系统,观察重启发生时,容器实例的资源状况
1.png
注:先别急着纳闷这个项目的监控数据图看着那么多毛刺。这个排序服务是为离线定时任务服务的。git

监控数据观察:github

  • 首先,线程数呈现出异常状况,最高接近8k。
  • 其次,发现最开始出现问题的时候,任务的数据量是比较少许,而不是大量计算才发生问题。
  • 第三,大部分状况下,重启的时间恰好跟线程达到峰值对上。连续重启通常是:上一次重启的时候,服务拥入大量请求,线程陡增,而后又重启了…
  • 第四,也不是每一次任务触发都会发生重启,且根据线程图能够知道,线程是有进行回收的动做,不大多是永久资源泄漏。好熟悉的感受,难道又是资源使用后没释放,直到垃圾回收时被动释放资源…

根据第一点,在下次任务来临时,dump下线程栈:jstack pid,使用线程分析网站:fastThread
2.png
此时个人表情是这样的:3.pngweb

  • 说好的8k个线程呢…难道是…我打开的方式不对…好吧,愣了一下。jstack命令dump下来的线程通常是由jvm生成的管理的线程,而native方法产生的线程是不禁jvm进行管理的,这也就是为啥jstack命令dump下来的线程栈就只有这么一点。
  • 注:别妄想用jstack -m参数dump线程,说实话,dump下来的东西看了以后心态更蹦,好吧实际上是我看不懂。
  • 经过跟运维大佬请教,监控中使用了ps命令的-L参数,使用ps -efL | grep pid | wc -l果真跟监控系统的统计是对的上的。
  • 此时怀疑是本地线程使用泛滥致使的。

定位那些native线程是由什么建立的:(如下方法来自李老师的指导,向李老师学习。)算法

4.png

  • 根据上图能够锁定mkl和nd4j,在项目中瞅瞅是哪里引入:mvn dependency:tree

5.png

  • 该库的引入来自于算法同事提供的模型计算包。翻看代码中,在作计算时确实用到了该库,如下随机抽了使用片断。
public float[] userWord2Vec(List<HashMap<String, Object>> list, Word2VEC word2vecModel, int audioVectorDim,
                                int topK, boolean norm){
        /*
         * @描述: 获取用户的分布式表示[将播放序列的节目向量的和或者均值做为用户的向量表示]
         * @参数: [list, topK, norm]
         * @返回值: float[]
         * @建立时间: 7/23/19
         */
        float[] userVec_ = new float[audioVectorDim];
        INDArray userVec = Nd4j.create(userVec_, new int[]{1, audioVectorDim});
  • 翻看算法同事的代码发现,INDArray对象使用后,并未作资源释放,尝试修改代码,在计算后,对使用的资源进行释放。但遗憾的是,尽管加入释放代码,发版后依然出现相同的情况。
  • 此时只能硬着头皮翻翻源码了,毕竟问题现象是线程泛滥,看看有没有地方能够设置,限制该库的线程使用数,牺牲并发度。如下为org.nd4j:nd4j-api:jar:1.0.0-beta4:compile依赖下ExecutorServiceProvider类源码
public class ExecutorServiceProvider {

    public static final String EXEC_THREADS = "org.nd4j.parallel.threads";
    public final static String ENABLED = "org.nd4j.parallel.enabled";

    private static final int nThreads;
    private static ExecutorService executorService;
    private static ForkJoinPool forkJoinPool;

    static {
        int defaultThreads = Runtime.getRuntime().availableProcessors();
        boolean enabled = Boolean.parseBoolean(System.getProperty(ENABLED, "true"));
        if (!enabled)
            nThreads = 1;
        else
            nThreads = Integer.parseInt(System.getProperty(EXEC_THREADS, String.valueOf(defaultThreads)));
    }
  • 经过上图,尝试在启动参数里加入-Dorg.nd4j.parallel.enabled=false,直接将并发计算一刀切。so sad,结果依然是线程泛滥。此处设置应该是限制单次计算由并行改成单线程计算,并无解决线程资源未回收的问题。
  • 无奈只能求助google:工做区指南 Deeplearning4j的本机CPU优化 尝试修改线程、垃圾回收等配置,但依然毫无改善问题。(ND4J确实不了解,工做区概念什么的也看懵了…)

5、解决方案

最终只能求助于算法大佬,看可否换其余库作计算或者本身实现计算。首先确认改动成本有多大,确保以最小的代价去解决:编程

  1. 该项目中大部分排序算法已迁移到“模型服务平台”中,剩余的算法也只是支持少许的计算工做,因此此处仅需修改在还在改项目中使用的算法。(嗯…其实就剩两个了。)
  2. 使用到的算法中,对于Nd4j的使用是在于矩阵计算,而非复杂的模型训练或者模型计算。因此替换的计算逻辑彻底能够由其余工具包快速替换或者快速手写实现。
  • 算法大佬修改实现后,从新引入发版观察。谢天谢地,总算回归正常。

6.png

  • 其实在解决发版后,又偷偷发了一个节点,版本是解决问题前的。将内存提高到15G,堆内依然是6G,堆外预留9G。留着该节点且预留大堆外内存是为了验证本次修改是否解决问题。果真该节点发版后,虽然未出现重启现象。可是其内存一度超过9G,若是未扩容,应该又是一次重启。且线程数又出现陡增状况。可是线程又出现回收状况,此处猜测是GC带来的影响,堆内的对象被回收以后,其指向外部的资源也被回收利用了。后续有时间将了解一番。如今持续观察是否再出现问题。

7.png

6、总结

涉及native方法调用的第三发库使用最好先了解其工做原理再进行使用,尽可能能作到资源使用可控,及时释放资源。虽然本次问题触及的知识领域比较陌生,仍是尽量去了解本身项目里面引入的东西会该来什么影响。后端

 

 

看完三件事❤️

若是你以为这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:api

  1. 点赞,转发,有大家的 『点赞和评论』,才是我创造的动力。

  2. 关注公众号 『 java烂猪皮 』,不按期分享原创知识。

  3. 同时能够期待后续文章ing🚀

  4. 欢迎关注做者gitee,但愿共同窗习

相关文章
相关标签/搜索