Infinispan1 是一个开源数据网格平台。它是一个分布式、内存中的键值 NoSQL 存储。软件架构师通常使用像 Infinispan 这样的数据网格,要么作为一种性能增强型的分布式内存缓存,置于昂贵、速度慢的数据存储(如关系型数据库)前面,要么作为一种分布式 NoSQL 数据存储来替换关系型数据库。无论哪种情况,在任何软件架构中考虑数据网格的主要原因都是性能。对快速低延迟数据访问的需求越来越普遍。
因此,性能是 Infinispan 的唯一 raison d'être。Infinispan 的代码库反过来对性能极其敏感。
在深入研究 Infinispan 的内部之前,让我们考虑一下 Infinispan 通常是如何使用的。Infinispan 属于一种称为中间件的软件类别。根据维基百科的定义,中间件“可以被描述为软件粘合剂”——位于服务器上的组件,介于应用程序(如网站)和操作系统或数据库之间。中间件通常用于提高应用程序开发人员的生产力、效率,并能以更快的速度推出更易于维护和测试的应用程序。所有这些都是通过模块化和组件重用实现的。Infinispan 特别是经常置于任何应用程序处理或业务逻辑与数据存储层之间。 数据存储(和检索)通常是最大的瓶颈,在数据库前面放置一个内存中的数据网格通常会使事情变得更快。此外,数据存储也往往是单点冲突和潜在故障点。再次利用 Infinispan 置于(甚至替代)更传统的数据存储前面,应用程序可以实现更高的弹性和可扩展性。
Infinispan 已应用于多个行业,从电信到金融服务,从高端电子商务到制造系统、游戏和移动平台。数据网格总体上一直很受金融服务行业的欢迎,因为它们对以隔离它们不受单个机器故障影响的方式,以极快的速度访问大量数据提出了严格的要求。这些要求已扩展到其他行业,这也促成了 Infinispan 在如此广泛的应用中的普及。
Infinispan 是用 Java(和一些 Scala)实现的,可以用两种不同的方式使用。首先,它可以用作库,嵌入到 Java 应用程序中,方法是包含 Infinispan JAR 文件并以编程方式引用和实例化 Infinispan 组件。这样,Infinispan 组件位于与应用程序相同的 JVM 中,应用程序堆内存的一部分被分配为数据网格节点。
图 7.1 - Infinispan 作为库
其次,它可以通过启动 Infinispan 实例并允许它们形成集群来用作远程数据网格。然后,客户端可以通过多个可用客户端库之一中的一个套接字连接到此集群。这样,每个 Infinispan 节点都存在于其自己的独立 JVM 中,并拥有整个 JVM 堆内存。
图 7.2 - Infinispan 作为远程数据网格
在这两种情况下,Infinispan 实例在网络上相互检测,形成一个集群,并开始共享数据以向应用程序提供透明地跨越集群中所有服务器的内存中数据结构。这允许应用程序理论上寻址无限量的内存存储,因为节点被添加到集群中,从而增加了总容量。
Infinispan 是一种点对点技术,其中集群中的每个实例都等于集群中的其他所有实例。这意味着没有单点故障,也没有单点瓶颈。最重要的是,它为应用程序提供了一种弹性数据结构,可以通过添加更多实例来进行横向扩展。也可以通过关闭一些实例来进行缩减,同时允许应用程序继续运行,不会丢失整体功能。
对像 Infinispan 这样的分布式数据结构进行基准测试时,最大的问题是工具。几乎没有什么工具可以让你在扩展和缩减时测量存储和检索数据的性能。也没有任何工具提供比较分析,能够测量和比较不同配置、集群大小等的性能。为了解决这个问题,创建了 Radar Gun。
Radar Gun 在 Radar Gun 中有更深入的介绍。这里提到的其他工具——雅虎云服务基准测试、The Grinder 和 Apache JMeter——尽管在对 Infinispan 进行基准测试时仍然非常重要,但介绍的深度并不那么深。关于这些工具的大量文献已经在网上存在。
Radar Gun2 是一个开源基准测试框架,旨在执行比较性(以及竞争性)基准测试,以测量可扩展性并从收集的数据点生成报告。Radar Gun 特别针对像 Infinispan 这样的分布式数据结构,在 Infinispan 的开发过程中被广泛使用,以识别和修复瓶颈。 有关 Radar Gun 的更多信息,请参阅 Radar Gun。
雅虎云服务基准测试3 (YCSB) 是一个开源工具,用于测试与远程数据存储进行通信以读取或写入不同大小数据的延迟。YCSB 将所有数据存储视为单个远程端点,因此它不尝试测量节点被添加到集群或从集群中移除时的可扩展性。由于 YCSB 无法理解分布式数据结构的概念,因此它仅适用于在客户端/服务器模式下对 Infinispan 进行基准测试。
The Grinder4 和 Apache JMeter5 是两个简单的开源负载生成器,可用于测试侦听套接字的任意服务器。它们是高度可脚本化的,并且与 YCSB 一样,在客户端/服务器模式下对 Infinispan 进行基准测试时非常有用。
由 Infinispan 核心开发团队创建的 Radar Gun 最初是一个名为缓存基准测试框架6 的 Sourceforge 项目,最初旨在对以不同模式运行的嵌入式 Java 缓存进行基准测试,并以各种配置运行。它被设计为比较性的,因此它会自动对各种缓存库(或同一库的不同版本)运行相同的基准测试,以测试性能回归。
自创建以来,它获得了一个新名称(Radar Gun)、一个新家 在 GitHub 上 以及许多新功能。
Radar Gun 很快就扩展到覆盖分布式数据结构。Radar Gun 仍然专注于嵌入式库,它能够在不同的服务器上启动框架的多个实例,这些实例反过来将启动分布式缓存库的实例。然后,基准测试在集群中的每个节点上并行运行。结果由 Radar Gun 控制器整理并生成报告。能够自动启动和关闭节点对于可扩展性测试至关重要,因为手动在不同大小的集群上运行和重新运行基准测试变得不可行且不切实际,从两个节点一直到数百个甚至数千个节点。
图 7.3 - Radar Gun
然后,Radar Gun 获得了在运行每个阶段的基准测试之前和之后执行状态检查的能力,以确保集群仍处于有效状态。这允许早期检测不正确的结果,并允许重新运行基准测试,而无需在运行结束时等待手动干预——这可能需要几个小时。
Radar Gun 还能够启动并附加分析器实例到每个数据网格节点,并拍摄分析器快照,以便更深入地了解每个节点在负载下的运行情况。
Radar Gun 还能够测量每个节点的内存使用状态,以测量内存性能。在内存中的数据存储中,性能不仅仅取决于读取或写入数据的速度,还取决于结构在内存使用方面的表现。这在基于 Java 的系统中尤为重要,因为垃圾回收可能会对系统的响应能力产生负面影响。垃圾回收将在后面更详细地讨论。
Radar Gun 以每秒事务数来衡量性能。这在每个节点上被捕获,然后在控制器上聚合。读取和写入都被单独测量和绘制,即使它们是同时执行的(以确保现实的测试,其中此类操作是交织的)。Radar Gun 还捕获读取和写入事务的平均值、中位数、标准差、最大值和最小值,并且这些值也会被记录,尽管它们可能不会被绘制。内存性能也会被捕获,通过任何给定迭代的内存占用量来表示。
Radar Gun 是一个可扩展的框架。它允许你插入自己的数据访问模式、数据类型和大小。此外,它还允许你向任何数据结构、缓存库或 NoSQL 数据库添加适配器,你想要测试这些数据库。
最终用户通常也被鼓励在尝试比较数据网格的不同配置的性能时使用 Radar Gun。
Infinispan 的几个子系统是性能瓶颈的主要嫌疑人,因此,它们是仔细审查和潜在优化的候选对象。让我们依次看看它们。
网络通信是 Infinispan 最昂贵的组成部分,无论是用于对等体之间的通信还是用于客户端和网格本身之间的通信。
Infinispan 利用 JGroups7,这是一个开源的点对点组通信库,用于节点间通信。JGroups 可以使用 TCP 或 UDP 网络协议,包括 UDP 多播,并提供高级功能,如消息传递保证、重传和消息排序,即使在 UDP 等不可靠协议上也是如此。
正确调整 JGroups 层变得至关重要,以匹配网络和应用程序的特性,例如生存时间、缓冲区大小和线程池大小。同样重要的是要考虑 JGroups 执行捆绑的方式——将多个小消息组合成单个网络数据包——或者碎片——捆绑的反面,其中大消息被分解成多个较小的网络数据包。
您操作系统上的网络栈和您的网络设备(交换机和路由器)也应调整以匹配此配置。操作系统 TCP 发送和接收缓冲区大小、帧大小、巨型帧等,都在确保数据网格中最昂贵的组件能够最佳地执行方面发挥作用。
netstat
和 wireshark
等工具可以帮助分析数据包,而 Radar Gun 可以帮助通过网格驱动负载。Radar Gun 还可以用于分析 Infinispan 的 JGroups 层,以帮助定位瓶颈。
Infinispan 利用流行的 Netty8 框架来创建和管理服务器套接字。Netty 是围绕异步 Java NIO 框架的包装器,该框架反过来利用操作系统的异步网络 I/O 功能。这允许在一些上下文切换的代价下有效地利用资源。总的来说,这在负载下表现得非常好。
Netty 提供了几种调整级别以确保最佳性能。这些包括缓冲区大小、线程池等等,也应该与操作系统 TCP 发送和接收缓冲区相匹配。
在将数据放到网络上之前,应用程序对象需要被序列化成字节,以便它们可以被推送到网络、网格中,然后再次在对等方之间传递。当应用程序读取字节时,这些字节需要被反序列化回应用程序对象。在大多数常见配置中,大约 20% 的请求处理时间花在了序列化和反序列化上。
默认 Java 序列化(和反序列化)在 CPU 周期和生成的字节方面都非常慢——它们通常是不必要的大,这意味着需要在网络上推送更多数据。
Infinispan 使用自己的序列化方案,其中完整的类定义不会写入流。相反,使用已知类型的幻数,其中每个已知类型由一个字节表示。这不仅极大地提高了序列化和反序列化速度,而且还为通过网络传输生成了更紧凑的字节流。为每个已知数据类型注册一个外部化器,注册到一个幻数。此外部化器包含将对象转换为字节以及反之的逻辑。
此技术适用于已知类型,例如在对等节点之间交换的内部 Infinispan 对象。内部对象——如命令、信封等——具有外部化器和相应的唯一幻数。但应用程序对象呢?默认情况下,如果 Infinispan 遇到一个未知的对象类型,它会回退到该对象的默认 Java 序列化。这允许 Infinispan 开箱即用——尽管在处理未知应用程序对象类型时效率较低。
为了解决这个问题,Infinispan 允许应用程序开发人员为应用程序数据类型注册外部化器。只要应用程序开发人员可以为每个应用程序对象类型编写和注册外部化器实现,这就可以允许对应用程序对象进行强大、快速、高效的序列化。
此外部化器代码已作为单独的可重用库发布,称为 JBoss Marshalling9。它与 Infinispan 打包在一起,包含在 Infinispan 发行版中,但也用于各种其他开源项目以提高序列化性能。
除了作为内存中的数据结构外,Infinispan 还可以选择性地持久化到磁盘。
持久化可以是为了持久性——为了在重启或节点故障中幸存,在这种情况下,内存中的所有内容也存在于磁盘上——或者可以将其配置为溢出,当 Infinispan 耗尽内存时,在这种情况下,它的行为类似于操作系统的页面到磁盘。在后一种情况下,只有当数据需要从内存中逐出以释放空间时,数据才会被写入磁盘。
当为了持久性而持久化时,持久化可以是联机的,应用程序线程被阻塞,直到数据安全地写入磁盘,或者可以是离线的,数据被定期且异步地刷新到磁盘。在后一种情况下,应用程序线程不会在持久化过程中被阻塞,以换取不确定性,即数据是否已成功持久化到磁盘。
Infinispan 支持多种可插拔的缓存存储——可用于将数据持久化到磁盘或任何形式的辅助存储的适配器。当前的默认实现是一个简单的哈希桶和链表实现,其中每个哈希桶由文件系统上的一个文件表示。虽然易于使用和配置,但这并不是性能最佳的实现。
目前路线图上有两个基于文件系统的高性能本地缓存存储实现。两者都将用 C 编写,并能够在可用时(例如在 Unix 系统上)进行系统调用并使用直接 I/O,以绕过内核缓冲区和缓存。
其中一个实现将针对用作分页系统进行优化,因此需要具有随机访问,可能是 b 树结构。
另一个将被优化为持久存储,并镜像内存中存储的内容。因此,它将是一个只追加的结构,旨在快速写入,但不一定快速读取/查找。
与大多数企业级中间件一样,Infinispan 非常偏向于现代的多核系统。为了利用多核和 SMP 系统中大量硬件线程以及在处理网络和磁盘通信时进行非阻塞、异步 I/O 的并行性,Infinispan 的核心数据结构利用了软件事务内存 技术来实现对共享数据的并发访问。这最大限度地减少了对显式锁、互斥锁和其他形式同步的需求,更倾向于在循环内使用诸如比较和设置操作之类的技术来在更新共享数据结构时实现正确性。这些技术已被证明可以提高多核和 SMP 系统中的 CPU 利用率,并且尽管代码复杂性增加,但在负载下总体性能已有所提高。
除了使用软件事务内存方法的好处之外,这也使 Infinispan 能够在将来利用 CPU 中的同步支持指令——硬件事务内存——的力量,当此类 CPU 变得普遍时,Infinispan 的设计只需要进行最小的更改。
Infinispan 中使用的几种数据结构直接来自学术研究论文。事实上,Infinispan 中使用的无阻塞、无锁的双端队列10 是该结构的第一个 Java 实现。其他示例包括针对锁摊销11 和自适应逐出策略12 的新颖设计。
各种 Infinispan 子系统利用发生在单独线程上的异步操作。例如,JGroups 为监视网络套接字分配线程,该线程随后解码消息并将消息传递给消息传递线程。这反过来可能会尝试将数据存储在磁盘上的缓存存储中——这也可以是异步的,并使用单独的线程。监听器也可能会收到更改通知,这可以配置为异步的。
在处理线程池以处理此类异步任务时,始终存在上下文切换开销。值得注意的是,线程并非廉价资源。为任何利用 Infinispan 异步功能的安装分配大小和配置合适的线程池非常重要。
要关注的特定区域是异步传输线程池(如果使用异步通信),并确保此线程池至少与每个节点预期处理的并发更新数量一样大。同样,在调整 JGroups 时,OOB13 和传入线程池应该至少与预期并发更新的数量一样大。
图 7.4 - Infinispan 中的线程
关于使用 JVM 垃圾收集器的通用最佳实践对于任何基于 Java 的软件来说都是一个重要的考虑因素,Infinispan 也不例外。如果有什么不同的话,它对于数据网格来说更加重要,因为容器对象可能会存活很长时间,而许多短暂对象——与特定操作或事务相关的对象——也会被创建。此外,垃圾回收暂停会对分布式数据结构产生不利影响,因为它们会使节点无响应,并导致节点被标记为已故障。
在设计和构建 Infinispan 时已经考虑到了这些因素,但同时在配置 JVM 以运行 Infinispan 时有很多需要考虑的因素。每个 JVM 都不同。但是,已经对运行 Infinispan 时某些 JVM 的最佳设置进行了分析14 。例如,如果使用 OpenJDK15 或 Oracle 的 HotSpot JVM16,则使用并发标记和清除收集器17 以及大型页面18 对于每个大约 12 GB 堆的 JVM 来说似乎是一个最佳配置。
此外,无暂停垃圾收集器——例如 Azul 的 Zing JVM20 中使用的 C419——值得考虑,在这些情况下,垃圾回收暂停会成为一个明显的问题。
以性能为中心的中间件,如 Infinispan,必须在各个阶段都以性能为导向进行架构、设计和开发。从使用最佳的无阻塞和无锁算法,到了解垃圾产生的特征,再到开发时注重 JVM 上下文切换开销,以及能够在需要时(例如编写本地持久性组件)走出 JVM,这些都是开发 Infinispan 所需思维方式的重要组成部分。此外,用于基准测试和分析的正确工具,以及以持续集成方式运行基准测试,有助于确保在添加功能时不会牺牲性能。
Http://www.md.chalmers.se/~tsigas/papers/Lock-Free-Deques-Doubly-Lists-JPDC.pdf.↩
Http://www.jgroups.org/manual/html/user-advanced.html#d0e3284.↩
Http://howtojboss.com/2013/01/08/data-grid-performance-tuning/.↩
Http://www.oracle.com/technetwork/java/javase/downloads/index.html.↩
Http://www.oracle.com/technetwork/java/javase/gc-tuning-6-140523.html#cms.↩
Http://www.oracle.com/technetwork/java/javase/tech/largememory-jsp-137182.html.↩
Http://www.azulsystems.com/technology/c4-garbage-collector.↩