Hvordan og hvorfor vi skrev en meget belastet skalerbar service til 1C: Enterprise: Java, PostgreSQL, Hazelcast

I denne artikel vil vi tale om, hvordan og hvorfor vi udviklede os Interaktionssystem - en mekanisme, der overfører information mellem klientapplikationer og 1C: Enterprise-servere - fra at sætte en opgave til at gennemtænke arkitekturen og implementeringsdetaljerne.

Interaktionssystemet (herefter - CB) er et distribueret fejltolerant meddelelsessystem med garanteret levering. CB er designet som en højbelastningstjeneste med høj skalerbarhed, tilgængelig både som en onlinetjeneste (leveret af 1C) og som et produktionsprodukt, der kan implementeres på egne serverfaciliteter.

SW bruger distribueret lagring hasselstøbt og søgemaskine Elasticsearch. Vi vil også tale om Java og hvordan vi horisontalt skalerer PostgreSQL.
Hvordan og hvorfor vi skrev en meget belastet skalerbar service til 1C: Enterprise: Java, PostgreSQL, Hazelcast

Formulering af problemet

For at gøre det klart, hvorfor vi lavede Interaction System, vil jeg fortælle dig lidt om, hvordan udviklingen af ​​forretningsapplikationer i 1C fungerer.

Først lidt om os til dem, der endnu ikke ved, hvad vi laver :) Vi er ved at udvikle teknologiplatformen 1C:Enterprise. Platformen inkluderer et udviklingsværktøj til forretningsapplikationer samt en runtime, der gør det muligt for forretningsapplikationer at arbejde i et miljø på tværs af platforme.

Klient-server udviklingsparadigme

Forretningsapplikationer oprettet på 1C:Enterprise fungerer i tre niveauer klient-server arkitektur "DBMS - applikationsserver - klient". Ansøgningskode skrevet ind indbygget sprog 1C, kan køre på applikationsserveren eller på klienten. Alt arbejde med applikationsobjekter (mapper, dokumenter osv.), samt læsning og skrivning af databasen, udføres kun på serveren. Formularer og kommandogrænsefladefunktionalitet er også implementeret på serveren. På klienten modtages, åbnes og vises formularer, "kommunikation" med brugeren (advarsler, spørgsmål ...), små beregninger i formularer, der kræver hurtig respons (f.eks. gange prisen med mængden), arbejde med lokale filer, arbejde med udstyr.

I applikationskoden skal overskrifterne for procedurer og funktioner udtrykkeligt angive, hvor koden vil blive eksekveret - ved hjælp af direktiverne &AtClient / &AtServer (&AtClient / &AtServer i den engelske version af sproget). 1C-udviklere vil nu rette mig ved at sige, at direktiverne faktisk er mere end, men for os er det ikke vigtigt nu.

Du kan kalde serverkode fra klientkode, men du kan ikke kalde klientkode fra serverkode. Dette er en grundlæggende begrænsning, som vi har lavet af en række årsager. Især fordi serverkoden skal skrives på en sådan måde, at den kører på samme måde, uanset hvor den kaldes fra - fra klienten eller fra serveren. Og i tilfælde af et opkald til serverkoden fra en anden serverkode, er der ingen klient som sådan. Og fordi under udførelsen af ​​serverkoden kunne klienten, der kaldte den, lukke, afslutte applikationen, og serveren ville ikke have nogen at ringe til.

Hvordan og hvorfor vi skrev en meget belastet skalerbar service til 1C: Enterprise: Java, PostgreSQL, Hazelcast
Kode, der håndterer et knapklik: at kalde en serverprocedure fra klienten fungerer, at kalde en klientprocedure fra serveren vil ikke

Det betyder, at hvis vi ønsker at sende en besked fra serveren til klientapplikationen, for eksempel om, at dannelsen af ​​en "langspillende" rapport er afsluttet, og rapporten kan ses, har vi ikke sådan en måde. Du skal bruge tricks, for eksempel periodisk polle serveren fra klientkoden. Men denne tilgang belaster systemet med unødvendige opkald, og generelt ser det ikke særlig elegant ud.

Og der er også et behov, for eksempel, når en telefon SIP-opkald, underret klientapplikationen om dette, så den kan finde det i modpartsdatabasen ved den, der ringer op, og vise brugerens oplysninger om den kaldende modpart. Eller for eksempel, når en ordre ankommer til lageret, underrette kundens klientapplikation om dette. Generelt er der mange tilfælde, hvor en sådan mekanisme ville være nyttig.

