Si dhe përse e shkruam 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 pĂ«r se si dhe pĂ«r çfarĂ« e zhvilluam Sistemin e NdĂ«rveprimit – mekanizmi qĂ« transmeton informacionin midis aplikacioneve klient dhe serverave 1C:Enterprise – nga vendosja e detyrĂ«s deri te mendimi i arkitekturĂ«s dhe detajeve tĂ« realizimit.

Sistemi i NdĂ«rveprimit (mĂ« pas – SN) Ă«shtĂ« njĂ« sistem i shpĂ«rndarĂ« me qĂ«ndrueshmĂ«ri tĂ« lartĂ« pĂ«r shkĂ«mbimin e mesazheve me dorĂ«zim tĂ« garantuar. SN Ă«shtĂ« projektuar si njĂ« shĂ«rbim me ngarkesĂ« tĂ« lartĂ« dhe me shkallĂ«zim tĂ« lartĂ«, dhe Ă«shtĂ« i disponueshĂ«m si njĂ« shĂ«rbim online (ofrohet nga firma 1C), ashtu edhe si njĂ« produkt tĂ« cilin mund ta zbarkoni nĂ« kapacitetet tuaja serverike.

SN përdor një depo të shpërndarë Hazelcast dhe një sistem kërkimi Elasticsearch. Gjithashtu do të flasim për Java dhe se si ne e shkallëzojmë horizontalisht PostgreSQL.
Si dhe përse e shkruam një shërbim të shkallëzuar me ngarkesë të lartë për 1C: Ndërmarrja: Java, PostgreSQL, Hazelcast

Formulimi i detyrës

Për ta kuptuar se përse e kemi bërë Sistemin e Ndërveprimit, do të tregoj pak rreth se si është organizuar zhvillimi i aplikacioneve biznesore në 1C.

Fillimisht – pak pĂ«r ne pĂ«r ata qĂ« ende nuk e dinĂ« se me çfarĂ« merremi :) Ne krijojmĂ« platformĂ«n teknologjike '1C:Enterprise'. Platforma pĂ«rmban mjete pĂ«r zhvillimin e aplikacioneve biznesore, si dhe runtime qĂ« lejon aplikacionet biznesore tĂ« funksionojnĂ« nĂ« njĂ« mjedis shumĂ«-platformor.

Paradigma e klient-server për zhvillimin

Aplikacionet biznesore, tĂ« krijuara nĂ« '1C:Enterprise', funksionojnĂ« nĂ« njĂ« arkitekturĂ« tre-nivele klient-server nĂ« arkitekturĂ«n 'DBMS – server i aplikacioneve – klient'. Kodi aplikativ, i shkruar nĂ« gjuhĂ«n e integruar 1C, mund tĂ« ekzekutohet nĂ« serverin e aplikacioneve ose nĂ« klient. TĂ« gjitha punĂ«t me objektet aplikative (referencat, dokumentet, etj.), si dhe leximi dhe shkrimi i bazĂ«s sĂ« tĂ« dhĂ«nave kryhen vetĂ«m nĂ« server. Funksionaliteti i formave dhe ndĂ«rfaqes komandore gjithashtu realizohet nĂ« server. NĂ« klient ekzekutohet marrja, hapja dhe shfaqja e formave, 'komunikimi' me pĂ«rdoruesin (paralajmĂ«rimet, pyetjet...), llogaritjet e vogla nĂ« forma qĂ« kĂ«rkojnĂ« reagim tĂ« shpejtĂ« (p.sh., shumĂ«zimi i çmimit me sasinĂ«), punimi me skedarĂ« lokalĂ«, puna me pajisjet.

NĂ« kodin aplikativ nĂ« titujt e procedurave dhe funksioneve duhet tĂ« specifikohet qartĂ« ku do tĂ« ekzekutohet kodi — duke pĂ«rdorur direktivat &NĂ«Klient / &NĂ«Server (&AtClient / &AtServer nĂ« variantin anglisht tĂ« gjuhĂ«s). TĂ« zhvilluesit nĂ« 1C tani do tĂ« mĂ« korrigjojnĂ« duke thĂ«nĂ« se direktivat nĂ« tĂ« vĂ«rtetĂ« mĂ« shumĂ«, por ne Ă«shtĂ« e rĂ«ndĂ«sishme pĂ«r ne tani.

Kodi i klientit mund të thërrasë kodin e serverit, ndërsa kodi i serverit nuk mund të thërrasë kodin e klientit. Kjo është një kufizim themelor, i vendosur nga ne për disa arsye. Kryesisht sepse kodi i serverit duhet të shkruhet në mënyrë që të ekzekutohet njësoj, pavarësisht se nga ku thirret - nga klienti apo nga serveri. Dhe në rastin e thirrjes së kodit të serverit nga një kod i serverit tjetër, klienti nuk është i pranishëm si i tillë. Gjithashtu, gjatë ekzekutimit të kodit të serverit, klienti që e thirri atë mund të ketë mbyllur aplikacionin, dhe serverit nuk do t'i mbetet askush për të thirrur.

