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

2022-09-03 07:44:57

我们有一个Java应用程序,它需要在其他环境中运行虚拟(Hyper-V)Windows 2012 R2 Server。在此虚拟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问题吗?Hyper-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 服务器上并排工作的更新(部分)屏幕截图:

enter image description here

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

  1. 替换为System.nanoTime()System.currentTimeMillis()
  2. 一种变体,其中被定期打印的StringBuilder所取代System.out.println()
  3. 一种变体,其中调度机制被替换为单个线程,该线程通过Thread.sleep()
  4. 其中 的变体是可变的lastRunAt

答案 1

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

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

不久前,我在一台机器上记录了大约ms的粒度。这可以解释您看到的所有值,但不能解释大值。150

运行测试的增强版本:

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}

巨大的异常值是第一个命中 - 当为零时。那是你后来被叫来的时候,但没有踢过其中一个蜱虫。请注意,按照我建议,在显示清晰峰值时的峰值。lastRunAt0=36110msSystem.currentTimeMillis()15=58515ms

我对.62=1


答案 2

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

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

事实上,您已经观察到在负载繁重的Linux VM上调度相当准确,这很有趣......但不一定有启发性。调度准确性将取决于系统上负载的性质。可能是平台中内存,VCPU和I / O带宽是否存在显着的“过载”。


有解决方案吗?

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


推荐