Як і навіщо ми написали високонавантажений масштабований сервіс для 1С: Підприємства: Java, PostgreSQL, Hazelcast

У цій статті розповімо про те, як і для чого ми розробили Систему взаємодії – механізм, що передає інформацію між клієнтськими додатками та серверами 1С: Підприємства – від постановки завдання до продумування архітектури та деталей реалізації.

Система Взаємодії (далі – СВ) – це розподілена відмовостійка система обміну повідомленнями з гарантованою доставкою. СВ спроектований як високонавантажений сервіс з високою масштабованістю, доступний як онлайновий сервіс (надається фірмою 1С), і як тиражний продукт, який можна розгорнути на своїх серверних потужностях.

СВ використовує розподілене сховище Ліщина та пошукову систему Elasticsearch. Ще мова піде про Java і про те, як горизонтально масштабуємо PostgreSQL.
Як і навіщо ми написали високонавантажений масштабований сервіс для 1С: Підприємства: Java, PostgreSQL, Hazelcast

Постановка завдання

Щоб було зрозуміло, навіщо ми робили Систему Взаємодії, розповім трохи про те, як влаштовано розробку бізнес-додатків у 1С.

Для початку – трохи про нас для тих, хто ще не знає, чим ми займаємось:) Ми робимо технологічну платформу «1С:Підприємство». Платформа включає засіб розробки бізнес-додатків, а також runtime, що дозволяє бізнес-додаткам працювати в крос-платформному оточенні.

Клієнт-серверна парадигма розробки

Бізнес-додатки, створені на «1С:Підприємстві», працюють у трирівневій клієнт-серверний архітектурі "СУБД - сервер додатків - клієнт". Прикладний код, написаний на вбудованою мовою 1С, може виконуватися на сервері програм або клієнта. Вся робота з прикладними об'єктами (довідниками, документами тощо), а також читання та запис бази даних виконується лише на сервері. Функціональність форм та командного інтерфейсу також реалізована на сервері. На клієнті виконується отримання, відкриття та відображення форм, «спілкування» з користувачем (попередження, питання…), невеликі розрахунки у формах, які потребують швидкої реакції (наприклад, збільшення ціни на кількість), робота з локальними файлами, робота з обладнанням.

У прикладному коді заголовках процедур і функцій треба явно вказувати, де виконуватиметься код — за допомогою директив &На Клієнті / &На Сервері (&AtClient / &AtServer в англомовному варіанті мови). Розробники на 1С зараз виправлять мене, сказавши, що директив насправді більшеАле для нас це зараз не суттєво.

З коду клієнта можна викликати серверний код, а ось з серверного коду викликати клієнтський не можна. Це фундаментальне обмеження, зроблене нами з низки причин. Зокрема тому, що серверний код має бути написаний так, щоб однаково виконуватися, звідки б його не викликали – з клієнта чи сервера. А у разі виклику серверного коду з іншого серверного коду клієнт відсутній як такий. І тому, що в ході виконання серверного коду клієнт, який викликав його, міг закритися, вийти з програми, і серверу вже нема кого викликати.

Як і навіщо ми написали високонавантажений масштабований сервіс для 1С: Підприємства: Java, PostgreSQL, Hazelcast
Код, що обробляє натискання кнопки: виклик серверної процедури з клієнта спрацює, виклик клієнтської процедури з сервера - ні

Це означає, що якщо ми з сервера захочемо передати якесь повідомлення в клієнтську програму, наприклад, що закінчилося формування «довгограючого» звіту і звіт можна подивитися – такого способу ми не маємо. Доводиться йти на хитрощі, наприклад, з коду клієнта періодично опитувати сервер. Але такий підхід завантажує систему зайвими викликами, та й взагалі виглядає не дуже витончено.

А ще є необхідність, наприклад, при телефонному SIP-дзвінку повідомити про це клієнтський додаток, щоб воно за номером того, хто дзвонив, знайшло його в базі контрагентів і показало користувачеві інформацію про дзвонить контрагента. Або, наприклад, при надходженні на склад замовлення повідомити про це клієнтську програму замовника. Загалом, кейсів, де такий механізм знадобився б, чимало.

Власне постановка

