Як і навошта мы напісалі высоканагружаны які маштабуецца сэрвіс для 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, Цытус, Але, увогуле-то, гэта не noSQL, якія маштабуюцца са скрынкі. 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 (калі вэбсокет не зачыняецца ў канцы сэмпла, а выкарыстоўваецца далей у плане) не працуюць Response pattern-ы (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

Дадаць каментар