Java 虚拟 windows 服务器上的调度程序执行器计时问题

Java scheduler executor timing issues on virtual windows server

我们有一个 Java 应用程序需要在虚拟 (Hyper-V) Windows 2012 R2 服务器上 运行,以及其他环境。在这个虚拟 Windows 服务器上执行时,它似乎遇到了奇怪的计时问题。我们已将问题追溯到 Java 调度执行程序中的不稳定调度:

public static class TimeRunnable implements Runnable {

    private long lastRunAt;

    @Override
    public void run() {
        long now = System.nanoTime();
        System.out.println(TimeUnit.NANOSECONDS.toMillis(now - lastRunAt));
        lastRunAt = now;
    }

}

public static void main(String[] args) {
    ScheduledExecutorService exec = Executors.newScheduledThreadPool(1);
    exec.scheduleAtFixedRate(new TimeRunnable(), 0, 10, TimeUnit.MILLISECONDS);
}

此代码应每 10 毫秒 运行 TimeRunnable,在服务器上产生如下结果:

12
15
2
12
15
0
14
16
2
12
140
0
0
0
0
0
0
0
0
0
0
0
0
1
0
7
15
0
14
16
2
12
15
2
12
1
123
0
0
0

而在其他机器上,包括负载很重的虚拟 Linux 盒子,以及一些 windows 台式机,典型的 运行 看起来像这样:

9
9
10
9
10
9
10
10
9
10
9
9
10
10
9
9
9
9
10
10
9
9
10
10
9
9
10
9
10
10
10
11
8
9
10
9
10
9
10
10
9
9
9
10
9
9
10
10
10
9
10

我们对 Windows Server 和 Hyper-V 的经验不多,所以谁能解释一下这种现象?是 Windows 服务器问题吗?超级V?这些平台上的 Java 错误?有解决办法吗?

编辑:一位同事编写了同一程序的 C# 版本:

private static Stopwatch stopwatch = new Stopwatch();

public static void Main()
{
    stopwatch.Start();
    Timer timer = new Timer(callback, null, TimeSpan.FromMilliseconds(10), TimeSpan.FromMilliseconds(10));
}

private static void callback(object state)
{
    stopwatch.Stop();
    TimeSpan span = stopwatch.Elapsed;
    Console.WriteLine((int)span.TotalMilliseconds);
    stopwatch.Restart();
}

这是在虚拟 windows 服务器上并行工作的两个应用程序的更新(部分)屏幕截图:

编辑: Java 程序的其他一些变体都产生(几乎)相同的输出:

  1. System.nanoTime() 替换为 System.currentTimeMillis()
  2. 的变体
  3. 一种变体,其中 System.out.println() 被定期打印的 StringBuilder 替换
  4. 一种变体,其中调度机制被替换为通过 Thread.sleep()
  5. 计时的单个线程
  6. lastRunAt 易变的变体

我也不知道为什么会这样。然而,这不太可能是 Java 的错。 Java 使用本机线程,这意味着线程调度由 "the operating system" 处理。

我认为这里的真实问题是您基于错误的前提构建了应用程序。如果您阅读 Java 文档(对于普通/非实时 JVM),您将找不到任何表明 Java 线程调度是准确的内容。甚至调度优先级也是 "best effort".

您观察到调度在重负载 Linux VM 上相当准确这一事实很有趣……但不一定具有指导意义。调度准确性将取决于系统负载的性质。并且可能是平台中是否有大量 "overcommit" 内存、VCPU 和 I/O 带宽。


Is there a solution?

也许您可以想办法在您的平台上安排更多 "accurate"(在顺风的好日子里)。但是,除非您切换到实时 OS 和实时 Java 发布,否则您将无法获得任何 保证 的准确性。您不会找到任何用于虚拟化平台的实时 Java 实现。所以真正的解决办法是避免依赖精准调度。

这是由 System.currentTimeMillis() 粒度引起的。注意那里的评论:

Note that while the unit of time of the return value is a millisecond, the granularity of the value depends on the underlying operating system and may be larger.

我前阵子在一台机器上记录了大约 15ms 的粒度。这将解释您看到的所有 0 值,但不是大值。