Створити механізм обміну повідомленнями. Швидкий, надійний, з гарантованою доставкою, можливість гнучкого пошуку повідомлень. На основі механізму продати месенджер (повідомлення, відеодзвінки), що працює всередині додатків 1С.

Спроектувати систему, що горизонтально масштабується. Зростаюче навантаження має закриватися збільшенням кількості нід.

Реалізація

Серверну частину СВ ми вирішили не вбудовувати безпосередньо в платформу 1С:Підприємство, а реалізовувати як окремий продукт, API якого можна викликати з прикладних рішень 1С. Зроблено це було з низки причин, головна з яких – хотілося уможливити обмін повідомленнями між різними додатками 1С (наприклад, між Управлінням Торгівлею та Бухгалтерією). Різні програми 1С можуть працювати різних версіях платформи 1С:Підприємство, перебувати різних серверах тощо. У таких умовах реалізація СВ як окремого продукту, що знаходиться збоку від інсталяцій 1С – оптимальне рішення.

Отже, ми вирішили робити СВ як окремий продукт. Невеликим компаніям ми рекомендуємо користуватися сервером СВ, який ми встановили у своїй хмарі (wss://1cdialog.com), щоб уникнути накладних витрат, пов'язаних з локальною установкою та налаштуванням сервера. Великі ж клієнти, можливо, вважатимуть за доцільне встановлення власного сервера СВ на своїх потужностях. Аналогічний підхід ми використали у нашому хмарному SaaS продукті 1cFresh – він випускається як тиражний продукт для інсталяції у клієнтів, а також розгорнутий у нашій хмарі https://1cfresh.com/.

додаток

Для розподілу навантаження і стійкості до відмови розгорнемо не один Java-додаток, а кілька, перед ними поставимо балансувальник навантаження. Якщо потрібно передати повідомлення з ноди на ноду – використовуємо publish/subscribe у Hazelcast.

Спілкування клієнта із сервером – по websocket. Він підходить для систем реального часу.

Розподілений кеш

Вибирали між Redis, Hazelcast та Ehcache. На подвір'ї 2015 рік. Redis тільки-но зарелізували новий кластер (занадто новий, страшно), є Sentinel з купою обмежень. Ehcache не вміє збиратися в кластер (цей функціонал з'явився пізніше). Вирішили скуштувати з Hazelcast 3.4.
Hazelcast збирається в кластер із коробки. У режимі однієї ноди він не дуже корисний і може пригодитися лише як кеш - не вміє скидати дані на диск, втратили єдину ноду - втратили дані. Ми розгортаємо кілька Hazelcast-ів, між якими бекапім критичні дані. Кеш не бекапім - його не шкода.

Для нас Hazelcast – це:

  • Сховище користувальницьких сесій. Щоразу ходити за сесією в базу – довго, тому всі сесії кладемо у Hazelcast.
  • Кеш. Шукаєш профіль користувача – перевір у кеші. Написав нове повідомлення – поклади у кеш.
  • Топики для спілкування інстанс програми. Нода генерує подію і поміщає її в топік Hazelcast. Інші ноди програми, підписані цей топік, отримують і обробляють подію.
  • Кластерне блокування. Наприклад, створюємо обговорення за унікальним ключем (обговорення-синглтон у рамках бази 1С):

conversationKeyChecker.check("БЕНЗОКОЛОНКА");

      doInClusterLock("БЕНЗОКОЛОНКА", () -> {

          conversationKeyChecker.check("БЕНЗОКОЛОНКА");

          createChannel("БЕНЗОКОЛОНКА");
      });

Перевірили, що каналу нема. Взяли блокування, знову перевірили, створили. Якщо після взяття блокування не перевіряти, то є шанс, що інший потік у цей момент також перевірив і зараз спробує створити таке ж обговорення – а воно вже існує. Робити блокування через synchronized чи звичайний java Lock не можна. Через базу – повільно, та й базу шкода, через Hazelcast – те, що треба.

Вибираємо СУБД

У нас великий та успішний досвід роботи з PostgreSQL та співпраці з розробниками цієї СУБД.

З кластером у PostgreSQL непросто – є XL, XC, ЦитусАле, загалом, це не SQL, які масштабуються з коробки. NoSQL як основне сховище розглядати не стали, вистачило й того, що беремо Hazelcast, з яким раніше не працювали.