Si dhe përse e shkruam një shërbim të shkallëzuar me ngarkesë të lartë për 1C: Ndërmarrja: Java, PostgreSQL, Hazelcast
Kodi që trajton klikimin e butonit: thirrja e procedurës së serverit nga klienti do të funksionojë, thirrja e procedurës së klientit nga serveri - jo.

Kjo do tĂ« thotĂ« se nĂ«se ne duam tĂ« dĂ«rgojmĂ« ndonjĂ« mesazh nĂ« aplikacionin e klientit nga serveri, pĂ«r shembull, qĂ« procesi i krijimit tĂ« raportit “me gjatĂ«si tĂ« gjatĂ«â€ ka pĂ«rfunduar dhe raporti mund tĂ« shikohet - nuk kemi njĂ« mĂ«nyrĂ« tĂ« tillĂ«. Na duhet tĂ« shkojmĂ« nĂ« fallxhorinĂ«, pĂ«r shembull, nga kodi i klientit pĂ«r tĂ« pyetur rregullisht serverin. Por njĂ« qasje e tillĂ« ngarkon sistemin me thirrje tĂ« tepĂ«rta, dhe gjithashtu duket jo shumĂ« elegante.

Dhe ka gjithashtu nevojë, për shembull, në rastin e një telefoni të pranuar SIP- të njoftojmë aplikacionin e klientit për këtë, në mënyrë që ai të gjejë në bazën e të dhënave palën që telefonon me numrin e telefonit dhe t'i tregojë përdoruesit informacionin mbi palën që telefonon. Ose, për shembull, në rastin e pranimit të një porosie në magazinë, të njoftojmë aplikacionin e klientit të porositësit. Në përgjithësi, ka shumë raste ku një mekanizëm i tillë do të ishte i dobishëm.

Pothuajse është vendosja

Të krijojmë një mekanizëm të shkëmbimit të mesazheve. I shpejtë, i besueshëm, me dorëzim të garantuar, me mundësi të kërkimit fleksibël të mesazheve. Në bazë të mekanizmit të realizohet një mesazher (mesazhe, video thirrje), që funksionon brenda aplikacioneve 1C.

Të projektojmë një sistem që është horizontalisht i shkallëzueshëm. Rritja e ngarkesës duhet të përballohet me rritjen e numrit të nodave.

Implementimi

Ne vendosëm pjesën e serverit të SV në platformën 1C: Enterprise, por do ta realizojmë si një produkt të veçantë, API i të cilit mund të thirret nga kodi i zgjidhjeve aplikative 1C. Kjo u bë për një numër arsyesh, kryesorja e të cilave ishte dëshira për të mundësuar shkëmbimin e mesazheve midis aplikacioneve të ndryshme 1C (për shembull, midis Menaxhimit të Tregtisë dhe Kontabilitetit). Aplikacione të ndryshme 1C mund të funksionojnë në versione të ndryshme të platformës 1C: Enterprise, të jenë në serverë të ndryshëm, etj. Në këto kushte, realizimi i SV si një produkt të veçantë që është "në anë" të instalimeve të 1C është zgjidhja optimale.

