性能文章>线程说:不是我想爆炸,只怪你Nd4j没用好>

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

1年前
562718

一、项目介绍

web_rec_comm_ctr

背景:

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

项目背景:

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

二、问题背景

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

三、问题结论:

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

四、问题排查流程

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

监控数据观察:

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

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

  • 说好的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确实不了解,工作区概念什么的也看懵了…)

五、解决方案

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

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

6.png

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

7.png

六、总结

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

请先登录,再评论

666666

3月前

为你推荐

关于内存溢出,咱再聊点有意思的?
概述 上篇文章讲了JVM在GC上的一个设计缺陷,揪出一个导致GC慢慢变长的JVM设计缺陷,可能有不少人还是没怎么看明白的,今天准备讲的大家应该都很容易看明白 本文其实很犹豫写不写,因为感觉没有
字符串字面量长度是有限制的
前言 偶然在一次单元测试中写了一个非常长的字符串字面量。 正文 在一次单元测试中,我写了一个很长的字符串字面量,大概10万个字符左右,编译时,编译器给出了异常告警 `java: constant
多次字符串相加一定要用StringBuilder而不用-吗?
今天在写一个读取Java class File并进行分析的Demo时,偶然发现了下面这个场景(基于oracle jdk 1.8.0_144): ``` package test; public c
如何通过反射获得方法的真实参数名(以及扩展研究)
前段时间,在做一个小的工程时,遇到了需要通过反射获得方法真实参数名的场景,在这里我遇到了一些小小的问题,后来在部门老大的指导下,我解决了这个问题。通过解决这个问题,附带着我了解到了很多新的知识,我觉得
谨防JDK8重复类定义造成的内存泄漏
概述 如今JDK8成了主流,大家都紧锣密鼓地进行着升级,享受着JDK8带来的各种便利,然而有时候升级并没有那么顺利?比如说今天要说的这个问题。我们都知道JDK8在内存模型上最大的改变是,放弃了Perm
高吞吐、低延迟 Java 应用的 GC 优化实践
本篇原文作者是 LinkedIn 的 Swapnil Ghike,这篇文章讲述了 LinkedIn 的 Feed 产品的 GC 优化过程,虽然文章写作于 April 8, 2014,但其中的很多内容和
「每日五分钟,玩转 JVM」:久识你名,初居我心
聊聊 JVMJVM,一个熟悉又陌生的名词,从认识Java的第一天起,我们就会听到这个名字,在参加工作的前一两年,面试的时候还会经常被问到JDK,JRE,JVM这三者的区别。JVM可以说和我们是老朋友了
JVM垃圾回收与一次线上内存泄露问题分析和解决过程
本文转载自:花椒技术微信公众号 前言内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。Ja