Якщо потрібно масштабувати реляційну БД – значить, шардинг. Як ви знаєте, під час шардингу ми поділяємо базу даних на окремі частини так, щоб кожну з них можна було винести на окремий сервер.

Перший варіант нашого шардингу передбачав можливість рознести кожну з таблиць нашого додатка з різних серверів у різних пропорціях. Багато повідомлень на сервері А – будь ласка, давайте перенесемо частину цієї таблиці на сервер Б. Таке рішення просто-таки кричало про передчасну оптимізацію, так що ми вирішили обмежитися multi-tenant підходом.

Почитати про multi-tenant можна, наприклад, на сайті Дані Citus.

У СВ є поняття програми та абонента. Додаток – це конкретна інсталяція бізнес-програми, наприклад, ERP або Бухгалтерії, зі своїми користувачами та бізнес-даними. Абонент – це організація або фізична особа, від імені якої виконується реєстрація програми на сервері СВ. У абонента може бути зареєстровано кілька програм, і ці програми можуть обмінюватись повідомленнями між собою. Абонент і став мешканцем (tenant) у нашій системі. Повідомлення кількох абонентів можуть бути в одній фізичній базі; якщо бачимо, що якийсь абонент став генерувати багато трафіку – ми виносимо їх у окрему фізичну базу (чи навіть окремий сервер БД).

У нас є головна БД, де зберігається таблиця роутингу з інформацією про локацію всіх абонентських баз даних.

Як і навіщо ми написали високонавантажений масштабований сервіс для 1С: Підприємства: Java, PostgreSQL, Hazelcast

Щоб головна БД була вузьким місцем, таблицю роутинга (та інші часто затребувані дані) ми тримаємо в кеші.

Якщо почне гальмувати БД абонента, різатимемо всередині на партиції. На інших проектах для партиціонування великих таблиць використовуємо pg_pathman.

Оскільки втрачати повідомлення користувачів погано, ми підтримуємо наші БД репліками. Комбінація синхронної та асинхронної реплік дозволяє підстрахуватися на випадок втрати основної БД. Втрата повідомлення відбудеться лише у разі одночасної відмови основної БД та її синхронної репліки.

Якщо губиться синхронна репліка – асинхронна репліка стає синхронною.
Якщо губиться основна БД – синхронна репліка стає основною БД, асинхронна репліка – синхронною реплікою.

Elasticsearch для пошуку

Оскільки, крім іншого, СВ – це ще й месенджер, тут потрібний швидкий, зручний та гнучкий пошук, з урахуванням морфології, за неточними відповідностями. Ми вирішили не винаходити велосипед і використовувати вільну пошукову систему Elasticsearch, створену на основі бібліотеки Люцена. Elasticsearch ми також розгортаємо в кластері (master – data – data), щоб унеможливити проблеми у разі виходу з ладу вузлів програми.

На github ми знайшли плагін російської морфології для Elasticsearch і використовуємо його. У індексі Elasticsearch ми зберігаємо коріння слів (що визначає плагін) та N-грами. У міру того, як користувач вводить текст для пошуку, ми шукаємо текст, що набирається серед N-грам. При збереженні в індекс слово "тексти" розіб'ється на наступні N-грами:

[ті, тек, текс, текст, тексти, ек, екс, екст, ексти, кс, кст, ксти, ст, сти, ти],

Також буде збережено корінь слова «текст». Такий підхід дозволяє шукати і спочатку, і в середині, і після закінчення слова.

Загальна картина

Як і навіщо ми написали високонавантажений масштабований сервіс для 1С: Підприємства: Java, PostgreSQL, Hazelcast
Повторення картинки з початку статті, але вже з роз'ясненнями:

  • Балансувальник, який виставлений в інтернет; у нас - nginx, може бути будь-хто.
  • Інстанси Java-програми спілкуються між собою через Hazelcast.
  • Для роботи з веб-сокетом використовуємо Нетті.
  • Java-додаток написано на Java 8, складається з бандлів OSGi. У планах – міграція на Java 10 та перехід на модулі.

Розробка і тестування

