Hur och varför vi skrev en högbelastad skalbar tjÀnst för 1C: Enterprise: Java, PostgreSQL, Hazelcast

I den hĂ€r artikeln berĂ€ttar vi om hur och varför vi utvecklade Interaktionssystemet – en mekanism som överför information mellan klientapplikationer och 1C: Enterprise servrar – frĂ„n att stĂ€lla upp uppgiften till att tĂ€nka pĂ„ arkitekturen och detaljerna i genomförandet.

Interaktionssystemet (nedan – IS) – Ă€r ett distribuerat, felresistent meddelandebytesystem med garanterad leverans. IS Ă€r utformat som en högbelastad tjĂ€nst med hög skalbarhet, tillgĂ€nglig bĂ„de som en online-tjĂ€nst (tillhandahĂ„lls av företaget 1C) och som en licensierad produkt som kan implementeras pĂ„ egna servrar.

IS anvÀnder ett distribuerat lagringssystem Hazelcast och söksystemet Elasticsearch. Det kommer ocksÄ att handla om Java och hur vi horisontellt skalade PostgreSQL.
Hur och varför vi skrev en högbelastad skalbar tjÀnst för 1C: Enterprise: Java, PostgreSQL, Hazelcast

UppgiftsstÀllande

För att förstÄ varför vi gjorde Interaktionssystemet, ska jag berÀtta lite om hur utvecklingen av affÀrsapplikationer i 1C Àr organiserad.

Först – lite om oss för dem som inte kĂ€nner till vad vi gör :) Vi bygger en teknologisk plattform "1C: Enterprise". Plattformen innehĂ„ller ett utvecklingsverktyg för affĂ€rsapplikationer, samt en runtime som gör att affĂ€rsapplikationer kan köras i en plattformsoberoende miljö.

Klient-server-paradigmen för utveckling

AffĂ€rsapplikationer skapade med "1C: Enterprise" fungerar i ett tre-lags klient-server arkitektur "DBMS – applikationsserver – klient". Applikationskoden som skrivits i det inbyggda 1C-sprĂ„ket, kan köras pĂ„ applikationsservern eller pĂ„ klienten. Allt arbete med applikationsobjekt (register, dokument etc.), samt lĂ€sning och skrivning till databasen, utförs endast pĂ„ servern. Funktionaliteten för formulĂ€r och kommandogrĂ€nssnitt Ă€r ocksĂ„ implementerad pĂ„ servern. PĂ„ klienten utförs hĂ€mtning, öppning och visning av formulĂ€r, "kommunikation" med anvĂ€ndaren (varningar, frĂ„gor...), smĂ„ berĂ€kningar i formulĂ€r som krĂ€ver snabb respons (t.ex. multiplikation av pris med antal), arbete med lokala filer, arbete med hĂ„rdvara.

I den praktiska koden för procedurer och funktioner mĂ„ste man tydligt ange var koden kommer att köras - med hjĂ€lp av direktiven &ĐĐ°ĐšĐ»ĐžĐ”ĐœŃ‚Đ” / &ĐĐ°ĐĄĐ”Ń€ĐČДрД (&AtClient / &AtServer i den engelska versionen av sprĂ„ket). Utvecklare inom 1C kommer nu att rĂ€tta mig genom att sĂ€ga att direktiven egentligen större, men för oss Ă€r det för nĂ€rvarande inte av betydelse.

FrÄn klientkoden kan man anropa serverkoden, men frÄn serverkoden kan man inte anropa klientkoden. Detta Àr en grundlÀggande begrÀnsning som vi har infört av flera skÀl. Bland annat för att serverkoden mÄste skrivas sÄ att den fungerar pÄ samma sÀtt oavsett varifrÄn den anropas - frÄn klienten eller servern. Vid ett anrop av serverkoden frÄn en annan serverkod finns ingen klient. Dessutom kan klienten som gjorde anropet ha stÀngts ner eller lÀmnat applikationen under exekveringen av serverkoden, och servern har ingen att anropa.

