【问题标题】:Java NIO selector minimum possible latencyJava NIO 选择器最小可能延迟
【发布时间】:2012-08-23 20:26:37
【问题描述】:

我正在 Linux over loopback (127.0.0.1) 上使用优化的 Java NIO 选择器进行一些基准测试。

我的测试很简单:

  • 一个程序将 UDP 数据包发送到另一个程序,该程序将其回显给发送者,并计算往返时间。下一个数据包仅在前一个数据包被确认(返回时)时发送。在执行基准测试之前,会使用数百万条消息进行适当的预热。该消息有 13 个字节(不包括 UDP 标头)。

对于往返时间,我得到以下结果:

  • 最短时间:13 微秒
  • 平均时间:19 微秒
  • 75% 百分位数:18,567 纳秒
  • 90% 百分位数:18,789 纳秒
  • 99% 百分位数:19,184 纳秒
  • 99.9% 百分位数:19,264 纳秒
  • 99.99% 百分位数:19,310 毫微
  • 99.999% 百分位数:19,322 纳秒

但这里的问题是我正在旋转 100 万条消息。

如果我只旋转 10 条消息,我会得到截然不同的结果:

  • 最短时间:41 微秒
  • 平均时间:160 微秒
  • 75% 百分位:150,701 纳秒
  • 90% 百分位数:155,274 毫微
  • 99% 百分位数:159,995 纳秒
  • 99.9% 百分位数:159,995 纳秒
  • 99.99% 百分位数:159,995 纳秒
  • 99.999% 百分位数:159,995 纳秒

如果我错了,请纠正我,但我怀疑一旦我们让 NIO 选择器旋转,响应时间就会变得最佳。但是,如果我们发送消息之间的间隔足够大,我们就要付出唤醒选择器的代价。

如果我只发送一条消息,我会收到 150 到 250 微秒之间的不同时间。

所以我对社区的问题是:

1 - 对于此往返数据包测试,我的最短时间为 13 微秒,平均为 19 微秒。到目前为止,我似乎击败了ZeroMQ,所以我可能在这里遗漏了一些东西。从这个基准来看,ZeroMQ 在标准内核上的平均时间为 49 微秒(99% 百分位数)=> http://www.zeromq.org/results:rt-tests-v031

2 - 当我旋转单个或很少的消息时,我可以做些什么来改善选择器的反应时间? 150微不好看。或者我应该假设在 prod 环境中选择器不会完全正确?


通过忙于在 selectNow() 周围旋转,我可以获得更好的结果。发送少量数据包仍然比发送很多数据包更糟糕,但我认为我现在达到了选择器性能限制。我的结果:

  • 发送单个数据包我得到一致的 65 微秒往返时间。
  • 发送两个数据包平均往返时间约为 39 微秒。
  • 发送 10 个数据包,平均往返时间约为 17 微秒。
  • 发送 10,000 个数据包,平均往返时间约为 10,098 纳秒。
  • 发送 100 万个数据包,平均往返时间为 9,977 纳秒。

结论

  • 所以看起来 UDP 数据包往返的物理障碍平均为 10 微秒,尽管我有一些数据包在 8 微秒(最短时间)内完成了行程。

  • 在忙碌的旋转中(感谢 Peter),我能够从平均 200 微秒增加到单个数据包的平均 65 微秒。

  • 不知道为什么 ZeroMQ 是 5 times slower。 (编辑:可能是因为我通过环回在同一台机器上测试这个,而 ZeroMQ 使用的是两台不同的机器?)

【问题讨论】:

  • 我认为这主要是由于 HotSpot JVM 预热时间而不是选择器的具体行为。
  • 感谢@EJP,但我确实在 -server 模式下对 JVM 做了一些热身。在发送将触发基准测试的消息之前,我发送了几百万条消息。你为什么认为会发生这种情况=>“如果我只发送一条消息,我会收到 150 到 250 微秒之间的不同时间。”
  • 叫我疯了,但你为什么不直接用 C 重新实现你的(根据描述)短程序并看看性能。
  • @NoSenseEtAl 说我疯了,但我很想有一个非阻塞选择器的 C 实现,我的 Java 程序可以通过 JNI 调用它。哪里有这么强大的东西?
  • @Julie 我的建议是关于暖/冷性能...您可以在 C 中编写简单的 UDP 代码并运行 1M 和 10 个消息,看看它是否具有相同的分布 - 如果有的话可能不是选择器预热问题。关于 C 实现-我不知道,尽管 wiki 建议可以这样做:“例如,符合 POSIX 的操作系统将直接表示这些概念,select()。”另外,您可能想查看 LMAX Distruptor,不只是为了 Disruptor,他们有很多博客解释如何编写低延迟 Java 代码。

