Hvordan og hvorfor vi skrev en skalerbar tjeneste med høy belastning for 1C: Enterprise: Java, PostgreSQL, Hazelcast

I denne artikkelen vil vi snakke om hvordan og hvorfor vi utviklet oss Interaksjonssystem – en mekanisme som overfører informasjon mellom klientapplikasjoner og 1C:Enterprise-servere – fra å sette en oppgave til å tenke gjennom arkitekturen og implementeringsdetaljene.

Interaksjonssystemet (heretter kalt SV) er et distribuert, feiltolerant meldingssystem med garantert levering. SV er designet som en høybelastningstjeneste med høy skalerbarhet, tilgjengelig både som en onlinetjeneste (levert av 1C) og som et masseprodusert produkt som kan distribueres på dine egne serverfasiliteter.

SV bruker distribuert lagring Hasselcast og søkemotor Elasticsearch. Vi skal også snakke om Java og hvordan vi horisontalt skalerer PostgreSQL.
Hvordan og hvorfor vi skrev en skalerbar tjeneste med høy belastning for 1C: Enterprise: Java, PostgreSQL, Hazelcast

Formulering av problemet

For å gjøre det klart hvorfor vi opprettet interaksjonssystemet, skal jeg fortelle deg litt om hvordan utviklingen av forretningsapplikasjoner i 1C fungerer.

Til å begynne med, litt om oss for de som ennå ikke vet hva vi gjør :) Vi lager teknologiplattformen 1C:Enterprise. Plattformen inkluderer et utviklingsverktøy for forretningsapplikasjoner, samt en kjøretid som lar forretningsapplikasjoner kjøre i et miljø på tvers av plattformer.

Klient-server utviklingsparadigme

Forretningsapplikasjoner opprettet på 1C:Enterprise opererer i tre nivåer klient server arkitektur "DBMS - applikasjonsserver - klient". Søknadskode skrevet inn innebygd 1C-språk, kan kjøres på applikasjonsserveren eller på klienten. Alt arbeid med applikasjonsobjekter (kataloger, dokumenter, etc.), samt lesing og skriving av databasen, utføres kun på serveren. Funksjonaliteten til skjemaer og kommandogrensesnitt er også implementert på serveren. Klienten utfører mottak, åpning og visning av skjemaer, "kommuniserer" med brukeren (advarsler, spørsmål...), små beregninger i skjemaer som krever rask respons (for eksempel multiplisere prisen med kvantitet), arbeider med lokale filer, arbeider med utstyr.

I applikasjonskode må overskriftene til prosedyrer og funksjoner eksplisitt indikere hvor koden vil bli utført - ved å bruke &AtClient / &AtServer-direktivene (&AtClient / &AtServer i den engelske versjonen av språket). 1C-utviklere vil nå korrigere meg ved å si at direktiver faktisk er det mer enn, men for oss er ikke dette viktig nå.

Du kan ringe serverkode fra klientkode, men du kan ikke ringe klientkode fra serverkode. Dette er en grunnleggende begrensning vi har gjort av flere grunner. Spesielt fordi serverkoden må skrives på en slik måte at den kjører på samme måte uansett hvor den kalles – fra klienten eller fra serveren. Og i tilfelle av å ringe serverkode fra en annen serverkode, er det ingen klient som sådan. Og fordi under kjøringen av serverkoden, kunne klienten som kalte den lukkes, avslutte applikasjonen, og serveren ville ikke lenger ha noen å ringe.

Hvordan og hvorfor vi skrev en skalerbar tjeneste med høy belastning for 1C: Enterprise: Java, PostgreSQL, Hazelcast
Kode som håndterer et knappeklikk: å kalle en serverprosedyre fra klienten vil fungere, å kalle en klientprosedyre fra serveren vil ikke

