Одно из выступлений в моем текущем портфолио – Переход от императивного к реактивному . Доклад основан на демо переходе с 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”