Рубрики
Без рубрики

Блокхаунд: как это работает

Один из докладов в моем текущем портфолио посвящен переходу от императивного к реактивному. Разговор основан на… Помечено реактивным программированием, java, blockhound.

Одно из выступлений в моем текущем портфолио – Переход от императивного к реактивному . Доклад основан на демо переходе с Spring WebMVC на Spring Web Flux в пошаговом подходе. Один из шагов включает установку Блок-Хаунд : это позволяет проверить, происходит ли блокирующий вызов в потоке, которого не должно быть, и создает исключение во время выполнения, когда это происходит.

На прошлой неделе я несколько раз представлял этот доклад, как в версии Java, так и в версии Kotlin. Одна из таких презентаций состоялась в Джавадай Стамбул . После выступления один из вопросов, которые я получил, был: “Как работает Фон? “Я признаю, что в то время я не мог вспомнить. После того, как удивление прошло, но слишком поздно, я вспомнил, что в нем участвовал агент Java. Но я хотел спуститься в кроличью нору.

Этот пост пытается объяснить то, что я обнаружил.

Почему Фон?

Реактивное программирование основано на асинхронной передаче сообщений. Различные фреймворки/библиотеки будут отличаться по своему подходу: например, в Project Reactor вызов API – это не блокирующий вызов запроса-ответа, а подписка на сообщение (сообщения), которое издатель доставит в будущем.

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

Если один вызов в цепочке блокируется, он “замораживает” всю цепочку до тех пор, пока работа не будет завершена. В этом случае это резко уменьшает преимущества реактивного подхода. Входит Блок-Хаунд :

Block Hound будет прозрачно обрабатывать классы JVM и перехватывать блокирующие вызовы (например, ввод-вывод), если они выполняются из потоков, помеченных как “только неблокирующие операции” (т.Е. потоки, реализующие Неблокирующий интерфейс маркера реактора, например, запущенные Планировщиками.parallel() ). Если и когда это произойдет (но помните, этого никогда не должно произойти! 😜 ), будет выдана ошибка.

В нескольких словах, проверка данных на предмет блокировки звонков в местах, где их быть не должно.

Использование Блок-хаунда

Использование Black Hound очень просто:

  • Добавьте зависимость в свой путь к классу
  • Вызов Заблокируйте гончего.установите()

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

public static void main(String[] args) throws InterruptedException {
    BlockHound.install();
    Thread.currentThread().sleep(200);
}

Хотя sleep() блокируется, программа выполняется нормально.

Небольшое знакомство с исходным кодом BlockHound показывает, что sleep() действительно настроен как блокирующий (наряду с несколькими другими):

public static class Builder {

    private final Map>> blockingMethods = new HashMap>>() {{
        put("java/lang/Thread", new HashMap>() {{
            put("sleep", singleton("(J)V"));
            put("yield", singleton("()V"));
            put("onSpinWait", singleton("()V"));
        }});
}

Итак, в чем проблема?

Блокировка вызовов в “главном” потоке

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

Проблема заключается в ненужной трате циклов процессора:

  1. Вы вызываете метод блокировки
  2. Для завершения метода требуется много времени, например , , выполните HTTP-вызов
  3. До тех пор, пока вызов не вернется, вы не сможете выполнять другие разделы блока

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

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

public static void main(String[] args) throws InterruptedException {
    BlockHound.install(builder -> {
        builder.nonBlockingThreadPredicate(current ->
                current.or(thread -> thread.getName().equals("main"))); // 1
    });
    Thread.currentThread().sleep(200);
}
  1. Поток main помечен как неблокирующий

Выполнение приведенного выше кода ожидаемо приводит к следующему:

Exception in thread "main" reactor.blockhound.BlockingOperationError: \
  Blocking call! java.lang.Thread.sleep
    at java.base/java.lang.Thread.sleep(Thread.java)
    at ch.frankel.blog.blockhound.B.main(B.java:11)

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

public static void main(String[] args) throws InterruptedException {
    var name = "non-blocking";
    BlockHound.install(builder -> {
        builder.nonBlockingThreadPredicate(current ->
                current.or(thread -> thread.getName().equals(name)));   // 1
    });
    var thread = new Thread(() -> {
        try {
            System.out.println("Other thread started");
            Thread.currentThread().sleep(2000);                         // 2
            System.out.println("Other thread finished");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }, name);
    thread.start();                                                     // 3
    Thread.currentThread().sleep(200);                                  // 4
    System.out.println("Main thread finished");
}
  1. Помечайте потоки с параметризованным именем как неблокирующие
  2. Ожидалось, что он бросит
  3. Запустите новый поток
  4. Ожидать не бросать

Как и ожидалось, предыдущий код выводит следующий журнал:

Other thread started
Exception in thread "non-blocking" reactor.blockhound.BlockingOperationError: \
  Blocking call! java.lang.Thread.sleep
    at java.base/java.lang.Thread.sleep(Thread.java)
    at ch.frankel.blog.blockhound.C.lambda$main$3(C.java:16)
    at java.base/java.lang.Thread.run(Thread.java:829)
Main thread finished

Блок-хаунд хорош только настолько, насколько хороша его конфигурация

Через свой артефакт Идентификатор группы является io.projectreactor

  • БАНКА самодостаточна. Единственная зависимость – это ByteBuddy, который оттеняет фон.
  • Фон обеспечивает интеграцию абстракцию. Интеграции позволяют предоставлять биты конфигурации. Он может быть общим или специфичным для фреймворка, например RxJava, реактор и т.д.
  • Интеграции загружаются с помощью загрузчика служб Java

С учетом этого пришло время немного проверить фоновый API.

Например, интеграция RxJava 2 интеграция настраивает Blockhound для RxJava 2:

builder.nonBlockingThreadPredicate(current -> current.or(NonBlockingThread.class::isInstance));

Но как это работает?

Пришло время вернуться к нашему первоначальному вопросу: как работает фон?

Вот что происходит, когда вы звоните Черная гончая.установить() :

Волшебство происходит в методе instrument() . Там Blockhound использует инструментарий байт-кода для ввода фрагмента кода перед каждым методом, помеченным как блокирующий. Метод блокируется, если:

  • Мы явно помечаем его как таковой, вызывая Builder.mark Как блокирующий()
  • Автоматически загружаемая интеграция вызывает для нас вышеупомянутый API

Поток кода фрагмента выглядит следующим образом:

  1. Если вызов разрешен, поток продолжается в обычном режиме.
  2. Если вызов динамический, он также продолжается в обычном режиме.
  3. В противном случае, т.е. , вызов блокируется и не динамичные, блокирующие броски Гончей.

Вывод

Использование Block Hound является простым, т.е. , Block Hound.install() . Краткое объяснение состоит в том, что Block Hound – это агент Java.

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

  1. Инструментарий Block Hound добавляет код к методам, помеченным как блокирующие.
  2. Когда они выполняются в нединамических потоках, блокируйте броски гончей.

Большое спасибо Виолетте Георгиевой за ее отзыв!

Идти дальше :

Первоначально опубликовано на Фанат Java 20 июня th 2021

Оригинал: “https://dev.to/nfrankel/blockhound-how-it-works-b55”