为什么选择Java语言用作高频交易?-Jad


在高频交易的世界中,自动化应用程序每天处理数亿个市场信号,并在全球各个交易所发送成千上万的订单。
为了保持竞争力,反应时间必须始终保持在微秒内,特别是在异常高峰(例如“黑天鹅”事件)期间。
在典型的体系结构中,金融交易信号将转换为单一的内部市场数据格式(交易所使用各种协议(例如TCP / IP,UDP多播)和多种格式(例如二进制,SBE,JSON,FIX等)。那些normalised的消息然后被发送到算法的服务器,统计引擎,用户接口,日志服务器,和所有种数据库(存储器内,物理,分布式)。沿这条路径进行的任何延迟都可能带来昂贵的后果,例如基于旧价格制定策略或过早到达市场的订单。
为了获得这些关键的微秒时间,大多数玩家都在昂贵的硬件上进行了投资:配备超频水冷CPU的服务器池(到2020年,您可以购买具有56个5.6 GHz内核和1 TB RAM的服务器),在主要交换数据中心的配置,纳秒级网络交换机,专用的次洋线(Hibernian Express是主要提供商),甚至是微波网络。常见的情况是,具有操作系统旁路的高度定制的Linux内核使数据直接从网卡“跳转”到应用程序,IPC(进程间通信)甚至FPGA(可编程的单用途芯片)。
对于编程语言,C ++似乎是服务器端应用程序的自然竞争者:它速度很快,尽可能接近机器代码,并且一旦为目标平台编译,就提供了恒定的处理时间。
 
我们做出了不同的选择。
在过去的14年中,我们一直在使用Java并使用出色且价格合理的硬件进行FX算法交易空间编码方面的竞赛。
由于团队规模小,资源有限以及熟练的开发人员缺乏工作市场,因此Java意味着我们可以快速添加软件改进,因为Java生态系统比C派生产品具有更快的上市时间。可以在早上讨论改进措施,并在下午在生产中实施,测试和发布改进。
与需要几周甚至几个月的软件更新时间的大型公司相比,这是一个关键优势。在一个漏洞可以在几秒钟内抹掉全年利润的领域中,我们还没有准备好在质量上做出妥协。我们使用许多开源库和项目实施了严格的敏捷环境,包括Jenkins,Maven,单元测试,夜间构建和Jira。
使用Java,开发人员可以专注于直观的面向对象的业务逻辑,而不必像C ++中那样调试一些晦涩的内存Core转储或管理指针。而且,由于Java强大的内部内存管理,初级程序员也可以在第一天以有限的风险增加价值。
 
凭借良好的设计模式和简洁的编码习惯,可以使用Java达到C ++低延迟
例如,Java将优化和编译在应用程序运行期间观察到的最佳路径,但是C ++会预先编译所有内容,因此即使未使用的方法也仍将是最终可执行二进制文件的一部分。
但是,这里有一个问题,一个主要问题要启动过程。使Java如此强大和令人愉悦的语言的原因还在于它的劣势(至少对于微秒敏感的应用程序而言),即Java虚拟机 (JVM):

  • Java随心所欲地编译代码(仅在时间编译器或JIT中进行),这意味着它第一次遇到某些代码时,会导致编译延迟。
  • Java管理内存的方式是在其“堆”空间中分配内存块。每隔一段时间,它将清理该空间并移走旧物体,以便为新物体腾出空间。主要问题是为了准确计数,应用程序线程需要暂时“冻结”。此过程称为垃圾收集(GC)。

GC是低延迟应用程序开发人员可能会先验丢弃Java的主要原因。
市场上有一些Java虚拟机:
  1. 最常见和标准的是Oracle Hotspot JVM,它在Java社区中被广泛使用,主要是出于历史原因。
  2. 对于要求非常高的应用程序,Azul Systems提供了一个很棒的替代方案,称为Zing
  3. Zing是标准Oracle Hotspot JVM的强大替代品。Zing解决了GC暂停和JIT编译问题。

让我们研究一下使用Java和可能的解决方案所固有的一些问题。
 

了解Java的即时编译器
像C ++这样的语言被称为编译语言,因为所提供的代码完全是二进制的,可以直接在CPU上执行。
PHP或Perl之所以称为解释型,是因为解释器(安装在目标计算机上)会在运行时编译每一行代码。
Java介于两者之间。它将代码编译为Java字节码,然后在认为合适的情况下将其编译为二进制。
Java在启动时不编译代码的原因与长期性能优化有关。通过观察应用程序的运行并分析实时方法调用和类初始化,Java可以编译经常调用的代码部分。它甚至可能根据经验做出一些假设(这部分代码永远不会被调用,或者该对象始终是String)。
因此,实际的编译代码非常快。但是存在三个缺点:

  1. 在优化和编译某个方法之前,必须先调用该方法一定次数才能达到编译阈值(该限制是可配置的,但通常约为10,000次调用)。在此之前,未优化的代码不会以“全速”运行。在获得更快的编译速度和获得高质量的编译速度之间存在折衷(如果假设错误,则会产生重新编译的费用)。
  2. Java应用程序重新启动时,我们回到第一个平方,必须等待再次达到该阈值。
  3. 某些应用程序(例如我们的应用程序)具有一些不常见但很关键的方法,它们只会被调用几次,但是在执行时需要非常快(考虑到风险或止损过程仅在紧急情况下才调用)。

Azul Zing通过让其JVM在其所谓的概要文件中“保存”已编译方法和类的状态来解决这些问题。名为ReadyNow! 的独特功能意味着Java应用程序即使在重启后也始终以最佳速度运行。
当您使用现有概要文件重新启动应用程序时,Azul JVM会立即撤回其先前的决定并直接编译概述的方法,从而解决了Java预热问题。
此外,您可以在开发环境中建立概要文件,以模仿生产行为。然后,知道所有关键路径都已编译和优化,然后可以将优化的概要文件部署到生产中。
在1%的时间内,热点JVM产生的延迟是Zing JVM的16倍。
 
解决垃圾回收(GC)暂停
第二个问题,在垃圾回收期间,整个应用程序可能冻结几毫秒到几秒钟之间的任何时间(延迟随着代码复杂性和堆大小而增加),更糟糕的是,您无法控制何时发生。
尽管对于许多Java应用程序来说,暂停应用程序几毫秒甚至几秒钟是可以接受的,但对于低延迟应用程序(无论是在汽车,航空航天,医疗还是金融领域)来说,这是一场灾难。
GC的影响是Java开发人员中的一个大话题。完整的垃圾回收通常称为“世界停止停顿”,因为它冻结了整个应用程序。
多年以来,许多GC算法都试图降低吞吐量(在实际的应用程序逻辑上而不是在垃圾回收上花费了多少CPU)与GC 暂停(我可以承受暂停应用​​程序多长时间?)。
从Java 9开始,G1收集器已成为默认的GC,其主要思想是根据用户提供的时间目标分割GC暂停。它通常提供较短的暂停时间,但以降低吞吐量为代价。另外,暂停时间随着堆的大小而增加。
Java提供了许多设置来调整其垃圾收集(通常是JVM),从堆大小到收集算法,以及分配给GC的线程数。因此,常见的情况是看到Java应用程序配置了过多的自定义选项。
许多开发人员(包括我们的开发人员)已转向各种技术来完全避免使用GC。主要是,如果我们创建的对象更少,那么以后需要清除的对象就会更少。
一种旧的(并且仍在使用)的技术是使用可重用对象的对象池。例如,数据库连接池将保存对10个打开的连接的引用,这些连接可以按需使用。
多线程通常需要锁,这会导致同步延迟和暂停(尤其是如果它们共享资源)。一种流行的设计是环形缓冲区 ring buffer队列系统,其中许多线程在无锁设置中进行读写操作(请参阅disruptor )。
出于无奈,一些专家甚至选择完全覆盖Java内存管理并自己管理内存分配,这在解决一个问题的同时,还带来了更多的复杂性和风险。
在这种情况下,很明显我们应该考虑其他JVM,因此我们决定尝试Azul Zing JVM
很快,我们就可以实现极高的吞吐量,而暂停时间却可以忽略不计。
这是因为Zing使用称为C4(连续并发压缩收集器)的唯一收集器,该收集器无论Java堆大小(最大为8 TB)如何,都可以进行无中断的垃圾收集。
这是通过在应用程序仍在运行时同时映射和压缩内存来实现的。
此外,它不需要任何代码更改,并且不需要冗长的配置即可立即获得延迟和速度方面的改进。
在这种情况下,Java程序员可以享受两全其美的优势,即Java的简单性(无需对创建新对象抱有幻想)以及Zing的基础性能,从而可以在整个系统中实现高度可预测的延迟。
由于GC容易,通用GC日志分析,我们可以quicky在一个真正的自动交易程序比较两者的JVM(在模拟环境中)。
在我们的应用程序中,Zing的GC大约是标准Oracle Hotspot JVM的180倍。
更为令人印象深刻的是,虽然GC暂停通常对应于实际的应用程序暂停时间,但Zing智能GC通常并行进行,实际中断最少或没有。