Java 和低延迟

我已经记不清有多少次我被告知 Java 不是一种适合开发性能是主要考虑因素的应用程序的语言。我的第一反应通常是要求澄清“性能”的实际含义,因为“性能”是两个最常见的度量标准 - 吞吐量和延迟,有时会相互冲突,并且针对其中一个进行优化的方法可能会对另一个产生不利影响。

存在一些用于开发 Java 应用程序的技术,这些技术与使用更传统的用于此目的的语言构建的应用程序的性能要求相匹配,甚至超过这些应用程序的性能要求。但是,从延迟的角度来看,即使这样也可能不足以获得最佳性能。Java 应用程序仍然必须依赖操作系统来提供对底层硬件的访问。通常,对延迟敏感(通常称为“实时”)应用程序在几乎可以直接访问底层硬件时运行最佳,这同样适用于 Java。在本文中,我们将介绍一些当我们希望我们的应用程序最有效地利用系统资源时可以采取的方法。

Java 从一开始就被设计为在二进制级别上可以在各种硬件和系统架构中移植。这是通过设计和实现一个虚拟机(一个执行平台的抽象模型)并让它执行Java源编译器的输出来完成的。其论点是,迁移到不同类型的硬件平台只需要移植虚拟机。应用程序和库无需修改即可运行(“编写一次,随处运行”的口号)。

但是,具有严格延迟和性能要求的应用程序通常要求在执行时尽可能靠近硬件 - 他们希望从硬件中榨取所有性能,并且不希望纯粹为了可移植性或抽象编程概念(如动态内存管理)而存在的中间代码成为阻碍。

多年来,Java 虚拟机已经发展成为一个极其复杂的执行平台,可以在运行时从 Java 字节码生成机器代码,并根据动态收集的指标优化该代码。这是静态编译的语言(如 C++)无法做到的,因为它们没有所需的运行时信息。在选择数据结构和算法时谨慎的方法可以最小化甚至消除垃圾回收的需求 - 这可能是 Java 运行时环境中最明显的方面,它阻止了一致的延迟时间。

但归根结底,Java 虚拟机就是虚拟的,它需要在操作系统上运行,以管理其对硬件平台的访问。无论该操作系统是 Linux(可能是服务器端环境中使用最广泛的)、Windows 还是其他操作系统,问题仍然存在。
Linux的“问题”
多年来,Linux 作为 Unix 操作系统家族的一员不断发展。Unix 的第一个版本是在 1960 年代后期开发的;它最初在学术界和研究界发展并广受欢迎,然后在商业世界中以各种形式出现。Linux 已成为 Unix 的主要变体 - 尽管它仍然保留了许多原始功能。如今,随着基于容器的执行环境和云的出现,其主导地位几乎已经完全确立。

但是,从实时或延迟敏感应用程序的角度来看,Linux/Unix 确实存在问题。这些很大程度上源于一个基本事实,即Unix被设计为一个时间共享系统。它最初的硬件平台是微型计算机,同时被许多不同的用户共享。所有用户都有自己的工作要做,而Unix竭尽全力确保所有人都能“公平分享”计算机的资源。

事实上,操作系统会偏爱那些执行大量 I/O 的用户 - 包括在终端上与系统交互 - 而牺牲了主要执行计算的任务(所谓的 CPU 绑定作业)。当我们考虑到当时的计算机几乎都是单个CPU(单核)时,这是有道理的。

然而,随着多CPU计算机的发展,需要对Unix操作系统的核心进行一些认真的重新设计,以使这些执行核心能够得到有效利用。但同样的方法仍然适用,交互式任务总是比 CPU 密集型任务更受青睐。在提供多个内核的情况下,最终效果仍然是提高整体性能。

如今,几乎每台计算机都有多个内核,从手机等移动设备到工作站,再到服务器级机器。检查这些环境并看看我们是否可以采取不同的方法来改进平台,以更有效地支持实时、延迟敏感的应用程序,这似乎是有效的。
我们如何解决这些问题?
在我工作的 Chronicle Software,我们根据在该领域多年的经验,开发了许多开源库,以支持构建针对低延迟进行优化的应用程序。本文的其余部分描述了我们学到的一些东西,这些东西帮助我们实现了这个目标。

Java 运行时
可能影响 Java 应用程序中延迟的主要问题是与垃圾收集堆的管理以及使用锁同步访问共享资源相关的问题。存在解决这两个问题的技术,尽管它们确实要求开发人员在某种程度上偏离惯用的 Java 编程风格。理想情况下,我们会使用封装较低级别细节和专业技术的库,但我们确实需要对“幕后”发生的事情有所了解。

