Я в шоке! CompletableFuture имеет проблемы с производительностью!

задняя часть
Я в шоке! CompletableFuture имеет проблемы с производительностью!

Эта статья участвовала в "Проект «Звезда раскопок»«Выиграйте креативные подарочные наборы и бросьте вызов творческим поощрениям.

Здравствуйте, я криворукий.

Во время Национального дня мне было нечего делать, поэтому я просто записал код конкурса, о котором упоминал ранее, цель — сохранить 100 лучших футболок для конкурса.

Сейчас она по-прежнему входит в топ-50 команд, причем стабильно.

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

Так как он основан на Dubbo, в процессе отладки было написано, что я видел это место:

org.apache.dubbo.rpc.protocol.AbstractInvoker#waitForResultIfSync

Сначала посмотрите на строку кода, которую я создал. В aysncResult есть CompletableFuture. Он вызывает метод get() с тайм-аутом. Тайм-аут — Integer.MAX_VALUE. Теоретически эффект эквивалентен методу get().

С моей интуитивной точки зрения, здесь не должно быть проблем с методом get(), или даже лучше понять.

Но почему бы не использовать метод get()?

На самом деле в комментарии к методу уже написали причину, боюсь что у таких как я будут такие вопросы:

Что бросилось в глаза, так это слова:

имеют серьезные падения производительности.

Производительность сильно снижена.

Вероятно, это означает, что мы должны позвонитьjava.util.concurrent.CompletableFuture#get(long, java.util.concurrent.TimeUnit)вместо метода get(), поскольку было показано, что метод get вызывает серьезное снижение производительности.

Для Dubbo метод waitForResultIfSync — это метод основной ссылки. Лично я считаю консервативным утверждать, что более 90% запросов будут приходить на этот метод, блокируя и ожидая результата. Поэтому, если с этим методом возникнут проблемы, это повлияет на производительность Dubbo.

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

Даже если Dubbo не упоминается, когда мы используем CompletableFuture, метод get() — это метод, который мы часто используем.

Кроме того, цепочка вызовов этого метода мне слишком знакома.

Поскольку первая статья в публичном аккаунте, которую я написал два года назад, была посвящена асинхронному преобразованию Dubbo,«Асинхронное преобразование новых функций Dubbo 2.7»

Тогда эта часть кода была точно не такой, по крайней мере, без этой подсказки.

Потому что если бы был намек, я бы точно заметил его в первый раз, когда писал.

Конечно же, я пошел посмотреть, хотя картина уже очень размыта, но я все еще смутно вижу, что метод get() был вызван раньше:

Я также называю это самой «дерзкой» строкой кода.

Потому что эта строка кода является ключевым кодом для Dubbo для преобразования асинхронного в синхронный.

Передняя часть — это просто введение, в этой статье мы не будем писать о знаниях, связанных с Dubbo.

Основной Write CompletableFuture получить () в конце концов, в чем проблема.

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

Изучите Dubbo и добавьте такой же NOTICE, где метод вызывается для непосредственного заполнения форса. Когда кто-то еще спросит, вы можете сказать это снова.

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

Какая проблема с производительностью?

Основываясь на этой информации в комментарии Даббо, я не знаю, в чем проблема, но я знаю, где искать.

Проблема такого рода должна быть записана в список ошибок openJDK, поэтому первая остановка — поиск ключевых слов здесь:

не говорите .open JDK.java.net/projects/JD…

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

Однако на этот раз мне повезло.Первое, что выскочило, было то, что я искал.Я был немного непривычен к этому.Это легендарный подарок к Национальному дню?Я не смею думать об этом.

Название: Улучшения производительности CompletableFuture.

В нем упоминается ошибка под номером 8227019.

не говорите .open JDK.java.net/browse/JDK-…

Давайте посмотрим, что описывает этот BUG.

Заголовок переведен, и, вероятно, в методе CompletableFuture.waitingget есть цикл, который вызывает в этом цикле метод Runtime.availableProcessors. И этот метод называют очень частым, это нехорошо.