KĂ«shtu, ne vendosĂ«m tĂ« bĂ«jmĂ« SV si njĂ« produkt tĂ« veçantĂ«. PĂ«r kompanitĂ« e vogla, ne rekomandojmĂ« tĂ« pĂ«rdorin serverin SV, tĂ« cilin e kemi instaluar nĂ« cloud-in tonĂ« (wss://1cdialog.com), pĂ«r tĂ« shmangur shpenzimet e lidhura me instalimin dhe konfigurimin lokal tĂ« serverit. KlientĂ«t e mĂ«dhenj mund ta gjejnĂ« tĂ« arsyeshme tĂ« instalojnĂ« njĂ« server SV nĂ« kapacitetet e tyre. Qasja e ngjashme Ă«shtĂ« pĂ«rdorur nĂ« produktin tonĂ« cloud SaaS. 1cFresh – lĂ«shon si produkt me seritĂ« pĂ«r instalim te klientĂ«t dhe gjithashtu Ă«shtĂ« vendosur nĂ« cloud-in tonĂ«. https://1cfresh.com/.

Aplikacioni

PĂ«r tĂ« shpĂ«rndarĂ« ngarkesĂ«n dhe pĂ«r qĂ«ndrueshmĂ«rinĂ«, do tĂ« pĂ«rcjellim jo njĂ« por disa aplikacione Java, pĂ«rpara do tĂ« vendosim njĂ« balancues ngarkese. NĂ«se duhet tĂ« transferojmĂ« njĂ« mesazh nga nyja nĂ« nyje – do tĂ« pĂ«rdorim publish/subscribe nĂ« Hazelcast.

Komunikimi midis klientit dhe serverit bëhet përmes websocket. Ky është shumë i përshtatshëm për sistemet në kohë reale.

Cache i shpërndarë

Kemi zgjedhur midis Redis, Hazelcast dhe Ehcache. Jemi në vitin 2015. Redis sapo ka lëshuar një klaster të ri (shumë i ri, frikshëm), ka Sentinel me një mori kufizimesh. Ehcache nuk dinte të grumbullonte në klaster (kjo funksionalitet do të vijë më vonë). Vendosëm të provojmë me Hazelcast 3.4.
Hazelcast grumbullohet nĂ« klaster "nga kutia". NĂ« mĂ«nyrĂ«n e njĂ« nyjeje, ai nuk Ă«shtĂ« shumĂ« i dobishĂ«m dhe mund tĂ« shĂ«rbejĂ« vetĂ«m si cache – nuk din tĂ« ruajĂ« tĂ« dhĂ«nat nĂ« disk, humbĂ«m nyjĂ«n e vetme – humbĂ«m tĂ« dhĂ«nat. Ne vendosim disa Hazelcast, midis tĂ« cilĂ«ve backupojmĂ« tĂ« dhĂ«nat kritike. Cache nuk e backupojmĂ« – nuk na intereson.

Për ne, Hazelcast është:

  • NjĂ« ruajtĂ«se pĂ«r sesionet e pĂ«rdoruesve. TĂ« shkojmĂ« çdo herĂ« pĂ«r njĂ« sesion nĂ« bazĂ« – zgjat shumĂ«, prandaj tĂ« gjithĂ« sesionet i vendosim nĂ« Hazelcast.
  • Cache. Looking for a user profile? Check the cache. Wrote a new message? Put it in the cache.
  • Topics for communication between application instances. A node generates an event and places it in the Hazelcast topic. Other application nodes subscribed to this topic receive and process the event.
  • Cluster locks. For example, we create a discussion with a unique key (singleton discussion within the 1C database):

conversationKeyChecker.check("Benzokolonka");

      doInClusterLock("Benzokolonka", () -> {

          conversationKeyChecker.check("Benzokolonka");

          createChannel("Benzokolonka");
      });

We checked that the channel does not exist. Took the lock, checked again, and created it. If we don't check after taking the lock, there is a chance that another thread checked at the same moment and is now trying to create the same discussion – and it already exists. Locking through synchronized or regular Java Lock is not possible. Using the database is slow and we don't want to burden the database, using Hazelcast is the way to go.

Choosing a DBMS

We have extensive and successful experience with PostgreSQL and collaborating with developers of this DBMS.

Working with a cluster in PostgreSQL is challenging – there is XL, XC, Citus, but generally, it is not like noSQL, which scales out of the box. We did not consider using NoSQL as the primary storage; it was enough to take Hazelcast, with which we had not worked before.

If we need to scale a relational database, then sharding. As you know, with sharding, we divide the database into separate parts so that each can be placed on a separate server.

The first option of our sharding assumed the ability to distribute every table of our application across different servers in various proportions. If there are many messages on server A, let’s move part of this table to server B. Such a solution screamed premature optimization, so we decided to limit ourselves to a multi-tenant approach.

You can read about multi-tenant on the website Citus Data.

Në SV ka koncepte të aplikacionit dhe abonentit. Aplikacioni është një instalim i caktuar i një aplikacioni biznesi, për shembull, ERP apo Kontabilitet, me përdoruesit dhe të dhënat e veta biznesore. Abonenti është një organizatë ose një person fizik, në emër të të cilit regjistrohet aplikacioni në serverin SV. Abonenti mund të ketë disa aplikacione të regjistruara, dhe këto aplikacione mund të komunikojnë mes tyre. Abonenti u bë një qiramarrës (tenant) në sistemin tonë. Mesazhet e disa abonentëve mund të ndodhen në një bazë të dhënash fizike; nëse e shohim që ndonjë abonent po gjeneron shumë trafik - e çojmë atë në një bazë të dhënash fizike të veçantë (ose madje në një server të veçantë DB).

Ne kemi një bazë të dhënash kryesore, ku ruhet tabela e routing-ut me informacionin mbi lokacionin e të gjitha bazave të të dhënave të abonentëve.

Si dhe përse e shkruam një shërbim të shkallëzuar me ngarkesë të lartë për 1C: Ndërmarrja: Java, PostgreSQL, Hazelcast

Për të mos pasur ngushticë në bazën e të dhënave kryesore, ne e mbajmë tabelën e routing-ut (dhe të dhënat e tjera që përdoren shpesh) në cache.

Nëse fillon të ngadalësohet baza e të dhënave të abonentit, do të ndajmë brenda në particione. Në projekte të tjera për particionimin e tabelave të mëdha përdorim pg_pathman.

Duke qenë se është e keqe të humbasim mesazhet e përdoruesve, ne i mbështesim bazat tona të dhënash me replika. Kombinimi i replikave sinkrone dhe asinkrone na lejon të jemi të mbrojtur në rast të humbjes së bazës kryesore të të dhënave. Humbja e një mesazhi do të ndodhë vetëm në rast të dështimit të njëkohshëm të bazës kryesore të të dhënave dhe replikës së saj sinkrone.

Nëse humbet replika sinkrone - replika asinkrone bëhet sinkrone.
Nëse humbet baza kryesore e të dhënave - replika sinkrone bëhet baza kryesore e të dhënave, ndërsa replika asinkrone bëhet replika sinkrone.

Elasticsearch për kërkimin

Duke qenë se, përveç të tjerave, SV është gjithashtu një mesazhues, ne kemi nevojë për një kërkim të shpejtë, të rehatshëm dhe fleksibël, duke marrë parasysh morfologjinë, për përputhjet jo të sakta. Ne e vendosëm të mos shpiknim biçikletën dhe të përdorim sistemin e kërkimit të lirë Elasticsearch, i krijuar mbi bazën e bibliotekës Lucene. Elasticsearch e vendosim gjithashtu në një klashtër (master - data - data), për të parandaluar problemet në rast se ndonjë nyje aplikacioni dështon.

Në github ne gjetëm plugin për morfologjinë ruse për Elasticsearch dhe e përdorim. Në indeksin Elasticsearch ruajmë rrënjët e fjalëve (të cilat përcakton plugin) dhe N-gjërat. Ndërsa përdoruesi shkruan tekst për kërkim, ne kërkojmë tekstin e shkruar midis N-gjërave. Kur ruhet në indeks, fjala "tekste" do të ndahet në N-gjërat e mëposhtme:

[te, tek, teks, tekst, tekste, ek, eks, ekst, ekstë, ks, kst, ksty, st, sty, ty],

Gjithashtu do të ruhet rrënja e fjalës "tekst". Ky qasje lejon që të kërkojmë si në fillim, ashtu edhe në mes, dhe në fund të fjalës.

Pamja e përgjithshme

Si dhe përse e shkruam një shërbim të shkallëzuar me ngarkesë të lartë për 1C: Ndërmarrja: Java, PostgreSQL, Hazelcast
Përsëritja e imazhit nga fillimi i artikullit, por tashmë me shpjegime:

  • Balancuesi, i vendosur nĂ« internet; ne kemi – nginx, mund tĂ« jetĂ« çfarĂ«do.
  • Instancat e aplikacionit Java komunikojnĂ« ndĂ«rmjet vete pĂ«rmes Hazelcast.
  • PĂ«r tĂ« punuar me websockets pĂ«rdorim Netty.
  • Aplikacioni Java Ă«shtĂ« shkruar nĂ« Java 8, pĂ«rbĂ«het nga bunde OSGi. NĂ« planet Ă«shtĂ« migrimi nĂ« Java 10 dhe kalimi nĂ« module.

Zhvillimi dhe testimi

Në procesin e zhvillimit dhe testimit të systemit, ne u përballëm me disa veçori interesante të produkteve që përdorim.

Testim ngarkese dhe rrjedhje memorjeje

Çdo lĂ«shim tĂ« systemit – Ă«shtĂ« njĂ« testim ngarkese. Ai kalon me sukses kur:

  • Testi funksionoi pĂ«r disa ditĂ« dhe nuk kishte ndalesa nĂ« shĂ«rbim
  • Koha e pĂ«rgjigjes pĂ«r operacionet kryesore nuk e kaloi pragun e rehatshĂ«m
  • PĂ«rkeqĂ«simi i performancĂ«s nĂ« krahasim me versionin e mĂ«parshĂ«m nuk Ă«shtĂ« mĂ« shumĂ« se 10%

BashkojmĂ« bazĂ«n testuese me tĂ« dhĂ«na – pĂ«r kĂ«tĂ« marrim nga serveri prodhues informacion mbi abonentin mĂ« aktiv, e shumĂ«zojmĂ« numrat e tij me 5 (numri i mesazheve, diskutimeve, pĂ«rdoruesve) dhe kĂ«shtu testojmĂ«.

Testimin e ngarkesës së sistemit të ndërveprimit e kryejmë në tre konfigurata:

  1. Stres-Test
  2. Vetëm lidhjet
  3. Regjistrimi i abonentëve

Gjatë testit të stresit ne aktivizojmë disa qindra rrjedha, dhe ato vazhdimisht ngarkojnë sistemin: dërgojnë mesazhe, krijojnë diskutime, marrin listën e mesazheve. Imiton veprimet e përdoruesve të zakonshëm (të merrni listën e mesazheve të mia të palexuara, të shkruani dikujt) dhe zgjidhjeve programore (të transferoni një paketë në konfigurata të tjera, të përpunoni njoftimin).

Për shembull, kështu duket një pjesë e stres-testit:

  • NjĂ« pĂ«rdorues hyn nĂ« sistem
    • KĂ«rkon diskutimet e tij tĂ« palexuara
    • Me 50% probabilitet lexon mesazhet
    • Me 50% probabilitet shkruan mesazhe
    • MĂ« pas pĂ«rdoruesi:
      • Me njĂ« probabilitet 20% krijon njĂ« diskutim tĂ« ri
      • Zgjedh rastĂ«sisht cilindo nga diskutimet e tij
      • Hyn brenda
      • KĂ«rkon mesazhe, profile pĂ«rdoruesish
      • Krijon pesĂ« mesazhe, tĂ« adresuara pĂ«rdoruesve rastĂ«sorĂ« nga ky diskutim
      • Dalet nga diskutimi
      • E pĂ«rsĂ«rit 20 herĂ«
      • Del nga sistemi, kthehet sĂ«rish nĂ« fillim tĂ« skenarit

    • NĂ« sistem hyn njĂ« chatbot (simulon shkĂ«mbimin e mesazheve nga kodi i aplikacioneve tĂ« zgjidhjeve)
      • Me njĂ« probabilitet 50% krijon njĂ« kanal tĂ« ri pĂ«r shkĂ«mbim tĂ« tĂ« dhĂ«nave (disktutim tĂ« veçantĂ«)
      • Me njĂ« probabilitet 50% shkruan njĂ« mesazh nĂ« cilindo nga kanalet ekzistuese

Skenari "VetĂ«m lidhje" nuk u shfaq krejt rastĂ«sisht. Ka njĂ« situatĂ«: pĂ«rdoruesit e lidhĂ«n sistemin, por ende nuk janĂ« angazhuar. Çdo pĂ«rdorues nĂ« mĂ«ngjes nĂ« 09:00 ndez kompjuterin, vendos lidhjen me serverin dhe hesht. KĂ«ta djem janĂ« tĂ« rrezikshĂ«m, janĂ« shumĂ« – nga paketat e tyre kanĂ« vetĂ«m PING/PONG, por mbajnĂ« lidhjen me serverin (nuk mund ta ndalin – ndoshta vjen njĂ« mesazh i ri). Testi riprodhon situatĂ«n kur brenda gjysmĂ« ore njĂ« numĂ«r i madh i kĂ«tyre pĂ«rdoruesve pĂ«rpiqen tĂ« regjistrohen nĂ« sistem. ËshtĂ« i ngjashĂ«m me njĂ« test stresi, por fokusi i tij Ă«shtĂ« pikĂ«risht nĂ« kĂ«tĂ« hyrje tĂ« parĂ« – pĂ«r tĂ« shmangur dĂ«shtimet (njĂ« person nuk e pĂ«rdor sistemin, por ai tashmĂ« ka filluar tĂ« bie – e vĂ«shtirĂ« tĂ« mendosh ndonjĂ« gjĂ« mĂ« keq).

Skenari i regjistrimit tĂ« abonentĂ«ve fillon me fillimin e parĂ«. Ne kemi kryer njĂ« test stresi dhe ishim tĂ« sigurt se nĂ« biseda sistemi nuk ngadalĂ«sohej. Por pĂ«rdoruesit erdhĂ«n dhe regjistrimi filloi tĂ« dĂ«shton pĂ«r shkak tĂ« kohĂ«ve tĂ« skadimit. GjatĂ« regjistrimit ne pĂ«rdorĂ«m /dev/random, i cili Ă«shtĂ« i lidhur me entropinĂ« e sistemit. Serveri nuk arriti tĂ« mbledhĂ« mjaft entropi dhe gjatĂ« kĂ«rkesĂ«s pĂ«r SecureRandom tĂ« ri ngeli pĂ«r disa dhjetĂ«ra sekonda. Ka shumĂ« zgjidhje pĂ«r kĂ«tĂ« situatĂ«, pĂ«r shembull: tĂ« kalosh nĂ« njĂ« mĂ« pak tĂ« sigurt /dev/urandom, tĂ« vendosĂ«sh njĂ« pllakĂ« speciale qĂ« gjeneron entropi, tĂ« gjenerosh numra rastĂ«sishĂ«m paraprakisht dhe t’i ruash nĂ« njĂ« rezervĂ«. Ne pĂ«rkohĂ«sisht e mbyllĂ«m problemin me njĂ« rezervĂ«, por qĂ« atĂ«herĂ« po ekzekutojmĂ« njĂ« test tĂ« veçantĂ« pĂ«r regjistrimin e abonentĂ«ve tĂ« rinj.

Si gjenerues ngarkese përdorim JMeter. Nuk di të punojë me websockets, është nevojshëm një plugin. Të parët në rezultatet e kërkimit për kërkesën "jmeter websocket" janë artikujt nga BlazeMeter, në të cilat rekomandohet plugin nga Maciej Zaleski.

Prej tij vendosëm të fillojmë.

Pikërisht pasi filloi testimi i rëndë, ne zbuluam se në JMeter filluan rrjedhjet e memories.

Plugini është një histori e veçantë, me 176 yje ka 132 fork në GitHub. Autori nuk ka bërë komente prej vitit 2015 (ne e morëm atë në vitin 2015, atëherë kjo nuk ngjalli dyshime), disa probleme në GitHub rreth rrjedhjeve të memories, 7 kërkesa të hapura përr këtu.
Nëse vendosni të zhvilloni testim stresi me këtë plugin, kushtoni vëmendje diskutimeve të mëposhtme:

  1. Në një mjedis me shumë procese, është përdorur një LinkedList i zakonshëm, duke rezultuar në NPE në runtime. Zgjidhet ose duke kaluar në ConcurrentLinkedDeque, ose me blloqe synchronized. Ne zgjodhëm opsionin e parë (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. Rrjedhje e memories, informacioni mbi lidhjen nuk hiqet kur bëhet disconnection (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. Në modin streaming (kur websocket nuk mbyllet në fund të mostrës, por përdoret më tej në plan), pattern-et e përgjigjes nuk funksionojnë (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

Kjo Ă«shtĂ« nga ato qĂ« janĂ« nĂ« GitHub. ÇfarĂ« bĂ«mĂ«:

  1. MorrĂ«m fork-un Elyran Kogan (@elyrank) – nĂ« tĂ« janĂ« rregulluar problemet 1 dhe 3
  2. Zgjidhëm problemin 2
  3. Përditësuam jetty nga 9.2.14 në 9.3.12
  4. E mbyllëm SimpleDateFormat në ThreadLocal; SimpleDateFormat nuk është thread-safe, gjë që çonte në NPE në runtime
  5. Eliminuan edhe një rrjedhje të memories (lidhja nuk u mbyll si duhet gjatë disconnection)

Dhe megjithatë, ai vazhdon të rrjedhë!

Memoria nuk përfundonte më pas një dite, por pas dy ditësh. Koha ishte shumë e limituar, vendosëm të nisnim më pak procese, por në katër agjentë. Kjo duhej të mjaftonte, të paktën për një javë.

Kaluar dy ditë 

Tani memoria po pĂ«rfundonte edhe me Hazelcast. NĂ« log ishte e dukshme se pas disa ditĂ«sh testimi, Hazelcast fillonte tĂ« ankohej pĂ«r mungesĂ« memorjeje, dhe pasi kalonte ca kohĂ«, klasteri shpartallohej dhe nodet vazhdonin tĂ« vdisnin njĂ« pas njĂ«. Ne lidhem JVisualVM me hazelcast dhe pamĂ« njĂ« "rrafshim nĂ« rritje" – ai rregullisht thĂ«rriste GC, por nuk mund tĂ« pastrohej memoria.

Si dhe përse e shkruam një shërbim të shkallëzuar me ngarkesë të lartë për 1C: Ndërmarrja: Java, PostgreSQL, Hazelcast

Doli se në hazelcast 3.4, kur fshije map / multiMap (map.destroy()) memoria nuk lirohej plotësisht:

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

Tani gabimi është rregulluar në 3.5, por atëherë kjo ishte një problem. Ne krijonim multiMap të reja me emra dinamikë dhe i fshihnim sipas logjikës tonë. Kodi dukej diçka i tillë:

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

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

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

Thirrja:

service.join(auth1, "NJOFTIMET_E_REJA_NË_DISKUTIM_UUID1");
service.join(auth2, "NJOFTIMET_E_REJA_NË_DISKUTIM_UUID1");

multiMap ishte krijuar për çdo abonim dhe është eliminuar kur nuk ishte më e nevojshme. Vendosëm që do të bëjmë një Map, me emrin e abonimit si çelës dhe me identifikuesit e sesioneve si vlera (nëpërmjet të cilave mund të shohim identifikuesit e përdoruesve, 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());
}

Grafikat janë stabilizuar.

Si dhe përse e shkruam një shërbim të shkallëzuar me ngarkesë të lartë për 1C: Ndërmarrja: Java, PostgreSQL, Hazelcast

ÇfarĂ« tjetĂ«r mĂ«suam pĂ«r testimin e ngarkesĂ«s

  1. JSR223 duhet shkruar nĂ« groovy dhe tĂ« pĂ«rfshijĂ« compilation cache – kjo Ă«shtĂ« shumĂ« mĂ« e shpejtĂ«. Lidhja.
  2. Grafikat e Jmeter-Plugins janë më të lehta për t'u kuptuar sesa ato standarde. Lidhja.

Për përvojën tonë me Hazelcast

Hazelcast ishte një produkt i ri për ne, filluam të punojmë me të nga versioni 3.4.1, tani në serverin tonë production është instaluar versioni 3.9.2 (në momentin e shkruarjes së artikelit, versioni më i fundit i Hazelcast është 3.10).

Gjenerimi i ID

Filluam me identifikues numĂ«rorĂ«. Le tĂ« imagjinojmĂ« se na nevojitet njĂ« Long i ri pĂ«r njĂ« entitet tĂ« ri. Sekuenca nĂ« DB nuk Ă«shtĂ« e pĂ«rshtatshme, tabelat marrin pjesĂ« nĂ« sharding – do tĂ« ndodhte qĂ« tĂ« ketĂ« mesazh ID=1 nĂ« DB1 dhe mesazh ID=1 nĂ« DB2, nĂ« Elasticsearch nuk mund ta vendosĂ«sh njĂ« ID tĂ« tillĂ«, as nĂ« Hazelcast, por mĂ« e keqja, nĂ«se dĂ«shiron tĂ« bashkosh tĂ« dhĂ«nat nga dy DB nĂ« njĂ« (p.sh., duke vendosur se njĂ« DB Ă«shtĂ« e mjaftueshme pĂ«r kĂ«ta abonen), mund tĂ« krijosh disa AtomicLong nĂ« Hazelcast dhe tĂ« mbash njĂ« numĂ«rues atje, atĂ«herĂ« performanca e marrjes sĂ« njĂ« ID tĂ« ri Ă«shtĂ« incrementAndGet plus koha pĂ«r tĂ« bĂ«rĂ« kĂ«rkesĂ« nĂ« Hazelcast. Por nĂ« Hazelcast ka diçka mĂ« tĂ« optimizuar – FlakeIdGenerator. Çdo klienti, kur bĂ«n njĂ« kĂ«rkesĂ«, iu jepet njĂ« interval ID-sh, p.sh., tĂ« parit – nga 1 nĂ« 10 000, tĂ« dytit – nga 10 001 nĂ« 20 000 dhe kĂ«shtu me radhĂ«. Tani klienti mund tĂ« gjenerojĂ« identifikues tĂ« rinj vetĂ«, derisa t'i skadojĂ« intervali i dhĂ«nĂ«. Punon shpejt, por kur aplikacioni riniset (dhe klienti Hazelcast) fillon njĂ« sekuencĂ« e re – prej andej ndodhin mungesat etj. Gjithashtu, zhvilluesit nuk e kuptojnĂ« shumĂ« mirĂ« pse ID-tĂ« janĂ« numĂ«rorĂ«, por vijnĂ« aq shumĂ« tĂ« çrregullt. E mbaruam por edhe vendosĂ«m tĂ« kalojmĂ« nĂ« UUID.

A propos, pour ceux qui veulent ressembler Ă  Twitter, il existe une bibliothĂšque Snowcast – une implĂ©mentation de Snowflake sur Hazelcast. Vous pouvez la consulter ici :

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

Mais nous n'avons déjà pas eu le temps de l'utiliser.

TransactionalMap.replace

Une autre surprise : TransactionalMap.replace ne fonctionne pas. Voici un 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();
        }
    });
}

