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

Только не еще один лесоруб!

Java не испытывает недостатка в библиотеках ведения журнала: Log4J, SLF4J, JBoss Logger Manager, Flogger, Apache Commons… С тегами java, производительность, микросервисы, наблюдаемость.

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”