查看原文
其他

在高频交易领域中,为什么我们选择 Java 开发外汇算法交易系统?

Jad Sarmo Java精选 2022-08-09

在高频交易的世界中,自动化应用程序每天处理数亿个市场信号,并在全球各个交易所发送成千上万的订单。
为了保持业务竞争力,响应时间必须始终保持在微秒级,尤其是在黑天鹅异常事件等高峰期。
在典型的体系结构中,金融交易信号将被转换为单一的内部市场数据格式(交易所使用各种协议例如TCP / IP,UDP多播)和多种格式(例如二进制,SBE,JSON,FIX等)。
然后,这些标准化的消息被发送到算法服务器、统计引擎、用户界面、日志服务器和各种数据库(内存、物理或分布式)。
这条路径上任何延迟都会带来昂贵的后果,比如策略根据旧的价格做出决策,或者下单太晚。
为了获得这几个关键的微秒,大多数玩家都会在昂贵硬件上进行投资:带有超频液冷 CPU 的服务器池(在 2020 年,你可以买到一台拥有 56 个核心、5.6GHz 和 1TB 内存服务器),在主要的交易所数据中心进行拼装,高端纳秒网络交换机,专用跨洋线路(Hibernian Express是一个主要供应商),甚至是微波网络。
常见的是使用高度定制的 Linux 内核,带有操作系统旁路,这样数据就可以直接从网卡 "跳转" 到应用程序、基于 IPC 进程间通信,甚至使用 FPGA(可编程单用途芯片)。
至于编程语言,C++ 似乎是这个领域的天然选择。它速度快,最接近机器代码,而且,一旦编译到目标平台,就能提供稳定的处理时间。
我们做了一个不同的选择。
在过去 14 年里,我们在外汇算法交易领域用 Java 进行开发,并使用具有竞争力的强大而廉价的硬件。
在一个团队小,资源有限以及熟练开发人员欠缺的工作环境,Java 意味着我们可以快速进行软件迭代,因为 Java 生态系统比 C 系列具有更快的开发时间。可以在早上讨论改进措施,并在下午在生产中实施、测试和发布。
与需要几周甚至几个月软件更新时间的大型公司相比,这是一个关键优势。
在我们这个领域,一个错误可以在几秒钟内抹去一整年的利润,因此不能在质量上妥协。我们使用了许多开源库和项目,实现了严格的敏捷开发环境,包括使用 Jenkins、Maven、单元测试、夜间构建和 Jira。
通过 Java,开发人员可以专注于业务逻辑,而不是像 C++ 那样调试一些晦涩的内存 Coredump 或跟指针打交道。而且,由于 Java 强大的内存管理,初级程序员也可以在入职第 1 天就开发代码,并且风险有限。
只要有良好的设计模式和干净的编码习惯,就可以用 Java 达到 C++ 的延迟。
Java 可以优化和编译应用程序运行期间观察到的最佳路径,但是 C++ 会预先编译所有内容,因此,即使未使用的方法,也仍将是最终可执行二进制文件的一部分。
没有银弹,Java 这块有一个问题,而且是一个重大的问题,让 Java 成为一门如此强大和令人愉快语言的原因,也是它的缺点(至少对于微秒级敏感的应用),那就是 Java 虚拟机(JVM)。
1. Java 即时编译代码(Just in Time 编译器),这意味着第一次遇到一些代码时,也可能产生编译延迟。
2. Java 管理内存的方式是通过在堆空间中分配内存块。每隔一段时间,它就会清理这个空间,删除旧的对象,为新的对象腾出空间。主要问题是,为了进行准确的统计,应用程序线程需要被瞬间 "冻结"。这个过程被称为垃圾收集(GC)。
GC 是低延迟应用程序开发人员放弃 Java 的主要原因。
市场上有一些 Java 虚拟机。最常见和标准的是 Oracle Hotspot JVM,它在 Java 社区中被广泛使用,主要是出于历史原因。
对于要求非常高的应用程序,Azul Systems 提供了一个很棒的替代方案,称为 Zing。
Zing 是 Oracle Hotspot JVM 一个强大的替代品。Zing 解决了 GC 暂停和 JIT 编译问题。
让我们来研究使用 Java 的固有问题和可能的解决方案。
理解 Java 即时编译器
像 C++ 这样的语言被称为编译语言,因为交付的代码完全是二进制的,可以直接在 CPU 上执行。
PHP 或 Perl 被称为解释语言,因为解释器(安装在目标机器上)会边运行边编译每一行代码。
Java 介于两者之间;它将代码编译成所谓的 Java 字节码,而字节码又可以在它认为合适的时候被编译成二进制。
Java 之所以不在启动时编译代码,与长期的性能优化有关。通过观察应用程序的运行情况,分析实时的方法调用和类的初始化,Java 会编译经常调用的部分代码。它甚至可能会根据经验做出一些假设(这部分代码永远不会被调用,或者这个对象永远是一个 String)。
因此,实际编译后的代码速度非常快。但有三个缺点。
1. 一个方法需要被调用一定的次数来达到编译阈值,然后才能被优化和编译(这个限制是可以配置,但通常是 10000 次左右的调用)。在此之前,未经优化的代码并没有以 "全速" 运行。Java 在更快的编译和高质量的编译之间做了一个取舍(如果假设不对,会有重新编译的代价)。
2. 当 Java 应用程序重启时,又回到了原点,必须等待再次达到这个阈值。
3. 有些应用程序(比如我们的场景)有一些不频繁但很关键的方法,这些方法只会被调用少数几次,但当它们被调用时,需要极快的速度(想想看,一个风险或止损函数只有在紧急情况下才会被调用)。
Azul Zing 通过让其 JVM 将编译后的方法和类的状态 "保存" 在它所谓的配置文件中来解决这些问题。这种名为 ReadyNow!® 的独特功能,意味着 Java 应用程序始终以最佳速度运行,即使在重新启动后也是如此。
当使用现有的配置文件重新启动应用程序时,Azul JVM 会立即调用其先前的结果并直接编译标注的的方法,从而解决了 Java 预热问题。
此外,可以在开发环境中建立一个配置文件,以模拟生产行为。然后,优化后的配置文件可以部署在生产环境中,因为所有的关键路径都被编译和优化了。
下图显示了一个交易应用程序的最大延迟(在模拟环境中)。
Hotspot JVM 大延迟峰值清晰可见,而 Zing 的延迟随着时间的推移保持相当稳定。
百分位数分布表明,1% 的时间里,Hotspot JVM 产生的延迟是 Zing JVM 的 16 倍。
解决垃圾收集(GC)暂停的问题
第二个问题,在垃圾收集过程中,整个应用程序可能会冻结几毫秒到几秒不等(延迟随着代码复杂度和堆大小而增加),更糟糕的是,你无法控制这种情况何时发生。
虽然暂停一个应用程序几毫秒甚至几秒钟对于许多Java应用程序来说可能是可以接受的,但对于低延迟应用程序来说却是一场灾难,无论是汽车、航空航天、医疗还是金融领域。
GC 的影响在 Java 开发者中是一个很大的话题;一个完整的垃圾收集通常被称为 "stop-the-world 暂停",因为它会冻结整个应用程序。
多年来,许多 GC 算法都试图在吞吐量(多少 CPU 用于实际的应用逻辑而不是垃圾收集)与 GC 暂停(应用可以承受暂停多长时间)之间做一个取舍。
自 Java 9 以来,G1 收集器一直是默认 GC,其主要思想是根据用户提供的时间目标来划分 GC 暂停时间。它通常提供较短的暂停时间,但代价是较低的吞吐量。此外,暂停时间会随着堆的大小而增加。
Java 提供了大量的设置来调整其垃圾收集(以及 JVM),从堆大小到收集算法,以及分配给 GC 的线程数。所以,看到 Java 应用程序配置了大量的自定义选项是很常见的。
很多开发者(包括我们的工程师)已经转向各种技术来完全避免 GC。主要思路是,如果创建的对象少了,需要清除的对象就会变少。
一个古老的(现在仍然使用的)技术是使用可重用对象的对象池。例如,一个数据库连接池将持有 10 个已打开的连接的引用,准备在需要时使用。
多线程通常需要锁,这会导致同步延迟和暂停(特别是当它们共享资源时)。一个流行的设计是一个环形缓冲队列系统,在一个无锁的设置中,有许多线程写和读( 参阅 disruptor)。
https://lmax-exchange.github.io/disruptor/
一些专家甚至选择完全自己实现 Java 内存管理,自己管理内存分配,虽然解决了一个问题,但却带来了更多的复杂性和风险。
在这种情况下,显然应该考虑其他 JVM,于是我们决定尝试 Azul Zing JVM。
很快,我们就实现了非常高的吞吐量,停顿可以忽略不计。
这是因为 Zing 使用了一个独特的收集器,叫做 C4(Continuurrentously Concurrent Compacting Collector),它允许无暂停地收集垃圾,而不关心 Java 堆的大小(最高可达 8 TB)。
这是通过在应用程序仍在运行时,并发映射和压缩内存来实现。
此外,它不需要修改任何代码,延迟和速度的提升都是开箱即见,无需冗长的配置。
在这种情况下,Java 程序员可以享受到两全其美的好处,既可以享受到 Java 的简单性(无需偏执于创建新对象),又可以享受到 Zing 的底层性能,使整个系统的延迟高度可预测。
多亏了 GC easy,一个通用的 GC 日志分析器,我们可以在真实的自动交易应用中(在模拟环境中)快速比较两种 JVM。
在我们的应用中,使用 Zing 的 GC 比使用标准的 Oracle Hotspot JVM 小 180 倍左右。
更令人印象深刻的是,GC 暂停通常与实际应用暂停时间相对应,而 Zing 智能 GC 通常是在最小或没有实际暂停的情况下平行发生的。
小结
总之,Java 在享受简单性和面向业务的特性同时,仍然可以实现高性能和低延迟。虽然 C++ 仍然可用于特定的底层组件,如驱动程序、数据库、编译器和操作系统,但大多数现实生活中的应用程序都可以用 Java 来开发,包括那些最苛刻的应用程序。
这就是为什么根据 Oracle 公司的统计, Java 是第一大编程语言,在全球拥有数百万开发人员和超过 510 亿台 Java 虚拟机的原因。

作者:Jad Sarmo

medium.com/@jadsarmo/why-we-chose-java-for-our-high-frequency-trading-application-600f7c04da94

往期精选  点击标题可跳转

给我五分钟,带你彻底掌握 MyBatis 缓存的工作原理

这 6 个 MySQL 死锁案例,让你彻底理解死锁的原因!

史上最全面的 Lombok 注解介绍,Look 一下吧!

关于 Java 你可能不知道的那些事之 Java 注解和反射

Intellij IDEA 使用  Groovy 脚本一键生成带注解的实体类详细步骤

Spring Cloud 实战系列之 Zuul 微服务网关搭建及配置

Spring 源码分析(四)容器的基础 XmlBeanFactory(系列文章基于Spring 5.0)

Spring 源码分析(三)容器核心类(系列文章基于Spring 5.0)

Spring 源码分析(二)容器基本用法(系列文章基于Spring 5.0)

Spring 源码分析(一)从基本介绍开始(系列文章基于Spring 5.0)

点个赞,就知道你“在看”!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存