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 程序的其他一些变体都产生(几乎)相同的输出:
System.nanoTime()
替换为 System.currentTimeMillis()
的变体
- 一种变体,其中
System.out.println()
被定期打印的 StringBuilder 替换
- 一种变体,其中调度机制被替换为通过
Thread.sleep()
计时的单个线程
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.
我前阵子在一台机器上记录了大约 15
ms 的粒度。这将解释您看到的所有 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
处显示出清晰的峰值。
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
大多数现代硬件都提供多个定时器源。此外,大多数操作系统提供几个 API 来访问这些具有不同精度的定时器计数器(例如系统定时器和 RTC)。了解 Microsoft、.NET 平台(以及大多数 MS 产品)利用了对 Win32 API 和内核 API 的深入了解。我的直觉是 C# 中的定时器 class 使用与 Java 不同的 API (Hotspot VM 实现描述 here,尽管它对 Java 5 是正确的)。
虚拟环境中的计时器精度存在普遍问题。我发现非常有趣的测试结果 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 云上重现该问题。
所以 "What's this effect is" 的答案 - 这是管理程序错误与 Java 实现 "features" 相结合。可以肯定的是,您可以尝试 运行 使用 OpenJDK 进行此测试,您可以在其中查看代码并使用不同的计时器源。
但出于实际原因,我建议避免在 Windows VM 上使用 运行 计时器敏感 Java 代码。如果这是非常严格的要求,我会尝试使用 Win32 计时器并从那里调用 JVM 代码(使用 JNI)或实现任何其他计时器源(使用命名管道或任何其他特定于平台的补丁)。您可以尝试使用 Quartz 作为计时器和调度程序,但它可能也遇到同样的问题。
我们有一个 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 程序的其他一些变体都产生(几乎)相同的输出:
System.nanoTime()
替换为System.currentTimeMillis()
的变体
- 一种变体,其中
System.out.println()
被定期打印的 StringBuilder 替换 - 一种变体,其中调度机制被替换为通过
Thread.sleep()
计时的单个线程
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.
我前阵子在一台机器上记录了大约 15
ms 的粒度。这将解释您看到的所有 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
处显示出清晰的峰值。
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
大多数现代硬件都提供多个定时器源。此外,大多数操作系统提供几个 API 来访问这些具有不同精度的定时器计数器(例如系统定时器和 RTC)。了解 Microsoft、.NET 平台(以及大多数 MS 产品)利用了对 Win32 API 和内核 API 的深入了解。我的直觉是 C# 中的定时器 class 使用与 Java 不同的 API (Hotspot VM 实现描述 here,尽管它对 Java 5 是正确的)。
虚拟环境中的计时器精度存在普遍问题。我发现非常有趣的测试结果 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 云上重现该问题。
所以 "What's this effect is" 的答案 - 这是管理程序错误与 Java 实现 "features" 相结合。可以肯定的是,您可以尝试 运行 使用 OpenJDK 进行此测试,您可以在其中查看代码并使用不同的计时器源。
但出于实际原因,我建议避免在 Windows VM 上使用 运行 计时器敏感 Java 代码。如果这是非常严格的要求,我会尝试使用 Win32 计时器并从那里调用 JVM 代码(使用 JNI)或实现任何其他计时器源(使用命名管道或任何其他特定于平台的补丁)。您可以尝试使用 Quartz 作为计时器和调度程序,但它可能也遇到同样的问题。