如何显着提高Java性能?


23

LMAX团队进行了有关如何在不到1毫秒的延迟时间内完成100k TPS的演示。他们通过博客技术论文(PDF)源代码本身来备份该演示文稿。

最近,马丁·福勒Martin Fowler)发表了一篇有关LMAX架构出色论文,并提到他们现在每秒能够处理600万个订单,并着重介绍了该团队为提高性能而采取的一些步骤。

到目前为止,我已经解释了业务逻辑处理器速度的关键是在内存中按顺序进行所有操作。这样做(并没有真正愚蠢的事情)允许开发人员编写可处理10K TPS的代码。

然后,他们发现专注于良好代码的简单元素可以将其带入100K TPS范围。这只需要精心构造的代码和小的方法-本质上,这使Hotspot可以更好地进行优化,并使CPU在运行代码时更高效地缓存代码。

提升另一个数量级需要更多的技巧。LMAX团队发现有几件有助于实现目标的方法。一种是编写Java集合的自定义实现,这些实现被设计为对缓存友好并且谨慎处理垃圾。

达到最高性能水平的另一种技术是将注意力放在性能测试上。长期以来,我一直注意到人们在谈论提高性能的技术,但是真正起作用的一件事就是对其进行测试

福勒提到发现了几件事情,但他只提到了几件。

是否还有其他架构,库,技术或“事物”有助于达到这样的性能水平?


11
“还有哪些其他架构,库,技术或“事物”有助于达到这样的性能水平?” 为什么这么问?这报价最终名单。还有很多其他的东西,都没有影响列表中项目的种类。任何人可以命名的其他东西都不会像该列表那样有用。当您引用有史以来最好的优化列表之一时,为什么要问坏主意呢?
S.Lott

最好了解他们使用了哪些工具来查看生成的代码如何在系统上运行。

1
我听说过人们通过各种技术发誓。我发现最有效的是系统级性能分析。它可以向您显示程序和工作负载行使系统方式的瓶颈。我建议遵循关于性能和编写模块化代码的众所周知的准则,以便您以后可以轻松对其进行调优...我认为系统配置不会出错。
ritesh 2011年

Answers:


21

各种各样的技术都可以用于高性能事务处理,Fowler的文章中的技术只是最前沿的许多技术之一。我认为最好讨论基本原理以及LMAX如何解决大量基本原理,而不是列出一堆可能不适用于任何人的情况的技术。

对于大型事务处理系统,您希望尽可能执行以下所有操作:

  1. 最小化在最慢的存储层中花费的时间。从现代服务器上最快到最慢的服务器有:CPU / L1-> L2-> L3-> RAM->磁盘/ LAN-> WAN。从顺序最快的现代磁盘到最慢的RAM的跃迁都超过1000倍。随机访问甚至更糟。

  2. 最小化或消除等待时间。这意味着共享尽可能少的状态,并且,如果必须共享状态,则尽可能避免显式锁定。

  3. 分散工作量。在过去的几年中,CPU的速度并没有提高很多,但是它们变得越来越小,并且8核在服务器上非常普遍。除此之外,您甚至可以将工作分散到多台计算机上,这是Google的方法。这样做的好处是它可以扩展包括I / O在内的所有内容

根据Fowler的说法,LMAX对每种方法采用以下方法:

  1. 保留所有在内存中的状态,在所有的时间。如果整个数据库都可以容纳在内存中,大多数数据库引擎实际上都会执行此操作,但是它们不想让任何事情碰上机会,这在实时交易平台上是可以理解的。为了在不增加大量风险的情况下实现这一目标,他们必须构建一堆轻量级的备份和故障转移基础架构。

  2. 对输入事件流使用无锁队列(“中断器”)。与传统的持久消息队列相反,传统的持久消息队列肯定不是无锁的,实际上通常涉及痛苦缓慢的分布式事务

  3. 不多。LMAX是基于工作负载相互依赖的基础而将这一特性丢在总线下的;一个的结果改变了另一个的参数。这是一个重要的警告,也是Fowler明确指出的警告。他们这样做使一些使用并发的,以提供故障转移功能,但所有的业务逻辑都在处理单线程