Faktisk indstilling

Opret en meddelelsesmekanisme. Hurtig, pålidelig, med garanteret levering, med mulighed for fleksibel søgning efter beskeder. Baseret på mekanismen skal du implementere en messenger (beskeder, videoopkald), der fungerer i 1C-applikationer.

Design systemet horisontalt skalerbart. En stigende belastning bør dækkes af en stigning i antallet af noder.

implementering

Vi besluttede ikke at integrere serverdelen af ​​CB direkte i 1C:Enterprise-platformen, men at implementere den som et separat produkt, hvis API kan kaldes fra koden til 1C-applikationsløsninger. Dette blev gjort af en række årsager, hvoraf den vigtigste var at gøre det muligt at udveksle beskeder mellem forskellige 1C-applikationer (f.eks. mellem Handels- og Regnskabsministeriet). Forskellige 1C-applikationer kan køre på forskellige versioner af 1C:Enterprise-platformen, være placeret på forskellige servere osv. Under sådanne forhold er implementeringen af ​​CB som et separat produkt, placeret "på siden" af 1C-installationer, den optimale løsning.

Så vi besluttede at lave CB som et separat produkt. For mindre virksomheder anbefaler vi at bruge den CB-server, som vi installerede i vores sky (wss://1cdialog.com) for at undgå overhead forbundet med lokal serverinstallation og -konfiguration. Store kunder kan dog finde det formålstjenligt at installere deres egen CB-server på deres faciliteter. Vi brugte en lignende tilgang i vores cloud SaaS-produkt. 1cFrisk – det frigives som et produktionsprodukt til installation af kunder og er også implementeret i vores sky https://1cfresh.com/.

App

For belastningsfordeling og fejltolerance vil vi ikke implementere én Java-applikation, men flere, vi vil sætte en load balancer foran dem. Hvis du har brug for at sende en besked fra node til node, så brug publicer/subscribe i Hazelcast.

Kommunikation mellem klient og server - via websocket. Det er velegnet til realtidssystemer.

Distribueret cache

Vælg mellem Redis, Hazelcast og Ehcache. Udenfor i 2015. Redis har lige udgivet en ny klynge (for ny, skræmmende), der er en Sentinel med mange begrænsninger. Ehcache ved ikke, hvordan man klynger (denne funktionalitet dukkede op senere). Vi besluttede at prøve med Hazelcast 3.4.
Hazelcast er samlet ud af æsken. I enkelt node-tilstand er den ikke særlig nyttig og kan kun passe som en cache - den ved ikke, hvordan man dumper data til disken, hvis den eneste node går tabt, går dataene tabt. Vi implementerer flere Hazelcasts, mellem hvilke vi sikkerhedskopierer kritiske data. Vi tager ikke backup af cachen - vi har ikke ondt af ham.

For os er Hazelcast:

  • Lagring af brugersessioner. Det tager lang tid at gå til databasen for en session, så vi lægger alle sessioner i Hazelcast.
  • Cache. Leder du efter en brugerprofil - tjek i cachen. Skrev en ny besked - læg den i cachen.
  • Emner til kommunikation af applikationsinstanser. Noden genererer en begivenhed og placerer den på et Hazelcast-emne. Andre applikationsknudepunkter, der abonnerer på dette emne, modtager og behandler begivenheden.
  • klyngelåse. For eksempel skaber vi en diskussion med en unik nøgle (diskussion-singleton inden for rammerne af 1C-basen):

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

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

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

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

Vi tjekkede, at der ikke er nogen kanal. De tog låsen, tjekkede den igen, skabte den. Hvis du ikke tjekker efter at have taget låsen, så er der en chance for, at en anden tråd også tjekkede på det tidspunkt og nu vil forsøge at skabe den samme diskussion - og den eksisterer allerede. Det er umuligt at lave en lås gennem synkroniseret eller almindelig java Lock. Gennem basen - langsomt, og basen er en skam, gennem Hazelcast - hvad du har brug for.

Valg af DBMS

Vi har stor og succesfuld erfaring med PostgreSQL og samarbejde med udviklerne af dette DBMS.

Med en klynge er PostgreSQL ikke let – det er der XL, XC, Citus, men generelt er det ikke noSQL, der skaleres ud af boksen. NoSQL blev ikke betragtet som hovedlageret, det var nok at vi tog Hazelcast, som vi ikke havde arbejdet med før.

Da du skal skalere en relationel database, betyder det skæring. Som du ved, opdeler vi databasen i separate dele ved sharding, så hver af dem kan placeres på en separat server.

Den første version af vores sharding antog evnen til at sprede hver af tabellerne i vores applikation til forskellige servere i forskellige proportioner. Masser af beskeder på server A - lad os venligst flytte en del af denne tabel til server B. Denne beslutning skreg bare på for tidlig optimering, så vi besluttede at begrænse os til en multi-tenant tilgang.

Du kan for eksempel læse om multi-lejer på hjemmesiden Citus data.

I SV er der koncepter for applikationen og abonnenten. En applikation er en specifik installation af en forretningsapplikation, såsom ERP eller Accounting, med dens brugere og forretningsdata. En abonnent er en organisation eller en person, på vegne af hvilken applikationen er registreret på CB-serveren. En abonnent kan have flere applikationer registreret, og disse applikationer kan udveksle beskeder med hinanden. Abonnenten blev lejer i vores system. Beskeder fra flere abonnenter kan placeres i én fysisk base; hvis vi ser, at en eller anden abonnent er begyndt at generere en masse trafik, flytter vi den til en separat fysisk database (eller endda en separat databaseserver).

Vi har en hoveddatabase, hvor routingtabellen er gemt med information om placeringen af ​​alle abonnentdatabaser.

Hvordan og hvorfor vi skrev en meget belastet skalerbar service til 1C: Enterprise: Java, PostgreSQL, Hazelcast

For at forhindre, at hoveddatabasen bliver en flaskehals, beholder vi routingtabellen (og andre ofte efterspurgte data) i cachen.

Hvis abonnentens database begynder at blive langsommere, skærer vi den i partitioner indeni. På andre projekter bruger vi til at opdele store borde pg_pathman.

Da det er dårligt at miste brugermeddelelser, sikkerhedskopierer vi vores databaser med replikaer. Kombinationen af ​​synkrone og asynkrone replikaer giver dig mulighed for at forsikre dig mod tab af hoveddatabasen. Tab af besked vil kun forekomme i tilfælde af en samtidig fejl i hoveddatabasen og dens synkrone replika.

Hvis den synkrone replika går tabt, bliver den asynkrone replika synkron.
Hvis hoveddatabasen går tabt, bliver den synkrone replika hoveddatabasen, den asynkrone replika bliver en synkron replika.

Elasticsearch for søgning

Da blandt andet CB også er en messenger, har vi her brug for en hurtig, bekvem og fleksibel søgning, under hensyntagen til morfologi, ved upræcise matches. Vi besluttede ikke at genopfinde hjulet og bruge den gratis Elasticsearch søgemaskine, oprettet på grundlag af biblioteket Lucene. Vi implementerer også Elasticsearch i en klynge (master - data - data) for at eliminere problemer i tilfælde af fejl i applikationsknudepunkter.

På github fandt vi Russisk morfologi plugin til Elasticsearch og brug det. I Elasticsearch-indekset gemmer vi ordrødder (som plugin'et definerer) og N-gram. Når brugeren indtaster tekst for at søge, søger vi efter den indtastede tekst blandt N-gram. Når det gemmes i indekset, vil ordet "tekster" blive opdelt i følgende N-gram:

[te, tech, tex, text, texts, ek, eks, ext, exts, ks, kst, ksty, st, sty, you],

Og også roden af ​​ordet "tekst" vil blive gemt. Denne tilgang giver dig mulighed for at søge i begyndelsen, i midten og i slutningen af ​​ordet.

Generelt billede

Hvordan og hvorfor vi skrev en meget belastet skalerbar service til 1C: Enterprise: Java, PostgreSQL, Hazelcast
Gentager billedet fra begyndelsen af ​​artiklen, men med forklaringer:

  • Balancer udsat for internettet; vi har nginx, det kan være enhver.
  • Java-applikationsforekomster kommunikerer med hinanden via Hazelcast.
  • For at arbejde med en web-socket bruger vi Netty.
  • Java-applikation skrevet i Java 8, består af bundter OSGi. Planerne er at migrere til Java 10 og skifte til moduler.

Udvikling og test

Under udviklingen og testen af ​​CB'en stødte vi på en række interessante funktioner ved de produkter, vi bruger.

Belastningstest og hukommelseslækager

Frigivelsen af ​​hver CB-udgivelse er en belastningstest. Det bestod med succes, da:

  • Testen virkede i flere dage, og der var ingen afvisninger
  • Responstiden for nøgleoperationer oversteg ikke en behagelig tærskel
  • Ydeevneforringelse i forhold til den tidligere version er ikke mere end 10 %

Vi fylder testdatabasen med data - til dette får vi information om den mest aktive abonnent fra produktionsserveren, gange dens tal med 5 (antal beskeder, diskussioner, brugere) og så tester vi.

Vi udfører belastningstest af interaktionssystemet i tre konfigurationer:

  1. stress test
  2. Kun forbindelser
  3. Registrering af abonnenter

Under en stresstest starter vi flere hundrede tråde, og de indlæser systemet uden at stoppe: skriv beskeder, opret diskussioner, modtag en liste med beskeder. Vi simulerer almindelige brugeres handlinger (få en liste over mine ulæste beskeder, skriv til nogen) og programbeslutninger (overfør en pakke til en anden konfiguration, behandle en advarsel).

Sådan ser en del af stresstesten ud:

  • Bruger logger ind
    • Anmoder om dine ulæste tråde
    • 50 % chance for at læse beskeder
    • 50 % chance for at skrive beskeder
    • Næste bruger:
      • 20% chance for at oprette en ny tråd
      • Vælger tilfældigt nogen af ​​hans diskussioner
      • Kommer indenfor
      • Anmoder om beskeder, brugerprofiler
      • Opretter fem beskeder adresseret til tilfældige brugere fra denne tråd
      • Ude af diskussion
      • Gentages 20 gange
      • Logger ud, vender tilbage til begyndelsen af ​​scriptet

    • En chatbot kommer ind i systemet (emulerer beskeder fra koden for anvendte løsninger)
      • 50 % chance for at oprette en ny datakanal (særlig diskussion)
      • 50 % chance for at skrive en besked i en af ​​de eksisterende kanaler

Scenariet "Kun forbindelser" dukkede op af en grund. Der er en situation: brugere har tilsluttet systemet, men har endnu ikke været involveret. Hver bruger om morgenen kl. 09:00 tænder for computeren, etablerer forbindelse til serveren og er tavs. Disse fyre er farlige, der er mange af dem - de har kun PING / PONG ud af pakkerne, men de bevarer forbindelsen til serveren (de kan ikke beholde den - og pludselig en ny besked). Testen gengiver situationen, når et stort antal af sådanne brugere forsøger at logge ind på systemet på en halv time. Det ligner en stresstest, men dens fokus er netop på dette første input - så der ikke er fejl (en person bruger ikke systemet, men det falder allerede af - det er svært at finde på noget værre).

Abonnentregistreringsscenariet stammer fra den første lancering. Vi gennemførte en stresstest og var sikre på, at systemet ikke bremsede i korrespondancen. Men brugerne gik, og registreringen begyndte at falde af efter timeout. Ved tilmelding brugte vi / Dev / random, som er bundet til systemets entropi. Serveren havde ikke tid til at akkumulere nok entropi, og da en ny SecureRandom blev anmodet om, frøs den i ti sekunder. Der er mange veje ud af denne situation, for eksempel: Skift til en mindre sikker /dev/urandom, installer en speciel tavle, der genererer entropi, generer tilfældige tal på forhånd og gem dem i puljen. Vi lukkede midlertidigt problemet med puljen, men siden da har vi kørt en separat test for registrering af nye abonnenter.

Som belastningsgenerator bruger vi JMeter. Han ved ikke, hvordan man arbejder med en websocket, et plugin er nødvendigt. Den første i søgeresultaterne for forespørgslen "jmeter websocket" er artikler fra BlazeMeterhvor de anbefaler plugin af Maciej Zaleski.

Det var der, vi besluttede at starte.

Næsten umiddelbart efter starten af ​​seriøs test opdagede vi, at hukommelseslækager begyndte i JMeter.

Pluginnet er en separat stor historie, med 176 stjerner har det 132 gafler på github. Forfatteren selv har ikke forpligtet sig til det siden 2015 (vi tog det i 2015, så vakte det ikke mistanke), flere github-problemer om hukommelseslækager, 7 ulukkede pull-anmodninger.
Hvis du vælger at indlæse test med dette plugin, skal du være opmærksom på følgende diskussioner:

  1. I et multi-threaded miljø blev der brugt en almindelig LinkedList, som et resultat fik vi NPE ved kørsel. Det løses enten ved at skifte til ConcurrentLinkedDeque eller ved synkroniserede blokke. Vi valgte den første mulighed for os selvhttps://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. Hukommelseslækage, forbindelsesoplysninger slettes ikke, når forbindelsen afbrydes (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. I streaming-tilstand (når websocket ikke er lukket i slutningen af ​​prøven, men bruges videre i planen), virker svarmønstre ikke (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

Dette er en af ​​dem på github. Hvad vi gjorde:

  1. Har taget gaffel af Elyran Kogan (@elyrank) - det løser problem 1 og 3
  2. Løste problem 2
  3. Opdateret anløbsbro fra 9.2.14 til 9.3.12
  4. Indpakket SimpleDateFormat i ThreadLocal; SimpleDateFormat er ikke trådsikkert, hvilket fører til NPE under kørsel
  5. Rettet endnu en hukommelseslækage (forbindelsen lukket forkert ved afbrydelse)

Og alligevel flyder det!

Hukommelsen begyndte ikke at ende på en dag, men på to. Der var slet ikke tid, vi besluttede at køre færre tråde, men på fire agenter. Dette burde have været nok i mindst en uge.

Det er to dage siden...

Nu er Hazelcast ved at løbe tør for hukommelse. Logfilerne viste, at efter et par dages test begynder Hazelcast at klage over manglen på hukommelse, og efter et stykke tid falder klyngen fra hinanden, og noderne fortsætter med at dø én efter én. Vi tilsluttede JVisualVM til hasselstøbning og så "opadgående save" - ​​den kaldte jævnligt GC, men kunne ikke rydde hukommelsen på nogen måde.

Hvordan og hvorfor vi skrev en meget belastet skalerbar service til 1C: Enterprise: Java, PostgreSQL, Hazelcast

Det viste sig, at i hazelcast 3.4, når du sletter et kort / multiMap (map.destroy()), er hukommelsen ikke helt frigivet:

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

Fejlen er nu rettet i 3.5, men det var et problem dengang. Vi oprettede nyt multiMap med dynamiske navne og slettede i henhold til vores logik. Koden så nogenlunde sådan ud:

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

Opkald:

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

multiMap blev oprettet for hvert abonnement og fjernet, når det ikke var nødvendigt. Vi besluttede, at vi ville starte et kort , nøglen vil være navnet på abonnementet, og værdierne vil være sessionsidentifikatorer (hvorved du så kan få brugeridentifikatorer, hvis det er nødvendigt).

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

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

Diagrammerne er blevet forbedret.

Hvordan og hvorfor vi skrev en meget belastet skalerbar service til 1C: Enterprise: Java, PostgreSQL, Hazelcast

Hvad har vi ellers lært om belastningstest

  1. JSR223 skal skrives i groovy og inkludere kompileringscache - det er meget hurtigere. Link.
  2. Jmeter-Plugins-diagrammer er nemmere at forstå end standard. Link.

Om vores erfaring med Hazelcast

Hazelcast var et nyt produkt for os, vi begyndte at arbejde med det fra version 3.4.1, nu har vi version 3.9.2 på vores produktionsserver (i skrivende stund er den seneste version af Hazelcast 3.10).

ID generering

Vi startede med heltals-id'er. Lad os forestille os, at vi har brug for endnu en Længe for en ny enhed. Sekvens i databasen er ikke egnet, tabeller er involveret i sharding - det viser sig, at der er en besked ID=1 i DB1 og en besked ID=1 i DB2, du kan ikke sætte dette ID i Elasticsearch, også i Hazelcast, men det værste er, hvis du vil reducere data fra to databaser til én (for eksempel at beslutte, at en abonnentdatabase er nok til disse). Du kan have flere AtomicLongs i Hazelcast og holde tælleren der, så er ydeevnen for at få et nyt ID incrementAndGet plus tiden til at forespørge i Hazelcast. Men Hazelcast har noget mere optimalt - FlakeIdGenerator. Ved henvendelse får hver klient en række ID'er, for eksempel den første - fra 1 til 10, den anden - fra 000 til 10, og så videre. Nu kan klienten udstede nye identifikatorer på egen hånd, indtil det område, der er udstedt til den, slutter. Virker hurtigt, men genstart af appen (og Hazelcast-klienten) starter en ny sekvens - derfor springer over osv. Derudover er det ikke særlig klart for udviklere, hvorfor ID'er er heltal, men de går så meget i modstrid. Vi vejede alt og skiftede til UUID'er.

For dem, der ønsker at være som Twitter, er der sådan et Snowcast-bibliotek - dette er en implementering af Snowflake oven på Hazelcast. Du kan se her:

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

Men vi er ikke nået til det endnu.

TransactionalMap.erstat

En anden overraskelse: TransactionalMap.replace virker ikke. Her er en 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

Jeg var nødt til at skrive min egen erstatning ved hjælp af 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);
}