Hur och varför vi skrev en högbelastad skalbar tjÀnst för 1C: Enterprise: Java, PostgreSQL, Hazelcast
Koden som hanterar knapptryckning: anrop av serverprocedur frÄn klienten kommer att fungera, anrop av klientprocedur frÄn servern - nej.

Detta innebÀr att om vi frÄn servern vill överföra ett meddelande till klientapplikationen, exempelvis att skapandet av en "lÄngvarig" rapport Àr klart och rapporten kan visas - finns det inget sÀtt att göra det. Vi tvingas att vidta ÄtgÀrder, som att regelbundet frÄga servern frÄn klientkoden. Men detta tillvÀgagÄngssÀtt belastar systemet med onödiga anrop och ser överlag inte sÀrskilt stiligt ut.

Det finns dessutom behov av att vid ett inkommande telefonsamtal SIP-informera den klientapplikationen om detta, sÄ den kan hitta den ringande personen i databas och visa informationen om den ringande kontakten för anvÀndaren. Eller, till exempel, vid ankomst av en bestÀllning till lagret informera bestÀllarens klientapplikation om detta. Det finns mÄnga scenarier dÀr en sÄdan mekanism skulle vara anvÀndbar.

Faktiskt sÀtta upp

Skapa en mekanism för meddelandeutbyte. Snabb, pÄlitlig, med garanterad leverans, med möjlighet till flexibel sökning av meddelanden. Baserat pÄ mekanismen implementera en meddelandetjÀnst (meddelanden, videosamtal) som fungerar inom 1C-applikationer.

Designa system för horisontell skalning. Ökande belastning ska hanteras genom att öka antalet noder.

Implementering

Vi har beslutat att inte integrera serverdelen av SV direkt i 1C:Enterprise-plattformen, utan istÀllet implementera den som en separat produkt vars API kan anropas frÄn koden i applikationslösningarna för 1C. Detta gjordes av flera skÀl, dÀr det frÀmsta Àr att vi ville möjliggöra meddelandeutbyte mellan olika 1C-applikationer (till exempel mellan Handelshantering och Bokföring). Olika 1C-applikationer kan köras pÄ olika versioner av 1C:Enterprise-plattformen, vara placerade pÄ olika servrar, etc. Under sÄdana omstÀndigheter Àr implementeringen av SV som en separat produkt, placerad "vid sidan om" 1C-installationerna, den optimala lösningen.

