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

Выпил бокал пива и захотелось немного пофилософствовать)
У меня часть просят готовое решение какой-то проблемы. Это может быть способ интеграции, выбор места для хранения данных, языка программирования, способ разбиения проекта на микросервисы и модули, как сделать правильное API...
Что на это хочется сказать.
1) иметь каталог готовых решений - это круто. Именно он оправдывает существование архитекторов и техлидов в команде. Использование паттернов, #patterns - это как раз про это, про переиспользование знаний. И я над таким каталогом работаю)
2) вместе с тем приходится признать, что далеко не всегда есть готовые и главное наиболее подходящие к конкретной ситуации решения.
Хорошо если бы всегда было так: "Как нам сделать ххх?". Думаешь 10 минут. "Делайте так и так, а так не делайте". Всё понятно, все счастливы, приложение легко и безболезненно попадает на ПРОД. Мечты, мечты)

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

В этом плане само название должности - архитектор - неверное. "Каменную" архитектуру сложно отрефакторить. Программную при правильном подходе к разработке - легко. Если бы все дома вокруг строились по индивидуальным проектам - это банально было бы очень дорого. С ПО - это суровая правда жизни.

Вывод: надо изучать паттерны и существующие решения. После любого проекта нужна "ретроспектива" - индивидуальная или общекомандная, на которой стоит отметить что "выстрелило", а что лучше в будущем не использовать. Ну и конечно учиться, учиться и учиться...)))

#arch #arch_compromises
Всем привет!

Продолжение про сагу.

Когда мы говорим про транзакции, сначала всплывает аббревиатура ACID. Транзакции должны обеспечивать принципы ACID. Посмотрим что тут у нас с сагой.
A - атомарность: или все выполняется, или все откатывается. Собственно атормарность есть в определении паттерна, см. выше. Единственное отличие - у нас нет волшебного rollback на всю распределённую транзакцию, бизнес логику отката придётся писать руками.
C - консистентность данных. Сага обеспечивает т.наз. eventually consistentcy - конечную согласованность. Т.е. данные будут согласованы только после окончания распределённой транзакции. В течение транзакции данные в разных микросервисах могут расходится. Транзакция в БД может обеспечить строгую согласованность изменяемых данных с нужным уровнем изоляции. Поэтому переходим к
I - изоляции изменений внутри транзакции от других операций. Сага не обеспечивает ее совсем, что с этим можно сделать описано в статье про это патерн от Microsoft по ссылке выше. Важный момент - в отличие от транзакции в БД, которая как правило длится миллисекунды, распределённая транзакция - это секунды, может даже десятки секунд. Несогласованность данных в течение этого времени из-за отсутствия изоляции нужно иметь в виду. В дополнение к описанным в статье по ссылке способом скажу ещё один - завершать транзакцию как можно быстрее и игнорировать несогласованность данных) Пример: клиент вряд ли будет жаловаться в службу поддержки, если после отмены заказа деньги и бонусы вернутся на счёт в течение минуты. И скорее всего будет - если это не будет сделано через час.
D - надёжность хранения данных, к саге отношения напрямую не имеет, обеспечивается используемыми хранилищами.

Т.к. в итоге мы получили ACD, причем неполноценный, то для распределенных транзакций придумали новую аббревиатуру - Basically Available, Soft-state, Eventually consistent - https://ru.m.wikipedia.org/wiki/Теорема_CAP#BASE-архитектура

Ещё один интересный момент про сагу: определение последовательности шагов - локальных транзакций. Единственно верной схемы нет, но есть рекомендации. Первая - fail fast. Т.е. если есть локальная транзакция, которая упадёт с большей вероятностью - ее нужно ставить вначале. Пример: резерв билета или товара. Вторая - если какая-то локальная транзакция проводит к критичной для клиента несогласованность данных - ее нужно выполнять как можно позже. Что делать, если эти рекомендации противоречат друг другу - зависит от сценария, но в целом я бы выбрал уменьшение времени неконсистентности.

Еще интересный момент касается саги в виде оркестрации. Т.к. ее главный плюс - сделать простой и понятной бизнес логику саги, то самая очевидная ее реализация вот такая:

class OrderSaga {
SagaResult execute() {
// шаг 1
// шаг 2
// ...
}
}

Назовём этот подход Transaction Script, есть такой Паттерн организации бизнес логики.
Просто - да. Но если процесс сложный, каждый шаг тоже, то мы ухудшим читаемость кода, получим замечание SonarQube про длину метода да и нарушим S из SOLID, принцип единой ответственности. Что делать? Использовать event driven подход:
class OrderSaga {
PrepareEvent start(...) {..}
ReserveEvent makeReservation(...) {...}
// ...
}
При необходимости обработку событий можно разнести в разные классы. Чтобы было понимание как работает процесс нужно написать пару модульных тестов - позитивный и негативные сценарии, ведь тесты в идеале - лучшая документация к коду. Ещё один плюс - в событийной стиле легко сделать весь процесс неблокирующим, например, через адаптер отправляя и принимая все события в Kafka. Да, есть ещё БД, запись в БД в эту парадигме - это такое же событие. В этом случае стоит посмотреть в сторону R2DBC https://www.baeldung.com/r2dbc Для REST endpoint и client есть Spring WebFlux.
К слову, Transaction script тоже может обеспечить неблокирующее выполнение, но только в языках программирования с async await: c#, python, rust https://learn.microsoft.com/ru-ru/dotnet/csharp/language-reference/operators/await

