Si dhe pse kemi shkruar një shërbim të shkallëzuar me ngarkesë të lartë për 1C: Ndërmarrja: Java, PostgreSQL, Hazelcast

Në këtë artikull do të flasim se si dhe pse u zhvilluam Sistemi i ndërveprimit – një mekanizëm që transferon informacionin midis aplikacioneve të klientit dhe serverëve 1C: Enterprise - nga vendosja e një detyre deri te të menduarit përmes arkitekturës dhe detajeve të zbatimit.

Sistemi i Ndërveprimit (në tekstin e mëtejmë i referuar si SV) është një sistem mesazhesh i shpërndarë, tolerant ndaj gabimeve me dërgesë të garantuar. SV është projektuar si një shërbim me ngarkesë të lartë me shkallëzim të lartë, i disponueshëm si një shërbim online (i ofruar nga 1C) dhe si një produkt i prodhuar në masë që mund të vendoset në pajisjet e serverit tuaj.

SV përdor ruajtje të shpërndarë lajthi dhe motor kërkimi Elasticsearch. Do të flasim gjithashtu për Java-në dhe mënyrën se si ne shkallëzojmë horizontalisht PostgreSQL.
Si dhe pse kemi shkruar një shërbim të shkallëzuar me ngarkesë të lartë për 1C: Ndërmarrja: Java, PostgreSQL, Hazelcast

Formulimi i problemit

Për ta bërë të qartë pse krijuam Sistemin e Ndërveprimit, do t'ju tregoj pak se si funksionon zhvillimi i aplikacioneve të biznesit në 1C.

Për të filluar, pak për ne për ata që nuk e dinë ende se çfarë bëjmë :) Ne po krijojmë platformën e teknologjisë 1C:Enterprise. Platforma përfshin një mjet për zhvillimin e aplikacioneve të biznesit, si dhe një kohë ekzekutimi që lejon aplikacionet e biznesit të ekzekutohen në një mjedis ndër-platformë.

Paradigma e zhvillimit klient-server

Aplikacionet e biznesit të krijuara në 1C: Enterprise funksionojnë në tre nivele klient-server arkitektura “DBMS – server aplikacioni – klient”. Kodi i aplikacionit i shkruar në gjuhë e integruar 1C, mund të ekzekutohet në serverin e aplikacionit ose në klient. E gjithë puna me objektet e aplikacionit (drejtoritë, dokumentet, etj.), si dhe leximi dhe shkrimi i bazës së të dhënave, kryhet vetëm në server. Funksionaliteti i formave dhe ndërfaqja e komandës zbatohet gjithashtu në server. Klienti kryen marrjen, hapjen dhe shfaqjen e formularëve, "komunikim" me përdoruesin (paralajmërime, pyetje...), llogaritje të vogla në forma që kërkojnë përgjigje të shpejtë (për shembull, shumëzimin e çmimit sipas sasisë), duke punuar me skedarë lokalë, duke punuar me pajisje.

Në kodin e aplikacionit, titujt e procedurave dhe funksioneve duhet të tregojnë qartë se ku do të ekzekutohet kodi - duke përdorur direktivat &AtClient / &AtServer (&AtClient / &AtServer në versionin anglisht të gjuhës). Zhvilluesit e 1C tani do të më korrigjojnë duke thënë se direktivat janë në të vërtetë больше, por për ne kjo nuk është e rëndësishme tani.

Ju mund të telefononi kodin e serverit nga kodi i klientit, por nuk mund të telefononi kodin e klientit nga kodi i serverit. Ky është një kufizim themelor që kemi bërë për një sërë arsyesh. Në veçanti, sepse kodi i serverit duhet të shkruhet në atë mënyrë që të ekzekutohet në të njëjtën mënyrë, pavarësisht se ku thirret - nga klienti ose nga serveri. Dhe në rastin e thirrjes së kodit të serverit nga një kod tjetër serveri, nuk ka klient si i tillë. Dhe për shkak se gjatë ekzekutimit të kodit të serverit, klienti që e thirri mund të mbyllej, të dilte nga aplikacioni dhe serveri nuk do të kishte më askënd për të thirrur.

