问题背景:

  上周发现了一个spark job的执行时间从原来的10-15分钟延迟到了7个小时!wtf,这是出了什么事引起了这么大的性能问题!!

  立马查看job的运行日志,发现多次运行都是在某一个固定的stage速度特别慢,大概在5000-6000s,这样的stage一共有3-4次。究竟是什么样的原因引起这样的问题,第一个想法是寻找之前执行时间短的任务和现在执行时间长的任务有哪些不同的地方:1,检查spark提交的参数,包括executor个数,内存配置和核数配置,发现前后都没有改动;2,检查git代码仓库master的代码变更,发现前后有3次提交。现在我把问题的最大可能性放在这些代码的改动上。

问题排查:

  查看代码改动,首先想到的就是diff两个版本的代码:发布master在git上都会留下tag,在发布系统jenkins上找到两个发布的release tag,diff之。

  从diff结果看,这几次提交主要是添加了新功能,添加新的工具类,java bean的重构(抽取公共的属性作为父类属性),和我们的job逻辑相关的代码基本没有改动,从代码逻辑上并没有看出什么大的性能问题。

  排查陷入困境。。。

  受到同事启发,决定把改动之前的tag复制出一个新的branch(我们称之为before),把改动之后的tag复制一个新的branch(称之为after),把before和after之间的差异分批次加到before上,执行before,看加那些代码时会出现问题(实在差不出了问题所在,只能选择笨方法,一点一点试)。分批次加到before上,在Intelij idea上有个简单方法,分支切换到before,右键项目->Git->Compare with branch,选择after分支,diff两者区别,这时候diff页面上两分支不同的代码旁边会有>>的箭头形状,可以快速把要添加的代码加到before分支。

  先把最可能影响我们job执行的代码加到了before分支,执行都没有问题。。。

  继续把其他的代码分批加到before分支,(这是个体力活,添加代码-run job-添加代码-run job)

  分了5-6次之后,发现加了三个java bean的重构后job变慢,???改了bean能导致job性能变差,有点怀疑人生

  检查跟job相关的bean,除了抽取了公共属性为父类之外,重写了hashcode和equals方法!!!会不会是这个引起的

    @Override
public boolean equals(Object obj) {
if(Objects.nonNull(obj) && obj instanceof XXX){
return Objects.equals(hashCode(),obj.hashCode());
}
return false;
} @Override
public int hashCode() {
String value = x1 +
x2 +
x3 +
x4 +
x5 +
x6 +
x7 +
x8 +
x9;
return StringUtils.trim(value).hashCode();
}

  代码如上,类名这里修改为XXX,字段修改为x1,x2,x3,x4,x5,x6,x7,x8,x9

  写一个test,循环100w次 执行equals,发现也是秒秒钟跑完!!!

  继续回到性能差的那块逻辑里排查,找可能会用到model的equals方法的地方

  有重大发现,对于修改了equals方法的model,有一个removeAll操作:从一个List<XXX> source中安条件filter出一些实例,作为list1,又list2 = source.removeAll(list1);作为list2,这里的source大概在几十万到100万的数据量,list1里几乎是source的全量(此次聚合对应的分组区分度不高,所以在每一个执行器上数据量较大)

  而removeAll时会调用model的equals方法,时间复杂度为m*n(n为source的数量,m为list1的数量),在千亿-万亿的equals操作下,任何耗时的操作都会成千亿-万亿倍增加,所以会出现没有修改任何逻辑,只重写了equals方法就会出现性能问题。

  贴一下ArrayList的源码:

/**
* Removes from this list all of its elements that are contained in the
* specified collection.
*
* @param c collection containing elements to be removed from this list
* @return {@code true} if this list changed as a result of the call
* @throws ClassCastException if the class of an element of this list
* is incompatible with the specified collection
* (<a href="Collection.html#optional-restrictions">optional</a>)
* @throws NullPointerException if this list contains a null element and the
* specified collection does not permit null elements
* (<a href="Collection.html#optional-restrictions">optional</a>),
* or if the specified collection is null
* @see Collection#contains(Object)
*/
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, false);
} private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}

  可以看出,先按照source的size做循环,循环内判断contains,

      for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];

  我们再看一下ArrayList(被remove的也是ArrayList类型)的contains

/**
* Returns <tt>true</tt> if this list contains the specified element.
* More formally, returns <tt>true</tt> if and only if this list contains
* at least one element <tt>e</tt> such that
* <tt>(o==null ? e==null : o.equals(e))</tt>.
*
* @param o element whose presence in this list is to be tested
* @return <tt>true</tt> if this list contains the specified element
*/
public boolean contains(Object o) {
return indexOf(o) >= 0;
} /**
* Returns the index of the first occurrence of the specified element
* in this list, or -1 if this list does not contain the element.
* More formally, returns the lowest index <tt>i</tt> such that
* <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>,
* or -1 if there is no such index.
*/
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}

  indexOf内部又一层循环,时间复杂度为m*n

  虽然问题是equals由 == 操作变为9个字段拼接做hashcode 这个变更引起的,但核心问题还在removeAll,before没有出现问题只是因为==操作快,大概产生2-3分钟的执行时间并没有引起问题和关注。重写equals会增加一些时间,在极大的基数上就产生了性能问题

