当前位置: 首页 > 科技观察

为什么选择Java开发高频交易系统?

时间:2023-03-13 17:20:57 科技观察

在过去的14年里,我们一直在用Java开发外汇算法交易系统,使用的是非常棒但价格合理的硬件。这一切是怎么发生的?在高频交易的世界里,自动化应用程序每天处理数以亿计的市场交易信号,并在世界各地的交易所之间发送数以千计的订单。为了保持竞争力,响应时间必须始终保持在微秒范围内,尤其是在“黑天鹅”事件等异常峰值期间。在典型的架构中,来自金融市场的交易信号被转换成内部市场数据格式(使用各种协议,如TCP/IP、UDP多播和多种格式,如二进制、SBE、JSON、FIX等)。这些规范化的消息被发送到算法服务器、统计引擎、用户界面、日志服务器和各种类型的数据库(内存数据库、物理数据库、分布式数据库)。这条道路上的任何延误都可能产生严重后果(例如根据旧价格做出战略决策或交易市场中来得太晚的订单)并让他们付出高昂的代价。为了加快这些关键的微秒,大多数经纪商投入了昂贵的硬件:配备超频液冷CPU的服务器群(到2020年,您可以购买单个56核5.6GHzCPU和1TB内存服务器)、靠近交易所的数据中心、纳秒级高端网络交换机、海底租用线路(HibernianExpress是主要供应商),甚至微波网络。我们经常看到高度定制化的Linux内核,可以绕过操作系统,数据可以直接从网卡“跳”到应用程序、IPC(进程间通信),甚至FPGA(可编程单用途芯片)。就编程语言而言,C++似乎是服务器端应用程序的天然竞争者:它速度快,非常接近机器代码,并且在为目标平台编译后提供恒定的处理时间。然而,我们做出了不同的选择。在过去的14年里,我们一直在用Java开发外汇算法交易系统,使用的是非常棒但价格合理的硬件。在小团队和有限资源的情况下,很难找到有技术能力的开发人员,所以使用Java意味着我们可以快速改进软件功能,因为Java生态系统比C语言生态系统发布得更快。上午讨论功能改进,下午实施、测试并发布到生产环境。与需要数周甚至数月才能发布更新的大公司相比,这是一个关键优势。在高频交易的世界里,一个错误可以在几秒钟内抹去一整年的利润,所以我们不会在质量上妥协。我们建立了一个严格的敏捷开发环境,包括Jenkins、Maven、单元测试、夜间构建和Jira,使用了许多开源库和项目。使用Java,开发人员可以专注于直观的面向对象的业务逻辑,而不是浪费时间调试一些晦涩的内存核心转储或管理C++指针。而且,由于Java强大的内存管理功能,即使是初级程序员在第一天加入项目时也可以为系统带来价值,而且风险很小。凭借良好的设计模式和干净的编码习惯,Java在速度上可以与C++媲美。例如,Java优化并编译它在应用程序运行时观察到的最佳路径,但C++预编译所有内容,因此即使未使用的方法也成为可执行二进制文件的一部分。然而,Java有一个问题使Java成为如此强大和可爱的编程语言,但也是Java的缺点之一(至少对于微秒级应用程序而言)-Java虚拟机(JVM):Java编译代码(JIT),它意味着当它第一次运行一些代码时,会有一个编译延迟。Java管理内存的方式是在“堆”空间中分配内存块。每隔一段时间,它就会清理空间,移除旧物品,为新物品腾出空间。主要问题是应用程序线程需要暂时“冻结”才能获得准确的计数。此过程称为垃圾收集(GC)。GC是低延迟应用程序开发人员可能放弃Java的主要原因。市场上有几种Java虚拟机。最常见的是OracleHotspotJVM,它在Java社区中被广泛使用,主要是一些历史原因。对于要求非常苛刻的应用程序,有一个很好的选择,即来自AzulSystems的Zing。Zing是标准OracleHotspotJVM的强大替代品。Zing解决了GC暂停和JIT编译问题。接下来,让我们检查一下Java的一些固有问题和可能的解决方案。1、认识Java的JIT编译器像C++这样的编程语言之所以被称为编译型语言,是因为发布的代码完全是二进制的,可以直接在CPU上执行。PHP或Perl之所以被称为解释型语言,是因为解释器(安装在目标机器上)在运行时编译每一行代码。Java介于两者之间,将代码编译为Java字节码,并在必要时编译为二进制。Java在启动时不编译代码的原因与后续的性能优化有关。通过观察应用程序运行并分析实时方法调用和类初始化,Java编译了代码中最常调用的部分。它甚至可能根据经验做出一些假设(一些代码永远不会被调用,或者一些对象永远是一个字符串)。编译后的代码执行速度非常快,但有三个缺点:一个方法需要被调用一定次数达到一个编译阈值,才能被编译优化(这个阈值是可配置的,一般在10000次左右)。在那之前,未优化的代码不会“全速”运行。在更快的编译和高质量的编译之间存在权衡(如果假设错误,编译成本就会产生)。当Java应用程序重新启动时,我们就回到了开始的地方,必须等待再次达到阈值。一些应用程序不经常调用但只调用几次但调用时需要非常快的关键方法。Zing通过让其JVM“保存”已编译方法和类(也称为配置文件)的状态来解决这些问题。这种独特的功能称为ReadyNow,这意味着Java应用程序始终可以以最佳速度运行,即使在重新启动后也是如此。当您使用现有配置文件重新启动应用程序时,AzulJVM会立即收回之前的决定并直接编译解决Java预热问题的重要方法。此外,您还可以在开发环境中建立配置文件来模拟生产行为。知道所有关键路径都已编译和优化后,可以将优化的配置文件部署到生产中。下图显示了交易应用程序的最大延迟(在模拟环境中)。HotspotJVM的延迟峰值很明显,而Zing的延迟保持相当稳定。百分比分布显示,在1%的时间里,HotspotJVM经历的延迟是ZingJVM的16倍。2.解决垃圾回收暂停问题第二个问题是,在垃圾回收过程中,整个应用程序可能会暂停几毫秒到几秒(延迟会随着代码复杂度和堆大小的增加而增加),更糟的是,你没有控制何时发生。虽然暂停应用程序几毫秒甚至几秒对于许多Java应用程序来说是可以接受的,但这对于低延迟应用程序来说是一场灾难,无论是在汽车、航空航天、医疗还是金融领域。GC影响是Java开发人员的一个大话题,FullGC也常被称为“stop-the-world”,因为它会冻结整个应用程序。多年来,有许多GC算法试图降低吞吐量(多少CPU时间用于应用程序逻辑执行而不是垃圾收集)和GC暂停(我可以暂停应用程序多长时间)。自Java9发布以来,G1一直是默认的垃圾收集器,其主要思想是根据用户提供的计时目标来划分GC暂停。它通常提供更短的暂停时间,但以降低吞吐量为代价。此外,暂停时间会随着堆的大小而增加。Java提供了大量的设置参数,从堆大小到收集算法以及分配给GC的线程数。因此,Java应用程序通常配置了大量的参数:许多开发人员使用各种技术来避免GC。最主要的是,如果我们创建的对象越少,那么以后要清理的对象就越少。一种古老的(仍在使用的)技术是使用对象池。例如,一个数据库连接池可以容纳10个打开的数据库连接,以供需要时使用。多个线程通常需要锁,这会导致同步延迟和停顿(尤其是当它们共享资源时)。一种流行的方法是使用环形缓冲区队列系统,其中多个线程可以在无锁环境中读写(参见disruptor)。https://lmax-exchange.github.io/disruptor/有些高手甚至迫于无奈选择了完全覆盖Java的内存管理机制,自己管理内存分配。这虽然解决了问题,但也带来了更多的问题复杂性和风险。所以我们需要考虑使用另一个JVM,所以我们决定尝试AzulZingJVM。很快,我们就能够在几乎没有停顿的情况下实现非常高的吞吐量。这是因为Zing使用了一个叫做C4(ContinuouslyConcurrentCompactingCollector,连续并发压缩收集器)的垃圾收集器,无论Java堆大小(可以达到8TB),它都可以进行非暂停垃圾收集。这是通过在应用程序运行时同时映射和压缩内存来实现的。此外,它不需要更改代码,延迟和速度方面的改进是开箱即用的,无需大量配置。Java程序员可以两全其美:Java的简单性(无需担心创建太多新对象)和Zing的低级性能,它允许系统中高度可预测的延迟。GCeasy提供了一个通用的GC日志分析器,我们可以在真实的自动交易应用程序(模拟环境)中快速比较JVM。https://gceasy.io/在我们的应用程序中,使用Zing的GC比使用标准OracleHotspotJVM的GC快大约180倍。更令人印象深刻的是,GC暂停通常对应于实际的应用程序暂停时间,而Zing的GC通常并行发生,几乎没有实际暂停。总之,在享受Java的简单性和特性的同时,仍然可以实现高性能和低延迟。C++一般用于开发特定的底层组件,如驱动程序、数据库、编译器和操作系统,但大多数现实生活中的应用程序都可以用Java开发,即使是要求很高的应用程序。这就是为什么Java是排名第一的编程语言(根据Oracle)并且在全球拥有数百万开发人员和超过510亿台Java虚拟机。