Si dhe pse kemi shkruar një shërbim të shkallëzuar me ngarkesë të lartë për 1C: Ndërmarrja: Java, PostgreSQL, Hazelcast
Kodi që trajton një klikim butoni: thirrja e një procedure serveri nga klienti do të funksionojë, thirrja e një procedure klienti nga serveri nuk do të funksionojë

Kjo do të thotë që nëse duam të dërgojmë një mesazh nga serveri në aplikacionin e klientit, për shembull, se gjenerimi i një raporti "të gjatë" ka përfunduar dhe raporti mund të shikohet, ne nuk kemi një metodë të tillë. Ju duhet të përdorni truket, për shembull, të anketoni periodikisht serverin nga kodi i klientit. Por kjo qasje e ngarkon sistemin me thirrje të panevojshme dhe në përgjithësi nuk duket shumë elegante.

Dhe ka gjithashtu nevojë, për shembull, kur vjen një telefonatë SIP- kur bëni një telefonatë, njoftoni aplikacionin e klientit për këtë, në mënyrë që ai të përdorë numrin e telefonuesit për ta gjetur atë në bazën e të dhënave të palës tjetër dhe t'i tregojë përdoruesit informacionin për palën telefonuese. Ose, për shembull, kur një porosi arrin në magazinë, njoftoni aplikacionin e klientit të klientit për këtë. Në përgjithësi, ka shumë raste kur një mekanizëm i tillë do të ishte i dobishëm.

Vetë prodhimi

Krijo një mekanizëm mesazhesh. I shpejtë, i besueshëm, me dorëzim të garantuar, me aftësinë për të kërkuar në mënyrë fleksibël mesazhe. Bazuar në mekanizmin, zbatoni një mesazher (mesazhe, thirrje video) që funksionon brenda aplikacioneve 1C.

Dizenjoni sistemin që të jetë i shkallëzuar horizontalisht. Ngarkesa në rritje duhet të mbulohet duke rritur numrin e nyjeve.

Zbatimi

Ne vendosëm të mos integrojmë pjesën e serverit të SV direkt në platformën 1C: Enterprise, por ta zbatojmë atë si një produkt të veçantë, API i të cilit mund të quhet nga kodi i zgjidhjeve të aplikacionit 1C. Kjo u bë për një sërë arsyesh, kryesore prej të cilave ishte se doja të bëja të mundur shkëmbimin e mesazheve midis aplikacioneve të ndryshme 1C (për shembull, midis Menaxhimit të Tregtisë dhe Kontabilitetit). Aplikacione të ndryshme 1C mund të ekzekutohen në versione të ndryshme të platformës 1C:Enterprise, të vendosen në serverë të ndryshëm, etj. Në kushte të tilla, zbatimi i SV si një produkt i veçantë i vendosur "në anën" e instalimeve 1C është zgjidhja optimale.

