问题背景:

  上周发现了一个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. PHP正则相关

    描述字符串排列模式的一种自定义语法规则 如果可以使用字符串函数处理的任务 就不要使用正则 正则表达式 就是通过构建具有特定规则的模式,与输入的字符信息比较 在进行 分割 匹配 查找 替换 等工作   ...

  2. [ Continuously Update ] The Paper List of Image / Video Captioning

    Papers Published in 2018 Convolutional Image Captioning - Jyoti Aneja et al., CVPR 2018 - [ Paper Re ...

  3. 有关WCSF的几点整理

    本文示例代码 一.CreateNew Attribute实现属性注入 Steps: 1/ aspx创建某个服务的属性. 2/ 为其添加[CreateNew] Attribute. 3/ 页面继承自Mi ...

  4. HDU 1512 Monkey King(左偏树)

    Description Once in a forest, there lived N aggressive monkeys. At the beginning, they each does thi ...

  5. 2018-8-29安装Jitamin过程实录

    2018-8-29安装Jitamin过程实录 新建 模板 小书匠 欢迎走进zozo的学习之旅. 简介 安装 nginx + php + mysql 安装composer 安装Jitamin 简介 在考 ...

  6. 用URL传参带特殊字符,特殊字符丢失

    文章:URL中编码URL特殊字符 文章:用URL传参带特殊字符,特殊字符丢失(encode) 如果url中有特殊字符,需要对url进行编码,否则特殊字符丢失,导致最终接收到的值不对.

  7. [离散化]人潮最多的時段( Interval Partitioning Problem )

    範例:人潮最多的時段( Interval Partitioning Problem ) 一群訪客參加宴會,我們詢問到每一位訪客的進場時刻與出場時刻,請問宴會現場擠進最多人的時段. 換個角度想,想像會場 ...

  8. LintCode-68.二叉树的后序遍历

    二叉树的后序遍历 给出一棵二叉树,返回其节点值的后序遍历. 样例 给出一棵二叉树 {1,#,2,3}, 返回 [3,2,1] 挑战 你能使用非递归实现么? 标签 递归 二叉树 二叉树遍历 code / ...

  9. ssl证书验证的问题

    对于https请求,是需要ssl证书验证的请求的,所以如果在请求时如果不带ssl证书,那么可以忽略证书的验证 有三种方法去实现: 1.Requests请求: 在文档中可以看到:http://docs. ...

  10. 【Docker 命令】- kill命令

    docker kill :杀掉一个运行中的容器. 语法 docker kill [OPTIONS] CONTAINER [CONTAINER...] OPTIONS说明: -s :向容器发送一个信号 ...