Linkedin工程师是如何优化他们的Java代码的

标签: linkedin 工程师 优化 | 发表时间:2014-12-15 15:10 | 作者:
出处:http://kb.cnblogs.com/

   英文原文: LinkedIn Feed: Faster with Less JVM Garbage

  最近在刷各大公司的技术博客的时候,我在Linkedin的技术博客上面发现了一篇很不错博文。这篇博文介绍了Linkedin信息流中间层Feed Mixer,它为Linkedin的Web主页,大学主页,公司主页以及客户端等多个分发渠道提供支撑(如下图所示)。

feed_mixer_1

  在Feed Mixer里面用到了一个叫做SPR(念“super”)的库。博文讲的就是如何优化SPR的java代码。下面就是他们总结的优化经验。

  1. 谨慎对待Java的循环遍历

  Java中的列表遍历可比它看起来要麻烦多了。就以下面两段代码为例:

  • A:
    private final List<Bar> _bars;
    for(Bar bar : _bars) {
    //Do important stuff
    }
  • B:
    private final List<Bar> _bars;
    for(int i = 0; i < _bars.size(); i++) {
    Bar bar = _bars.get(i);
    //Do important stuff
    }

  代码A执行的时候  会为这个抽象列表创建一个迭代器,而代码B就直接使用  get(i) 来获取元素,相对于代码A省去了迭代器的开销。

  实际上这里还是需要一些权衡的。代码A使用了迭代器,保证了在获取元素的时候的时间复杂度是  O(1) (使用了  getNext()hasNext() 方法),最终的时间复杂度为  O(n) 。但是对于代码B,循环里每次在调用  _bars.get(i) 的时候花费的时间复杂度为  O(n)  (假设这个list为一个 LinkedList),那么最终代码B整个循环的时间复杂度就是  O(n^2)  (但如果代码B里面的list是  ArrayList, 那  get(i) 方法的时间复杂度就是  O(1)了)。

  所以在决定使用哪一种遍历的方式的时候,我们需要考虑列表的底层实现,列表的平均长度以及所使用的内存。最后因为我们需要优化内存,再加上  ArrayList 在大多数情况下查找的时间复杂度为  O(1) ,最后决定选择代码B所使用的方法。

  2.在初始化的时候预估集合的大小

  从Java的这篇  文档我们可以了解到: “一个HashMap 实例有两个影响它性能的因素:初始大小和加载因子( load factor)。 […] 当哈希表的大小达到初始大小和加载因子的乘积的时候,哈希表会进行 rehash操作 […] 如果在一个HashMap 实例里面要存储多个映射关系时,我们需要设置足够大的初始化大小以便更有效地存储映射关系而不是让哈希表自动增长让后rehash,造成性能瓶颈。”

  在Linkedin实践的时候,常常碰到需要遍历一个  ArrayList 并将这些元素保存到  HashMap 里面去。将这个  HashMap 初始化预期的大小可以避免再次哈希所带来的开销。初始化大小可以设置为输入的数组大小除以默认加载因子的结果值(这里取0.7):

  • 优化前的代码:
    HashMap<String,Foo> _map;
    void addObjects(List<Foo> input)
    {
    _map = new HashMap<String, Foo>();
    for(Foo f: input)
    {
    _map.put(f.getId(), f);
    }
    }
  • 优化后的代码
    HashMap<String,Foo> _map;
    void addObjects(List<Foo> input)
    {
    _map = new HashMap<String, Foo>((int)Math.ceil(input.size() / 0.7));
    for(Foo f: input)
    {
    _map.put(f.getId(), f);
    }
    }

  3. 延迟表达式的计算

  在Java中,所有的方法参数会在 方法调用之前,只要有方法参数是一个表达式的都会先这个表达式进行计算(从左到右)。这个规则会导致一些不必要的操作。考虑到下面一个场景:使用 ComparisonChain比较两个  Foo 对象。使用这样的比较链条的一个好处就是在比较的过程中只要一个 compareTo 方法返回了一个非零值整个比较就结束了,避免了许多无谓的比较。例如现在这个场景中的要比较的对象最先考虑他们的score, 然后是 position, 最后就是  _bar 这个属性了:

public class Foo {
private float _score;
private int _position;
private Bar _bar;

public int compareTo (Foo other) {
  return ComparisonChain.start().
  compare(_score, other.getScore()).
  compare(_position, other.getPosition()).
  compare(_bar.toString(), other.getBar().toString()).
  result;
}
}

  但是上面这种实现方式总是会先生成两个  String 对象来保存 bar.toString() 和 other.getBar().toString() 的值,即使这两个字符串的比较可能不需要。避免这样的开销,可以为Bar 对象实现一个 comparator:

public class Foo {
private float _score;
private int _position;
private Bar _bar;
private final BarComparator BAR_COMPARATOR = new BarComparator();

public int compareTo (Foo other) {
return ComparisonChain.start().
compare(_score, other.getScore()).
compare(_position, other.getPosition()).
compare(_bar, other.getBar(), BAR_COMPARATOR).
result();
}

private static class BarComparator implements Comparator<Bar> {
@Override
public int compare(Bar a, Bar b) {
return a.toString().compareTo(b.toString());
}
}
}

  4. 提前编译正则表达式

  字符串的操作在Java中算是开销比较大的操作。还好Java提供了一些工具让正则表达式尽可能地高效。动态的正则表达式在实践中比较少见。在接下来要举的例子中,每次调用 String.replaceAll() 都包含了一个常量模式应用到输入值中去。因此我们预先编译这个模式可以节省CPU和内存的开销。

  • 优化前:
    private String transform(String term) {
    return outputTerm = term.replaceAll(_regex, _replacement);
    }
  • 优化后:
    private final Pattern _pattern = Pattern.compile(_regex);
    private String transform(String term) {
    String outputTerm = _pattern.matcher(term).replaceAll(_replacement);
    }

  5. 尽可能地缓存Cache it if you can

  将结果保存在缓存里也是一个避免过多开销的方法。但缓存只适用于在相同数据集撒花姑娘吗的相同数据操作(比如对一些配置的预处理或者一些字符串处理)。现在已经有多种LRU(Least Recently Used )缓存算法实现,但是Linkedin使用的是  Guava cache (具体原因见 这里) 大致代码如下:

private final int MAX_ENTRIES = 1000;
private final LoadingCache<String, String> _cache;
// Initializing the cache
_cache = CacheBuilder.newBuilder().maximumSize(MAX_ENTRIES).build(new CacheLoader<String,String>() {
@Override
public String load(String key) throws Exception {
return expensiveOperationOn(key);
}
}
);

//Using the cache
String output = _cache.getUnchecked(input);

  6. String的intern方法有用,但是也有危险

  String 的 intern 特性有时候可以代替缓存来使用。

  从这篇 文档,我们可以知道:

“A pool of strings, initially empty, is maintained privately by the class String. When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned”.

  这个特性跟缓存很类似,但有一个限制,你不能设置最多可容纳的元素数目。因此,如果这些intern的字符串没有限制(比如字符串代表着一些唯一的id),那么它会让内存占用飞速增长。Linkedin曾经在这上面栽过跟头——当时是对一些键值使用intern方法,线下模拟的时候一切正常,但一旦部署上线,系统的内存占用一下就升上去了(因为大量唯一的字符串被intern了)。所以最后Linkedin选择使用 LRU 缓存,这样可以限制最大元素数目。

  最终结果

  SPR的内存占用减少了75%,进而将feed-mixer的内存占用减少了 50% (如下图所示)。这些优化减少了对象的生成,进而减少了GC得频率,整个服务的延迟就减少了25%。 MemUtil_incapacity

相关 [linkedin 工程师 优化] 推荐:

Linkedin工程师是如何优化他们的Java代码的

- - 博客园_知识库
   英文原文: LinkedIn Feed: Faster with Less JVM Garbage.   最近在刷各大公司的技术博客的时候,我在Linkedin的技术博客上面发现了一篇很不错博文. 这篇博文介绍了Linkedin信息流中间层Feed Mixer,它为Linkedin的Web主页,大学主页,公司主页以及客户端等多个分发渠道提供支撑(如下图所示).

首席工程师揭秘:LinkedIn大数据后台是如何运作的

- - 博客园_知识库
   英文原文: The Log: What every software engineer should know about real-time data's unifying abstraction.   我在六年前的一个令人兴奋的时刻加入到LinkedIn公司. 从那个时候开始我们就破解单一的、集中式数据库的限制,并且启动到特殊的分布式系统套件的转换.