Pra, vendosëm të bëjmë SV si një produkt më vete. Ne rekomandojmë që kompanitë e vogla të përdorin serverin CB që kemi instaluar në renë kompjuterike (wss://1cdialog.com) për të shmangur kostot e përgjithshme që lidhen me instalimin dhe konfigurimin lokal të serverit. Klientët e mëdhenj mund ta kenë të këshillueshme të instalojnë serverin e tyre CB në objektet e tyre. Ne përdorëm një qasje të ngjashme në produktin tonë të cloud SaaS 1cE freskët - prodhohet si një produkt i prodhuar në masë për instalim në faqet e klientëve, dhe është gjithashtu i vendosur në renë tonë https://1cfresh.com/.

Aplikim

Për të shpërndarë tolerancën e ngarkesës dhe gabimeve, ne do të vendosim jo një aplikacion Java, por disa, me një balancues ngarkese përpara tyre. Nëse keni nevojë të transferoni një mesazh nga nyja në nyje, përdorni publikimin/subscribe në Hazelcast.

Komunikimi ndërmjet klientit dhe serverit bëhet nëpërmjet websocket. Është i përshtatshëm për sistemet në kohë reale.

Cache e shpërndarë

Ne zgjodhëm midis Redis, Hazelcast dhe Ehcache. Është viti 2015. Redis sapo lëshoi ​​një grup të ri (shumë i ri, i frikshëm), ka Sentinel me shumë kufizime. Ehcache nuk di se si të grumbullohet në një grup (ky funksion u shfaq më vonë). Ne vendosëm ta provonim me Hazelcast 3.4.
Hazelcast është mbledhur në një grup nga kutia. Në modalitetin e një nyjeje, nuk është shumë i dobishëm dhe mund të përdoret vetëm si cache - nuk di të hedhë të dhënat në disk, nëse humbet nyjen e vetme, humbet të dhënat. Ne vendosim disa Hazelcast, midis të cilave bëjmë kopje rezervë të të dhënave kritike. Ne nuk bëjmë kopje rezervë të cache - nuk na shqetëson.

Për ne, Hazelcast është:

  • Ruajtja e sesioneve të përdoruesve. Duhet shumë kohë për të shkuar në bazën e të dhënave për një seancë çdo herë, kështu që ne i vendosim të gjitha seancat në Hazelcast.
  • Cache. Nëse jeni duke kërkuar për një profil përdoruesi, kontrolloni cache. Shkruani një mesazh të ri - vendoseni në cache.
  • Temat për komunikimin ndërmjet instancave të aplikacionit. Nyja gjeneron një ngjarje dhe e vendos atë në temën Hazelcast. Nyjet e tjera të aplikacionit të abonuara në këtë temë marrin dhe përpunojnë ngjarjen.
  • Bravë grupi. Për shembull, ne krijojmë një diskutim duke përdorur një çelës unik (diskutim i vetëm brenda bazës së të dhënave 1C):

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

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

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

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

Ne kontrolluam që nuk kishte kanal. Ne morëm bllokimin, e kontrolluam përsëri dhe e krijuam. Nëse nuk e kontrolloni bllokimin pas marrjes së bllokimit, atëherë ekziston mundësia që në atë moment të kontrollohet edhe një fill tjetër dhe tani do të përpiqet të krijojë të njëjtin diskutim - por ai tashmë ekziston. Nuk mund të kyçeni duke përdorur java Lock të sinkronizuar ose të rregullt. Përmes bazës së të dhënave - është e ngadaltë, dhe është për të ardhur keq për bazën e të dhënave; përmes Hazelcast - kjo është ajo që ju nevojitet.

Zgjedhja e një DBMS

Ne kemi përvojë të gjerë dhe të suksesshme duke punuar me PostgreSQL dhe duke bashkëpunuar me zhvilluesit e kësaj DBMS.

Nuk është e lehtë me një grup PostgreSQL - ekziston XL, XC, Citus, por në përgjithësi këto nuk janë NoSQL që shkallëzohen jashtë kutisë. Ne nuk e konsideruam NoSQL si hapësirën kryesore të ruajtjes; mjaftoi që morëm Hazelcast, me të cilin nuk kishim punuar më parë.

Nëse keni nevojë të shkallëzoni një bazë të dhënash relacionale, kjo do të thotë copëtimi. Siç e dini, me sharding ne e ndajmë bazën e të dhënave në pjesë të veçanta në mënyrë që secila prej tyre të vendoset në një server të veçantë.

Versioni i parë i ndarjes sonë supozoi aftësinë për të shpërndarë secilën nga tabelat e aplikacionit tonë nëpër serverë të ndryshëm në përmasa të ndryshme. Ka shumë mesazhe në serverin A - ju lutemi, le të zhvendosim një pjesë të kësaj tabele te serveri B. Ky vendim thjesht bërtiste për optimizimin e parakohshëm, kështu që vendosëm të kufizohemi në një qasje me shumë qiramarrës.

Ju mund të lexoni për shumë qiramarrës, për shembull, në faqen e internetit Citus Data.

SV ka konceptet e aplikacionit dhe abonentit. Një aplikacion është një instalim specifik i një aplikacioni biznesi, si ERP ose Kontabiliteti, me përdoruesit e tij dhe të dhënat e biznesit. Një pajtimtar është një organizatë ose individ në emër të të cilit aplikacioni është regjistruar në serverin SV. Një pajtimtar mund të ketë disa aplikacione të regjistruara dhe këto aplikacione mund të shkëmbejnë mesazhe me njëri-tjetrin. Abonenti u bë qiramarrës në sistemin tonë. Mesazhet nga disa abonentë mund të vendosen në një bazë të dhënash fizike; nëse shohim që një pajtimtar ka filluar të gjenerojë shumë trafik, e zhvendosim atë në një bazë të dhënash fizike të veçantë (ose edhe në një server të veçantë të bazës së të dhënave).

Ne kemi një bazë të dhënash kryesore ku ruhet një tabelë rutimi me informacione për vendndodhjen e të gjitha bazave të të dhënave të pajtimtarëve.

Si dhe pse kemi shkruar një shërbim të shkallëzuar me ngarkesë të lartë për 1C: Ndërmarrja: Java, PostgreSQL, Hazelcast

Për të parandaluar që baza e të dhënave kryesore të jetë një pengesë, ne mbajmë tabelën e rrugëzimit (dhe të dhëna të tjera të nevojshme shpesh) në një cache.

Nëse baza e të dhënave të pajtimtarit fillon të ngadalësohet, ne do ta ndajmë atë në ndarje brenda. Në projekte të tjera ne përdorim pg_pathman.

Meqenëse humbja e mesazheve të përdoruesve është e keqe, ne i mbajmë bazat e të dhënave tona me kopje. Kombinimi i kopjeve sinkrone dhe asinkrone ju lejon të siguroni veten në rast të humbjes së bazës së të dhënave kryesore. Humbja e mesazhit do të ndodhë vetëm nëse baza e të dhënave primare dhe kopja e saj sinkrone dështojnë njëkohësisht.

Nëse një kopje sinkrone humbet, kopja asinkrone bëhet sinkrone.
Nëse baza e të dhënave kryesore humbet, kopja sinkrone bëhet baza e të dhënave kryesore dhe kopja asinkrone bëhet një kopje sinkrone.

Elastics Kërko për kërkim

Meqenëse, ndër të tjera, SV është edhe një mesazher, ai kërkon një kërkim të shpejtë, të përshtatshëm dhe fleksibël, duke marrë parasysh morfologjinë, duke përdorur ndeshje të pasakta. Ne vendosëm të mos rishpiknim timonin dhe të përdorim motorin e kërkimit falas Elasticsearch, të krijuar në bazë të bibliotekës Lucene. Ne gjithashtu vendosim Elasticsearch në një grup (master - të dhëna - të dhëna) për të eliminuar problemet në rast të dështimit të nyjeve të aplikacionit.

Ne gjetëm në github Shtojca e morfologjisë ruse për Elasticsearch dhe përdorni atë. Në indeksin Elasticsearch ruajmë rrënjët e fjalëve (të cilat shtojca i përcakton) dhe N-gram. Ndërsa përdoruesi fut tekstin për të kërkuar, ne kërkojmë tekstin e shtypur midis N-gramëve. Kur ruhet në indeks, fjala "tekste" do të ndahet në N-gramët e mëposhtëm:

[ata, tek, tex, tekst, tekste, ek, ish, ext, tekste, ks, kst, ksty, st, sty, ju],

Dhe rrënja e fjalës "tekst" do të ruhet gjithashtu. Kjo qasje ju lejon të kërkoni në fillim, në mes dhe në fund të fjalës.

Pasqyra e madhe

Si dhe pse kemi shkruar një shërbim të shkallëzuar me ngarkesë të lartë për 1C: Ndërmarrja: Java, PostgreSQL, Hazelcast
Përsëriteni foton nga fillimi i artikullit, por me shpjegime:

  • Balancues i ekspozuar në internet; ne kemi nginx, mund të jetë çdo.
  • Instancat e aplikacionit Java komunikojnë me njëri-tjetrin nëpërmjet Hazelcast.
  • Për të punuar me një prizë ueb ne përdorim Netty.
  • Aplikacioni Java është shkruar në Java 8 dhe përbëhet nga paketa OSGi. Planet përfshijnë migrimin në Java 10 dhe kalimin në module.

Zhvillimi dhe testimi

Në procesin e zhvillimit dhe testimit të SV, ne hasëm në një sërë veçorish interesante të produkteve që përdorim.

Testimi i ngarkesës dhe rrjedhjet e kujtesës

Lëshimi i çdo lëshimi SV përfshin testimin e ngarkesës. Është i suksesshëm kur:

  • Testi funksionoi për disa ditë dhe nuk pati asnjë dështim në shërbim
  • Koha e përgjigjes për operacionet kryesore nuk e ka tejkaluar një prag të rehatshëm
  • Përkeqësimi i performancës në krahasim me versionin e mëparshëm nuk është më shumë se 10%

Ne mbushim bazën e të dhënave të provës me të dhëna - për ta bërë këtë, marrim informacione për pajtimtarin më aktiv nga serveri i prodhimit, shumëzojmë numrat e tij me 5 (numrin e mesazheve, diskutimeve, përdoruesve) dhe e testojmë në atë mënyrë.

Ne kryejmë testimin e ngarkesës së sistemit të ndërveprimit në tre konfigurime:

  1. testi i stresit
  2. Vetëm lidhjet
  3. Regjistrimi i pajtimtarit

Gjatë testit të stresit, ne lëshojmë disa qindra fije, dhe ato ngarkojnë sistemin pa u ndalur: shkrimi i mesazheve, krijimi i diskutimeve, marrja e një liste mesazhesh. Ne simulojmë veprimet e përdoruesve të zakonshëm (merrni një listë të mesazheve të mia të palexuara, shkruajini dikujt) dhe zgjidhjet softuerike (transmetoni një paketë të një konfigurimi tjetër, përpunoni një alarm).

Për shembull, kjo është se si duket pjesa e testit të stresit:

  • Përdoruesi regjistrohet
    • Kërkon diskutimet tuaja të palexuara
    • 50% gjasa për të lexuar mesazhe
    • 50% gjasa për të shkruar
    • Përdoruesi tjetër:
      • Ka një shans 20% për të krijuar një diskutim të ri
      • Zgjedh rastësisht ndonjë nga diskutimet e tij
      • Shkon brenda
      • Kërkon mesazhe, profile të përdoruesve
      • Krijon pesë mesazhe drejtuar përdoruesve të rastit nga ky diskutim
      • Largohet nga diskutimi
      • Përsëritet 20 herë
      • Dal, kthehet në fillim të skenarit

    • Një chatbot hyn në sistem (imiton mesazhet nga kodi i aplikacionit)
      • Ka një shans 50% për të krijuar një kanal të ri për shkëmbimin e të dhënave (diskutim i veçantë)
      • 50% ka të ngjarë të shkruajë një mesazh në ndonjë nga kanalet ekzistuese

Skenari "Vetëm lidhjet" u shfaq për një arsye. Ekziston një situatë: përdoruesit kanë lidhur sistemin, por ende nuk janë përfshirë. Çdo përdorues ndez kompjuterin në orën 09:00 të mëngjesit, vendos një lidhje me serverin dhe qëndron në heshtje. Këta djem janë të rrezikshëm, ka shumë prej tyre - të vetmet paketa që kanë janë PING/PONG, por ata e mbajnë lidhjen me serverin (nuk mund ta mbajnë atë - po sikur të ketë një mesazh të ri). Testi riprodhon një situatë ku një numër i madh përdoruesish të tillë përpiqen të hyjnë në sistem në gjysmë ore. Është i ngjashëm me një test stresi, por fokusi i tij është pikërisht në këtë hyrje të parë - në mënyrë që të mos ketë dështime (një person nuk e përdor sistemin, dhe ai tashmë bie - është e vështirë të mendosh për diçka më të keqe).

Skripti i regjistrimit të abonentëve fillon që nga fillimi i parë. Ne kryem një test stresi dhe ishim të sigurt që sistemi nuk u ngadalësua gjatë korrespondencës. Por përdoruesit erdhën dhe regjistrimi filloi të dështonte për shkak të një afati kohor. Gjatë regjistrimit kemi përdorur / dev / rastësisht, e cila lidhet me entropinë e sistemit. Serveri nuk kishte kohë për të grumbulluar entropi të mjaftueshme dhe kur u kërkua një SecureRandom i ri, ai ngriu për dhjetëra sekonda. Ka shumë mënyra për të dalë nga kjo situatë, për shembull: kaloni në /dev/urandom më pak të sigurt, instaloni një tabelë të veçantë që gjeneron entropi, gjeneroni numra të rastësishëm paraprakisht dhe ruajini ato në një pishinë. Ne e mbyllëm përkohësisht problemin me pishinën, por që atëherë kemi kryer një test të veçantë për regjistrimin e abonentëve të rinj.

Ne përdorim si gjenerues të ngarkesës JMeter. Nuk di të punojë me websocket; ka nevojë për një shtojcë. Rezultatet e para në kërkim për pyetjen "jmeter websocket" janë: artikuj nga BlazeMeter, të cilat rekomandojnë shtojcë nga Maciej Zaleski.

Aty vendosëm të fillonim.

Pothuajse menjëherë pas fillimit të testimit serioz, zbuluam se JMeter filloi të rrjedhë memorie.

Shtojca është një histori më vete e madhe; me 176 yje, ka 132 forks në github. Vetë autori nuk është angazhuar për të që nga viti 2015 (e kemi marrë në 2015, më pas nuk ka ngritur dyshime), disa çështje github në lidhje me rrjedhjet e kujtesës, 7 kërkesa për tërheqje të pambyllura.
Nëse vendosni të kryeni testimin e ngarkesës duke përdorur këtë shtojcë, ju lutemi kushtojini vëmendje diskutimeve të mëposhtme:

  1. Në një mjedis me shumë fije, u përdor një LinkedList i rregullt dhe rezultati ishte NPE në kohën e ekzekutimit. Kjo mund të zgjidhet ose duke kaluar në ConcurrentLinkedDeque ose me blloqe të sinkronizuara. Ne zgjodhëm opsionin e parë për veten tonë (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. Rrjedhje memorie; kur shkëputeni, informacioni i lidhjes nuk fshihet (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. Në modalitetin e transmetimit (kur priza e internetit nuk është e mbyllur në fund të mostrës, por përdoret më vonë në plan), modelet e përgjigjes nuk funksionojnë (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

Ky është një nga ato në github. Çfarë bëmë:

  1. Kanë marrë pirun Elyran Kogan (@elyrank) – rregullon problemet 1 dhe 3
  2. Zgjidhet problemi 2
  3. Porta e përditësuar nga 9.2.14 në 9.3.12
  4. I mbështjellë SimpleDateFormat në ThreadLocal; SimpleDateFormat nuk është i sigurt për thread, gjë që çoi në NPE në kohën e ekzekutimit
  5. Rregulloi një rrjedhje tjetër të kujtesës (lidhja u mbyll gabimisht kur u shkëput)

E megjithatë ajo rrjedh!

Kujtesa filloi të mbaronte jo në një ditë, por në dy. Absolutisht nuk kishte mbetur kohë, kështu që vendosëm të hapnim më pak tema, por me katër agjentë. Kjo duhet të kishte mjaftuar për të paktën një javë.

Kanë kaluar dy ditë...

Tani Hazelcast po i mbaron memoria. Regjistrat treguan se pas disa ditësh testimi, Hazelcast filloi të ankohej për mungesë memorie dhe pas ca kohësh grupi u shpërbë dhe nyjet vazhduan të vdisnin një nga një. Ne lidhëm JVisualVM me hazelcast dhe pamë një "sharrë në rritje" - ajo thërriste rregullisht GC, por nuk mund të pastronte kujtesën.

Si dhe pse kemi shkruar një shërbim të shkallëzuar me ngarkesë të lartë për 1C: Ndërmarrja: Java, PostgreSQL, Hazelcast

Doli që në hazelcast 3.4, kur fshini një hartë / multiMap (map.destroy()), memoria nuk çlirohet plotësisht:

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

Defekti tani është rregulluar në 3.5, por ishte një problem në atë kohë. Ne krijuam multiHarta të reja me emra dinamikë dhe i fshimë sipas logjikës sonë. Kodi dukej diçka si ky:

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();
    }
}

Vyzov:

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

multiMap u krijua për çdo abonim dhe u fshi kur nuk ishte e nevojshme. Ne vendosëm që të fillonim Map , çelësi do të jetë emri i pajtimit dhe vlerat do të jenë identifikuesit e sesioneve (nga të cilat më pas mund të merrni identifikuesit e përdoruesit, nëse është e nevojshme).

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

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

Grafikët janë përmirësuar.

Si dhe pse kemi shkruar një shërbim të shkallëzuar me ngarkesë të lartë për 1C: Ndërmarrja: Java, PostgreSQL, Hazelcast

Çfarë tjetër kemi mësuar rreth testimit të ngarkesës?

  1. JSR223 duhet të shkruhet në groovy dhe të përfshijë cache-n e përpilimit - është shumë më i shpejtë. Lidhje.
  2. Grafikët Jmeter-Plugins janë më të lehtë për t'u kuptuar sesa ato standarde. Lidhje.

Rreth përvojës sonë me Hazelcast

Hazelcast ishte një produkt i ri për ne, ne filluam të punojmë me të nga versioni 3.4.1, tani serveri ynë i prodhimit po ekzekuton versionin 3.9.2 (në kohën e shkrimit, versioni i fundit i Hazelcast është 3.10).

gjenerimi i ID

Ne filluam me identifikuesit e numrave të plotë. Le të imagjinojmë se na duhet një tjetër Long për një entitet të ri. Sekuenca në bazën e të dhënave nuk është e përshtatshme, tabelat janë të përfshira në ndarjen - rezulton se ka një mesazh ID=1 në DB1 dhe një ID=1 mesazhi në DB2, nuk mund ta vendosni këtë ID në Elasticsearch, as në Hazelcast , por gjëja më e keqe është nëse doni të kombinoni të dhënat nga dy baza të të dhënave në një (për shembull, të vendosni që një bazë e të dhënave është e mjaftueshme për këta abonentë). Mund të shtoni disa AtomicLong në Hazelcast dhe ta mbani numëruesin atje, atëherë performanca e marrjes së një ID të re është në rritjeDhe Merr plus kohën për një kërkesë për Hazelcast. Por Hazelcast ka diçka më optimale - FlakeIdGenerator. Kur kontaktojnë çdo klient, atyre u jepet një gamë ID, për shembull, e para - nga 1 në 10, e dyta - nga 000 në 10, e kështu me radhë. Tani klienti mund të lëshojë vetë identifikues të rinj derisa të përfundojë diapazoni i lëshuar për të. Funksionon shpejt, por kur rinisni aplikacionin (dhe klientin Hazelcast), fillon një sekuencë e re - rrjedhimisht kapërcimet, etj. Për më tepër, zhvilluesit nuk e kuptojnë vërtet pse ID-të janë numër të plotë, por janë kaq të paqëndrueshme. Ne peshuam gjithçka dhe kaluam në UUID.

Nga rruga, për ata që duan të jenë si Twitter, ekziston një bibliotekë e tillë Snowcast - ky është një zbatim i Snowflake në krye të Hazelcast. Mund ta shikoni këtu:

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

Por ne nuk e kemi arritur më atë.

TransactionMap.zëvendësoj

Një surprizë tjetër: TransactionalMap.replace nuk funksionon. Këtu është një test:

@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

Më duhej të shkruaja zëvendësimin tim duke përdorur 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);
}