В подробном описании упоминается еще одна ОШИБКА под номером 8227006. Эта ОШИБКА описывает, почему нехорошо часто вызывать availableProcessors, но мы не будем перечислять ее первой.

Сначала изучите строку кода, которую он упомянул:

 spins = (Runtime.getRuntime().availableProcessors() > 1) ?
                    1 << 8 : 0; // Use brief spin-wait on multiprocessors

Он сказал, что ждет, пойдем посмотрим, что происходит.

Но моя локальная версия JDK — 1.8.0_271, а исходный код ждущегоGet выглядит так:

java.util.concurrent.CompletableFuture#waitingGet

Мне все равно, что означают эти строки кода, но я обнаружил, что не видел кода, упомянутого в баге, а только видел его.spins=SPINS, хотя SPINS вызываетRuntime.getRuntime().availableProcessors()метод, но поле модифицируется статическим и окончательным, поэтому нет "частого вызова", описанного в BUG.

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

Наконец нашел этот код в версии JDK 1.8.0_202:

Отличие от исходного кода на предыдущем скриншоте в том, что в первом есть дополнительное поле SPINS,Runtime.getRuntime().availableProcessors()Возврат метода кэшируется.

Причина, по которой мне пришлось найти эту строку кода, заключалась в том, чтобы доказать, что такой код действительно присутствует в какой-то версии JDK.

Хорошо, теперь давайте посмотрим, что делает метод waitGet.

Во-первых, при вызове метода get(), если результат по-прежнему нулевой, это означает, что результат выполнения асинхронного потока не готов, тогда вызовите метод waitGet:

Когда дело доходит до метода waitGet, мы фокусируемся только на этих двух ответвлениях, связанных с BUG:

Сначала инициализируйте значение спинов до -1.

Затем, когда результат равен нулю, цикл while продолжается.

Итак, если вы входите в цикл, метод availableProcessors должен быть вызван в первый раз. Затем выясняется, что это многопроцессорная операционная среда, а для спинов установлено значение 1

Затем снова выполните цикл, перейдите к оценке ветви спинов> 0, а затем выполните случайную операцию.Если случайное значение больше или равно 0, вычтите одну операцию из спинов.

Только когда спины снизятся до 0, он войдет в следующую сформулированную мной логику:

То есть количество вращений уменьшается с 256 до 0, а из-за наличия случайной функции количество петель должно быть больше 256 раз.

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

Итак, что, по вашему мнению, делает этот код?

На самом деле, комментарии были написаны очень четко:

Используйте короткое ожидание вращения на мультипроцессорах.

Кратко, это словарь четвертого уровня, его надо запомнить, нужно проверить. Это означает «краткий» и является неправильным глаголом, превосходная степень которого является самой короткой.

Кстати, слово спина должно быть известно всем. Я забыл научить вас этому слову раньше, поэтому мы поговорим о нем вместе. Посмотрите на маленькую доску:

Итак, в комментариях говорится: если это мультипроцессор, используйте короткое вращение для ожидания.

Процесс уменьшения с 256 до 0 и есть это "короткое ожидание вращения".

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

Так почему вы говорите, что это потребляет производительность?

Да, это правда, что метод get() вызывается только один раз, но вы не можете выдержать количество мест, где вызывается метод get().

Возьмем, к примеру, Dubbo.В большинстве случаев все методы вызова используют схему синхронного вызова по умолчанию. Таким образом, каждый вызов будет переходить в блок асинхронного преобразования в синхронный и ждать результата, а это означает, что метод get() будет вызываться каждый раз, то есть метод availableProcessors будет вызываться один раз.

Итак, каково решение?

Я показал вам раньше, то есть найти поле для кэширования возвращаемого значения метода availableProcessors:

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

Итак, есть код, который все видели ранее, а именно: «мы можем кэшировать это значение в поле»:

Конкретные изменения кода, отраженные в этом, следующие:

взрослый.open JDK.java.net/~kill/8227…

