两个JVM之间的低CPU使用率轮询架构

3

服务器环境

  • Linux/RedHat操作系统
  • 6核心CPU
  • Java 7/8版本

关于应用程序:

  • 我们正在开发一个使用Java的低延迟(7-8毫秒)高速交易平台
  • 该平台有2个模块A和B,每个模块都在自己的JVM上运行
  • B从A获取数据

架构:

  • 我们利用了内存映射和Unsafe技术。在这种情况下,模块A将数据写入到一个内存映射文件中,模块B从该文件中读取数据(两个模块都持有文件的地址位置)
  • 我们采用无限循环的方式继续读取,直到从内存映射文件中获得所需的值为止

问题:

  • CPU利用率飙升到100%并保持在该水平直到其生命周期结束

问题:

是否有更复杂的方式来轮询内存映射文件中的值,并且需要最小的开销、延迟和CPU利用率?请注意,每微秒的延迟都会降低性能。

代码片段:

模块B的代码片段(用于轮询和从内存映射文件中读取数据的无限循环)如下:

FileChannel fc_pointer = new RandomAccessFile(file, "rw").getChannel();
      MappedByteBuffer mem_file_pointer =fc_pointer.map(FileChannel.MapMode.READ_ONLY, 0, bufferSize);
      long address_file_pointer = ((DirectBuffer) mem_file_pointer).address();


    while(true)
    {
        int value_from_memory_mapped_file = unsafe.getInt(address_file_pointer);

        if (value_from_memory_mapped_file .. is different from the last read value)
        {
            //do some operation.... 
        //exit the routine;
        }
        else
        {
            continue;
        }
}//end of while

好的,如果您使用传统的线程间信号(例如信号量)而不是浪费CPU和内存带宽的轮询,延迟会变得更糟吗? - Martin James
另外,如果您需要高性能,为什么要使用两个JVM? - Martin James
Martin James,理论上,即使对于线程间信号传递,自旋锁在吞吐量和延迟方面都比锁要好得多。例如,请参见著名的https://code.google.com/p/disruptor/wiki/PerformanceResults 当然,缓存丢失可能会降低差异,但正如我所说的“在理论上...” :) - AnatolyG
1个回答

4
  1. 高负载的CPU是实现最低延迟的真正代价。在使用无锁信号的实际架构中,每个CPU插槽应该运行不超过一对生产者和消费者线程。一对线程占用一个或两个核心(如果不将其钉到启用超线程的单个Intel CPU核心上)几乎完全占据了CPU(这就是为什么在构建面向多客户端的超低延迟服务器系统时大多需要考虑水平可扩展性)。另外,在性能测试之前,请不要忘记使用“taskset”将每个进程固定到特定的核心,并禁用电源管理。

  2. 当你长时间自旋等待仍然没有结果时,锁定消费者是众所周知的技巧。但是,您需要花费一些时间来停放并取消停放线程。当线程停放时,这里会出现间歇性的延迟增加,当然,但CPU核心处于空闲状态。例如,请参见: http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf (8.4.4长期同步) 此外,可以在这里找到有关Java的不同等待策略的良好说明:https://github.com/LMAX-Exchange/disruptor/wiki/Getting-Started (替代等待策略)

  3. 如果您谈论的是毫秒(ms),而不是微秒(µs),则可以尝试通过环回进行TCP套接字通信。它增加了约10 µs的时间来传递从生产者到消费者的少量数据,并且这是阻塞技术。命名管道比套接字具有更好的延迟特性,但它们实际上是非阻塞的,您必须再次构建类似自旋循环的东西。内存映射文件+内在Unsafe.getXXX(它是单个x86 MOV)仍然是IPC技术方面延迟和吞吐量方面最好的技术,因为在读取和写入时不需要系统调用。

  4. 如果您仍然要使用无锁和Memory Mapped Files以及使用Unsafe的直接访问,请不要忘记为生产者和消费者都添加适当的内存屏障。例如,如果您不确定您的代码是否始终运行在较新的x86上,则使用“unsafe.getIntVolatile”而不是首先使用“unsafe.getInt”。

  5. 如果您看到意外的CPU利用率超过应该为一对生产者-消费者使用的30-40%(6个核心的CPU中使用了2个核心),则必须使用标准工具检查在其他核心上运行的内容和整个系统性能。如果您看到与映射文件相关的密集IO,请确保将其映射到tmpfs文件系统以防止实际磁盘IO。检查最“胖”的进程的内存总线负载和L3缓存未命中,因为正如我们所知,CPU时间=(CPU执行时钟周期+_memory_stall_cycles_)*时钟周期时间。

最后,还有一个相似而且有趣的开源项目,它提供了如何使用内存映射文件的良好示例:http://openhft.net/products/chronicle-queue/


如果自旋锁计数超过了限制,使用sleep()调用是没有意义的。应该退而求其次,使用事件/信号量内核同步。 - Martin James
我并不是指“sleep()”函数的调用。这只是一个比喻,表示线程的停放/唤醒(例如使用操作系统互斥锁)。从字面上讲,我的意思是这个:http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf(8.4.4 长时间同步)。一个技巧的例子是http://hg.openjdk.java.net/jdk7/jdk7/hotspot/file/fa83ab460c54/src/share/vm/runtime/objectMonitor.cpp,当我们尝试使用CAS时,先自旋,然后才将线程停放。但是谢谢,我已经稍微更新了一下我的文本 :) - AnatolyG

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接