Attendu : newValue
Réel : oldValue

Nous avons dû écrire notre propre replace, en utilisant getForUpdate :

protected  boolean replaceInMap(String mapName, K key, V oldValue, V newValue) {
    TransactionalTaskContext context = HazelcastTransactionContextHolder.getContext();
    if (context != null) {
        log.trace("[CACHE] Remplacement de valeur dans une carte transactionnelle");
        TransactionalMap map = context.getMap(mapName);
        V value = map.getForUpdate(key);
        if (oldValue.equals(value)) {
            map.put(key, newValue);
            return true;
        }

        return false;
    }
    log.trace("[CACHE] Remplacement de valeur dans une carte non transactionnelle");
    IMap map = hazelcastInstance.getMap(mapName);
    return map.replace(key, oldValue, newValue);
}

Testez non seulement les structures de données ordinaires, mais aussi leurs versions transactionnelles. Parfois, IMap fonctionne, mais TransactionalMap ne fonctionne pas.

DĂ©ployer un nouveau JAR sans temps d'arrĂȘt

Au début, nous avons décidé d'enregistrer dans Hazelcast des objets de nos classes. Par exemple, nous avons une classe Application, nous voulons la sauvegarder et la lire. Sauvegardons :

IMap map = hazelcastInstance.getMap("application");
map.set(id, application);

