【问题标题】:Java scheduler executor timing issues on virtual windows server虚拟 Windows 服务器上的 Java 调度程序执行程序计时问题
【发布时间】:2016-05-27 15:24:11
【问题描述】:

我们有一个 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 Server 问题吗?超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. System.out.println() 被定期打印的 StringBuilder 替换的变体
  3. 一种变体,其中调度机制被替换为单个线程,该线程通过Thread.sleep() 对自身进行计时
  4. lastRunAt 易变的变体

【问题讨论】:

  • 我不知道为什么会发生这种情况,但我确实有一个建议,尝试使用一个小的 c# 应用程序来做同样的事情,看看结果是否相似。在windows server主机上运行这个app(希望也是windows 2012 R2),看看是不是也有时间问题,这是为了判断是windows server还是hyper-v的问题。
  • @Augusto 我已经用 C# 版本的程序更新了问题
  • @Malt 真的很奇怪!!!你能使用virtual vm 生成线程转储吗?可能正在分析转储将导致帮助..
  • 罪魁祸首是sysout?您可以尝试用appending to a StringBuilder 替换打印语句,然后尝试打印相同的语句,以尽量减少外部因素的影响..
  • 那些与程序不相等,nanoTime() 与 .net 调用不同,因此它可能从不同的来源获取信息。你试过System.currentTime()吗?

标签: java windows hyper-v


【解决方案1】:

我也不知道为什么会这样。但是,这不太可能是 Java 的错。 Java 使用本地线程,这意味着线程调度由“操作系统”处理。

我认为这里的真正问题是您基于错误的前提构建了一个应用程序。如果您阅读 Java 文档(对于普通/非实时 JVM),您将找不到任何说明 Java 线程调度是准确的。甚至安排优先级也是“尽力而为”。

您观察到调度在负载很重的 Linux 虚拟机上相当准确的事实很有趣……但不一定具有指导意义。调度的准确性将取决于系统负载的性质。并且可能是平台中是否存在显着的内存、VCPU 和 I/O 带宽“过度使用”。


有解决办法吗?

也许您可以想办法让您的平台上的日程安排更加“准确”(在天气好的时候,顺风顺水)。但是,除非您切换到实时操作系统和实时 Java 版本,否则您不会获得任何准确性的保证。您不会找到任何虚拟化平台的实时 Java 实现。所以真正的解决方案是避免依赖准确的调度。

【讨论】:

  • 我们从未假设我们有任何硬准确性保证,因此这种行为不会破坏我们的任何逻辑。然而,在所有其他环境中,如果我们安排一个任务以 10 毫秒的周期运行,我们通常会看到该任务每 8 到 12 毫秒执行一次,偶尔会跳到 100 毫秒。然而,在这台服务器上,我们还差得很远。有些东西似乎真的坏了,这让我们很好奇。
  • 此外,.NET 版本的测试程序(请参阅更新的问题)在调度其任务方面似乎非常一致。这表明该平台上的 Java 可能会出现问题。
  • IMO 这在预期行为范围内。因此,我认为你应该认为你的“好奇心”是“满足的”,然后继续前进。可能存在系统管理问题;例如调整特定的 VM 或管理程序以提高调度“准确性”,但这不是编程问题 (IMO)。
【解决方案2】:

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

请注意,虽然返回值的时间单位是毫秒,但值的粒度取决于底层操作系统,可能更大。

不久前,我在一台机器上记录了大约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 是你后来被称为 10ms 的时候,但 System.currentTimeMillis() 没有踢过它的一个刻度。请注意15=585 处的峰值,正如我所建议的那样,15ms 处显示出明显的峰值。

我对@9​​87654333@没有任何解释。

【讨论】:

【解决方案3】:

我认为您需要同时提高 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 p>

【讨论】:

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

    2. 虚拟环境中的计时器精度存在一般问题。我发现非常有趣的测试结果http://www.ncbi.nlm.nih.gov/pmc/articles/PMC4503740/ 描述了不同管理程序的类似问题。 Hyper-V 在那里没有提到的有趣的事情,但是对于特定的设置,这个问题看起来 不是唯一的。 Microsoft 有一个issue,关于在 Windows 2008 R2 上运行的 Hyper-V 提供的计时器正确性。上帝知道不同云提供商在云中运行的是什么。我个人能够在 AWS 云上重现该问题。

    3. 因此,“这是什么效果”的答案 - 这是虚拟机管理程序错误与 Java 实现“功能”相结合。可以肯定的是,您可以尝试使用 OpenJDK 运行此测试,您可以在其中查看代码并使用不同的计时器源。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-03-18
      • 2014-07-20
      • 2021-07-20
      • 2013-02-18
      • 1970-01-01
      • 1970-01-01
      • 2013-12-10
      相关资源
      最近更新 更多