LinkedIn用一幅图告诉你:工程师最想去的10家硅谷创业公司

- - 36氪
旧金山湾区,包括整个硅谷,到处都是天才级别的软件工程师. 但即使如此,对于创业公司来说,能否吸引最好的工程师仍然是企业成败关键的因素之一. 而最火爆最有前景的创业公司,往往也就能吸引最多天才工程师的加入. 而作为全球最大的职业社交网络 LinkedIn,自然能发现好的工程师都去了哪里,从而知道目前哪些创业公司是工程师们梦寐以求的地方.

中国的LinkedIn们

- - It Talks-魏武挥的blog
我倒并不想完全断言中国BSNS没有一点点的未来,但做生意是真金白银的消耗,非常讲究一个timing问题. 中国BSNS,要想走出中国的LinkedIn的道路,恐怕得花上比LinkedIn自身发展更长的时间. 与目前股价一路扶摇直上的LinkedIn相比,中国的BSNS(商务社交,也有自称PSNS专业社交的)显得有些不愠不火,差强人意.

向LinkedIn学习什么

- 车东 - 《商业价值》杂志
准确的定位和极优的数据整理能力,是LinkedIn最终成功的原因. 中国模仿者们需要模仿到基因层面才会有希望. 2010年12月,美国非上市公司股票交易平台SecondMarket评选出五大估值超10亿美元的非上市公司,LinkedIn挤掉Youtube等大热门而上榜. LinkedIn这家比Facebook还早的老牌社交网站,在将近10年的互联网大潮中,一直以低调稳健但内容乏味的姿态潜行.

中国会不会有Linkedin?

- zhangv - It Talks--上海魏武挥的博客
本周根据外电,Linkedin已经为自己的IPO做了定价,区间大致在32-35美元,预期募集资金2.71亿,估值在30-33亿美元. 这个主打所谓高端人群,74%会员受过高等教育,被誉为“职场SNS”的网络公司,拥有1亿用户,2010年营收2.43亿美元,利润1500多万. 据公司声称,在linkedin上,有200万个公司页面,73%的财富100强公司用过它的招聘解决方案,世界500强则全数成为它的会员.

[原]LinkedIn Cubert安装指南

- - OopsOutOfMemory盛利的博客
最近工作需要,调研了一下LinkedIn开源的用于复杂大数据分析的高性能计算引擎Cubert. 自己测了下,感觉比较适合做报表统计中的Cube计算和Join计算,效率往往比Hive高很多倍,节省资源和时间. 下面看下这个框架的介绍:. Cubert完全用Java开发,并提供一种脚本语言. 它是针对报表领域里经常出现的复杂连接和聚合而设计的.

LinkedIn架构这十年

- - 鸟窝
原文: A Brief History of Scaling LinkedIn. Josh Clemm是LinkedIn的高级工程经理,自2011年加入LinkedIn. 他最近(2015/07/20)写了一篇文章,介绍了LinkedIn针对用户规模急速扩大带来的架构方面的变革. 文章有点像子柳写的 淘宝技术这十年.

Android客户端性能优化(魅族资深工程师毫无保留奉献)

- - CSDN博客推荐文章
本文由魅族科技有限公司资深Android开发工程师degao(嵌入式企鹅圈原创团队成员)撰写,是degao在嵌入式企鹅圈发表的第一篇原创文章,毫无保留地总结分享其在领导魅族多个项目开发中的Android客户端性能优化经验,极具实践价值. 即日起,嵌入式企鹅圈将在之前五个专栏(Linux内核驱动情景分析、资源紧缺型SOC嵌入式架构设计、嵌入式交叉工具链及其应用、嵌入式设计和编程、微信硬件平台和物联网解决方案)新增Android开发专栏.

用户到底如何使用 LinkedIn?

- jl1987 - 爱范儿 · Beats of Bits
作为最热门的职业社交网络,LinkedIn 正以每秒增加一位新注册用户的速度快速扩张. 近日,由互联网调研公司 Lab42 根据500位LinkedIn用户的调查反馈,制作了一张名为 “The LinkedIn Profile”的信息图. 调查问卷就用户使用LinkedIn 网站的目的和效果进行了分析和归总.