Проблемы времени выполнения планировщика Java на сервере виртуальных окон

У нас есть приложение 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);
}

Этот код, который должен запускать TimeRunnable каждые 10 мс, дает такие результаты на сервере:

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? Явная ошибка на этих платформах? Есть ли решение?

EDIT: коллега написал версию С# той же программы:

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:

введите описание изображения здесь

EDIT: Несколько других вариантов программы Java производят (в значительной степени) один и тот же вывод:

  • Вариант, в котором System.nanoTime() был заменен на System.currentTimeMillis()
  • Вариант, в котором System.out.println() был заменен периодически напечатанным StringBuilder
  • Вариант, в котором механизм планирования был заменен одним потоком, который сам по себе проходит через Thread.sleep()
  • Вариант, в котором lastRunAt является изменчивым

Ответ 1

Я также не знаю, почему это происходит. Однако вряд ли это будет ошибка Java. Java использует собственные потоки, что означает, что планирование потоков обрабатывается "операционной системой".

Я думаю, что настоящая проблема заключается в том, что вы создали приложение, основанное на ложной предпосылке. Если вы читаете документацию по Java (для JVM обычного/не реального времени), вы не найдете ничего, что говорит о том, что планирование потоков Java является точным. Даже приоритеты планирования - это "наилучшие усилия".

То, что вы наблюдали за планированием, чтобы быть достаточно точным на загруженной Linux VM, интересно... но не обязательно поучительно. Точность планирования будет зависеть от характера нагрузки на систему. И, вероятно, один из них имеет существенный "избыточный" объем памяти, VCPU и пропускную способность ввода/вывода на платформе.


Есть ли решение?

Возможно, вы могли бы подумать о том, как сделать планирование более "точным" на вашей платформе (в хороший день со следующим ветром). Тем не менее, вы не получите никаких гарантий точности, если не будете переключаться на ОС реального времени и выпуск в режиме реального времени Java. Вы не найдете реализаций Java в реальном времени для виртуальной платформы. Таким образом, реальное решение состоит в том, чтобы не полагаться на точное планирование.

Ответ 2

Это вызвано степенью детализации System.currentTimeMillis(). Обратите внимание на комментарий:

Обратите внимание, что, хотя единица времени возвращаемого значения равна миллисекунде, степень детализации зависит от базовой операционной системы и может быть больше.

Я записал зернистость около 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.

Ответ 3

Я думаю, вам нужно увеличить приоритет процесса Java-приложения и рабочего потока внутри java-приложения. Его легко увеличить приоритет рабочего потока внутри java-приложения. Но его сложно установить java-приложение, чтобы получить более высокий процессор, чем то, что вы получаете. Вероятно, это может помочь получить более высокий процессор для вашей программы.

Как изменить приоритет запущенного Java-процесса?

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

Вы также можете посмотреть, как получить процессор реального времени, но обратите внимание, что он может задержать другие действия ядра, включая события мыши и клавиатуры.

Задержка, определенно, из-за задачи не может начаться в указанное время, и, следовательно, следующая задача была запущена до периода времени, чтобы скорректировать фиксированную скорость, как указано здесь: Java Timer

Ответ 4

  • Большинство современных HW предоставляют несколько источников таймера. Кроме того, большинство операционных систем предоставляют несколько API-интерфейсов для доступа к этим счетчикам таймера с различной точностью (например, системный таймер и RTC). Знание платформы Microsoft,.NET(как и большинство продуктов MS) использует тесные знания API Win32 и API-интерфейсов ядра. Моя интуиция говорит, что класс Timer в С# использует разные API, чем Java (реализация Hotspot VM описана здесь здесь, хотя это правильно для Java 5).

  • Существует общая проблема с точностью таймера в виртуальных средах. Я нашел очень интересные результаты тестов http://www.ncbi.nlm.nih.gov/pmc/articles/PMC4503740/, описывающие похожие проблемы с разными гипервизорами. Самое смешное, что Hyper-V не упоминается там, но проблема выглядит как не уникальная для конкретной установки. Microsoft имеет вопрос относительно корректности таймеров, предоставляемой Hyper-V, работающей в Windows 2008 R2. бог знает, что работает в облаке для разных поставщиков облачных вычислений. Я лично смог воспроизвести проблему в облаке AWS.

  • Итак, ответ на вопрос "Что это за эффект" - это ошибка гипервизоров в сочетании с "функциями" реализации Java. Разумеется, вы можете попробовать запустить этот тест с OpenJDK, где вы можете увидеть код и играть с разными источниками таймера.

  • Но по практическим соображениям я предлагаю избегать использования Java-кода, чувствительного к таймеру, в Windows VM. В случае, если это очень сложно, я бы попытался использовать таймер Win32 и вызвать JVM-код оттуда (используя JNI) или реализовать любой другой источник таймера (используя именованный канал или любой другой патч для конкретной платформы). Вы можете попробовать использовать кварц в качестве таймера и планировщика, но, вероятно, он страдает от той же проблемы.