SĂ„ vi har beslutet att göra SV som en separat produkt. För smĂ„ företag rekommenderar vi att anvĂ€nda SV-servern som vi har installerat i vĂ„r molntjĂ€nst (wss://1cdialog.com) för att undvika kostnader relaterade till lokal installation och konfiguration av servern. Större kunder kan dock tycka att det Ă€r rimligt att installera sin egen SV-server pĂ„ sina egna resurser. En liknande strategi har vi anvĂ€nt i vĂ„r molntjĂ€nst SaaS-produkt. 1cFresh – det slĂ€pps som en serieprodukt för installation hos kunder, och Ă€r ocksĂ„ distribuerat i vĂ„rt moln. https://1cfresh.com/.

Applikation

För att fördela belastningen och sĂ€kerstĂ€lla driftsĂ€kerhet kommer vi att distribuera flera Java-applikationer istĂ€llet för en, och sĂ€tta en belastningsutjĂ€mnare framför dem. Om ett meddelande behöver överföras frĂ„n nod till nod – anvĂ€nder vi publish/subscribe i Hazelcast.

Kommunikationen mellan klienten och servern sker via websocket. Det passar bra för realtidsystem.

Distribuerad cache

Vi valde mellan Redis, Hazelcast och Ehcache. Året Ă€r 2015. Redis har just slĂ€ppt en ny klusterlösning (för ny, kĂ€nns osĂ€kert), det finns Sentinel med mĂ„nga begrĂ€nsningar. Ehcache kan inte sĂ€ttas ihop i ett kluster (denna funktionalitet kom senare). Vi bestĂ€mde oss för att prova med Hazelcast 3.4.
Hazelcast kan sĂ€ttas samman i ett kluster "out-of-the-box". I en envĂ€gsnod Ă€r det inte sĂ„ nyttigt och kan bara anvĂ€ndas som cache – den kan inte spara data till disk, och om vi förlorar den enda noden, förlorar vi data. Vi distribuerar flera Hazelcast-instanser, mellan vilka vi sĂ€kerhetskopierar kritiska data. Cachen sĂ€kerhetskopieras inte – vi Ă€r inte rĂ€dda om den.

För oss Àr Hazelcast:

  • Ett lager för anvĂ€ndarsessioner. Att varje gĂ„ng hĂ€mta sessionen frĂ„n databasen tar för lĂ„ng tid, sĂ„ vi placerar alla sessioner i Hazelcast.
  • Cache. Letar du efter en anvĂ€ndarprofil - kolla i cachen. Skrev du ett nytt meddelande - spara i cachen.
  • Ämnen för kommunikation mellan applikationsinstanser. Noden genererar ett hĂ€ndelse och placerar den i Hazelcast-Ă€mnet. Andra noder i applikationen som prenumererar pĂ„ detta Ă€mne fĂ„r och bearbetar hĂ€ndelsen.
  • Klustersperrningar. Till exempel skapar vi en diskussion med en unik nyckel (singleton-diskussion inom 1C-databasen):

conversationKeyChecker.check("BENSINSTATION");

      doInClusterLock("BENSINSTATION", () -> {

          conversationKeyChecker.check("BENSINSTATION");

          createChannel("BENSINSTATION");
      });

Vi kontrollerade att kanalen inte finns. Tog en lÄs, kontrollerade igen, skapade. Om vi inte kontrollerar efter att vi har tagit lÄsen finns det en risk att en annan trÄd ocksÄ kontrollerade och nu försöker skapa samma diskussion - och den finns redan. Det gÄr inte att lÄsa med synchronized eller vanliga java Lock. Genom databasen - lÄngsamt, och det Àr synd om databasen, genom Hazelcast - precis vad vi behöver.

VĂ€ljer databashanterare

Vi har stor och framgÄngsrik erfarenhet av att arbeta med PostgreSQL och samarbeta med utvecklarna av denna databas.

Att arbeta med kluster i PostgreSQL Àr inte enkelt - det finns XL, XC, Citus, men i allmÀnhet Àr detta inte noSQL, som skalar ur lÄdan. Vi övervÀgde inte NoSQL som huvudsaklig lagring, vi har redan beslutat att anvÀnda Hazelcast, som vi inte arbetat med tidigare.

Eftersom vi behöver skala den relationella databasen - innebÀr det sharding. Som ni vet delar vi upp databasen i separata delar sÄ att varje del kan flyttas till en separat server.

Det första alternativet för vÄr sharding förutsatte att vi skulle kunna sprida varje tabell i vÄr applikation över olika servrar i olika proportioner. Mycket information pÄ server A - lÄt oss flytta en del av denna tabell till server B. Denna lösning skrek verkligen av för tidig optimering, sÄ vi beslutade att hÄlla oss till en multi-tenant-ansats.

För att lÀsa om multi-tenant kan man till exempel besöka webbplatsen Citus Data.

I SV finns begreppen applikation och abonnent. En applikation Àr en specifik installation av ett affÀrsprogram, till exempel ERP eller Redovisning, med sina anvÀndare och affÀrsdata. En abonnent Àr en organisation eller en fysisk person som registrerar applikationen pÄ SV-servern. En abonnent kan ha flera registrerade applikationer, och dessa applikationer kan utbyta meddelanden med varandra. Abonnenten har blivit en hyresgÀst (tenant) i vÄrt system. Meddelanden frÄn flera abonnenter kan finnas i en och samma fysiska databas; om vi ser att en abonnent genererar mycket trafik flyttar vi denne till en separat fysisk databas (eller till och med en separat databasserver).

Vi har en huvuddatabas som lagrar en routingtabell med information om platsen för alla abonnentdatabaser.

Hur och varför vi skrev en högbelastad skalbar tjÀnst för 1C: Enterprise: Java, PostgreSQL, Hazelcast

För att huvuddatabasen inte ska bli en flaskhals hÄller vi routingtabellen (och andra ofta efterfrÄgade data) i cache.

Om abonnentens databas börjar gÄ lÄngsamt kommer vi att dela upp den i partitioner. För partitionering av stora tabeller i andra projekt anvÀnder vi pg_pathman.

Eftersom det Àr oacceptabelt att förlora anvÀndarmeddelanden, upprÀtthÄller vi vÄra databaser med repliker. Kombinationen av synkrona och asynkrona repliker gör att vi kan skydda oss mot förlust av huvuddatabasen. Förlust av meddelanden intrÀffar endast vid samtidig fel pÄ huvuddatabasen och dess synkrona replik.

Om den synkrona repliken förloras blir den asynkrona repliken synkron.
Om huvuddatabasen förloras blir den synkrona repliken huvuddatabas, och den asynkrona repliken blir synkron replik.

Elasticsearch för sökning

Eftersom SV, förutom andra saker, ocksĂ„ Ă€r en meddelandeplattform, behövs en snabb, anvĂ€ndarvĂ€nlig och flexibel sökning som tar hĂ€nsyn till morfologi och ofullstĂ€ndiga matchningar. Vi beslutade oss för att inte uppfinna hjulet pĂ„ nytt utan anvĂ€nde det fria söksystemet Elasticsearch, baserat pĂ„ biblioteket Lucene. Vi distribuerar Ă€ven Elasticsearch i en klusterkonfiguration (master – data – data) för att utesluta problem om applikationsnoder gĂ„r ner.

PÄ github hittade vi ett plugin för rysk morfologi för Elasticsearch och anvÀnder det. I Elasticsearch-indexet lagrar vi rotord (som definieras av pluginet) och N-gram. NÀr anvÀndaren skriver in text för sökning letar vi efter den skrivna texten bland N-gram. NÀr vi sparar ordet "texter" bryts det ner i följande N-gram:

[de, tek, teks, text, texter, ek, eks, ekst, ekst-, k, kst, ksty, st, sty, ty],

Och Àven roten av ordet "text" kommer att sparas. Denna metod gör det möjligt att söka frÄn början, mitt i och i slutet av ord.

Övergripande bild

Hur och varför vi skrev en högbelastad skalbar tjÀnst för 1C: Enterprise: Java, PostgreSQL, Hazelcast
Upprepning av bilden frÄn början av artikeln, men med förklaringar:

  • Lastbalanseraren, som Ă€r stĂ€lld i internet; hos oss – nginx, kan vara vilken som helst.
  • Instanser av Java-applikationen kommunicerar med varandra via Hazelcast.
  • För arbete med webbsocket anvĂ€nder vi Netty.
  • Java-applikationen Ă€r skriven i Java 8, bestĂ„r av paket OSGi. Planen Ă€r att migrera till Java 10 och gĂ„ över till moduler.

Utveckling och testning

Under utvecklings- och testprocessen av DB stötte vi pÄ flera intressanta funktioner av produkterna som vi anvÀnde.

Belastningstestning och minneslÀckor

Utgivning av varje version av DB inkluderar belastningstestning. Det har genomförts framgÄngsrikt nÀr:

  • Testet kördes i flera dagar utan serviceavbrott
  • Svars-tiden för nyckeloperationer översteg inte den bekvĂ€ma grĂ€nsen
  • PrestandaförsĂ€mringen i förhĂ„llande till föregĂ„ende version Ă€r högst 10%

Vi fyller testbasen med data – för detta fĂ„r vi frĂ„n produktionsservern information om den mest aktiva abonnenten, multiplicerar deras siffror med 5 (antal meddelanden, diskussioner, anvĂ€ndare) och testar sĂ„.

Belastningstestning av interaktionssystemet utför vi i tre konfigurationer:

  1. Stress-test
  2. Endast anslutningar
  3. Registrering av abonnenter

Vid stress-testet startar vi flera hundra trÄdar som utan avbrott belastar systemet: skriver meddelanden, skapar diskussioner, fÄr listan över meddelanden. Vi simulerar agerandet av vanliga anvÀndare (fÄ min olÀsta meddelandelista, skriva till nÄgon) och programlösningar (överföra ett paket med en annan konfiguration, behandla en avisering).

Till exempel ser en del av stress-testet ut sÄ hÀr:

  • En anvĂ€ndare loggar in i systemet
    • BegĂ€r sina olĂ€sta diskussioner
    • Med 50% sannolikhet lĂ€ser meddelanden
    • Med 50% sannolikhet skriver meddelanden
    • DĂ€refter anvĂ€ndaren:
      • Skapar en ny diskussion med 20% sannolikhet
      • VĂ€ljer slumpmĂ€ssigt ett av sina diskussioner
      • GĂ„r in
      • BegĂ€r meddelanden, anvĂ€ndarprofiler
      • Skapar fem meddelanden riktade till slumpmĂ€ssiga anvĂ€ndare i denna diskussion
      • LĂ€mnar diskussionen
      • Upprepar 20 gĂ„nger
      • Loggar ut och Ă„tervĂ€nder till början av scenariot

    • En chattbot loggar in i systemet (emulerar meddelandeutbyte frĂ„n applikationslösningens kod)
      • Skapar en ny kanal för datakommunikation med 50% sannolikhet (specialdiskussion)
      • Skriver ett meddelande i vilken som helst av de befintliga kanalerna med 50% sannolikhet

Scenariot "Bara anslutningar" dök inte upp av en slump. Det finns situationer: anvĂ€ndare har anslutit systemet, men har inte kommit igĂ„ng. Varje anvĂ€ndare sĂ€tter pĂ„ sin dator klockan 09:00 pĂ„ morgonen, ansluter till servern och Ă€r tyst. Dessa personer Ă€r farliga, de Ă€r mĂ„nga – i paketen finns bara PING/PONG, men de hĂ„ller anslutningen till servern (de kan inte slĂ€ppa den – vad hĂ€nder om det kommer ett nytt meddelande). Testet reproducerar en situation dĂ€r ett stort antal sĂ„dana anvĂ€ndare försöker logga in i systemet inom en halvtimme. Det liknar ett stresstest, men fokuset ligger pĂ„ just det första inloggningen – för att unvika avvisningar (personen anvĂ€nder inte systemet, men det slĂ€pper redan – svĂ„rt att tĂ€nka pĂ„ nĂ„got vĂ€rre).

Scenariot för registrering av abonnenter börjar med den första starten. Vi genomförde ett stresstest och var sÀkra pÄ att systemet inte hÀngde i kommunikationen. Men anvÀndarna kom och registreringen började falla pÄ grund av timeout. Vid registreringen anvÀnde vi /dev/random, vilket Àr kopplat till systemets entropi. Servern hann inte samla tillrÀckligt med entropi och nÀr den begÀrde en ny SecureRandom stannade den i flera sekunder. Det finns mÄnga sÀtt att lösa detta problem, till exempel: gÄ över till den mindre sÀkra /dev/urandom, installera ett sÀrskilt kort som genererar entropi, generera slumpmÀssiga nummer i förvÀg och lagra i poolen. Vi stÀngde tillfÀlligt problemet med en pool, men har sedan dess kört ett separat test för registrering av nya abonnenter.

Som belastningsgenerator anvÀnder vi JMeter. Det kan inte arbeta med WebSocket, en plugin behövs. Första sökresultatet pÄ "jmeter websocket" Àr artiklar frÄn BlazeMeter, dÀr de rekommenderar plugin frÄn Maciej Zaleski.

Det var dÀr vi bestÀmde oss för att börja.

NÀstan direkt efter att vi började med seriös testning upptÀckte vi att det började förekomma minneslÀckor i JMeter.

Pluginet Àr en stor historia i sig, med 176 stjÀrnor har det 132 fork pÄ github. SjÀlv författaren har inte committat i det sedan 2015 (vi tog det Är 2015, dÄ vÀckte det ingen misstanke), nÄgra github-Àrenden angÄende minneslÀckor, 7 öppna pull requests.
Om ni beslutar er för att köra belastningstestning med detta plugin, var uppmÀrksamma pÄ följande diskussioner:

  1. I en flertrÄdad miljö anvÀndes en vanlig LinkedList, vilket resulterade i NPE i runtime. Detta löses antingen genom att byta till ConcurrentLinkedDeque eller med synchronized-block. Vi valde den första varianten (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. MinneslÀcka, vid frÄnkoppling raderas inte information om anslutningen (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. I streaminglÀge (nÀr websocket inte stÀngs i slutet av provet utan anvÀnds vidare i planen) fungerar inte Response-mönster (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

Detta Àr frÄn de som finns pÄ github. Vad vi gjorde:

  1. Vi tog forken av Elyran Kogan (@elyrank) – den har fixat problem 1 och 3
  2. Vi löste problem 2
  3. Uppdaterade jetty frÄn 9.2.14 till 9.3.12
  4. Vi omsvepte SimpleDateFormat i ThreadLocal; SimpleDateFormat Àr inte trÄdsÀker, vilket ledde till NPE i runtime.
  5. Vi ÄtgÀrdade ytterligare en minneslÀcka (anslutningen stÀngdes inte korrekt vid frÄnkoppling)

Och ÀndÄ lÀcker den!

Minnet tog slut inte pÄ en dag utan pÄ tvÄ. Det fanns inte tid kvar, sÄ vi beslutade att köra fÀrre trÄdar, men pÄ fyra agenter. Det borde ha rÀckt, Ätminstone, en vecka.

TvÄ dagar har gÄtt...

Nu började minnet ta slut hos Hazelcast. I loggarna syntes det att efter ett par dagars testning började Hazelcast klaga pĂ„ brist pĂ„ minne, och en stund senare kollapsade klustret, och noder fortsatte att dö en och en. Vi kopplade JVisualVM till hazelcast och sĂ„g en "uppĂ„tgĂ„ende sĂ„g" – den kallade regelbundet GC men kunde inte frigöra minnet.

Hur och varför vi skrev en högbelastad skalbar tjÀnst för 1C: Enterprise: Java, PostgreSQL, Hazelcast

Det visade sig att i hazelcast 3.4 frigörs inte minnet helt vid borttagning av map / multiMap (map.destroy()):

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

Nu Àr felet ÄtgÀrdat i 3.5, men dÄ var det ett problem. Vi skapade nya multiMap med dynamiska namn och tog bort dem enligt vÄr logik. Koden sÄg ungefÀr ut sÄ hÀr:

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

Anrop:

service.join(auth1, "NÄRMARE_MEDDELANDEN_I_DISKUSSION_UUID1");
service.join(auth2, "NÄRMARE_MEDDELANDEN_I_DISKUSSION_UUID1");

multiMap skapades för varje prenumeration och togs bort nÀr den inte lÀngre behövdes. Vi beslutade att skapa en Map, dÀr nyckeln skulle vara prenumerationens namn, och som vÀrden identifieringarna av sessionerna (som man senare kan fÄ anvÀndaridentifieringar för, om det behövs).

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

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

Diagrammen har jÀmnats ut.

Hur och varför vi skrev en högbelastad skalbar tjÀnst för 1C: Enterprise: Java, PostgreSQL, Hazelcast

Vad mer har vi lÀrt oss om belastningstestning

  1. JSR223 bör skrivas i groovy och ha inkluderad kompileringscache – det Ă€r mycket snabbare. LĂ€nk.
  2. Jmeter-Plugins-diagram Àr enklare att förstÄ Àn standarddiagram. LÀnk.

Om vÄr erfarenhet med Hazelcast

Hazelcast var en ny produkt för oss, vi började arbeta med den frĂ„n version 3.4.1, nu kör vi version 3.9.2 pĂ„ vĂ„r produktionsserver (vid tidpunkten för skrivandet var den senaste versionen av Hazelcast – 3.10).

ID-generering

Vi började med heltal som identifierare. LĂ„t oss förestĂ€lla oss att vi behöver ytterligare en Long för en ny entitet. En sekvens i databasen Ă€r inte lĂ€mplig, tabellerna deltar i sharding – det skulle innebĂ€ra att det finns ett meddelande ID=1 i DB1 och ett meddelande ID=1 i DB2; i Elasticsearch kan man inte placera det baserat pĂ„ ett sĂ„dant ID, inte heller i Hazelcast, men det vĂ€rsta av allt Ă€r om du vill kombinera data frĂ„n tvĂ„ databaser till en (till exempel bestĂ€mmer att en databas Ă€r tillrĂ€cklig för dessa abonnenter). Man kan ha flera AtomicLong i Hazelcast och hĂ„lla rĂ€kningen dĂ€r, dĂ„ blir prestandan för att hĂ€mta ett nytt ID – incrementAndGet plus tiden för anropet till Hazelcast. Men det finns nĂ„got mer optimalt i Hazelcast – FlakeIdGenerator. Vid varje klientanrop tilldelas ett ID-intervall, till exempel det första – frĂ„n 1 till 10 000, den andra – frĂ„n 10 001 till 20 000 och sĂ„ vidare. Nu kan klienten sjĂ€lv tilldela nya identifierare, sĂ„ lĂ€nge intervallet som tilldelats honom inte tar slut. Det fungerar snabbt, men vid omstart av applikationen (och Hazelcast-klienten) börjar den en ny sekvens – vilket resulterar i hopp och sĂ„dant. Dessutom Ă€r det inte helt klart för utvecklarna varför identifierarna Ă€r heltal men sprids sĂ„ mycket. Vi har vĂ€gt fördelarna och övergĂ„tt till UUID:er.

Förresten, för dem som vill vara som Twitter, finns det ett bibliotek som heter Snowcast – det Ă€r en implementation av Snowflake ovanpĂ„ Hazelcast. Du kan kolla pĂ„ det hĂ€r:

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

Men vi har inte haft tid att ta oss an det Àn.

TransactionalMap.replace

En annan överraskning: TransactionalMap.replace fungerar inte. HÀr Àr ett 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();
        }
    });
}

