(java || kotlin) && devOps
372 subscribers
6 photos
1 video
7 files
317 links
Полезное про Java и Kotlin - фреймворки, паттерны, тесты, тонкости JVM. Немного архитектуры. И DevOps, куда без него
Download Telegram
Всем привет!

Продолжим рассказ про разные способы ускорения Java. Для начала я бы разделил ускорение в целом на 4 более конкретных направления:
1) ускорение запуска приложения за счет оптимизации\отмены первоначальной загрузки классов
2) ускорение выхода приложения на оптимальную производительность путем оптимизации JIT - Just In Time - компиляции байт-кода в нативный
3) ускорение запуска и в какой-то степени выполнения приложения за счет более легковесного фреймворка, используемого для разработки приложения
4) оптимизация сборщика мусора для достижения нужного баланса между затрачиваемыми ресурсами и паузой в обслуживании клиентских запросов, она же Stop the World

Сегодня поговорим про первое направление. С одной стороны упомянутые ранее и native image, и CRaC тоже ускоряют запуск. Но обе технологии имеют ограничения. native image запрещает reflection и динамическую загрузку классов. Образ, сохраненный с помощью CRaC, может содержать что-то лишнее, и с данной технологией нельзя просто так перезапустить приложение при сбое - т.к. возможно причина сбоя лежит в данных, подгруженные из образа.

Начну издалека.
В Java 5 появилась вот такая фича - https://docs.oracle.com/en/java/javase/21/vm/class-data-sharing.html Class-Data Sharing, сокращенно CDS.
Фича появилась и была забыта. Есть такие фичи, про которые все забывают сразу после релиза новой Java) Еще модульность из Java 9 можно вспомнить.

О чем эта фича? Мы записываем в файл метаданные загруженных классов из classpath. Потом этот файл мапился в память работающей JVM. Зачем? Цели было две:
1) расшаривание классов между несколькими инстансами JVM и т.об. уменьшение потребления RAM
2) ускорение запуска (вот оно!)

Вначале фича работала только с классами Java core. Файл с архивом классов Java core входит в состав JDK, найти его можно по имени classes.jsa. Занимает на диске сравнительно немного - 10-15 Мб. И кстати, CDS в Java включена по умолчанию, используется как раз этот файл.

Позже, в Java 10 https://openjdk.org/jeps/310 появилась возможность дампить и пользовательские классы, эту фичу назвали AppCDS. В Java 13 создание архива было упрощено https://openjdk.org/jeps/350
Пользовательские классы можно добавить в архив предварительно запустив процесс со специальной опцией командной строки -XX:ArchiveClassesAtExit

А если у нас Spring? Ребята в Spring 6.1 обратили внимание на данную опцию и добавили ключ командной строки, позволяющий собрать информацию о динамически загружаемых классах именно для Spring Boot приложения https://docs.spring.io/spring-framework/reference/integration/cds.html
А еще дали рекомендации, как максимально точно собрать информацию о классах и подтвердили, что данная опция ускоряет загрузку на ~30% https://spring.io/blog/2023/12/04/cds-with-spring-framework-6-1 Почему подтвердили - именно такую цель ставили разработчики CDS в JEP 310, упомянутом выше.

Итого - идея в чем-то похожа на Profile-Guided Optimization. Только здесь мы предварительно собираем информацию не об использовании кода, а о загруженных классах. Чем больше информации соберем - тем быстрее будет старт приложения. Минусы - версия JDK, Spring и classpath в целом должны совпадать при тестовом прогоне и использовании в ПРОМе.


#jre #performance #spring_boot #spring #java_start_boost
June 30, 2024
Всем привет!

Ну и еще одна оптимизация времени старта Java приложения. Самые внимательные уже могли ее заметить пройдя по ссылкам из предыдущего поста.

С момента появления Spring Boot упаковка приложения в fat jar - jar содержащий все зависимости и Tomcat в придачу (или другой контейнер сервлетов) - стала неким стандартом.
Но fat jar при исполнении требуется распаковать. А разархивация всегда требовала времени, не зря архиваторы используются как бенчмарки для процессорных тестов.

Соответственно, можно заранее разложить зависимости по отдельным файлам для ускорения старта. Вот как рекомендует это делать Spring https://docs.spring.io/spring-boot/reference/packaging/efficient.html
Судя по данным статьи из вчерашнего поста это даст еще 25% ускорения при старте https://spring.io/blog/2023/12/04/cds-with-spring-framework-6-1

#performance #spring #jvm #java_start_boost
July 1, 2024
Всем привет!

Ну и последний вариант ускорения старта Java приложения. Самый радикальный, наверное. Отказ от Spring.