У процесі розробки та тестування СВ ми зіткнулися з низкою цікавих особливостей продуктів, які ми використовуємо.

Навантажувальне тестування та витоку пам'яті

Випуск кожного релізу СВ – це тестування навантаження. Воно пройдено успішно, коли:

  • Тест працював кілька діб і не було відмов в обслуговуванні
  • Час відгуку за ключовими операціями не перевищив комфортного порога
  • Погіршення продуктивності порівняно з попередньою версією не більше ніж 10%

Тестову базу заповнюємо даними – для цього отримуємо з продакшн-сервера інформацію про найактивнішого абонента, множимо його цифри на 5 (кількість повідомлень, обговорень, користувачів) і так тестуємо.

Навантажувальне тестування системи взаємодії ми проводимо у трьох конфігураціях:

  1. Стрес-тест
  2. Тільки підключення
  3. Реєстрація абонентів

При стрес-тесті ми запускаємо кілька сотень потоків, і ті безперервно навантажують систему: пишуть повідомлення, створюють обговорення, отримують список повідомлень. Імітуємо дії звичайних користувачів (отримати список моїх непрочитаних повідомлень, написати комусь) та програмних рішень (передати пакет іншої конфігурації, обробити оповіщення).

Наприклад, ось так виглядає частина стрес-тесту:

  • У систему заходить користувач
    • Запитує свої непрочитані обговорення
    • З 50% ймовірністю читає повідомлення
    • Із 50% ймовірністю пише повідомлення
    • Далі користувач:
      • Із 20% ймовірністю створює нове обговорення
      • Випадково обирає будь-яке зі своїх обговорень
      • Заходить усередину
      • Запитує повідомлення, профілі користувачів
      • Створює п'ять повідомлень, адресованих випадковим користувачам цього обговорення
      • Виходить із обговорення
      • Повторює 20 разів
      • Виходить із системи, повертається назад до початку сценарію

    • У систему входить чат-бот (емулює обмін повідомленнями з коду прикладних рішень)
      • Із 50% ймовірністю створює новий канал для обміну даними (спеціальне обговорення)
      • З 50% ймовірністю пише повідомлення в будь-якому з існуючих каналів

Сценарій "Тільки підключення" з'явився не так. Буває ситуація: користувачі підключили систему, але поки що не втяглися. Кожен користувач вранці о 09:00 вмикає комп'ютер, встановлює з'єднання з сервером і мовчить. Ці хлопці небезпечні, їх багато – з пакетів вони лише PING/PONG, але з'єднання до сервера вони тримають (не тримати не можуть – а раптом нове повідомлення). Тест відтворює ситуацію, коли за півгодини у системі намагаються авторизуватися велика кількість таких користувачів. Він схожий на стрес-тест, але фокус у нього саме на цьому першому вході – щоб не було відмов (людина не користується системою, а вона вже відвалюється – складно вигадати щось гірше).

Сценарій реєстрації абонентів бере свій початок із першого запуску. Ми провели стрес-тест і були впевнені, що у листуванні система не гальмує. Але пішли користувачі та почала по таймууту відвалюватись реєстрація. Під час реєстрації ми використовували / dev / випадково, який зав'язаний до ентропії системи. Сервер не встигав накопичити достатньо ентропії і при запиті нового SecureRandom застигав на десятки секунд. Виходів із такої ситуації багато, наприклад: перейти на менш безпечний /dev/urandom, поставити спеціальну плату, яка формує ентропію, генерувати випадкові числа заздалегідь та зберігати в пулі. Ми тимчасово закрили проблему пулом, але з того часу проганяємо окремий тест на реєстрацію нових абонентів.

Як генератор навантаження ми використовуємо JMeter. Працювати з вебсокетом він не вміє, потрібний плагін. Першими в пошуковій видачі на запит «jmeter websocket» йдуть статті з BlazeMeter, у яких рекомендують плагін від Maciej Zaleski.

З нього ми й вирішили розпочати.

Майже відразу після початку серйозного тестування ми виявили, що JMeter почалися витоку пам'яті.

