再次出现。. 回顾 Peters关于Unsafe用法的书面概述 ,我将简要介绍一下Java中的低级技术如何通过启用更高级别的抽象或允许Java性能级别来节省开发工作可能很多人都不知道。
我的主要观点是表明,将对象转换为字节,反之亦然是一个重要的基础,实际上影响了任何现代Java应用程序。
硬件喜欢处理字节流,而不是处理通过指针连接的对象图,因为“所有内存都是磁带” (如果我没记错的话,M.Thompson ..)。
因此,许多基本技术很难与原始Java堆对象一起使用:
- 内存映射文件 –一种出色而简单的技术,可安全,快速,轻松地保存应用程序数据。
- 网络通信基于发送字节数据包
- 进程间通信 (共享内存)
- 当今服务器的大主内存 (64GB至256GB)。 (GC问题)
- CPU高速缓存最适合在内存中以连续字节流形式存储的数据
因此在大多数情况下使用Unsafe类有助于将Java对象图转换为连续内存区域,反之亦然
- [性能增强] 对象序列化或
- 包装器类,以简化对存储在连续内存区域中的数据的访问。
(本文的代码和示例可在此处找到)
基于序列化的堆外
考虑一个零售Web应用程序,其中可能有数百万个注册用户。 实际上,我们不希望在关系数据库中表示数据,因为所有必要的操作是在用户登录后快速检索与用户相关的数据。此外,我们希望快速遍历社交图。
让我们看一个简单的用户类,其中包含一些属性和构成社交图的“朋友”列表。
将其存储在堆上的最简单方法是简单的大型HashMap。
或者,可以使用堆外映射来存储大量数据。 堆外映射将其键和值存储在本机堆中,因此垃圾回收不需要跟踪此内存。 此外,可以告知本机堆自动与磁盘(内存映射文件)同步。 甚至在您的应用程序崩溃时也可以使用,因为操作系统可以管理对更改的内存区域的回写。
有一些具有各种功能集的开源堆外地图实现(例如ChronicleMap ),在此示例中,我将使用一种简单且简单的实现,该实现具有快速迭代(可选的全扫描搜索)和易用性的特点。
序列化用于存储对象,反序列化用于将它们再次拉到Java堆。 令人愉快的是,我写了这个星球上最快的,完全符合JDK的对象序列化 (afaik),因此我将利用它。
完成:
- 通过内存映射文件实现持久性(映射将在创建时重新加载)。
- Java Heap仍然是空的,无法使用Full GC <100ms进行真实的应用程序处理。
- 整体内存消耗明显减少。 序列化的用户记录约为60个字节,因此理论上3亿条记录可容纳180GB的服务器内存。 无需引发大数据标志并在AWS上运行4096个hadoop节点。
比较常规内存中的Java HashMap和基于快速序列化的持久性堆外映射(拥有1500万条用户记录),将显示以下结果(在3Ghz较旧的XEON 2×6上):
消耗的Java堆(MB) | 完整GC | 本机堆(MB) | 每秒钟获取/输入操作 | 所需的VM大小(MB) | |
哈希图 | 6.865,00 | 26,039 | 0 | 3.800.000,00 | 12.000,00 |
OffheapMap(基于序列化) | 63,00 | 0,026 | 3.050 | 750.000,00 | 500,00 |
[ 测试源/博客项目 ]注意:您至少需要16GB的RAM才能执行它们。
如人们所见,无论如何,即使进行快速序列化,访问性能也要付出沉重的代价(约5倍):与其他持久性替代方案相比,其性能仍然更好(每个“ get”操作“ put()”为1-3微秒)非常相似)。
使用JDK序列化的速度至少要慢5到10倍(下面直接比较),因此使该方法无用。
相对于更高的抽象水平,交易性能有所提高:“使我服务器化”
单个服务器将无法为成千上万的用户提供服务,因此我们需要以某种方式在进程之间甚至跨机器共享数据,甚至更好。
使用快速实现,可以为网络消息传递慷慨地使用(快速)序列化。 再说一次:如果运行速度要慢5至10倍,那将是不可行的。 替代方法需要更多数量级的工作才能获得相似的结果。
通过使用Actor实现(异步ftw!)包装持久性堆外哈希映射,一些代码行构成了具有基于TCP和HTTP接口的持久性KeyValue服务器(使用kontraktor actors )。 当然,如果稍后决定,仍可以在过程中使用Actor。
现在,这是一个微服务。 鉴于它没有进行任何优化的尝试并且是单线程的 ,因此它的速度相当快[与上述XEON机器相同]:
- 每秒280_000次成功的远程查找
- 如果失败查找,则为800_000(找不到密钥)
- 基于序列化的TCP接口(1个内衬)
- REST-of-us(1个班轮)的严格Web服务。
[ 来源:KVServer,KVClient ]注意:您至少需要16GB的RAM才能执行测试。
现实世界中的实现可能希望通过将接收到的序列化对象byte []直接放入映射中而不是对其进行两次编码(一次编码/解码以便通过网络传输,然后解码/编码以用于拆分映射)来使性能提高一倍。
“ RestActorServer.Publish(..);” 除了原始tcp之外,还可以将KVActor作为Web服务公开:
使用flyweight包装器/结构获得类似C的性能
通过序列化,常规Java对象将转换为字节序列。 一个可以做相反的事情:创建包装器类,该包装器类从基础字节数组或本机内存地址的固定或计算位置读取数据。 (例如,请参阅此博客文章 )。
通过移动基本指针,仅通过移动包装器的偏移量就可以访问不同的记录。 复制这样的“打包对象”归结为内存副本。 此外,以这种方式编写分配免费的代码非常容易。 缺点是,与常规Java对象相比,读/写单个字段会降低性能。 这可以通过使用Unsafe类来弥补。
如引用的博客文章中所示,“ flyweight”包装器类可以手动实现,但是随着代码的增长,这种情况变得难以维护。
快速序列化提供了一个副产品“结构仿真”,支持在运行时从常规Java类创建flyweight包装器类。 这样可以在很大程度上避免应用程序代码中的低级字节摆弄。
常规Java类如何映射到平面内存(fst-struct):
当然,有一些更简单的工具可以帮助减少编码的手动编程(例如Slab ),这可能更适合许多情况并且使用较少的“魔术”。
使用不同的方法(悲伤事实传入)可以期待什么样的性能?
让我们采用以下结构类,包括价格更新和表示可交易工具(例如股票)的嵌入式结构,并使用各种方法对其进行编码:
代码中的“结构”
纯编码性能:
结构 | fast-Ser(无共享裁判) | 快速服务 | JDK Ser(未共享) | JDK系列 |
26.315.000,00 | 7.757.000,00 | 5.102.000,00 | 649.000,00 | 644.000,00 |
具有消息传递吞吐量的真实世界测试:
为了对实际应用中的差异进行基本估算,我做了一个实验,当通过可靠的UDP消息以高速率发送和接收消息时,不同的编码如何执行:
考试:
发送方尽可能快地对消息进行编码,然后使用可靠的多播将其发布,订户接收并对其进行解码。
结构 | fast-Ser(无共享裁判) | 快速服务 | JDK Ser(未共享) | JDK系列 |
6.644.107,00 | 4.385.118,00 | 3.615.584,00 | 81.582,00 | 79.073,00 |
(在I7 / Win8,XEON / Linux上进行的测试得分略高,结构的msg大小约为70字节,序列化约为60字节)。
与最低速度相比,最慢速度:82。测试突出显示了微基准测试未涵盖的问题:编码和解码应执行类似的操作,因为实际吞吐量由Min(编码性能,解码性能)确定。 出于未知原因,JDK序列化设法对每秒测试的消息进行编码,例如每秒500_000次,解码性能仅为每秒80_000次,因此在测试中,接收器Swift下降:
”
…
*****接收速率统计:每秒80351 **********
*****接收速率统计:每秒78769 **********
SUB-ud4q已被服务1的PUB-9afs丢弃
致命的,无法跟上。 退出
“
(在此处创建背压可能不是解决此问题的正确方法!)
结论
- 快速序列化允许在分布式应用程序中实现某种程度的抽象,如果序列化实现是
- 太慢了
–不完整。 例如无法处理任何可序列化的对象图 –需要手动编码/修改。 (会对演员消息类型,期货,孢子,维护噩梦施加许多限制) - 诸如“不安全”之类的低级实用程序可实现数据的不同表示,从而为特殊工作负载提供超常的吞吐量或有保证的等待时间边界(无分配主路径)。 使用JDK的公共工具集不可能实现这些目标。
- 在分布式系统中,通信性能至关重要。 查看上面的数字,删除不安全并不是最大的麻烦。.JSON或XML不能解决此问题。
- 尽管HotSpot VM已达到非凡的性能和可靠性水平,但JDK的某些部分却浪费了CPU,就像没有明天一样。 考虑到我们生活在分布式应用程序和数据时代,应该很容易实现(而不是手动编码)通过网络移动内容。
附录:有限的延迟
快速的Ping Pong RTT延迟基准测试表明Java可以轻松地与C解决方案竞争,只要主要路径是无分配的,并且采用了上述技术即可:
[学分:图表和使用HdrHistogram进行的测量]
这是一个“实验”,而不是一个基准测试(因此,请不要阅读:“ 证明:Java比C更快” ),它表明低级Java至少可以在此低级领域与C竞争。
当然,它不是完全惯用的 Java代码,但是与JNI或纯C(++)解决方案相比,它仍然更易于处理,移植和维护。 低延迟的C(++)代码也不是惯用的!
翻译自: https://www.javacodegeeks.com/2015/01/a-persistent-keyvalue-server-in-40-lines-and-a-sad-fact.html