Денис Агапитов @DenAgapitov
Руководитель группы Platform Core
Information
- Rating
- 491-st
- Location
- Санкт-Петербург, Санкт-Петербург и область, Россия
- Works in
- Date of birth
- Registered
- Activity
Specialization
Backend Developer, Database Developer
Lead
Java
High-loaded systems
Designing application architecture
Database design
Multiple thread
Code Optimization
SOA
REST
SQL
NoSQL
Kafka тоже когда-то была велосипедом. Если цель - увеличить трудозатраты инженеров поддержки, то однозначно лучше выбрать Kafka, RabbitMQ или любую другую очередь / JMS реализацию. У нас присутствует негативный опыт с реализацией от Oracle - JMS AQ.
Зачем же вешать такие ярлыки, хотя я бы с удовольствием послушал какой вариант доставки событий вы бы выбрали и внедрили и почему. C учётом распределённого standalone приложения (без Java EE, spring'ов и прочих обвесов), хранящего состояние в БД.
Спасибо, хорошее дополнение.
Если нагрузка по созданию событий большая, то указанный вами подход будет более предпочтителен. Надо только переписать процедуры add_partition и remove_partition.
Спасибо за вопрос. Конкретно ResultSet.TYPE_SCROLL_SENSITIVE тут, скорее всего излишен. Полагаю, случайно попал при формировании упрощённого кода из фабрики запросов. Однако, в данном случае тип скроллинга ResultSet не особо имеет значение, будет работать в любом случае.
Проброс исключения нужнен для последующей обработки его в классе Controller:
controller.onDatabaseDown(ex);
AutoCloseable
как раз и используется в коде. Вызов rs.close() и stmt.close() вручную вызываются для того, чтобы как раз обеспечить правильное "автозакрытие" класса Lock.Здесь FOR NO KEY UPDATE не подойдёт, так как нам нужна эксклюзивная блокировка для менеджмента партиций.
synchronized необходим для корректного старта и останова потоков. Плюс wait/notify можно осуществлять под синхронизацией по монитору объекта, у которого они вызываются. Не совсем понимаю что в использовании synchronized у вас вызывает сомнения?
Если используется только PostgreSQL, то да, pg_partman будет хорошим подспорьем и уменьшит объём кода. Однако, наши системы могут ставиться на разные БД, здесь PostgreSQL приведён только как один из вариантов.
Спасибо за пояснения относительно SimpleDateFormat, для многих оно действительно не очевидно. Что касается кода в статье, то область видимости FORMATTER доступна только потоку
"partition-serve-worker"
.Не вижу ничего плохого в ручной корректировке времени в long-формате и паре констант вместо пакета java.time - уж точно это не геморрой. Я бы сказал, что это скорее вкусовщина.
Я не утверждал, что код читать не надо, всё же это "Туториал", а не позновательный текст. Код сильно упрощён и, как я надеялся, сильно понятен большинству заинтересованных темой. Если для осознания нужен глубокий анализ, прошу прощения. В следующий раз постараюсь раскрывать тему более глубоко ещё и текстом.
На вопросы вам ответили дважды, спасибо @kmatveev.
Спасибо, действительно пропущен
while (rs.next())
, поправил.Спасибо за вопрос. Раскрою более широко. Данный подход применяется для кластерных приложений (да, мы их пишем сами), полное состояние которых хранится в БД.
Возьмём для примера приложение Notification Broker, которое у нас как раз и реализует pub/sub модель взаимодействия по спецификации WS-Brokered-Notification, о которой вы говорите. При старте экземпляра, он идёт в БД и вычитывает состояние: опубликованные темы, подписки к нему, подписки от него и другую необходимую информацию. И так как это кластерное приложение, то состояние должно распространяться между всеми активными в текущий момент экземплярами. Таким образом подписчик в рамках WS-Atomic-Notification получит событие с любого активного экземпляра, даже если подписался не у него.
Примеры событий, распространяемых на кластере: опубликована новая тема, приложение подписалось к producer, появился новый consumer.
Было бы странно писать брокер сообщений, используя другой брокер или очередь.
Раскрыв более широко, теперь могу дать ответы на ваши 3 вопроса:
Не ограничено.
Если эти стратегии как-то натянуть на предмет статьи, то я бы сказал, что это ближе всего к at least once.
Это пример разных событий. Их можно назвать как угодно. В нашем Notification Broker, например, типов событий несколько десятков.
Ещё для меня очень странно выглядит желание видеть в статье ответы на вопросы, даже не поняв суть статьи. Простыни кода анализировать не обязательно, но хотя бы прочитать текст было бы уважительно по отношению к автору.
Во первых вы не правильно оцениваете жизненный цикл. В любом коде мы можем рассчитывать, что метод внёс какие-то изменения только по выходу из метода, а не на основании входа в метод. Если исходить из правильных предпосылок, то после выхода пишущего потока из метода setValue, все другие потоки будут иметь актуальные данные.
Во вторых, если вы, говоря Immutable, подразумеваете unmodifableMap, то в статье было написано, что это избыточная конструкция. Как вы правильно заметили, parameters имеет область видимости только внутри класса. Если класс написан правильно, то нет нужды закрывать его враппером с защитой от изменений. Однако, сам паттерн Immutable здесь нужен, чтобы обеспечить чтение без блокировки.
В третьих, если вы добавите блокировку на чтение, то всё, что написано в статье вообще не нужно, так как принцип happends before будет обеспечен ключевым словом synchronized. Но это будет уже не lock-free алгоритм, а банальная блокировка по монитору объекта и читающие потоки будут выстраиваться друг за другом для занятия этой блокировки.
Вы невнимательно читали статью. Здесь synchronized не защищает доступ к переменной parameters, а является критической секцией на время пересборки Immutable-объекта. Это нужно для того, чтобы разные потоки не смогли это сделать одноврменно.
Это всё так, но, как говорится, есть нюанс. А именно, любая синхронизация данных между потоками в Java делает всё тоже самое, чтобы соблюдался принцип
happens-before
.Даже если взять самый быстрый lock в JDK - ReentrantReadWriteLock.ReadLock и посмотреть на его реализацию, то видно, что он работает через CAS своего состояния.
На самом деле, принцип
happens-before
как раз и говорит о том, что запрещён instruction reordering и данные в КЭШах долны быть перезачитаны из RAM.Таким образом, volatile является самым быстрым вариантом межпоточного взаимодействия в Java.
И да, ставить модификатор volatile у каждой переменной класса и надеяться на то, что всё будет само по себе волшебно - это, конечно глупость. В случае, если область видимости данной переменной - это только один поток, то модификатор volatile только затормозит выполнение кода.
Модификатор volatile нужен именно для межпотокового взаимодействия и маркировать им нужно переменные понимая их жизненный цикл - из какого потока создаются, из какого - закрываются (выходят из области видимости всех потоков для сборки GC), из каких потоков читаются и из каких изменяются.
Здесь этот код только для того, чтобы показать как можно обойтись без блокировок в принципе на уже существующей в статье кодовой базе. В реальном продукте, такое использовать ни в коем случае нельзя, согласен с вами.
Когда начиналась наша шина данных (во времена становления Glassfish и JBoss), проект netty существовал только как часть JBoss (вроде не ошибаюсь). В то время, при исследовании производительности веб-сервисов на серверах Glassfish и JBoss (ещё спецификация EJB 2.x на xml-файлах без аннотаций и асинхронности), стало понятно, что их производительности не достаточно для telecom-нагрузок. Это стало той самой точкой, с которой началось развитие нашей шины на Java (более 15 лет назад).
Да, вы правильно поняли. Относительно асинхронности - все наши приложения полностью асинхронны с самого начала, как и шина. Есть, конечно, и синхронный API, но внутри он всё равно работает через асинхронный.
Запись с потока бизнес-логики уменьшает время отклика до определённого уровня нагрузки на данном железе. На принимающей стороне ответ также пытается уйти с потока бизнес-логики. У нас есть реальные сервисы, которые отвечают со временем отклика менее 1 мс. Многократное перекладывание с потока на поток ощутимо увеличивает время отклика при цепочке из нескольких вызовов.
Скажу даже больше: статья будет в помощь для довольно узкого круга проектов и большинству не понадобится даже "jdk.nio.maxCachedBufferSize", так как сейчас довольно небольшое число проектов имеют собственный сетевой стэк.
Не думаю, что для тех, кто пишет микро-сервисы при помощи аннотаций RestController, PostMapping и тому подобных, вообще интересно знание о том, как Spring работает с сетью.
Но, если кто-то написал свой сетевой клиент/сервер при помощи NIO, то вопрос производительности в этом случае, как правило, стоит остро. Искренне надеюсь, что для таких проектов статья будет полезна.
Решение с установкой "jdk.nio.maxCachedBufferSize", конечно, рабочее. Причём рабочее без изменения кода вообще, благодаря free при возвращении буфера:
Однако, такое решение прилично снизит пропускную способность, так как в этом случае мы имеем и копирование памяти из HeapByteBuffer в DirectByteBuffer и аллокацию direct-памяти при создании DirectByteBuffer.
На мой взгляд, для сервисов с небольшими нагрузками проще сразу работать с DirectByteBuffer и самому его аллоцировать.