运行 测试的 增强 版本:

static final TreeMap<Long, AtomicInteger> counts = new TreeMap<>();

public static final AtomicInteger inc(AtomicInteger i) {
    i.incrementAndGet();
    return i;
}

public static class TimeRunnable implements Runnable {

    private long lastRunAt;

    @Override
    public void run() {
        long now = System.nanoTime();
        long took = TimeUnit.NANOSECONDS.toMillis(now - lastRunAt);
        counts.compute(took, (k, v) -> (v == null) ? new AtomicInteger(1) : inc(v));
        //System.out.println(TimeUnit.NANOSECONDS.toMillis(now - lastRunAt));
        lastRunAt = now;
    }

}

public void test() throws InterruptedException {
    System.out.println("Hello");
    ScheduledExecutorService exec = Executors.newScheduledThreadPool(1);
    exec.scheduleAtFixedRate(new TimeRunnable(), 0, 10, TimeUnit.MILLISECONDS);
    // Wait a bit.
    Thread.sleep(10000);
    // Shut down.
    exec.shutdown();
    while (!exec.awaitTermination(60, TimeUnit.SECONDS)) {
        System.out.println("Waiting");
    }
    System.out.println("counts - " + counts);
}

我得到输出:

counts - {0=361, 2=1, 8=2, 13=2, 14=18, 15=585, 16=25, 17=1, 18=1, 22=1, 27=1, 62=1, 9295535=1}

巨大的异常值是第一个命中 - 当 lastRunAt 为零时。 0=361 是当你后来被称为 10msSystem.currentTimeMillis() 没有踢过它的一个滴答声。请注意 15=585 处的峰值,正如我建议的那样,在 15ms 处显示出清晰的峰值。

62=1我没有任何解释。

我认为您需要提高 java 应用程序进程和 java 应用程序内部工作线程的优先级。在 java 应用程序中增加工作线程的优先级很容易。但是将 java 应用程序设置为比您得到的更高 cpu 是很棘手的。 这可能有助于提高您的程序 cpu

How to change the priority of a running java process?

https://blogs.msdn.microsoft.com/oldnewthing/20100610-00/?p=13753

您也可以查看实时信息 cpu 但请注意,它可能会延迟您的其他内核活动,包括鼠标和键盘事件

延迟肯定是由于任务无法在指定时间开始,因此下一个任务在调整固定速率的周期时间之前被触发,如下所述: Java Timer

  1. 大多数现代硬件都提供多个定时器源。此外,大多数操作系统提供几个 API 来访问这些具有不同精度的定时器计数器(例如系统定时器和 RTC)。了解 Microsoft、.NET 平台(以及大多数 MS 产品)利用了对 Win32 API 和内核 API 的深入了解。我的直觉是 C# 中的定时器 class 使用与 Java 不同的 API (Hotspot VM 实现描述 here,尽管它对 Java 5 是正确的)。

  2. 虚拟环境中的计时器精度存在普遍问题。我发现非常有趣的测试结果 http://www.ncbi.nlm.nih.gov/pmc/articles/PMC4503740/ describing similar issues with different hypervisors. The funny thing that Hyper-V not mentioned there, but the problem looks like not unique for the particular setup. Microsoft has an issue 关于 Hyper-V 运行 在 Windows 2008 R2 上提供的计时器正确性。天知道不同云提供商的云 运行 是什么。我个人能够在 AWS 云上重现该问题。

  3. 所以 "What's this effect is" 的答案 - 这是管理程序错误与 Java 实现 "features" 相结合。可以肯定的是,您可以尝试 运行 使用 OpenJDK 进行此测试,您可以在其中查看代码并使用不同的计时器源。

  4. 但出于实际原因,我建议避免在 Windows VM 上使用 运行 计时器敏感 Java 代码。如果这是非常严格的要求,我会尝试使用 Win32 计时器并从那里调用 JVM 代码(使用 JNI)或实现任何其他计时器源(使用命名管道或任何其他特定于平台的补丁)。您可以尝试使用 Quartz 作为计时器和调度程序,但它可能也遇到同样的问题。