Nous lisons :

IMap map = hazelcastInstance.getMap("application");
return map.get(id);

Tout fonctionne. Puis nous avons décidé de créer un index dans Hazelcast pour rechercher à son sujet :

map.addIndex("subscriberId", false);

Et en Ă©crivant une nouvelle entitĂ©, nous avons commencĂ© Ă  recevoir ClassNotFoundException. Hazelcast essayait de complĂ©ter l'index, mais ne savait rien sur notre classe et voulait qu'on lui fournisse le JAR avec cette classe. Nous l'avons fait, tout a fonctionnĂ©, mais un nouveau problĂšme est apparu : comment mettre Ă  jour le JAR sans arrĂȘter complĂštement le cluster ? Hazelcast ne prend pas en compte le nouveau JAR lors de la mise Ă  jour Ă  la volĂ©e. À ce moment-lĂ , nous avons dĂ©cidĂ© que nous pouvions vivre sans recherche par index. En effet, si nous utilisons Hazelcast comme un stockage clĂ©-valeur, tout fonctionne-t-il ? Pas tout Ă  fait. Ici, il y a encore un comportement diffĂ©rent entre IMap et TransactionalMap. LĂ  oĂč IMap n'a pas d'importance, TransactionalMap renvoie une erreur.

IMap. Regjistrojmë 5000 objekte, lexojmë. Gjithçka është siç pritej.