LMAX 不是实现大规模OLTP的唯一方法。因此,即便是在自己的权利相当辉煌,你就不会需要以决绝的性能水平使用出血边缘技术。

在上述所有原则中,#3可能是最重要和最有效的,因为坦率地说,硬件很便宜。如果您可以将工作负载正确地分配到六个核心和几十个计算机上,那么常规并行计算技术将无处不在。您会惊讶地发现,只有一堆消息队列和一个循环分发程序,您可以实现多少吞吐量。它的效率显然不如LMAX(实际上甚至不接近LMAX),但是吞吐量,延迟和成本效益是独立的问题,在这里我们专门讨论吞吐量。

如果您具有与LMAX相同的特殊需求-特别是与业务现实相对应的共享状态,而不是仓促的设计选择-那么我建议您尝试一下它们的组件,因为我对此了解不多否则就适合那些要求。但是,如果我们只是在谈论高可伸缩性,那么我敦促您对分布式系统进行更多研究,因为它们是当今大多数组织(Hadoop和相关项目,ESB和相关体系结构,CQRS以及Fowler还在使用的规范方法)提及等等)。

SSD也将成为游戏规则的改变者。可以说,它们已经是。现在,您可以拥有对RAM的访问时间相似的永久存储,尽管服务器级SSD仍然非常昂贵,但是一旦采用率提高,它们最终将降低价格。 已经对其进行了广泛的研究,其结果令人难以置信,并且随着时间的推移只会越来越好,因此,整个“将一切都保留在内存中”的概念比以前重要得多。因此,我将再次尝试尽可能地关注并发。


讨论的原则是基本原则是伟大的,您的评论是优秀的,......除非福勒的论文有没有在脚注缓存不经意算法参考en.wikipedia.org/wiki/Cache-oblivious_algorithm(这能装进类别1以上)我永远不会偶然发现他们。那么...关于上述每个类别,您知道一个人应该知道的三件事吗?
达科他州北部

@Dakotah:我甚至不会开始约缓存局部性的担心,除非和直到我已经完全消除了磁盘I / O,这是在绝大多数时间花费在绝大多数应用等。除此之外,“一个人应该知道的三件事”是什么意思?前三名,知道什么?
亚伦诺特,2011年

从RAM访问延迟(〜10 ^ -9s)到磁盘延迟(〜10 ^ -3s平均情况)的跃迁又比1000x大几个数量级。甚至SSD的访问时间仍以数百微秒为单位。
安达(Sedate Alien)2011年

@Sedate:延迟是的,但这是吞吐量的问题,而不是原始延迟,一旦您过去了访问时间并达到了总传输速度,磁盘就不会那么糟糕了。这就是为什么我要区分随机访问和顺序访问的原因。对于随机访问方案,它确实主要成为延迟问题。
亚伦诺特,2011年

@Aaronaught:重新阅读后,我想你是正确的。也许应该指出,所有数据访问应尽可能地顺序进行;从RAM中按顺序访问数据时,还可以获得显着的好处。
安达(Sedate Alien)2011年

10

我认为最大的教训是,您需要从基础开始:

  • 好的算法,适当的数据结构,并且不做任何“真正愚蠢的事情”
  • 精心设计的代码
  • 性能测试

在性能测试期间,您可以分析代码,查找瓶颈并逐一修复它们。

太多的人跳入“一步一步地解决问题”部分。他们花了很多时间来编写“ java集合的自定义实现”,因为他们只知道其系统运行缓慢的全部原因是由于高速缓存未命中。这可能是一个促成因素,但是如果您跳到那样来调整低级代码,则当您应该使用LinkedList时,您可能会错过使用ArrayList的更大问题,或者这是系统真正原因的原因。之所以缓慢,是因为您的ORM正在延迟加载实体的子级,因此对于每个请求都会进行400次单独的数据库访问。


7