为低延迟应用程序设计的框架和库所青睐的一种方法是绕过 Java 垃圾收集器,方法是利用不属于正常 Java 堆的内存(称为“堆外”内存)。内存使用正常的操作系统机制映射到持久性存储,或者通过网络连接复制到其他系统。

使用这种方法的明显优点是,对内存的访问不受垃圾收集器的非确定性干预的影响。缺点是,在这些区域中创建的对象的生存期的管理成为应用程序或库的责任。
现代应用程序的常见体系结构在组件之间包含某种形式的通信,通常基于消息传递。在通信过程中,消息会序列化为标准格式(如 JSON 或 YAML)并从标准格式反序列化,提供此功能的库通常会引入高级别的对象分配。通过一些仔细的考虑,可以选择经过精心设计的库,以最大程度地减少新 Java 对象的创建,从而在性能方面产生积极影响。

从 Java 的早期开始,对共享可变数据的并发访问就使用互斥锁进行同步。如果一个线程尝试获取另一个线程持有的锁,则它将被阻塞,直到锁被释放。在多核环境中,可以使用不需要获取线程阻塞的替代技术来实现同步,并且已经表明,在大多数情况下,这对减少延迟有积极作用。

编写这种代码并不简单,但是,可以在标准 Java 库的 Lock 接口后面封装,或者通过定义允许通过标准 API 进行安全、无锁并发访问的数据结构来进一步封装。一些标准的 Java 集合库使用这种方法,尽管这对用户是透明的。

Linux操作系统
可以说,多年来,Unix的“实时”变体已经有一段时间了,它们为专门的应用程序提供了不同的执行环境。虽然这些通常是小众产品,但现在其中许多方法和功能在 Unix 和 Linux 的主流发行版中都可用。

最小化延迟的功能通常分为两类:内存管理和线程调度。

Linux 进程中的所有内存,包括 Java 的垃圾回收堆,都会被暂时“分页”到磁盘,以便其他进程可以在需求需要重新引入内存之前将 RAM 用于自己的目的。这一切都对流程完全透明地发生,内存中的数据和后备存储上的数据之间的访问时间差异可能达到几个数量级。当然,堆外内存也会受到相同行为的影响。

但是,现代 Unix 和 Linux 系统允许标记内存区域,以便操作系统在寻找要从进程中回收的区域时忽略它们。这意味着,对于该过程中的那些内存区域,内存访问时间将是一致的(并且总体上被认为更快)。不得不说,在繁忙的 Java 应用程序中,访问进程内存的频率将降低该内存被分页出去的可能性,但风险仍然存在。

以这种方式固定进程的内存意味着其他进程的内存较少,因此可能会受到影响,但在“实时”世界中,我们必须有点自私!

为低延迟而设计的数据结构通常会在默认情况下或通过选项提供将其内存锁定或固定在 RAM 中的能力。

Java 程序中的线程,就像来自其他应用程序甚至操作系统任务的线程一样,它们对 CPU 的访问由操作系统的一个组件(称为调度器)管理。调度器有一组策略,用于决定选择哪些需要访问 CPU 的线程(称为 Runnable 线程) - 通常 Runnable 线程数会比 CPU 多。

如前所述,Unix/Linux 中的传统调度策略旨在支持交互式线程而不是 CPU 绑定的线程。如果我们尝试运行对延迟敏感的应用程序,这对我们没有帮助 - 我们希望我们的线程以某种方式优先于其他对延迟不敏感的线程。

现代 Unix/Linux 系统提供了可以提供这些功能的替代调度策略,通过允许将线程调度优先级固定在较高级别,因此当它们可运行时,它们将始终从其他线程接管 CPU 资源,这意味着它们可以更快地响应事件。

但是,在影响调度程序的行为方面,也有可能走得更远。通常,在管理线程时会使用所有可用的 CPU 资源。如今,可以更改调度器使用的 CPU。我们可以从调度器可用的 CPU 中完全删除 CPU,并将这些 CPU 专门用于我们的专用线程。

或者,我们可以将 CPU 划分为多个组,并将一组 CPU 与一组特定的线程相关联。此功能是 Linux 中称为组的更通用的资源管理组件的一部分。它是 Linux 对虚拟化支持的一部分,并且是在现代环境中实现容器(例如由 Docker 生成的容器)的关键。但是,它可以通过特定的系统调用供常规应用程序使用。

就像上面描述的内存固定一样,我们是自私的,因为这样做显然会对系统的其他部分产生负面影响。配置以获得最佳结果需要非常小心,因为出错的可能性很高,出错的后果可能很严重。

结论
编写和部署低延迟应用程序是一项高技能活动,不仅需要了解所使用的语言,还需要了解应用程序运行的环境。在本文中,我概述了一些需要考虑的领域,以及如何解决这些问题。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小徐博客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值