Testoni jo vetëm strukturat e rregullta të të dhënave, por edhe versionet e tyre transaksionale. Ndodh që IMap funksionon, por TransactionalMap nuk ekziston më.

Fusni një JAR të ri pa ndërprerje

Së pari, vendosëm të regjistronim objektet e klasave tona në Hazelcast. Për shembull, ne kemi një klasë Application, ne duam ta ruajmë dhe lexojmë atë. Ruaj:

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

Ne lexojmë:

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

Gjithçka po funksionon. Pastaj vendosëm të ndërtonim një indeks në Hazelcast për të kërkuar sipas:

map.addIndex("subscriberId", false);

Dhe kur shkruanin një entitet të ri, ata filluan të merrnin ClassNotFoundException. Hazelcast u përpoq të shtonte në indeks, por nuk dinte asgjë për klasën tonë dhe donte që t'i jepej një JAR me këtë klasë. Ne bëmë pikërisht atë, gjithçka funksionoi, por u shfaq një problem i ri: si të përditësoni JAR pa e ndaluar plotësisht grupin? Hazelcast nuk e merr JAR-in e ri gjatë një përditësimi nyje pas nyje. Në këtë pikë vendosëm që mund të jetonim pa kërkuar indeks. Në fund të fundit, nëse përdorni Hazelcast si një dyqan me vlerë kryesore, atëherë gjithçka do të funksionojë? Jo ne te vertete. Këtu përsëri sjellja e IMap dhe TransactionalMap është e ndryshme. Aty ku IMap nuk i intereson, TransactionalMap hedh një gabim.