@Test
void get5000() {
    IMap 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());
    }
}

Dhe në transaksion nuk funksionon, marrim ClassNotFoundException:

@Test
void get_transaction() {
    IMap 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 transactionalMap = context.getMap("application_t");
            Application retrievedInside = transactionalMap.get(id);

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

Në versionin 3.8 u prezantua mekanizmi i Dislokimit të Klaseve të Përdoruesit. Ju mund të caktoni një nodë kryesore dhe të përditësoni skedarin JAR në të.

Tani ne e kemi ndryshuar plotësisht qasjen: ne vetë serializojmë në JSON dhe e ruajmë në Hazelcast. Hazelcast-i nuk ka nevojë të dijë strukturën e klasave tona, ndërsa ne mund të përditësohemi pa ndonjë ndalim. Menaxhimi i versionimit të objekteve të domenit bëhet nga aplikacioni. Njëkohësisht, mund të jenë në funksionim versione të ndryshme të aplikacionit, dhe mund të ndodhë që aplikacioni i ri të shkruajë objekte me fusha të reja, ndërsa ai i vjetër ende nuk i njeh këto fusha. Po ashtu, aplikacioni i ri lexon objekte, të shkruara nga aplikacioni i vjetër, të cilat nuk kanë fusha të reja. Këto situata i trajtojmë brenda aplikacionit, por për thjeshtësi nuk i ndryshojmë dhe nuk i fshijmë fushat, thjesht i zgjeruam klasat duke shtuar fusha të reja.

Si sigurojmë performancë të lartë

KatĂ«r vizita nĂ« Hazelcast – mirĂ«, dy nĂ« DB – keq

Tërheqja e të dhënave nga cache është gjithmonë më e mirë sesa nga BD, por gjithashtu nuk dua të ruaj informacionet e pavlefshme. Vendimi se çfarë të cache-ojmë e shtyjmë në fazën e fundit të zhvillimit. Kur funksionaliteti i ri është koduar, ne aktivizojmë logimin e të gjitha kërkesave në PostgreSQL (log_min_duration_statement në 0) dhe fillojmë testimin e ngarkesës për rreth 20 minuta. Sipas log-eve të mbledhura, utilitetet si pgFouine dhe pgBadger dinë të ndërtoshin raporte analitike. Në raporte, në radhë të parë, kërkojmë kërkesa të ngadalta dhe të shpeshta. Për kërkesat e ngadalta krijojmë një plan ekzekutimi (EXPLAIN) dhe vlerësojmë nëse një kërkesë e tillë mund të shpejtohet. Kërkesat e shpeshta me të njëjtat të dhëna hyrëse futen mirë në cache. Mundoheni të mbani kërkesat "të sheshta", me një tabelë në çdo kërkesë.

Eksplorimi

SV si një shërbim online u lançua në pranverën e vitit 2017, ndërsa si një produkt i veçantë, SV doli në nëntor 2017 (në atë kohë në statusin beta).

Gjatë më shumë se një viti në përdorim, nuk ka pasur probleme serioze me funksionimin e shërbimit online SV. Ne monitorojmë shërbimin online përmes Zabbix, mbledhim dhe deployed nga Bamboo.

Distribucioni i serverit SV ofrohet në formën e pakove nativ: RPM, DEB, MSI. Për më tepër, për Windows ne ofrojmë një instalues të vetëm në formën e një EXE, i cili instalon serverin, Hazelcast dhe Elasticsearch në një makinë. Fillimisht e quajtëm këtë version instalimi "demonstrativ", por tani është e qartë se ky është varianti më i popullarizuar i shpërndarjes.

Burimi: habr.com

Blini hosting tĂ« besueshĂ«m pĂ«r faqe interneti me mbrojtje nga DDoS, serverĂ« VPS VDS đŸ”„ Blini hosting tĂ« besueshĂ«m pĂ«r faqe interneti me mbrojtje nga DDoS, serverĂ« VPS VDS | ProHoster