Надо отметить, что чистый hello world Spring сервис в плане старта не так уж плох, плюс минус 4 секунды. Основные проблемы начинаются с ростом числа зависимостей. И Spring можно тюнить, подробнее про это можно почитать здесь: https://www.baeldung.com/spring-boot-startup-speed Единственный момент, который мне не понравился - я бы не отключал C2 компиляцию - скорость старта может и увеличится, а вот выйти на оптимальную производительность не получится. И еще интересное исследование - https://github.com/dsyer/spring-boot-allocations Авторы выключили в Spring Boot все, за что мы его любим - Dependency Injection и быструю автоконфигурацию, повесили все на единственный classloader и ускорили старт в 5(!) раз. Только зачем нужен такой Spring?)

Но вернемся к отказу от Spring. Писать на голой Java я не предлагаю) Есть две альтернативы - Quarkus и Micronaut. Оба при создании основной целью ставили получить более быстрый и легковесный фреймворк, чем Spring.

Вот сравнительный бенчмарк Quarkus https://habr.com/ru/companies/haulmont/articles/443242/ Ускорение старта простейшего приложения в 5 раз, до 0.75 секунд. Я беру цифры без native image (GraalVM ), т.к. в этом случае и Spring будет "летать". Для интереса я сравнил локально, разница получилась не в 5 раз, а примерно в 2, с 2.5 до 1.2 секунды. За счет чего получилось ускориться можно почитать тут https://dev.to/nutrymaco/how-quarkus-use-build-time-to-start-your-application-faster-50n Если вкратце - Dependency Injection происходит во время достаточно сложного процесса компиляции.

А вот сравнение Micronaut со Spring https://www.baeldung.com/micronaut-vs-spring-boot Разница чуть поменьше, в 2,5 раза, но тоже ничего) Вот тут, авторы объясняют, почему они быстрее Spring - https://guides.micronaut.io/latest/building-a-rest-api-spring-boot-vs-micronaut-data-gradle-java.html И снова - внедрение зависимостей на этапе компиляции, нет рефлексии и создаваемых в runtime прокси.

Почему я назвал этот вариант самым тяжелым - оба фреймворка сильно отличаются от Spring - по используемым аннотациям, по API в целом. Кроме того они не такие зрелые, им порядка 5-6 лет, поэтому там просто меньше функционала.

#performance #spring #quarkus #micronaut #java_start_boost
July 2, 2024
Всем привет!

Недавно я уже писал о пользе стандартизации на примере формата данных для сохранения информации о микросервисе - https://t.me/javaKotlinDevOps/291 (cyclonedx). А еще в одном посте - про работу с исключениями - упоминал библиотечку для вывода информации об ошибке в API - https://t.me/javaKotlinDevOps/310 (jdoctor).
Так вот - стандартизация добралась и сюда)
Во-первых, как выяснилось - уже с 2016 года есть стандарт "Problem Details for HTTP APIs" https://datatracker.ietf.org/doc/html/rfc7807
Во-вторых - в Spring Boot 3, т.е. с 2021 года, появилась его имплементация. Вот неплохая статья, описывающая детали https://www.baeldung.com/spring-boot-return-errors-problemdetail
Если вкратце - ответ при ошибке будет иметь стандартизированный вид похожий на такой:
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "Invalid request content.",
"instance": "/sales/calculate"
}

Библиотеки, решающие подобную задачу в Java уже были:
1) упомянутый ранее https://github.com/melix/jdoctor,
2) https://www.wimdeblauwe.com/blog/2022/12/01/the-error-handling-spring-boot-starter-library-vs-spring-6-problemdetail/
...
Но я думаю, что именно данное решение - от Spring, да еще по стандарту - имеет шансы распространиться достаточно широко и популяризовать данную практику.

Что еще хочется отметить:
1) стандартизация - это хорошо. Каждый поставщик API разрабатывает формат для ответа при ошибке, каждый потребитель - специфический код обработки ошибок для всех вызываемых сервисов. А при этом понятно, что формат информации об ошибке мало зависит от бизнес-процесса
2) еще в 2018 году в Сбере формат ответа для REST запроса был стандартизирован. Это круто. Не круто то, что формат отличается от RFC. И то, что область его применения была ограничена общением с фронтом

#standardization #java #problem_details
August 12, 2024
Всем привет!

Сегодня расскажу про еще один поучительный факап из моей практики.