FörvÀntat: newValue
Aktuellt: oldValue

Vi var tvungna att skriva vÄr egen replace, anvÀnda getForUpdate:

protected  boolean replaceInMap(String mapName, K key, V oldValue, V newValue) {
    TransactionalTaskContext context = HazelcastTransactionContextHolder.getContext();
    if (context != null) {
        log.trace("[CACHE] ErsÀtter vÀrde i en transaktionell karta");
        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] ErsÀtter vÀrde i en icke-transaktionell karta");
    IMap map = hazelcastInstance.getMap(mapName);
    return map.replace(key, oldValue, newValue);
}

Testa inte bara vanliga datatyper, utan ocksÄ deras transaktionella versioner. Ibland fungerar IMap, men TransactionalMap gör det inte.

LĂ€gg till en ny JAR utan driftstopp

Först beslutade vi att spara vÄra klassobjekt i Hazelcast. Till exempel har vi en klass Application, vi vill spara och lÀsa den. Vi sparar:

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

Vi lÀser:

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

Allt fungerar. Sedan beslutade vi att bygga ett index i Hazelcast för att söka efter det:

map.addIndex("subscriberId", false);

Och nÀr vi skrev en ny enhet började vi fÄ ClassNotFoundException. Hazelcast försökte uppdatera indexet, men visste ingenting om vÄr klass och ville att vi skulle lÀgga till en JAR med den klassen. SÄ vi gjorde det, allt fungerade, men ett nytt problem uppstod: hur uppdaterar man JAR-filen utan att helt stoppa klustret? Hazelcast fÄngar inte den nya JAR-filen vid uppdatering av noden. Vid det hÀr laget beslutade vi att vi kan klara oss utan sökning via index. Om man anvÀnder Hazelcast som en lagring av nyckel-vÀrde-typ, borde allt fungera, eller hur? Inte riktigt. HÀr Àr beteendet olika för IMap och TransactionalMap. DÀr dÀr IMap Àr likgiltig, kastar TransactionalMap ett fel.