Итак, когда вы посмотрите на эту часть исходного кода, вы увидите, что на самом деле в поле SPINS есть длинный абзац, который выглядит примерно так:

Перевести для вас:

1. В методе waitGet выполнить ротацию перед операцией блокировки.

2. Нет необходимости крутиться на одном процессоре.

3. Стоимость вызова метода Runtime. availableProcessors высока, поэтому значение кэшируется здесь. Но это значение — количество процессоров, доступных при первой инициализации. Если система запускается только с одним доступным ЦП, значение SPINS будет инициализировано равным 0, которое не изменится, даже если позже будут подключены другие ЦП.

Когда у вас будет предвестие в описании предыдущего БАГА, вы поймете, почему здесь написан такой большой абзац.

Некоторые студенты действительно обращаются к коду, возможно, вы видите следующее:

какова ситуация? Я вообще не вижу кода, относящегося к SPINS, разве это не обман честных людей?

Не паникуйте, обезьяна спешит, я еще не закончил говорить, не так ли?

Обратим внимание на это предложение на картинке:

Это исправление необходимо выполнить только в JDK 8, так как код JDK 9 и более поздних версий не написан таким образом.

Например, в JDK 9 логика всех SPINS напрямую удалена, так что не ждите этого короткого спина:

Korea.open JDK.Java.net/JDK9/JDK9/ просто…

Хотя это короткое ожидание отжима было удалено, на самом деле это была сложная операция.

В: Как сделать эффект ожидания вращения без введения времени?

Ответ - код, который был удален.

Но одно скажу, когда я впервые увидел этот код, мне стало неловко. Как долго можно продлить это короткое вращение?

Это вращение добавляется для выполнения кода парковки в последующей логике позже, что является немного более сложной операцией. Но я думаю, что преимущества этого «краткого ожидания» на самом деле минимальны.

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

И автор, упомянутый здесь, на самом деле мистер Дуг Ли.

Почему я говорю это?

Согласно ОШИБКЕ под номером 8227018, упомянутой в этой ссылке ОШИБКА, они на самом деле описывают одно и то же:

Вот разговор между Дэвидом Холмсом и Дугом Ли:

Холмс упомянул о решении «кэшировать это значение в поле» и согласился с Дугом.

Дуг сказал: JDK 9 больше не крутится.

Поэтому мое личное понимание таково, что Дуг удалил логику SPIN, не зная, что в этом месте была ошибка. Что касается причины, я предполагаю, что польза действительно мала, а код несколько запутан. Лучше снять его и понять интуитивно.

Все знакомы с Дагом Ли.Кто такой Дэвид Холмс?

Один из авторов книги «Параллельное программирование на Java на практике», чай кончился.

И если вы достаточно впечатлены моими предыдущими статьями, то вы обнаружите, что уже«ОШИБКА, написанная Дугом Ли в сумке J.U.C, снова была обнаружена пользователями сети. 》В этой статье он уже фигурировал:

Снова появился старый друг и предложил железным сокам вынести связь сновидения на всеобщее обозрение.

Какова причина?

Основная идея заключается в том, что стоимость вызова метода Runtime. availableProcessors высока, поэтому этот метод не следует часто вызывать в методе CompletableFuture.waitingGet.

Но почему стоимость вызова доступных процессоров так высока, и что лежит в основе?Вы должны взять его и посмотреть!

В этом разделе мы покажем вам, что такое основа.

Основа в этом описании ОШИБКИ:

не говорите .open JDK.java.net/browse/JDK-…

В заголовке говорится: в среде Linux время выполнения Runtime. availableProcessors увеличилось в 100 раз.

Он увеличился в 100 раз, должно быть сравнение двух разных версий, так какие же это две версии?

В версиях JDK до 1.8b191 приведенный ниже пример программы может выполнять более 4 миллионов вызовов Runtime.availableProcessors в секунду.

Но в сборке JDK 1.8b191 и во всех последующих основных и второстепенных выпусках (включая 11) максимум, которого можно достичь, составляет около 40 тыс. вызовов в секунду, что означает падение производительности в 100 раз.