Dette betyr at hvis vi ønsker å sende en melding fra serveren til klientapplikasjonen, for eksempel at genereringen av en "langvarig" rapport er ferdig og rapporten kan ses, har vi ikke en slik metode. Du må bruke triks, for eksempel periodisk polle serveren fra klientkoden. Men denne tilnærmingen belaster systemet med unødvendige samtaler, og ser generelt ikke veldig elegant ut.

Og det er også et behov for eksempel når det kommer en telefon SIP- når du ringer, varsle klientapplikasjonen om dette slik at den kan bruke innringerens nummer til å finne det i motpartsdatabasen og vise brukerinformasjon om den anropende motparten. Eller for eksempel, når en ordre ankommer lageret, gi beskjed til kundens klientapplikasjon om dette. Generelt er det mange tilfeller der en slik mekanisme vil være nyttig.

Selve produksjonen

Lag en meldingsmekanisme. Rask, pålitelig, med garantert levering, med muligheten til fleksibelt å søke etter meldinger. Basert på mekanismen, implementer en messenger (meldinger, videosamtaler) som kjører i 1C-applikasjoner.

Design systemet slik at det er horisontalt skalerbart. Den økende belastningen må dekkes ved å øke antall noder.

implementering

Vi bestemte oss for ikke å integrere serverdelen av SV direkte i 1C:Enterprise-plattformen, men å implementere den som et eget produkt, hvis API kan kalles fra koden til 1C-applikasjonsløsninger. Dette ble gjort av en rekke grunner, hvorav den viktigste var at jeg ønsket å gjøre det mulig å utveksle meldinger mellom ulike 1C-applikasjoner (for eksempel mellom Trade Management og Accounting). Ulike 1C-applikasjoner kan kjøres på forskjellige versjoner av 1C:Enterprise-plattformen, ligge på forskjellige servere osv. Under slike forhold er implementering av SV som et eget produkt plassert "på siden" av 1C-installasjoner den optimale løsningen.