To be continued...

#patterns #saga #microservices #acid #arch_compromises
Всем привет!

При проектировании системы применяя микросервисный подход всегда появляется главный вопрос - как делить?
Сделаешь слишком крупно - получишь маленький монолит. Это как правило всем понятно, т.к. от монолита мы пытаемся уйти создавая микросервисы.
Но есть и другая крайность - слишком мелкое деление. Уже немного писал об этом https://t.me/javaKotlinDevOps/57
Сейчас же хочу проиллюстрировать эту крайность примером.
Предположим у нас есть некая система, представляющая клиентам CRUD REST API. Create, Read, Update, Delete методы. И еще List, который сильно отличается от Read поэтому должен быть выделен отдельно - pagination, сортировка, кэширование...
Можно применить назовем его "наивный" подход к микросервисам и сделать 5 микросервисов по числу методов API. Точнее даже "миллисервисов")
Что получим?
Вспоминаем, что у каждого микросервиса должна быть своя БД.
Это значит что от микросервисов Create и Delete зависят все остальные, т.к. им нужно будет обновить свою копию данных. Это может быть event driven подход с Kafka, CQRS или что-то другое, но в любом случае это зависимость.
От микросервиса Update зависят Read и List.
А если структура данных меняется?
И это зависимости "из коробки" на сферическом CRUD в вакууме. В реальном кейсе по мере развития системы число зависимостей будет больше. Что получилось? Получился распределённый "ком грязи". Такой же "ком грязи", как в старом неподдерживаемом монолите, от которого мы уходили, только хуже. Там хоть БД одна была и интеграций сильно меньше.
Можно попробовать вынести все взаимодействие с БД в отдельный микросервис Storage, но тогда мы нарушаем Single Responsibility - за ту же операцию Create отвечает и микросервис Create, и микросервис Storage. И Create скорее всего станет слишком простым для отдельного микросервиса.
Пример специально взят простой, в реальности выбор может быть сложнее. Зато на этом примере хорошо видны недостатки "миллисервисов".

P.S. За идею примера спасибо все из той же книжке по DDD, расскажу о ней в ближайшее время.

#microservices #rest #arch_compromises
Всем привет!

Одна из моих любимых тем: разработка - искусство компромиссов. Поиск по тэгам #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
Всем привет!

Хочу порекомендовать хорошую статью на Хабре о необходимости кэша.
https://habr.com/ru/companies/oleg-bunin/articles/883422/
Со сравнительными тестами Redis, Memcached, PostgreSQL и MySQL.

Из статьи я почерпнул для себя несколько основных тезисов:

1) в наше время получить 1 000 000 rps с сервера на чтение - это реальность. И это круто! Речь про стандартный сервер, а не 100 ядер\1000 Гб памяти как можно было бы подумать

2) реляционные СУБД приблизились к кэшам (key-value noSQL хранилищам если хотите) по скорости чтения

3) как правильно заметил автор: будь он СТО - не разрешил бы использовать СУБД как кэш. И вот почему. Сравнимая производительность БД-кэш достигается при 2 условиях - нет операций записи в БД (а соответственно и блокировок записей) и все выборки идут по первичному ключу (это самая быстрая операция выборки). Казалось бы - соблюдай эти условия и все будет работать. Но ведь у нас СУБД. Окей, GRANT на запись отберем у всех. Но ведь СУБД может сложные JOIN-ы. И это никак не ограничить правами. Там могут быть сложные индексы. И рано или поздно найдется разработчик, которые эти возможности захочет использовать))) И в пике производительность упадет даже не в разы, а на порядки. Например, не провели НТ. Или забыли обновить профиль НТ. С кэшом такого по понятным причинам не произойдет. Вывод - у каждого инструмента свое назначение.

4) проблема неконсистентности данных кэш-БД все равно будет. Поэтому перед тем, как добавлять в систему кэш - стоит подумать, провести НТ и еще раз подумать. Возможно где-то есть или планируется архивная реплика БД. Там проблема констистентности данных решается механизмом репликации .Если часть нагрузки на чтение увести на нее - возможно кэш и не нужен.

P.S. Отдельная интересная тема: PostgreSQL показывает, что принцип число процессов ОС = числу ядер - не аксиома)

#rdbmc #cache #arch_compromises