为了保持业务竞争力,响应时间必须始终保持在微秒级,尤其是在黑天鹅异常事件等高峰期。
在典型的体系结构中,金融交易信号将被转换为单一的内部市场数据格式(交易所使用各种协议例如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 虚拟机的原因。