Så vi bestemte oss for å lage SV som et eget produkt. Vi anbefaler at små selskaper bruker CB-serveren som vi installerte i skyen vår (wss://1cdialog.com) for å unngå overheadkostnader forbundet med lokal installasjon og konfigurasjon av serveren. Store kunder kan finne det tilrådelig å installere sin egen CB-server på deres anlegg. Vi brukte en lignende tilnærming i vårt sky SaaS-produkt 1cFrisk – det produseres som et masseprodusert produkt for installasjon hos kundene, og er også distribuert i skyen vår https://1cfresh.com/.

App

For å fordele belastningen og feiltoleransen vil vi ikke distribuere én Java-applikasjon, men flere, med en lastbalanser foran seg. Hvis du trenger å overføre en melding fra node til node, bruk publiser/abonner i Hazelcast.

Kommunikasjon mellom klient og server er via websocket. Den er godt egnet for sanntidssystemer.

Distribuert cache

Vi valgte mellom Redis, Hazelcast og Ehcache. Det er 2015. Redis har nettopp gitt ut en ny klynge (for ny, skummel), det er Sentinel med mange begrensninger. Ehcache vet ikke hvordan man setter sammen til en klynge (denne funksjonaliteten dukket opp senere). Vi bestemte oss for å prøve det med Hazelcast 3.4.
Hazelcast er satt sammen til en klynge ut av esken. I enkelt node-modus er det ikke veldig nyttig og kan bare brukes som en cache - det vet ikke hvordan du dumper data til disk, hvis du mister den eneste noden, mister du dataene. Vi distribuerer flere Hazelcasts, mellom hvilke vi sikkerhetskopierer kritiske data. Vi sikkerhetskopierer ikke cachen – vi har ikke noe imot det.

For oss er Hazelcast:

  • Lagring av brukerøkter. Det tar lang tid å gå til databasen for en økt hver gang, så vi legger alle øktene i Hazelcast.
  • Cache. Hvis du leter etter en brukerprofil, sjekk cachen. Skrev en ny melding - legg den i cachen.
  • Emner for kommunikasjon mellom applikasjonsinstanser. Noden genererer en hendelse og plasserer den i Hazelcast-emnet. Andre applikasjonsnoder som abonnerer på dette emnet mottar og behandler hendelsen.
  • Klyngelåser. For eksempel lager vi en diskusjon ved å bruke en unik nøkkel (enkeltdiskusjon i 1C-databasen):

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

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

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

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

Vi sjekket at det ikke er noen kanal. Vi tok låsen, sjekket den igjen og opprettet den. Hvis du ikke sjekker låsen etter å ha tatt låsen, er det en sjanse for at en annen tråd også sjekket i det øyeblikket og vil nå prøve å opprette den samme diskusjonen - men den eksisterer allerede. Du kan ikke låse med synkronisert eller vanlig java-lås. Gjennom databasen - det er tregt, og det er synd for databasen; gjennom Hazelcast - det er det du trenger.

Velge en DBMS

Vi har lang og vellykket erfaring med å jobbe med PostgreSQL og samarbeide med utviklerne av dette DBMS.

Det er ikke lett med en PostgreSQL-klynge - det er det XL, XC, Citus, men generelt er dette ikke NoSQL-er som skaleres ut av boksen. Vi betraktet ikke NoSQL som hovedlagringen; det var nok at vi tok Hazelcast, som vi ikke hadde jobbet med før.

Hvis du trenger å skalere en relasjonsdatabase, betyr det skjæring. Som du vet, med sharding deler vi databasen i separate deler slik at hver av dem kan plasseres på en egen server.

Den første versjonen av vår sharding antok muligheten til å distribuere hver av tabellene i applikasjonen vår på tvers av forskjellige servere i forskjellige proporsjoner. Det er mange meldinger på server A - vær så snill, la oss flytte en del av denne tabellen til server B. Denne beslutningen skrek ganske enkelt om for tidlig optimalisering, så vi bestemte oss for å begrense oss til en multi-tenant-tilnærming.

Du kan for eksempel lese om flerleietaker på nettsiden Citus Data.

SV har begrepene applikasjon og abonnent. En applikasjon er en spesifikk installasjon av en forretningsapplikasjon, for eksempel ERP eller Accounting, med dens brukere og forretningsdata. En abonnent er en organisasjon eller person på vegne av applikasjonen er registrert i SV-serveren. En abonnent kan ha flere applikasjoner registrert, og disse applikasjonene kan utveksle meldinger med hverandre. Abonnenten ble leietaker i systemet vårt. Meldinger fra flere abonnenter kan ligge i én fysisk database; hvis vi ser at en abonnent har begynt å generere mye trafikk, flytter vi den til en egen fysisk database (eller til og med en egen databaseserver).

Vi har en hoveddatabase hvor det lagres en rutetabell med informasjon om plassering av alle abonnentdatabaser.

Hvordan og hvorfor vi skrev en skalerbar tjeneste med høy belastning for 1C: Enterprise: Java, PostgreSQL, Hazelcast

For å forhindre at hoveddatabasen blir en flaskehals, holder vi rutingtabellen (og andre ofte nødvendige data) i en cache.

Hvis abonnentens database begynner å avta, vil vi kutte den i partisjoner inne. På andre prosjekter vi bruker pg_pathman.

Siden det er dårlig å miste brukermeldinger, vedlikeholder vi databasene våre med replikaer. Kombinasjonen av synkrone og asynkrone replikaer lar deg forsikre deg i tilfelle tap av hoveddatabasen. Meldingstap vil bare skje hvis primærdatabasen og dens synkrone replika feiler samtidig.

Hvis en synkron kopi går tapt, blir den asynkrone kopien synkron.
Hvis hoveddatabasen går tapt, blir den synkrone replikaen hoveddatabasen, og den asynkrone replikaen blir en synkron replika.

Elasticsearch for søk

Siden blant annet SV også er en budbringer, krever det et raskt, praktisk og fleksibelt søk, tatt i betraktning morfologi, ved bruk av upresise treff. Vi bestemte oss for å ikke finne opp hjulet på nytt og bruke den gratis søkemotoren Elasticsearch, laget basert på biblioteket Lucene. Vi distribuerer også Elasticsearch i en klynge (master – data – data) for å eliminere problemer i tilfelle feil på applikasjonsnoder.

På github fant vi Russisk morfologi plugin for Elasticsearch og bruk den. I Elasticsearch-indeksen lagrer vi ordrøtter (som plugin bestemmer) og N-gram. Når brukeren skriver inn tekst for å søke, ser vi etter den innskrevne teksten blant N-gram. Når det er lagret i indeksen, vil ordet "tekster" deles inn i følgende N-gram:

[de, tek, tex, tekst, tekster, ek, ex, ext, tekster, ks, kst, ksty, st, sty, du],

Og roten til ordet "tekst" vil også bli bevart. Denne tilnærmingen lar deg søke i begynnelsen, i midten og på slutten av ordet.

Det store bildet

Hvordan og hvorfor vi skrev en skalerbar tjeneste med høy belastning for 1C: Enterprise: Java, PostgreSQL, Hazelcast
Repetisjon av bildet fra begynnelsen av artikkelen, men med forklaringer:

  • Balanser eksponert på Internett; vi har nginx, det kan være hvilken som helst.
  • Java-applikasjonsforekomster kommuniserer med hverandre via Hazelcast.
  • For å jobbe med en web-socket bruker vi Netty.
  • Java-applikasjonen er skrevet i Java 8 og består av bunter OSGi. Planene inkluderer migrering til Java 10 og overgang til moduler.

Utvikling og testing

I prosessen med å utvikle og teste SV-en kom vi over en rekke interessante funksjoner ved produktene vi bruker.

Lasttesting og minnelekkasjer

Utgivelsen av hver SV-utgivelse innebærer lasttesting. Det er vellykket når:

  • Testen virket i flere dager, og det var ingen tjenestefeil
  • Responstiden for nøkkeloperasjoner oversteg ikke en komfortabel terskel
  • Ytelsesforringelse sammenlignet med forrige versjon er ikke mer enn 10 %

Vi fyller testdatabasen med data - for å gjøre dette mottar vi informasjon om den mest aktive abonnenten fra produksjonsserveren, multipliserer tallene med 5 (antall meldinger, diskusjoner, brukere) og tester den på den måten.

Vi utfører lasttesting av interaksjonssystemet i tre konfigurasjoner:

  1. stresstest
  2. Kun tilkoblinger
  3. Registrering av abonnenter

Under stresstesten lanserer vi flere hundre tråder, og de laster systemet uten å stoppe: skrive meldinger, lage diskusjoner, motta en liste med meldinger. Vi simulerer handlingene til vanlige brukere (få en liste over mine uleste meldinger, skriv til noen) og programvareløsninger (send en pakke med en annen konfigurasjon, behandle et varsel).

Dette er for eksempel hvordan en del av stresstesten ser ut:

  • Bruker logger inn
    • Ber om dine uleste diskusjoner
    • 50 % sannsynlighet for å lese meldinger
    • 50 % sannsynlighet for å sende tekstmeldinger
    • Neste bruker:
      • Har 20 % sjanse for å skape en ny diskusjon
      • Velger tilfeldig noen av diskusjonene
      • Går inn
      • Forespørsler om meldinger, brukerprofiler
      • Oppretter fem meldinger adressert til tilfeldige brukere fra denne diskusjonen
      • Etterlater diskusjon
      • Gjentas 20 ganger
      • Logger ut, går tilbake til begynnelsen av skriptet

    • En chatbot kommer inn i systemet (emulerer meldinger fra applikasjonskode)
      • Har 50 % sjanse for å opprette en ny kanal for datautveksling (spesiell diskusjon)
      • 50 % sannsynlighet for å skrive en melding til noen av de eksisterende kanalene

Scenarioet "Kun tilkoblinger" dukket opp av en grunn. Det er en situasjon: brukere har koblet til systemet, men har ennå ikke blitt involvert. Hver bruker slår på datamaskinen klokken 09:00 om morgenen, oppretter en tilkobling til serveren og forblir stille. Disse karene er farlige, det er mange av dem - de eneste pakkene de har er PING/PONG, men de beholder tilkoblingen til serveren (de kan ikke holde det oppe - hva om det er en ny melding). Testen gjengir en situasjon der et stort antall slike brukere prøver å logge seg på systemet på en halvtime. Den ligner på en stresstest, men dens fokus er nettopp på denne første inngangen - slik at det ikke er noen feil (en person bruker ikke systemet, og det faller allerede av - det er vanskelig å tenke på noe verre).

Abonnentregistreringsskriptet starter fra første lansering. Vi gjennomførte en stresstest og var sikre på at systemet ikke bremset under korrespondanse. Men brukere kom og registreringen begynte å mislykkes på grunn av et tidsavbrudd. Ved registrering brukte vi / dev / tilfeldig, som er relatert til systemets entropi. Serveren hadde ikke tid til å akkumulere nok entropi, og når en ny SecureRandom ble bedt om, frøs den i flere titalls sekunder. Det er mange måter ut av denne situasjonen, for eksempel: bytt til det mindre sikre /dev/urandom, installer et spesialbrett som genererer entropi, generer tilfeldige tall på forhånd og lagre dem i en pool. Vi lukket midlertidig problemet med bassenget, men siden den gang har vi kjørt en egen test for registrering av nye abonnenter.

Vi bruker som lastgenerator JMeter. Den vet ikke hvordan den fungerer med websocket; den trenger en plugin. De første i søkeresultatene for søket "jmeter websocket" er: artikler fra BlazeMeter, som anbefaler plugin av Maciej Zaleski.

Det var der vi bestemte oss for å begynne.

Nesten umiddelbart etter å ha startet seriøs testing, oppdaget vi at JMeter begynte å lekke minne.

Plugin er en egen stor historie; med 176 stjerner har den 132 gafler på github. Forfatteren selv har ikke forpliktet seg til det siden 2015 (vi tok det i 2015, da vakte det ikke mistanke), flere github-problemer angående minnelekkasjer, 7 ulukkede pull-forespørsler.
Hvis du bestemmer deg for å utføre lasttesting ved å bruke denne plugin-modulen, vær oppmerksom på følgende diskusjoner:

  1. I et flertrådsmiljø ble en vanlig LinkedList brukt, og resultatet ble det NPE i løpetid. Dette kan løses enten ved å bytte til ConcurrentLinkedDeque eller ved synkroniserte blokker. Vi valgte det første alternativet for oss selv (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. Minnelekkasje; når du kobler fra, slettes ikke tilkoblingsinformasjon (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. I strømmemodus (når websocket ikke er lukket på slutten av prøven, men brukes senere i planen), fungerer ikke svarmønstre (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

Dette er en av dem på github. Hva vi gjorde:

  1. Har tatt gaffel Elyran Kogan (@elyrank) – den løser problemer 1 og 3
  2. Løste problem 2
  3. Oppdatert brygge fra 9.2.14 til 9.3.12
  4. Pakket SimpleDateFormat i ThreadLocal; SimpleDateFormat er ikke trådsikkert, noe som førte til NPE under kjøring
  5. Rettet en annen minnelekkasje (tilkoblingen ble lukket feil når den ble koblet fra)

Og likevel flyter det!

Hukommelsen begynte å gå tom ikke på en dag, men på to. Det var absolutt ingen tid igjen, så vi bestemte oss for å lansere færre tråder, men på fire agenter. Dette burde vært nok i minst en uke.

To dager har gått...

Nå går Hazelcast tom for minne. Loggene viste at etter et par dager med testing begynte Hazelcast å klage over mangel på hukommelse, og etter en tid falt klyngen fra hverandre, og nodene fortsatte å dø en etter en. Vi koblet JVisualVM til hasselcast og så en "stigende sag" - den kalte regelmessig GC, men kunne ikke tømme minnet.

Hvordan og hvorfor vi skrev en skalerbar tjeneste med høy belastning for 1C: Enterprise: Java, PostgreSQL, Hazelcast

Det viste seg at i hazelcast 3.4, når du sletter et kart / multiMap (map.destroy()), er ikke minnet fullstendig frigjort:

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

Feilen er nå fikset i 3.5, men det var et problem den gang. Vi opprettet nye multikart med dynamiske navn og slettet dem i henhold til vår logikk. Koden så omtrent slik ut:

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

Anrop:

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

multiMap ble opprettet for hvert abonnement og slettet når det ikke var nødvendig. Vi bestemte oss for å starte Map , nøkkelen vil være navnet på abonnementet, og verdiene vil være øktidentifikatorer (hvorfra du kan få brukeridentifikatorer, om nødvendig).

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

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

Kartene har blitt bedre.

Hvordan og hvorfor vi skrev en skalerbar tjeneste med høy belastning for 1C: Enterprise: Java, PostgreSQL, Hazelcast

Hva annet har vi lært om belastningstesting?

  1. JSR223 må skrives i groovy og inkludere kompileringsbuffer - det er mye raskere. Ссылка.
  2. Jmeter-Plugins-grafer er lettere å forstå enn standard. Ссылка.

Om vår erfaring med Hazelcast

Hazelcast var et nytt produkt for oss, vi begynte å jobbe med det fra versjon 3.4.1, nå kjører produksjonsserveren vår versjon 3.9.2 (i skrivende stund er siste versjon av Hazelcast 3.10).

ID generering

Vi startet med heltallsidentifikatorer. La oss forestille oss at vi trenger en annen Long for en ny enhet. Sekvens i databasen er ikke egnet, tabellene er involvert i sharding - det viser seg at det er en melding ID=1 i DB1 og en melding ID=1 i DB2, du kan ikke legge denne IDen i Elasticsearch, og heller ikke i Hazelcast , men det verste er hvis du vil kombinere dataene fra to databaser til én (for eksempel bestemme at én database er nok for disse abonnentene). Du kan legge til flere AtomicLongs til Hazelcast og holde telleren der, så er ytelsen for å få en ny ID incrementAndGet pluss tiden for en forespørsel til Hazelcast. Men Hazelcast har noe mer optimalt – FlakeIdGenerator. Når de kontakter hver klient, får de et ID-område, for eksempel den første – fra 1 til 10 000, den andre – fra 10 001 til 20 000, og så videre. Nå kan klienten utstede nye identifikatorer på egen hånd inntil rekkevidden utstedt til den slutter. Det fungerer raskt, men når du starter applikasjonen på nytt (og Hazelcast-klienten), begynner en ny sekvens - derav hoppene osv. I tillegg forstår ikke utviklerne hvorfor ID-ene er heltall, men er så inkonsekvente. Vi veide alt og gikk over til UUID.

For de som vil være som Twitter, finnes det forresten et slikt Snowcast-bibliotek - dette er en implementering av Snowflake på toppen av Hazelcast. Du kan se den her:

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

Men vi har ikke fått til det lenger.

TransactionalMap.erstatt

En annen overraskelse: TransactionalMap.replace fungerer 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 måtte skrive min egen erstatning ved å bruke 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 bare vanlige datastrukturer, men også deres transaksjonelle versjoner. Det hender at IMap fungerer, men TransactionalMap eksisterer ikke lenger.

Sett inn en ny JAR uten nedetid

Først bestemte vi oss for å spille inn objekter fra klassene våre i Hazelcast. For eksempel har vi en Application-klasse, vi ønsker å lagre og lese den. Lagre:

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

Vi leser:

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

Alt fungerer. Så bestemte vi oss for å bygge en indeks i Hazelcast for å søke etter:

map.addIndex("subscriberId", false);

Og da de skrev en ny enhet, begynte de å motta ClassNotFoundException. Hazelcast prøvde å legge til indeksen, men visste ikke noe om klassen vår og ønsket at en JAR med denne klassen skulle leveres til den. Vi gjorde nettopp det, alt fungerte, men et nytt problem dukket opp: hvordan oppdatere JAR uten å stoppe klyngen helt? Hazelcast plukker ikke opp den nye JAR under en node-for-node-oppdatering. På dette tidspunktet bestemte vi oss for at vi kunne leve uten indekssøk. Tross alt, hvis du bruker Hazelcast som en nøkkelverdibutikk, vil alt fungere? Ikke egentlig. Også her er oppførselen til IMap og TransactionalMap annerledes. Der IMap ikke bryr seg, gir TransactionalMap en feil.

IMap. Vi skriver 5000 gjenstander, les dem. Alt er forventet.

@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 fungerer ikke i en transaksjon, 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 dukket User Class Deployment-mekanismen opp. Du kan angi én hovednode og oppdatere JAR-filen på den.

Nå har vi fullstendig endret tilnærmingen vår: vi serialiserer den selv til JSON og lagrer den i Hazelcast. Hazelcast trenger ikke å kjenne strukturen til klassene våre, og vi kan oppdatere uten nedetid. Versjon av domeneobjekter styres av applikasjonen. Ulike versjoner av applikasjonen kan kjøres samtidig, og en situasjon er mulig når den nye applikasjonen skriver objekter med nye felt, men den gamle vet ikke om disse feltene ennå. Og samtidig leser den nye applikasjonen objekter skrevet av den gamle applikasjonen som ikke har nye felt. Vi håndterer slike situasjoner i applikasjonen, men for enkelhets skyld endrer eller sletter vi ikke felt, vi utvider kun klassene ved å legge til nye felt.

Hvordan vi sikrer høy ytelse

Fire turer til Hazelcast - bra, to til databasen - dårlig

Å gå til hurtigbufferen for data er alltid bedre enn å gå til databasen, men du vil heller ikke lagre ubrukte poster. Vi lar avgjørelsen om hva som skal cache til siste utviklingsstadium. Når den nye funksjonaliteten er kodet, slår vi på logging av alle spørringer i PostgreSQL (log_min_duration_statement til 0) og kjører belastningstesting i 20 minutter. Ved å bruke de innsamlede loggene kan verktøy som pgFouine og pgBadger bygge analytiske rapporter. I rapporter ser vi først og fremst etter trege og hyppige spørringer. For trege spørringer bygger vi en utførelsesplan (EXPLAIN) og vurderer om en slik spørring kan akselereres. Hyppige forespørsler om de samme inndataene passer godt inn i cachen. Vi prøver å holde søk "flate", en tabell per forespørsel.

Utnyttelse

SV som nettjeneste ble satt i drift våren 2017, og som et eget produkt ble SV sluppet i november 2017 (den gang i betaversjonsstatus).

På mer enn ett års drift har det ikke vært noen alvorlige problemer i driften av CB-netttjenesten. Vi overvåker nettjenesten via Zabbix, samle inn og distribuere fra Bamboo.

SV-serverdistribusjonen leveres i form av native pakker: RPM, DEB, MSI. Pluss for Windows tilbyr vi et enkelt installasjonsprogram i form av en enkelt EXE som installerer serveren, Hazelcast og Elasticsearch på én maskin. Vi omtalte først denne versjonen av installasjonen som "demo"-versjonen, men det har nå blitt klart at dette er det mest populære distribusjonsalternativet.

Kilde: www.habr.com

Legg til en kommentar