Java не испытывает недостатка в библиотеках ведения журнала: Log4J , SLF4J , JBoss Logger Manager , Flogger , Apache Commons Logger , Войдите обратно плюс, вероятно, еще много чего, о чем я не слышал. Есть также очень нелюбимый java.util.logger
(или июль), который является частью Java начиная с версии v1.4. Даже со всеми этими вариантами я думал, что нам нужен новый регистратор.
Отойди от вил и выслушай меня.
Многие из этих библиотек были созданы для решения конкретной проблемы, в первую очередь производительности:
- Избегание выделения массива или автоматического боксирования, вызывающего отток памяти,
- Производительность ввода-вывода в консоль или файл,
- Асинхронное выполнение,
- Низкий/нулевой мусор.
Со всеми этими другими библиотеками это действительно переполненное пространство, и это не помогает с минимальными микросервисами. Низкий/нулевой мусор потребовал бы полной перезаписи и если это вызывает беспокойство, придерживайтесь Log4J v2, но мы можем обратиться к остальным… так я и сделал: dansiviter.uk:juli
.
О нет, только не очередная система ведения журнала, я слышу, как ты стонешь… ну, вроде того, но сначала давайте посмотрим на это в действии.
Сейчас март, так что нет лучшего времени для очистки вашего кода. Текущий способ использования JUL обычно выглядит примерно так:
var log = Logger.getLogger(getClass().getName()); if (log.isLoggable(Level.INFO)) { // <- needed to avoid array initialisation and auto-boxing log.log(Level.INFO, "Hello {0}", "world"); // Grrr, can't use #info(...) with params!? }
Это можно было бы упростить, чтобы выглядеть следующим образом:
var log = LogProducer.log(MyLog.class); log.hello("world");
Что это за ересь?! Что ж, я создал интерфейс, который с помощью обработки аннотаций создает реализацию, которая делегирует JUL, но автоматически накладывает слои на #isLoggable(...)
, чтобы избежать страшной инициализации массива и автоматического бокса, если это строго необходимо.
@Log public interface MyLog { @Message("Hello {0}") void hello(String str); }
Чтобы проверить, достигнем ли мы повышения производительности, я написал небольшой тест JMH, сравнивающий два способа, за исключением принудительной автоматической упаковки и инициализации массива в сценариях:
Benchmark Mode Cnt Score Error Units LogBenchmark.legLog thrpt 25 27921.784 ± 1280.815 ops/s LogBenchmark.legLog:Used memory heap thrpt 25 26950.054 ± 8478.381 KB LogBenchmark.newLog thrpt 25 40111.066 ± 1170.407 ops/s LogBenchmark.newLog:Used memory heap thrpt 25 25288.212 ± 10124.257 KB
Цифры основаны на моем i7-1065G7, использующем Docker для рабочего стола, поэтому являются чисто ориентировочными, однако новый подход дает как увеличение пропускной способности на ~ 44%, так и снижение использования памяти на ~ 6%. Я должен упомянуть, что при повторных запусках я действительно видел некоторые случаи, когда использование памяти было выше, но пропускная способность постоянно улучшалась примерно на 40%.
Это отличное начало, но мы можем пойти дальше.
Каждая отдельная реализация java.util.logger. Обработчик
, предоставляемый Java, является синхронным; он помещает ввод-вывод непосредственно в критический путь выполнения. Log4J и Logback имеют асинхронные реализации, чтобы отделить это и ускорить код, так почему же JUL не может? Итак, я создал Великобритания.дансивитер.джули. AsyncHandler
для решения этой проблемы.
Существует конкретная реализация этого, которая делегируется в java.util.logging. Консольный обработчик
. Поэтому я создал еще несколько тестов JMH:
Benchmark (handlerName) Mode Cnt Score Error Units ConsoleHandlerBenchmark.handler ConsoleHandler thrpt 25 31041.836 ± 7986.031 ops/s ConsoleHandlerBenchmark.handler:Used memory heap ConsoleHandler thrpt 25 37237.451 ± 15369.245 KB ConsoleHandlerBenchmark.handler AsyncConsoleHandler thrpt 25 85540.769 ± 6482.011 ops/s ConsoleHandlerBenchmark.handler:Used memory heap AsyncConsoleHandler thrpt 25 41799.724 ± 16472.828 KB
При увеличении использования памяти на ~ 12% объем использования памяти, который мы получаем при увеличении пропускной способности на ~ 175%, велик, однако важно иметь некоторый контекст с этими результатами. При этом используется java.util.concurrent. Поток
под капотом и он использует размер буфера по умолчанию 256. Чтобы предотвратить сброс сообщений журнала, обратное давление обрабатывается блокировкой, когда буфер заполнен. Следовательно, насыщение буфера замедлит его, отсюда и увеличенная дисперсия. Однако на самом деле насыщение будет происходить редко, и это отделит ввод-вывод от создания сообщений журнала, повышая производительность. В небольших, менее “болтливых” тестах массовые (часто неисчислимые) улучшения являются обычным явлением:
x50 - Sync=PT0.018177S, Async=PT0S (NaN%) // <- async so fast it didn't even register x100 - Sync=PT0.0317765S, Async=PT0S (NaN%) // <- ...and again x200 - Sync=PT0.0644191S, Async=PT0.0009579S (6725.03400%) x400 - Sync=PT0.1168272S, Async=PT0.0450558S (259.294500%) // <- dramatic slow down x800 - Sync=PT0.2164862S, Async=PT0.1705798S (126.912000%) x1600 - Sync=PT0.4423355S, Async=PT0.4237862S (104.377000%) // <- almost parity
См. uk.dansiviter.juli. Приблизительный бенчмарк
для кода.
ℹ️ С современными контейнерными рабочими нагрузками FileHandler
довольно бессмыслен, поскольку у них редко есть постоянные диски для записи и, как правило, они записывают непосредственно в STDERR/STDOUT или непосредственно в агрегатор журналов. Так что я не создавал асинхронную версию.
Итак, с помощью этой оболочки ведения журнала и асинхронных обработчиков я могу съесть свой торт и съесть его:
- Более чистый код,
- Повышение производительности (часто огромное!),
- Минимальное увеличение размера двоичного файла (~14 КБ).
Тем не менее, я чувствую, что должен обратиться к названию этого поста. Нет, это не новый регистратор, но он делает работу с существующим регистратором намного приятнее. Ознакомьтесь с README , в котором подробнее объясняется, как он используется, включая обработку исключений.
Я уже использую это с проектом CDI, который вводит экземпляры в код, делая модульное тестирование очень простым. Я планирую в ближайшее время посмотреть, хорошо ли это работает с JPMS и GraalVM для еще большей гибкости.
Теперь я готов к своему линчеванию… делай все, что в твоих силах!
Оригинал: “https://dev.to/dansiviter/not-another-logger-2lc4”