我不会特别评论LMAX代码,因为我认为它已被充分描述,但是这里有一些我做过的事例,这些事例可导致明显的性能改进。

与往常一样,一旦您知道自己有问题并需要提高性能,就应该应用这些技术-否则,您可能只是在进行过早的优化。

  • 使用正确的数据结构,并在需要时创建自定义数据结构-正确的数据结构设计使您无法从微优化中获得的改进相形见,,因此,请首先执行此操作。如果您的算法取决于许多快速O(1)随机访问读取的性能,请确保您具有支持此功能的数据结构!值得一遍,以获得正确的结果,例如找到一种方法,可以在数组中表示数据以利用非常快的O(1)索引读取。
  • CPU的速度比内存访问的速度快 -如果内存不在L1 / L2高速缓存中,则可以花很多时间来读取一个随机内存。如果它可以节省您的内存读取,通常值得进行计算。
  • 通过final帮助JIT编译器 -将字段,方法和类设为final可以实现确实有助于JIT编译器的特定优化。具体示例:

    • 编译器可以假定最终类没有子类,因此可以将虚拟方法调用转换为静态方法调用
    • 编译器可以将static final字段视为常量,以提高性能,尤其是如果该常量随后用于可在编译时计算的计算中时。
    • 如果将包含Java对象的字段初始化为final,则优化程序可以消除空检查和虚拟方法分派。真好
  • 用数组替换集合类 -这会导致代码可读性较低,并且维护起来比较棘手,但是由于它消除了一层间接访问并从许多不错的数组访问优化中受益,因此几乎总是更快。在将内部循环/性能敏感的代码确定为瓶颈之后,通常这是一个好主意,但为便于阅读,请避免其他情况!

  • 尽可能使用基元 -从根本上说,基元要比其基于对象的等效项更快。特别是,装箱会增加大量开销,并可能导致讨厌的GC暂停。如果您在乎性能/延迟,请不要将任何原语装箱。

  • 最小化低级锁定 - 低级锁定非常昂贵。寻找避免完全锁定或锁定在粗粒度级别的方法,以便只需要很少地锁定大型数据块,并且可以继续执行低级代码,而不必担心锁定或并发问题。

  • 避免分配内存 -由于JVM垃圾回收的效率非常高,这实际上可能会使您的整体速度降低,但是如果您试图将等待时间降至极低的延迟并需要最大程度地减少GC暂停,这将非常有帮助。您可以使用一些特殊的数据结构来避免分配- 特别是http://javolution.org/库,在这些方面非常有用。

我不同意使方法最终化。JIT能够确定一个方法永远不会被覆盖。此外,如果子类稍后加载,则可以撤消优化。另请注意,“避免分配内存”也可能使GC的工作更加困难,从而使您的速度降低-因此请谨慎使用。
maaartinus

@maaartinus:关于final某些准时制可能会弄清楚,而其他准时制可能不会。它取决于实现(许多性能调优技巧也是如此)。同意分配-您必须对此进行基准测试。通常,我发现最好删除分配,但是要删除YMMV。
mikera 2013年

4

除了已经在Aaronaught出色回答中指出的那样,我还要指出,这样的代码可能很难开发,理解和调试。“虽然非常有效的...这是很容易搞砸了......”在中提到的球员之一LMAX博客

  • 对于习惯了传统查询和锁定的开发人员而言,为新方法进行编码可能就像骑着马一样。至少那是我自己在使用Phaser时的经验,该概念在LMAX技术论文中提到。从这个意义上讲,我会说这种方法将锁争用权换成开发人员的脑力争用

综上所述,我认为那些选择Disruptor和类似方法的人可以更好地确保他们拥有足够的开发资源来维持其解决方案。

总的来说,Disruptor的方法对我来说很有希望。即使您的公司由于上述原因而无法负担使用它的费用,也应考虑说服您的管理层“投入”一些精力来研究它(通常是SEDA)-因为如果他们不这样做,那么有一天他们的客户将使他们倾向于一些更具竞争力的解决方案,这些解决方案要求服务器减少4倍,8倍等。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.