标签: java real-time messaging nio zeromq


【解决方案1】:

您经常看到唤醒线程的情况可能非常昂贵,这不仅是因为线程唤醒需要时间,而且线程在缓存之后的数十微秒内运行速度会慢 2-5 倍,并且

我过去避免这种情况的方法是忙着等待。不幸的是 selectNow 每次调用它都会创建一个新集合,即使它是一个空集合。这会产生如此多的垃圾,不值得使用。

解决此问题的一种方法是忙于等待非阻塞套接字。这并不能很好地扩展,但可以为您提供最低的延迟,因为线程不需要唤醒,并且您在此之后运行的代码更有可能在缓存中。如果你也使用线程关联,它可以减少你的线程干扰。

我还建议尝试减少代码锁定和垃圾。如果您这样做,您可以在 Java 中创建一个进程,该进程在 90% 的时间里在 100 微秒内发送对传入数据包的响应。这将允许您在每个数据包到达时以 100 Mb 的速度处理它们(由于带宽限制,最多相隔 145 微秒)对于 1 Gb 连接,您可以非常接近。


如果你想在 Java 中的同一个盒子上进行快速的进程间通信,你可以考虑像 https://github.com/peter-lawrey/Java-Chronicle 这样的东西,它使用共享内存来传递消息,往返延迟(使用套接字更难有效)小于 200 纳秒。它还可以持久化数据,如果您只是想要一种快速生成日志文件的方法,它会很有用。

【讨论】:

  • 嗨,彼得。请根据您的 cmets 查看我的新结果。知道为什么 ZeroMQ 比这慢 5 倍吗?
  • ZeroMQ 要做的不仅仅是在单个套接字上发送数据包。它必须做更多的工作,路由等,所以它的延迟会更高。我还怀疑它使用后台线程来进行发送接收,这提高了对连接的可管理性和控制(或至少其中许多库这样做)您经常看到的权衡之一是,通过使用发送线程批处理消息,您可以将吞吐量提高 10 倍,这是许多库关注的重点,而不是延迟。
  • 我怀疑差异是因为我正在通过 LOOPBACK 测试它。我试图通过环回找到 ZeroMQ 基准进行比较。一个发送线程!?这太可怕了!为什么不能只调用通道写入并让操作系统完成其余的工作?对于低延迟,任何不同于 NIO 的东西都是无意义的恕我直言。
  • @Julie 使用 NIO,值得记住的是,您可以阻塞 NIO,并忙于等待发送/接收。使用选择器不是唯一的选择。为了在真正的低延迟网络上进行测试,我建议尝试 solarflare,因为他们有一个库,它支持从 Java 绕过内核,而无需使用 JNI。您可以通过这种方式从 Java 中实现个位数的延迟。
  • @Julie 从理论上讲,您可以使用任何设备实现内核绕过。但是,要做到这一点,您需要实现自己的设备驱动程序,这将比购买已经支持此功能的设备花费更多的开发成本。有许多支持内核绕过的网络适配器,并且大多数都需要您实现与 C 的集成。Solarflare 支持从 Java 中使用它而无需额外开发。
【解决方案2】:

如果您正确调整选择器,您可以在不到 2 微秒的时间内获得 Java 中的套接字间通信。以下是我对 256 字节 UDP 数据包的单向结果:

Iterations: 1,000,000
Message Size: 256 bytes
Avg Time: 1,680 nanos
Min Time: 1379 nanos
Max Time: 7020 nanos
75%: avg=1618 max=1782 nanos
90%: avg=1653 max=1869 nanos
99%: avg=1675 max=1964 nanos
99.9%: avg=1678 max=2166 nanos
99.99%: avg=1679 max=5094 nanos
99.999%: avg=1680 max=5638 nanos

我在我的文章Inter-socket communication with less than 2 microseconds latency 中详细讨论了 Java NIO 和反应器模式。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-02-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多