IMap. Shkruajmë 5000 objekte, i lexojmë. Gjithçka pritet.

@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());
    }
}

Por nuk funksionon në një transaksion, ne marrim një 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();
        }
    });
}

Në 3.8, u shfaq mekanizmi i vendosjes së klasës së përdoruesit. Mund të caktoni një nyje kryesore dhe të përditësoni skedarin JAR në të.

Tani e kemi ndryshuar plotësisht qasjen tonë: e serializojmë vetë në JSON dhe e ruajmë në Hazelcast. Hazelcast nuk ka nevojë të dijë strukturën e klasave tona dhe ne mund të përditësojmë pa ndërprerje. Versionimi i objekteve të domenit kontrollohet nga aplikacioni. Versione të ndryshme të aplikacionit mund të ekzekutohen në të njëjtën kohë, dhe një situatë është e mundur kur aplikacioni i ri shkruan objekte me fusha të reja, por i vjetri nuk di ende për këto fusha. Dhe në të njëjtën kohë, aplikacioni i ri lexon objektet e shkruara nga aplikacioni i vjetër që nuk kanë fusha të reja. Ne trajtojmë situata të tilla brenda aplikacionit, por për thjeshtësi nuk i ndryshojmë apo fshijmë fushat, ne i zgjerojmë vetëm klasat duke shtuar fusha të reja.