问题解决:

  去掉removeAll,用两个filter代替(满足业务逻辑为准)

效果:

  job执行7-8分钟,比before版本还快2-3分钟,因为去掉了千亿-万亿次 equals(虽然==很快)

经过两天多的排查,终于解决掉了问题。这个事情让我重新对List 的removeAll有了新认识,也认识到一个道理,对于你认为简单的东西 才是最容易挖坑的地方

java removeAll和重写equals、hashcode引起的性能问题的更多相关文章

  1. java构造方法和重写equals

    Cell的构造函数 package Test; import java.util.Objects; public class Cell { int a; int b; public int getA( ...

  2. java中为什么重写equals时必须重写hashCode方法?

    在上一篇博文Java中equals和==的区别中介绍了Object类的equals方法,并且也介绍了我们可在重写equals方法,本章我们来说一下为什么重写equals方法的时候也要重写hashCod ...

  3. 【原创】关于java对象需要重写equals方法,hashcode方法,toString方法 ,compareto()方法的说明

    在项目开发中,我们都有这样的经历,就是在新增表时,会相应的增加java类,在java类中都存在常见的几个方法,包括:equals(),hashcode(),toString() ,compareto( ...

  4. java 中为什么重写 equals 后需要重写 hashCode

    本文为博主原创,未经允许不得转载: 1. equals 和 hashCode 方法之间的关系 这两个方法都是 Object 的方法,意味着 若一个对象在没有重写 这两个方法时,都会默认采用 Objec ...

  5. 【Java基础】重写equals需要重写hashcode

    Object里的equals用来比较两个对象的相等性,一般情况下,当重写这个方法时,通常有必要也重写hashcode,以维护hashcode方法的常规协定,或者说这是JDK的规范,该协定声明相等对象必 ...

  6. RemoveAll 要重写equals方法

    public class User { private String name; private int age; //setter and getter public String getName( ...

  7. Java 基础 - 如何重写equals()

    ref:https://www.cnblogs.com/TinyWalker/p/4834685.html -------------------- 编写equals方法的建议: 显示参数命名为oth ...

  8. 重写Euqals & HashCode

    package com.test.collection; import java.util.HashMap; import java.util.Map; /** * 重写equals & ha ...

  9. 为什么要重写equals和hashcode方法

    equals hashcode  当新建一个java类时,需要重写equals和hashcode方法,大家都知道!但是,为什么要重写呢? 需要保证对象调用equals方法为true时,hashcode ...

随机推荐

  1. RDL/RDLC批量单据打印 [转]

    RDL/RDLC批量单据打印 使用RDL或RDLC进行单据打印时,单张单据打印比较直观简单,无需说明.下面我们来谈一下批量单据打印的实现方法.以下以RDL的ReportBuilder设计环境为例进行讲 ...

  2. [leetcode-663-Equal Tree Partition]

    Given a binary tree with n nodes, your task is to check if it's possible to partition the tree to tw ...

  3. GitHub把自己整个文件夹上传

    我已经有了自己github,但是我怎么对我的项目进行上传呢,普通的上传只有上传单一的文件 这不我去下载了Git(链接至机房ftp文件夹下文件ftp://10.64.130.1/%C8%ED%BC%FE ...

  4. canvas学习(三):文字渲染

    一.绘制基本的文字: var canvas = document.getElementById("myCanvas") var ctx = canvas.getContext('2 ...

  5. Python中__name__属性的妙用

    在Python中,每一个module文件都有一个built-in属性:__name__,这个__name__有如下特点: 1 如果这个module文件是被别的文件导入的,那么,该__name__属性的 ...

  6. 最短路径——floyd(多源最短路径)

    #include <iostream> #include <cstdio> #include <cstdlib> #include <cstring> ...

  7. Alpha发布-----欢迎来怼团队

    欢迎来怼项目小组—Alpha发布展示 一.小组成员 队长:田继平 成员:葛美义,王伟东,姜珊,邵朔,冉华 ,李圆圆 二.文案+美工展示 链接:http://www.cnblogs.com/wwd199 ...

  8. 第一周—Fortran语言学习

    使用教材:Fortran95程序设计[彭国伦] 第二章 编译器的使用 编译结果的好坏 1.翻译正确 2.执行文件的运行效率 3.翻译出来的执行码的长短 4.编译过程花费的时间 5.编译器提供Debug ...

  9. Java中I/O流之缓冲流

    Java 中的缓冲流: 1. 缓冲流要“套接”在相应的节点流之上,对读写的数据提供了缓冲的功能,提高了读写的效率,同时增加了一些新的方法(带缓冲区的,显著减少对 IO 的读写次数,保护硬盘). 2. ...

  10. 【转载】【翻译】Breaking things is easy///机器学习中安全与隐私问题(对抗性攻击)

    原文:Breaking things is easy 译文:机器学习中安全与隐私问题(对抗性攻击) 我是通过Infaraway的那篇博文才发现cleverhans-blog的博客的,这是一个很有意思的 ...