Более 10 лет назад. Стартап. Я на данный момент и СТО, и разработчик в одном лице. Есть задача, задача в целом понятна. Декомопзирую на микросервисы и подзадачи, начинаю пилить. Дохожу до XML binding - преобразования XML в объекты и обратно. Да, тогда API в основном были XML. Решил погуглить - что сейчас есть на рынке. Кроме известного многим JAXB нахожу JiXB. Не известный тогда, и тихо умерший сейчас. Читаю описание и нахожу бенчмарк, показывающий что он ... не помню точно, но скажем в 1.5 раза быстрее JAXB. Думаю - о, круто, надо брать. Начинаю прикручивать к Spring приложению. Наталкиваюсь на кучу подводных камней - это не работает, то не работает, документации мало, ошибки не информативные и не гуглятся, т.к. сообщества особо нет. В общем убил неделю на прикручивание к проекту одной библиотеки. В итоге все заработало, конечно. А проект не был доведен до работающего состояния и так и не взлетел - как по бизнесовым причинам, так и по причине неготовности к нужному моменту.

Итоги. До сих пор стыдно за такое проектное решение. Стыдно по следующим причинам:
1) напомню, речь про стартап, т.е. нужно быстро создать работающий прототип, а не исследовать новые технологии
2) преждевременная оптимизация - данных по требуемому RPS и доступному железу на тот момент у меня не было. Нагрузочное тестирование не проводилось. Стал бы JAXB узким местом - далеко не факт
3) использовать в промышленной разработке новые, не доказавшие свою зрелость библиотеки - это риск. Очень важны сообщество (ответы на stackoverflow, а сейчас в AI чате) и работающие без бубна связки, например, Spring+конкретная технология. А риск во-первых должен быть оправдан - см. предыдущий пункт, а во-вторых - должен быть сценарий отката. Если библиотека по факту оказалась сырой - не надо заниматься реверс-инжинирингом, плодить костыли, героически превозмогать трудности. Лучше откатится на какой-то надежный вариант.

#fuckup #xml #java
September 3, 2024
Всем привет!

Давненько не было постов на канале, что поделать - конец квартала, авральный режим on.
Но я вернулся) К делу.

Java всегда славилась своим boiler plate кодом. Ремарка - конечно же не только boiler plate, но сейчас про него и про борьбу с ним. Борьба идет, и в Java 16 (а точнее в Java 14 но в режиме preview) появились records, они же записи.
Делаешь вот такое объявление:
public record Point(int x, int y) {}
и получаешь из коробки конструктор, getter, equals(), hashСode() и toString().
Т.е все то, что раньше приходилось писать руками или использовать Lombok. Ага, Lombok. Т.е. он стал не нужен? Не совсем. Если внимательно глянуть на список того, что под капотом делает record, то можно заметить, что там нет setter-а. Т.е. record - это иммутабельный объект и value object. Две записи с одним и тем же набором полей всегда равны. Это не баг, это фича. Маленькое дополнение - record еще меняет классический getter c getXyz на просто xyz(), но это детали. Еще дополнение - вот тут среди прочих фичей Java 17 достаточно интересно о работе с record https://habr.com/ru/companies/jugru/articles/652821/

Т.е. получается, что record заменяет @Value из Lombok.

Но не заменяет:
1) полноценный @Data - изменяемый класс без boiler plate

2) кейс, когда мы не хотим все сразу getter, toString() ... а хотим только часть этих методов

3) @Builder - тут проще показать, чем объяснять:
Person.builder()
.name("Adam Savage")
.city("San Francisco")
.build();

4) @Log - простое добавление логгера в класс

5) и наконец @SneakyThrows - если вы не любите checked exception

#java #lombok #boiler_plate_free
September 22, 2024
Небольшое дополнение по Java records. Как известно, Java - это огромное количество библиотек и фреймворков и, следовательно, вопросы совместимости.
Так вот. Spring Boot Web записи поддерживает, как в контроллере, так и в http клиентах. Java сериализация естественно, это же часть спецификации Java. Jackson сериализация. Mapstruct. И Spring Data JPA, там где это возможно, учитывая иммутабельность https://www.baeldung.com/spring-jpa-java-records
Что я забыл? Наверное забыл, но 3 года прошло - поддержку должны были уже запилить)

#java #spring
September 23, 2024
Всем привет!

Давно не писал про Kotlin, а в названии канала он есть на почетном втором месте) Исправляюсь.

Основная фишка Kotlin - это упрощение написания и чтения кода за счет упрощения языка. В чем упрощение: все типы - объектные, функция всегда возвращает результат, нет неявных преобразований, нет проверяемых исключений, некоторые стандартные паттерны (синглтон, делегат) стали частью языка - не нужно изобретать велосипед. Возможность переопределения операций и полноценные функциональные типы - на самом деле тоже. Операция - краткий общеупотребительный вариант метода, функцию можно передавать как объект не создавая для этого объект.
Но как всегда есть нюансы.

Вот например inline методы и связанный с ним reified https://www.baeldung.com/kotlin/reified-functions
При беглом знакомстве возникают 2 вопроса:
1) разработчики Kotlin загрязняют язык, ведь компилятор, а скорее JVM, сами справятся с inline?
2) Kotlin хакнул type erasure для generic?