IMap. Vi registrerar 5000 objekt och lÀser dem. Allt fungerar som förvÀntat.

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

Men i transaktionen fungerar det inte, vi fÄr 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();
        }
    });
}

I version 3.8 introducerades mekanismen User Class Deployment. Du kan utse en huvudnod och uppdatera JAR-filen pÄ den.

Just nu har vi helt bytt tillvÀgagÄngssÀtt: vi serialiserar sjÀlva till JSON och sparar i Hazelcast. Hazelcast behöver inte veta strukturen pÄ vÄra klasser, och vi kan uppdatera utan nedtid. Versionshanteringen av domÀnobjekten hanteras av applikationen. Olika versioner av applikationen kan köras samtidigt och det Àr möjligt att den nya applikationen skriver objekt med nya fÀlt, medan den gamla fortfarande inte kÀnner till dessa fÀlt. Samtidigt lÀser den nya applikationen objekt skrivna av den gamla applikationen, dÀr de nya fÀlten saknas. SÄdana situationer hanteras internt av applikationen, men för enkelhetens skull Àndrar och tar vi inte bort fÀlt, utan utökar klasser genom att lÀgga till nya fÀlt.

Hur vi sÀkerstÀller hög prestanda

Fyra anrop till Hazelcast – bra, tvĂ„ till DB – dĂ„ligt