Плагін – це окрема велика історія, при 176 зірках у нього 132 форки на github-і. Сам автор у нього не комітіт з 2015 року (ми брали його в 2015 році, тоді це не викликало підозр), кілька github issues з приводу витоків пам'яті, 7 незакритих pull request-ів.
Якщо вирішите проводити тестування навантаження за допомогою цього плагіна, зверніть увагу на наступні обговорення:

  1. У багатопотоковій середовищі використовувався звичайний LinkedList, в результаті отримували NPE у рантаймі. Вирішується чи переходом на ConcurrentLinkedDeque, чи synchronized-блоками. Для себе вибрали перший варіант (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. Витік пам'яті, при дисконнекте не видаляється інформація про з'єднання (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. У режимі streaming (коли вебсокет не закривається в кінці семпла, а використовується далі в плані) не працюють.https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

Це з тих, що на github-і. Що ми зробили:

  1. Взяли форк Elyran Kogan (@elyrank) – у ньому виправлені проблеми 1 та 3
  2. Вирішили проблему 2
  3. Оновили Jetty з 9.2.14 на 9.3.12
  4. Обернули SimpleDateFormat в ThreadLocal; SimpleDateFormat не є потокообезпечним, що призводило до NPE в рантаймі.
  5. Усунули ще один витік пам'яті (неправильно закривалося з'єднання при дисконнекті)

І все-таки він тече!

Пам'ять почала закінчуватися не за день, а за два. Часу зовсім не лишалося, вирішили запускати менше потоків, але на чотирьох агентах. Цього мало вистачити, як мінімум, на тиждень.

Минуло два дні.

Тепер пам'ять почала закінчуватися у Hazelcast-а. У логах було видно, що через кілька днів тестування Hazelcast починає скаржитися на нестачу пам'яті, а ще через деякий час кластер розвалюється, і ноди продовжують гинути поодинці. Ми підключили JVisualVM до hazelcast-у і побачили «висхідну пилку» – він регулярно викликав GC, але ніяк не міг очистити пам'ять.

Як і навіщо ми написали високонавантажений масштабований сервіс для 1С: Підприємства: Java, PostgreSQL, Hazelcast

Виявилося, що в hazelcast 3.4 при видаленні map/multiMap (map.destroy()) пам'ять звільняється не повністю:

github.com/hazelcast/hazelcast/issues/6317
github.com/hazelcast/hazelcast/issues/4888

Наразі помилку виправлено в 3.5, але тоді це була проблема. Ми створювали нові multiMap з динамічними іменами та видаляли за нашою логікою. Код виглядав приблизно так:

public void join(Authentication auth, String sub) {
    MultiMap<UUID, Authentication> sessions = instance.getMultiMap(sub);
    sessions.put(auth.getUserId(), auth);
}

public void leave(Authentication auth, String sub) {
    MultiMap<UUID, Authentication> sessions = instance.getMultiMap(sub);
    sessions.remove(auth.getUserId(), auth);

    if (sessions.size() == 0) {
        sessions.destroy();
    }
}

Визов:

service.join(auth1, "НОВЫЕ_СООБЩЕНИЯ_В_ОБСУЖДЕНИИ_UUID1");
service.join(auth2, "НОВЫЕ_СООБЩЕНИЯ_В_ОБСУЖДЕНИИ_UUID1");

multiMap створювався для кожної підписки і видалявся, коли він був не потрібний. Вирішили, що заведемо Map , як ключ буде назва підписки, а в якості значень ідентифікатори сесій (за якими потім можна отримати ідентифікатори користувачів, якщо потрібно).

public void join(Authentication auth, String sub) {
    addValueToMap(sub, auth.getSessionId());
}

public void leave(Authentication auth, String sub) { 
    removeValueFromMap(sub, auth.getSessionId());
}

Графіки вирушили.

Як і навіщо ми написали високонавантажений масштабований сервіс для 1С: Підприємства: Java, PostgreSQL, Hazelcast

Що ще ми дізналися про тестування навантаження

  1. JSR223 потрібно писати на groovy та включати compilation cache – це дуже швидше. Посилання.
  2. Графіки Jmeter-Plugins простіше розуміти, ніж стандартні. Посилання.

Про наш досвід з Hazelcast

Hazelcast для нас був новим продуктом, ми почали працювати з ним із версії 3.4.1, зараз на нашому production сервері стоїть версія 3.9.2 (на момент написання статті остання версія Hazelcast – 3.10).

Генерація ID

Ми починали з цілих ідентифікаторів. Уявімо, що нам потрібен черговий Long для нової сутності. Sequence у БД не підходить, таблиці беруть участь у шардингу – вийде, що є повідомлення ID=1 у БД1 та повідомлення ID=1 у БД2, в Elasticsearch за таким ID не покладеш, у Hazelcast теж, але найстрашніше, якщо ви захочете звести дані із двох БД в одну (наприклад, вирішивши, що однієї бази достатньо для цих абонентів). Можна завести в Hazelcast кілька AtomicLong і тримати лічильник там, тоді продуктивність отримання нового ID – incrementAndGet плюс час на запит у Hazelcast. Але в Hazelcast є дещо більш оптимальне – FlakeIdGenerator. Кожному клієнту при зверненні видається діапазон ID, наприклад, першому – від 1 до 10 000, другому – від 10 001 до 20 000 тощо. Тепер клієнт може видавати нові ідентифікатори самостійно, доки закінчиться виданий йому діапазон. Працює швидко, але при перезапуску програми (і клієнт Hazelcast) починається нова послідовність - звідси пропуски і т.д. До того ж розробникам не дуже зрозуміло, чому ID цілочисленні, але йдуть так сильно вроздріб. Ми всі зважили і перейшли на UUID.

Для тих, хто хоче бути як Твіттер, є така бібліотека Snowcast – це реалізація Snowflake поверх Hazelcast. Подивитися можна тут:

github.com/noctarius/snowcast
github.com/twitter/snowflake

Але в нас уже до неї руки не дійшли.

TransactionalMap.replace

Ще один сюрприз: TransactionalMap.replace не працює. Ось такий тест:

@Test
public void replaceInMap_putsAndGetsInsideTransaction() {

    hazelcastInstance.executeTransaction(context -> {
        HazelcastTransactionContextHolder.setContext(context);
        try {
            context.getMap("map").put("key", "oldValue");
            context.getMap("map").replace("key", "oldValue", "newValue");
            
            String value = (String) context.getMap("map").get("key");
            assertEquals("newValue", value);

            return null;
        } finally {
            HazelcastTransactionContextHolder.clearContext();
        }        
    });
}

Expected : newValue
Actual : oldValue

Довелося написати свій replace, використовуючи getForUpdate:

protected <K,V> boolean replaceInMap(String mapName, K key, V oldValue, V newValue) {
    TransactionalTaskContext context = HazelcastTransactionContextHolder.getContext();
    if (context != null) {
        log.trace("[CACHE] Replacing value in a transactional map");
        TransactionalMap<K, V> map = context.getMap(mapName);
        V value = map.getForUpdate(key);
        if (oldValue.equals(value)) {
            map.put(key, newValue);
            return true;
        }

        return false;
    }
    log.trace("[CACHE] Replacing value in a not transactional map");
    IMap<K, V> map = hazelcastInstance.getMap(mapName);
    return map.replace(key, oldValue, newValue);
}

Тестуйте не тільки звичайні структури даних, але й їх версії транзакції. Буває, що IMap працює, а TransactionalMap вже немає.

Підкласти новий JAR без даунтайму

Спочатку ми вирішили записувати у Hazelcast об'єкти своїх класів. Наприклад, у нас є клас Application, ми хочемо його зберегти та прочитати. Зберігаємо:

IMap<UUID, Application> map = hazelcastInstance.getMap("application");
map.set(id, application);

Читаємо:

IMap<UUID, Application> map = hazelcastInstance.getMap("application");
return map.get(id);

Все працює. Потім ми вирішили побудувати індекс у Hazelcast, щоб шукати по ньому:

map.addIndex("subscriberId", false);

І при записі нової сутності почали отримувати ClassNotFoundException. Hazelcast намагався доповнити індекс, але нічого не знав про наш клас і хотів, щоб йому підклали JAR із цим класом. Ми так і зробили, все запрацювало, але постала нова проблема: як оновити JAR без повної зупинки кластера? Hazelcast не підхоплює новий JAR під час поновового оновлення. У цей момент ми вирішили, що цілком можемо жити без пошуку за індексом. Адже якщо використовувати Hazelcast як сховище типу ключ-значення, то все працюватиме? Не зовсім. Тут знову різна поведінка IMap та TransactionalMap. Там де IMap все одно, TransactionalMap кидає помилку.

IMap. Записуємо 5000 об'єктів, зчитуємо. Усі очікувано.

@Test
void get5000() {
    IMap<UUID, Application> map = hazelcastInstance.getMap("application");
    UUID subscriberId = UUID.randomUUID();

    for (int i = 0; i < 5000; i++) {
        UUID id = UUID.randomUUID();
        String title = RandomStringUtils.random(5);
        Application application = new Application(id, title, subscriberId);
        
        map.set(id, application);
        Application retrieved = map.get(id);
        assertEquals(id, retrieved.getId());
    }
}

А в транзакції не працює, отримуємо ClassNotFoundException:

@Test
void get_transaction() {
    IMap<UUID, Application> map = hazelcastInstance.getMap("application_t");
    UUID subscriberId = UUID.randomUUID();
    UUID id = UUID.randomUUID();

    Application application = new Application(id, "qwer", subscriberId);
    map.set(id, application);
    
    Application retrievedOutside = map.get(id);
    assertEquals(id, retrievedOutside.getId());

    hazelcastInstance.executeTransaction(context -> {
        HazelcastTransactionContextHolder.setContext(context);
        try {
            TransactionalMap<UUID, Application> transactionalMap = context.getMap("application_t");
            Application retrievedInside = transactionalMap.get(id);

            assertEquals(id, retrievedInside.getId());
            return null;
        } finally {
            HazelcastTransactionContextHolder.clearContext();
        }
    });
}

У 3.8 виник механізм User Class Deployment. Ви можете призначити одну головну ноду та оновлювати JAR-файл на ній.

Зараз ми повністю змінили підхід: самі серіалізуємо у JSON та зберігаємо у Hazelcast. Hazelcast не потрібно знати структуру наших класів, а ми можемо оновлюватися без даунтайму. Версіонуванням доменних об'єктів управляє програма. Одночасно можуть бути запущені різні версії програми, і можлива ситуація, коли нова програма пише об'єкти з новими полями, а старе ще про ці поля не знає. І в той же час нова програма вичитує об'єкти, записані старим додатком, в яких немає нових полів. Такі ситуації обробляємо всередині програми, але для простоти не змінюємо і не видаляємо поля, тільки розширюємо класи шляхом додавання нових полів.

Як ми забезпечуємо високу продуктивність

Чотири походи в Hazelcast – добре, два у БД – погано

Ходити за даними в кеш завжди краще, ніж у БД, але й зберігати незатребувані записи не хочеться. Рішення про те, що кешувати ми відкладаємо на останній етап розробки. Коли нова функціональність закодована, ми включаємо в PostgreSQL логування всіх запитів (log_min_duration_statement в 0) і запускаємо тестування навантаження хвилин на 20. За зібраними логами утиліти типу pgFouine і pgBadger вміють будувати аналітичні звіти. У звітах ми насамперед шукаємо повільні та часті запити. Для повільних запитів будуємо план виконання (EXPLAIN) та оцінюємо, чи можна прискорити такий запит. Часті запити за тими самими вхідними даними добре лягають у кеш. Запити намагаємося тримати «плоськими» по одній таблиці у запиті.

Експлуатація

СВ як онлайн-сервіс була запущена в експлуатацію навесні 2017 року, як окремий продукт СВ вийшов у листопаді 2017 року (на той момент у статусі бета-версії).

Більш як за рік експлуатації серйозних проблем у роботі онлайн-сервісу СВ не траплялося. Онлайн-сервіс моніторимо через Zabbix, збираємо і деплоїмо з Бамбук.

Дистрибутив сервера СВ поставляється як нативних пакетів: RPM, DEB, MSI. Плюс для Windows ми надаємо єдиний інсталятор у вигляді одного EXE, який встановлює сервер, Hazelcast та Elasticsearch на одну машину. Спочатку ми називали цю версію установки «демонстраційної», але зараз стало зрозуміло, що це найпопулярніший варіант розгортання.

Джерело: habr.com

Додати коментар або відгук