Test ikke kun almindelige datastrukturer, men også deres transaktionsversioner. Det sker, at IMap virker, men TransactionalMap eksisterer ikke længere.

Sæt ny JAR på uden nedetid

Først besluttede vi at skrive genstande fra vores klasser til Hazelcast. For eksempel har vi en Application-klasse, vi vil gemme og læse den. Gemme:

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

Læsning:

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

Alt fungerer. Så besluttede vi at bygge et indeks i Hazelcast for at søge i det:

map.addIndex("subscriberId", false);

Og da de skrev en ny enhed, begyndte de at modtage en ClassNotFoundException. Hazelcast forsøgte at tilføje til indekset, men vidste ikke noget om vores klasse og ønskede at sætte en JAR med denne klasse i den. Vi gjorde netop det, alt fungerede, men et nyt problem dukkede op: hvordan opdaterer man JAR uden helt at stoppe klyngen? Hazelcast henter ikke en ny JAR på en per-node-opdatering. På dette tidspunkt besluttede vi, at vi kunne leve uden indeksopslag. Når alt kommer til alt, hvis du bruger Hazelcast som en nøgleværdibutik, så vil alt fungere? Ikke rigtig. Her igen forskellig opførsel af IMap og TransactionalMap. Hvor IMap er ligeglad, kaster TransactionalMap en fejl.

