Одно из выступлений в моем текущем портфолио – Переход от императивного к реактивному . Доклад основан на демо переходе с 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")); }}); }
Итак, в чем проблема?
Блокировка вызовов в “главном” потоке
Блокировка вызовов не является проблемой сама по себе . Многие звонки блокируются, но в любом случае требуются. Было бы невозможно отвечать на каждый блокирующий вызов.
Проблема заключается в ненужной трате циклов процессора:
- Вы вызываете метод блокировки
- Для завершения метода требуется много времени, например , , выполните HTTP-вызов
- До тех пор, пока вызов не вернется, вы не сможете выполнять другие разделы блока
Следовательно, методы блокировки должны выполняться в выделенном потоке. Такой подход ничего не тратит впустую.
Из-за этого 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); }
- Поток
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"); }
- Помечайте потоки с параметризованным именем как неблокирующие
- Ожидалось, что он бросит
- Запустите новый поток
- Ожидать не бросать
Как и ожидалось, предыдущий код выводит следующий журнал:
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
Поток кода фрагмента выглядит следующим образом:
- Если вызов разрешен, поток продолжается в обычном режиме.
- Если вызов динамический, он также продолжается в обычном режиме.
- В противном случае, т.е. , вызов блокируется и не динамичные, блокирующие броски Гончей.
Вывод
Использование Block Hound является простым, т.е. , Block Hound.install()
. Краткое объяснение состоит в том, что Block Hound – это агент Java.
Но в этом посте мы немного покопались под обложкой, чтобы понять, как работал этот простой вызов.
- Инструментарий Block Hound добавляет код к методам, помеченным как блокирующие.
- Когда они выполняются в нединамических потоках, блокируйте броски гончей.
Большое спасибо Виолетте Георгиевой за ее отзыв!
Идти дальше :
Первоначально опубликовано на Фанат Java 20 июня th 2021
Оригинал: “https://dev.to/nfrankel/blockhound-how-it-works-b55”