Ответ на оба вопроса - нет.
И есть отличная статья на эту тему https://habr.com/ru/articles/775120/ Автора знаю лично, рекомендую почитать эту и другие его статьи про Kotlin.

Для ленивых ))) ответы:
1) inline нужен только для методов с параметрами функционального типа, чтобы избежать обвертывания функции в объект. Java компилятор не умеет работать с функциональными типами, увы
2) reified не нарушает спецификации Java, компилятор Kotlin лишь сохраняет тип там, где он его знает, и это касается только inline методов

И про простоту Kotlin в целом и сложность inline. Как выглядит процесс со стороны:
1) у нас полноценные функциональные типы
2) в коде их будет много
3) Java не умеет с ними работать
4) сделаем inline, чтобы не снизить производительность при работе с такими типами
5) появляются баги из-за inline, приходится вводить ключевые слова noinline и crossinline. Подробнее об этом есть в статье выше.
6) кто-то просит: раз при inline мы знаем исходный тип generic - давайте дадим возможность работы с ним, появляется reified
7) возникают новые баги, их фиксят
...

Процесс вымышленный, возможно, в реальности было по-другому. Я хотел подчеркнуть вот что: одна фича тянет за собой другую, другая - несколько особых случаев. И все это усложняет язык, хотя цель была противоположная.

P.S. Ну и да, получается, во всем виновата Java)

#kotlin #java
October 11, 2024
Всем привет!

Одна из моих любимых тем: разработка - искусство компромиссов. Поиск по тэгам #dev_compromises и #arch_compromises. Следствие этого подхода, принцип, который я бы назвал - "не все так однозначно".

Вопрос - как вы относитесь к рефлексии в Java?

Досрочный ответ: рефлексия - это плохо, лучше не использовать. Если дошло до рефлексии - значит в архитектуре проблема.

Чтобы лучше разобраться в теме надо ответить на вопрос: а почему плохо?
Ответа два:

1) рефлексия позволяет нарушить принципы ООП и, следовательно, архитектуру приложения. Автор скрыл внутренности класса через private, а мы туда лезем своими "грязными руками")))

2) снижение производительности. Тут частично работает тот факт, что делаются лишние вызовы кода. Но самое главное - JIT компилятор плохо умеет оптимизировать такой код, т.к. он слишком динамический. Изменится может сам класс, который приходит на вход метода с рефлексией

Окей, инкапсуляция нарушается, код работает медленно. Не используем?

А что с поиском аннотаций по коду? Не своих - до них мы еще дойдем - чужих, чтобы получить некие метаданные об объекте. Большинство вариантов вот тут основано на рефлексии https://www.baeldung.com/java-scan-annotations-runtime

Или у нас есть аннотации, созданные с помощью Spring AOP - это проще, чем AspectJ, если у вас используется Spring. А Spring AOP использует динамические прокси, создаваемые в runtime https://www.baeldung.com/spring-aop-vs-aspectj А с помощью чего создается прокси в runtime - правильно, рефлексии.

Да что там AOP - создание бинов из @Configuration - это тоже вызов методов @Bean через рефлексию.

Почему же рефлексию используют и предлагают к использованию, если это такая проблемная технология?

Вернемся к двум ее недостаткам:

1) не надо использовать рефлексию для вызова private методов или доступа к private полям. Если такая задача встала - у вас в самом деле проблемы с архитектурой

2) не надо использовать рефлексию часто и при этом в высоконагруженных приложениях. Тот же Spring использует рефлексию только при старте приложения для инициализации прокси и бинов. И к слову в т.ч. и поэтому старт Spring приложения может быть долгим, и люди с этим борются, см. мой цикл статей про ускорение старта #java_start_boost Более того, разработчикам Spring для поддержки native image пришлось серьезно допиливать его в т.ч. из-за динамических прокси и @Configuration https://docs.spring.io/spring-boot/reference/packaging/native-image/introducing-graalvm-native-images.html

Итого: рефлексию стоит рассматривать как возможность получить метаданные о классе. Помня при этом о производительности.

И если вопрос упирается в производительность - всегда есть альтернативы рефлексии. Это работа с байт-кодом не в runtime:
1) compile time (свой компилятор, см. статью про AspectJ)
2) post-compile time (свой плагин для сборки, см. Jandex для поиска аннотаций https://smallrye.io/jandex/jandex/3.2.2/index.html)
3) load-time (свой агент+classloader, см. статью про AspectJ)
Все варианты сложнее в реализации и подключении к проекту, зато вносят минимальное влияние в runtime.

P.S. Да, если при упоминании о динамических прокси вы вспомнили про задачку с собесов о вложенном @Transactional - это оно. И ответ на этот вопрос не так очевиден https://habr.com/ru/articles/347752/

#java #reflection #dev_compromises
October 14, 2024