Это вызывало проблемы с производительностью CompletableFuture.waitingGet, который вызывал Runtime.availableProcessors в цикле. Поскольку наше приложение демонстрировало значительные проблемы с производительностью в асинхронном коде, именно в waitGet мы впервые обнаружили проблему.

Код теста такой:

  public static void main(String[] args) throws Exception {
        AtomicBoolean stop = new AtomicBoolean();
        AtomicInteger count = new AtomicInteger();

        new Thread(() -> {
            while (!stop.get()) {
                Runtime.getRuntime().availableProcessors();
                count.incrementAndGet();
            }
        }).start();

        try {
            int lastCount = 0;
            while (true) {
                Thread.sleep(1000);
                int thisCount = count.get();
                System.out.printf("%s calls/sec%n", thisCount - lastCount);
                lastCount = thisCount;
            }
        }
        finally {
            stop.set(true);
        }
    }

Как описал специалист по устранению ошибок, если бы вы работали на 64-битном Linux с версиями JDK 1.8b182 и 1.8b191, вы бы увидели разницу почти в 100 раз.

Что касается того, почему существует 100-кратная разница в производительности, пожилой человек по имени Файроз Матте сказал, что он отладил ее и обнаружил проблему при вызове метода «OSContainer::is_containerized()»:

И он также нашел номер первой версии, где возникла проблема, это 8u191 b02, и код после этой версии будет иметь такую ​​​​проблему.

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

Так что, если у вас JDK 8 версии до 8u191 b02, и параллелизм системных вызовов очень высокий, то поздравляю, у вас есть возможность наступить на эту яму.

Затем следующие большие ребята дали множество решений, основанных на этой проблеме, и обсудили различные решения.

Некоторые решения кажутся громоздкими и требуют много кода.

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

Посмотрите еще раз на метод get

Теперь, когда мы знаем эту бесполезную точку знаний, давайте посмотрим, почему вызывается метод get() с тайм-аутом, и такой проблемы нет.

java.util.concurrent.CompletableFuture#get(long, java.util.concurrent.TimeUnit)

Прежде всего, вы можете видеть, что методы, вызываемые внутри, отличаются:

Метод get() с периодом ожидания внутренне вызывает метод timedGet, а входным параметром является период ожидания.

Нажмите на метод timedGet, чтобы узнать, почему можно без проблем вызвать метод get() с тайм-аутом:

Ответ уже написан для вас в комментариях к коду: мы намеренно не делаем здесь ротацию (как это делает waitGet), потому что приведенный выше вызов nanoTime() во многом похож на ротацию.

Видно, что внутри этого метода вообще нет вызова Runtime. availableProcessors, поэтому соответствующей проблемы нет.

Теперь вернемся к тому, с чего начали:

Тогда вы говорите, следующееasyncResult.get(Integer.MAX_VALUE, TimeUnit.MILLISECONDS)Если мы изменим наasyncResult.get()Эффект остался прежним?

Это определенно не то же самое.

Еще раз: как промежуточное программное обеспечение с открытым исходным кодом, Dubbo может работать в различных версиях JDK, и этот метод является основным кодом в его основной ссылке.Для конкретной версии JDK эта оптимизация действительно является улучшением производительности.Улучшение очень помогает.

Так что написание промежуточного программного обеспечения все еще немного интересно.

Наконец, дайте вам еще один шанс отправить исходный код для Dubbo.

В этом классе ниже:

org.apache.dubbo.rpc.AsyncRpcResult

Есть еще два метода:

Но метод get() выше вызывается только тестовым классом:

Вы можете полностью изменить их все, чтобы вызвать метод get(long timeout, TimeUnit unit), а затем удалить метод get() напрямую.

Я думаю, что это определенно может быть объединено.

Если вы хотите внести свой вклад в проект с открытым исходным кодом и познакомиться с процессом, это хорошая небольшая возможность.

Эта статья размещена в личном блоге, приглашаю всех в игру

www.whywhy.vip/