Si sigurojmë performancë të lartë

Katër udhëtime në Hazelcast - të mira, dy në bazën e të dhënave - të këqija

Shkuarja në cache për të dhëna është gjithmonë më e mirë sesa të shkoni në bazën e të dhënave, por nuk dëshironi të ruani as të dhëna të papërdorura. Ne e lëmë vendimin se çfarë të ruajmë memorien deri në fazën e fundit të zhvillimit. Kur funksionaliteti i ri është i koduar, ne aktivizojmë regjistrimin e të gjitha pyetjeve në PostgreSQL (log_min_duration_statement në 0) dhe ekzekutojmë testimin e ngarkesës për 20 minuta. Duke përdorur regjistrat e mbledhur, shërbimet si pgFouine dhe pgBadger mund të krijojnë raporte analitike. Në raporte, ne kryesisht kërkojmë pyetje të ngadalta dhe të shpeshta. Për pyetje të ngadalta, ne ndërtojmë një plan ekzekutimi (EXPLAIN) dhe vlerësojmë nëse një pyetje e tillë mund të përshpejtohet. Kërkesat e shpeshta për të njëjtat të dhëna hyrëse përshtaten mirë në cache. Ne përpiqemi t'i mbajmë pyetjet "të sheshta", një tabelë për çdo pyetje.

Shfrytëzimi

SV si një shërbim online u vu në funksion në pranverën e vitit 2017, dhe si produkt i veçantë, SV u lëshua në nëntor 2017 (në atë kohë në statusin e versionit beta).

Në më shumë se një vit funksionimi, nuk ka pasur probleme serioze në funksionimin e shërbimit online CB. Ne monitorojmë shërbimin online nëpërmjet Zabbix, mblidhni dhe vendoseni nga Bambu.

Shpërndarja e serverit SV ofrohet në formën e paketave vendase: RPM, DEB, MSI. Plus për Windows ne ofrojmë një instalues ​​të vetëm në formën e një EXE të vetëm që instalon serverin, Hazelcast dhe Elasticsearch në një makinë. Ne fillimisht iu referuam këtij versioni të instalimit si versioni "demo", por tani është bërë e qartë se ky është opsioni më i popullarizuar i vendosjes.

Burimi: www.habr.com

Shto një koment