IMap. Vi skriver 5000 genstande ned, vi læser. Alt forventes.

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

Men det virker ikke i en transaktion, vi får en 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();
        }
    });
}

I 3.8 dukkede User Class Deployment-mekanismen op. Du kan udpege én masterknude og opdatere JAR-filen på den.

Nu har vi fuldstændig ændret vores tilgang: vi serialiserer selv til JSON og gemmer til Hazelcast. Hazelcast behøver ikke at kende strukturen i vores klasser, og vi kan opdatere uden nedetid. Versionering af domæneobjekter styres af applikationen. Forskellige versioner af applikationen kan lanceres på samme tid, og det er muligt, at en ny applikation skriver objekter med nye felter, mens den gamle endnu ikke kender til disse felter. Og samtidig læser den nye applikation de objekter skrevet af den gamle applikation, som ikke har nye felter. Vi håndterer sådanne situationer inde i applikationen, men for nemheds skyld ændrer eller fjerner vi ikke felterne, vi udvider kun klasserne ved at tilføje nye felter.

Hvordan vi leverer høj ydeevne

Fire ture til Hazelcast er godt, to ture til databasen er dårligt

At gå efter data i cachen er altid bedre end i databasen, men du ønsker heller ikke at gemme uhævede poster. Beslutningen om, hvad der skal cache, er overladt til det sidste udviklingstrin. Når den nye funktionalitet er kodet, aktiverer vi logning af alle forespørgsler i PostgreSQL (log_min_duration_statement til 0) og kører belastningstest i 20 minutter. Hjælpeprogrammer som pgFouine og pgBadger kan bygge analytiske rapporter baseret på de indsamlede logfiler. I rapporter søger vi primært efter langsomme og hyppige forespørgsler. Ved langsomme forespørgsler opbygger vi en eksekveringsplan (EXPLAIN) og vurderer, om en sådan forespørgsel kan accelereres. Hyppige anmodninger om det samme input passer godt ind i cachen. Vi forsøger at holde forespørgsler "flade", en tabel pr. forespørgsel.

Udnyttelse

CB som online-tjeneste blev lanceret i foråret 2017, da et separat CB-produkt blev udgivet i november 2017 (dengang i beta-status).

I mere end et års drift har der ikke været alvorlige problemer i driften af ​​CB's onlinetjeneste. Vi overvåger onlinetjenesten gennem Zabbix, indsamle og implementere fra Bamboo.

CB-serverdistributionen kommer i form af native pakker: RPM, DEB, MSI. Plus, til Windows leverer vi et enkelt installationsprogram i form af en enkelt EXE, der installerer serveren, Hazelcast og Elasticsearch på én maskine. Først kaldte vi denne version af installationen "demo", men nu er det blevet klart, at dette er den mest populære implementeringsmulighed.

Kilde: www.habr.com

Tilføj en kommentar