Att hÀmta data frÄn cachen Àr alltid bÀttre Àn frÄn databasen, men det kÀnns inte bra att lagra oanvÀnda poster. Beslutet om vad vi ska cacha skjuter vi upp till den sista utvecklingsfasen. NÀr den nya funktionen Àr kodad aktiverar vi loggning av alla förfrÄgningar i PostgreSQL (log_min_duration_statement pÄ 0) och startar belastningstestning i cirka 20 minuter. Med de insamlade loggarna kan verktyg som pgFouine och pgBadger generera analytiska rapporter. I rapporterna letar vi frÀmst efter lÄngsamma och frekventa förfrÄgningar. För lÄngsamma förfrÄgningar bygger vi en exekveringsplan (EXPLAIN) och bedömer om förfrÄgan kan snabba upp. Frekventa förfrÄgningar med samma ingÄngsdata passar bra i cachen. Vi försöker hÄlla förfrÄgningarna "platta", en tabell per förfrÄgan.

Drift

SV som en online-tjÀnst lanserades i drift vÄren 2017, och som en separat produkt slÀpptes SV i november 2017 (dÄ i beta-version).

Under mer Àn ett Ärs drift har det inte förekommit nÄgra allvarliga problem med online-tjÀnsten SV. Vi övervakar online-tjÀnsten genom Zabbix, samlar in och distribuerar frÄn Bamboo.

Serverdistributionen av SV levereras i form av inbyggda paket: RPM, DEB, MSI. Dessutom erbjuder vi för Windows en enskild installerare i form av en EXE som installerar servern, Hazelcast och Elasticsearch pÄ en och samma maskin. Vi kallade först denna installationsversion "demonstration", men nu har det visat sig att detta Àr det mest populÀra sÀttet att distribuera.

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster