Kako i zašto smo napisali skalabilnu uslugu visokog opterećenja za 1C: Enterprise: Java, PostgreSQL, Hazelcast

U ovom članku ćemo govoriti o tome kako i zašto smo se razvili Sustav interakcije – mehanizam koji prenosi informacije između klijentskih aplikacija i poslužitelja 1C:Enterprise – od postavljanja zadatka do razmišljanja o arhitekturi i detaljima implementacije.

Sustav interakcije (u daljnjem tekstu SV) je distribuirani sustav za razmjenu poruka otporan na greške sa zajamčenom isporukom. SV je dizajniran kao visokoopterećena usluga s visokom skalabilnošću, dostupna i kao online usluga (koju pruža 1C) i kao proizvod masovne proizvodnje koji se može postaviti na vlastite poslužiteljske objekte.

SV koristi distribuiranu pohranu lješnjak i tražilicu Elasticsearch. Također ćemo govoriti o Javi i kako horizontalno skaliramo PostgreSQL.
Kako i zašto smo napisali skalabilnu uslugu visokog opterećenja za 1C: Enterprise: Java, PostgreSQL, Hazelcast

Formuliranje problema

Kako bismo razjasnili zašto smo stvorili Interaction System, reći ću vam nešto o tome kako funkcionira razvoj poslovnih aplikacija u 1C.

Za početak malo o nama za one koji još ne znaju čime se bavimo :) Radimo tehnološku platformu 1C:Enterprise. Platforma uključuje alat za razvoj poslovnih aplikacija, kao i runtime koji omogućuje rad poslovnih aplikacija u međuplatformskom okruženju.

Razvojna paradigma klijent-poslužitelj

Poslovne aplikacije izrađene na 1C:Enterprise rade na tri razine klijent-poslužitelj arhitektura “DBMS – aplikacijski poslužitelj – klijent”. Kod aplikacije napisan ugrađeni 1C jezik, može se izvršiti na aplikacijskom poslužitelju ili na klijentu. Sav rad s objektima aplikacije (imenici, dokumenti itd.), kao i čitanje i pisanje baze podataka, obavlja se samo na poslužitelju. Na poslužitelju je implementirana i funkcionalnost obrazaca i naredbenog sučelja. Klijent obavlja zaprimanje, otvaranje i prikaz obrazaca, „komunikaciju“ s korisnikom (upozorenja, pitanja...), male kalkulacije u obrascima koji zahtijevaju brzi odgovor (npr. množenje cijene s količinom), rad s lokalnim datotekama, rad s opremom.

U aplikacijskom kodu, zaglavlja procedura i funkcija moraju eksplicitno naznačiti gdje će se kod izvršiti - korištenjem direktiva &AtClient / &AtServer (&AtClient / &AtServer u engleskoj verziji jezika). 1C programeri će me sada ispraviti govoreći da su direktive zapravo više od, ali za nas to sada nije važno.

Možete nazvati kod poslužitelja iz koda klijenta, ali ne možete nazvati kod klijenta iz koda poslužitelja. Ovo je temeljno ograničenje koje smo napravili iz više razloga. Konkretno, zato što kod poslužitelja mora biti napisan na takav način da se izvršava na isti način bez obzira gdje je pozvan - s klijenta ili s poslužitelja. A u slučaju pozivanja poslužiteljskog koda s drugog poslužiteljskog koda, ne postoji klijent kao takav. I zato što bi se tijekom izvršavanja serverskog koda klijent koji ga je pozvao mogao zatvoriti, izaći iz aplikacije i server više ne bi imao koga zvati.

Kako i zašto smo napisali skalabilnu uslugu visokog opterećenja za 1C: Enterprise: Java, PostgreSQL, Hazelcast
Kod koji obrađuje klik na gumb: pozivanje procedure poslužitelja s klijenta će funkcionirati, pozivanje procedure klijenta s poslužitelja neće

To znači da ako želimo poslati neku poruku sa servera klijentskoj aplikaciji, na primjer, da je generiranje “dugotrajnog” izvješća završeno i da se izvješće može pogledati, nemamo takvu metodu. Morate koristiti trikove, na primjer, povremeno anketirati poslužitelj iz koda klijenta. Ali ovaj pristup opterećuje sustav nepotrebnim pozivima i općenito ne izgleda baš elegantno.

A postoji i potreba, na primjer, kad stigne telefonski poziv SIP- prilikom upućivanja poziva o tome obavijestiti klijentsku aplikaciju kako bi ga pomoću broja pozivatelja mogla pronaći u bazi protustrane i prikazati korisniku podatke o sugovorniku koji poziva. Ili, na primjer, kada narudžba stigne u skladište, obavijestite klijentovu aplikaciju o tome. Općenito, postoji mnogo slučajeva u kojima bi takav mehanizam bio koristan.

Sama proizvodnja

Stvorite mehanizam za slanje poruka. Brzo, pouzdano, s zajamčenom dostavom, s mogućnošću fleksibilnog pretraživanja poruka. Na temelju mehanizma implementirajte messenger (poruke, video pozive) koji radi unutar 1C aplikacija.

Dizajnirajte sustav tako da bude horizontalno skalabilan. Sve veće opterećenje mora se pokriti povećanjem broja čvorova.

Provedba

Odlučili smo ne integrirati poslužiteljski dio SV-a izravno u platformu 1C:Enterprise, već ga implementirati kao zaseban proizvod, čiji se API može pozvati iz koda 1C aplikacijskih rješenja. To je učinjeno iz više razloga, od kojih je glavni bio taj što sam želio omogućiti razmjenu poruka između različitih 1C aplikacija (na primjer, između upravljanja trgovinom i računovodstva). Različite 1C aplikacije mogu se izvoditi na različitim verzijama platforme 1C:Enterprise, nalaziti se na različitim poslužiteljima itd. U takvim uvjetima implementacija SV-a kao zasebnog proizvoda smještenog “sa strane” 1C instalacija je optimalno rješenje.

Stoga smo odlučili napraviti SV kao zaseban proizvod. Malim tvrtkama preporučujemo korištenje CB poslužitelja koji smo instalirali u našem oblaku (wss://1cdialog.com) kako bi izbjegli režijske troškove povezane s lokalnom instalacijom i konfiguracijom poslužitelja. Velikim klijentima može biti preporučljivo instalirati vlastiti CB poslužitelj u svojim objektima. Koristili smo sličan pristup u našem cloud SaaS proizvodu 1cSvježe – proizvodi se kao proizvod masovne proizvodnje za instalaciju na lokacijama klijenata, a također je postavljen u našem oblaku https://1cfresh.com/.

primjena

Kako bismo rasporedili opterećenje i toleranciju na greške, nećemo implementirati jednu Java aplikaciju, već nekoliko, s balanserom opterećenja ispred njih. Ako trebate prenijeti poruku s čvora na čvor, upotrijebite objavljivanje/pretplatu u Hazelcastu.

Komunikacija između klijenta i poslužitelja odvija se putem websocketa. Vrlo je prikladan za sustave u stvarnom vremenu.

Distribuirana predmemorija

Birali smo između Redisa, Hazelcasta i Ehcachea. 2015. je. Redis je upravo izbacio novi klaster (previše nov, zastrašujući), tu je Sentinel s puno ograničenja. Ehcache ne zna sastaviti u klaster (ta se funkcionalnost pojavila kasnije). Odlučili smo isprobati s Hazelcastom 3.4.
Hazelcast je odmah sastavljen u klaster. U načinu rada s jednim čvorom nije baš koristan i može se koristiti samo kao predmemorija - ne zna kako izbaciti podatke na disk, ako izgubite jedini čvor, izgubit ćete podatke. Postavljamo nekoliko Hazelcastova, među kojima radimo sigurnosnu kopiju kritičnih podataka. Ne stvaramo sigurnosnu kopiju predmemorije - ne smeta nam.

Za nas je Hazelcast:

  • Pohrana korisničkih sesija. Odlazak u bazu podataka za sesiju svaki put traje dugo, pa smo sve sesije stavili u Hazelcast.
  • Predmemorija. Ako tražite korisnički profil, provjerite predmemoriju. Napisao novu poruku - stavi je u predmemoriju.
  • Teme za komunikaciju između instanci aplikacije. Čvor generira događaj i postavlja ga u temu Hazelcast. Drugi čvorovi aplikacije pretplaćeni na ovu temu primaju i obrađuju događaj.
  • Klaster brave. Na primjer, kreiramo raspravu pomoću jedinstvenog ključa (singleton rasprava unutar 1C baze podataka):

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

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

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

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

Provjerili smo da nema kanala. Uzeli smo bravu, ponovno je provjerili i stvorili. Ako ne provjerite zaključavanje nakon preuzimanja zaključavanja, onda postoji mogućnost da je druga nit također provjerila u tom trenutku i sada će pokušati stvoriti istu raspravu - ali ona već postoji. Ne možete zaključati pomoću sinkroniziranog ili uobičajenog java Lock-a. Preko baze podataka - sporo je i šteta je za bazu podataka; kroz Hazelcast - to je ono što vam treba.

Odabir DBMS-a

Imamo opsežno i uspješno iskustvo u radu s PostgreSQL-om i suradnji s programerima ovog DBMS-a.

Nije lako s PostgreSQL klasterom - postoji XL, XC, Citus, ali općenito to nisu NoSQL-ovi koji se skaliraju izvan okvira. NoSQL nismo smatrali glavnom pohranom, dovoljno je bilo da smo uzeli Hazelcast s kojim prije nismo radili.

Ako trebate skalirati relacijsku bazu podataka, to znači usitnjavanje. Kao što znate, shardingom dijelimo bazu podataka na zasebne dijelove tako da se svaki od njih može postaviti na poseban poslužitelj.

Prva verzija našeg dijeljenja pretpostavljala je mogućnost distribucije svake tablice naše aplikacije na različite poslužitelje u različitim omjerima. Puno je poruka na poslužitelju A – molim vas, premjestimo dio ove tablice na poslužitelj B. Ova odluka je jednostavno vrištala o preuranjenoj optimizaciji, pa smo se odlučili ograničiti na multi-tenant pristup.

Možete pročitati o multi-tenantu, na primjer, na web stranici Podaci o citusima.

SV ima pojmove aplikacije i pretplatnika. Aplikacija je posebna instalacija poslovne aplikacije, poput ERP-a ili računovodstva, sa svojim korisnicima i poslovnim podacima. Pretplatnik je organizacija ili pojedinac u čije ime je aplikacija registrirana na SV poslužitelju. Pretplatnik može imati registrirano više aplikacija, a te aplikacije mogu međusobno razmjenjivati ​​poruke. Pretplatnik je postao stanar u našem sustavu. Poruke od nekoliko pretplatnika mogu se nalaziti u jednoj fizičkoj bazi podataka; ako vidimo da je pretplatnik počeo generirati puno prometa, premještamo ga u zasebnu fizičku bazu podataka (ili čak na zasebni poslužitelj baze podataka).

Imamo glavnu bazu podataka u kojoj je pohranjena tablica usmjeravanja s informacijama o lokaciji svih baza podataka pretplatnika.

Kako i zašto smo napisali skalabilnu uslugu visokog opterećenja za 1C: Enterprise: Java, PostgreSQL, Hazelcast

Kako bismo spriječili da glavna baza podataka bude usko grlo, tablicu usmjeravanja (i druge podatke koji su često potrebni) čuvamo u predmemoriji.

Ako baza podataka pretplatnika počne usporavati, izrezat ćemo je na particije iznutra. Na drugim projektima koje koristimo pg_pathman.

Budući da je gubitak korisničkih poruka loš, održavamo naše baze podataka s replikama. Kombinacija sinkronih i asinkronih replika omogućuje vam da se osigurate u slučaju gubitka glavne baze podataka. Gubitak poruke dogodit će se samo ako primarna baza podataka i njezina sinkrona replika zakažu istovremeno.

Ako se sinkrona replika izgubi, asinkrona replika postaje sinkrona.
Ako se glavna baza podataka izgubi, sinkrona replika postaje glavna baza podataka, a asinkrona replika postaje sinkrona replika.

Elasticsearch za pretraživanje

Budući da je, između ostalog, SV i glasnik, zahtijeva brzo, praktično i fleksibilno pretraživanje, uzimajući u obzir morfologiju, korištenjem nepreciznih podudaranja. Odlučili smo ne izmišljati kotač i koristiti besplatnu tražilicu Elasticsearch, stvorenu na temelju biblioteke Lucen. Također implementiramo Elasticsearch u klasteru (master – podaci – podaci) kako bismo uklonili probleme u slučaju kvara aplikacijskih čvorova.

Na githubu smo pronašli Ruski morfološki dodatak za Elasticsearch i koristite ga. U indeksu Elasticsearch pohranjujemo korijene riječi (koje određuje plugin) i N-grame. Dok korisnik upisuje tekst za pretraživanje, tražimo upisani tekst među N-gramima. Kada se spremi u indeks, riječ "tekstovi" bit će podijeljena u sljedeće N-grame:

[oni, tek, tex, tekst, tekstovi, ek, bivši, ext, tekstovi, ks, kst, ksty, st, sty, ti],

I korijen riječi "tekst" će također biti sačuvan. Ovaj vam pristup omogućuje pretraživanje na početku, u sredini i na kraju riječi.

Velika slika

Kako i zašto smo napisali skalabilnu uslugu visokog opterećenja za 1C: Enterprise: Java, PostgreSQL, Hazelcast
Ponavljanje slike s početka članka, ali uz objašnjenja:

  • Balancer razotkriven na internetu; imamo nginx, može biti bilo koji.
  • Instance Java aplikacija međusobno komuniciraju putem Hazelcasta.
  • Za rad s web socketom koristimo Netty.
  • Java aplikacija je napisana u Javi 8 i sastoji se od paketa OSGi. Planovi uključuju migraciju na Javu 10 i prelazak na module.

Razvoj i testiranje

U procesu razvoja i testiranja SV-a naišli smo na niz zanimljivih značajki proizvoda koje koristimo.

Testiranje opterećenja i curenje memorije

Izdanje svakog SV izdanja uključuje testiranje opterećenja. Uspješan je kada:

  • Test je radio nekoliko dana i nije bilo kvarova na servisu
  • Vrijeme odziva za ključne operacije nije premašilo ugodan prag
  • Pogoršanje performansi u usporedbi s prethodnom verzijom nije veće od 10%

Ispunjavamo testnu bazu podataka - da bismo to učinili, primamo informacije o najaktivnijem pretplatniku s produkcijskog poslužitelja, množimo njegove brojeve s 5 (broj poruka, rasprava, korisnika) i na taj način testiramo.

Provodimo testiranje opterećenja interakcijskog sustava u tri konfiguracije:

  1. stres test
  2. Samo veze
  3. Registracija pretplatnika

Tijekom stres testa pokrećemo nekoliko stotina niti, a one učitavaju sustav bez prestanka: pisanje poruka, kreiranje rasprava, primanje liste poruka. Simuliramo radnje običnih korisnika (dobijte popis mojih nepročitanih poruka, pišite nekome) i softverska rješenja (prenesite paket drugačije konfiguracije, obradite upozorenje).

Na primjer, ovako izgleda dio stres testa:

  • Korisnik se prijavljuje
    • Zahtijeva vaše nepročitane rasprave
    • 50% vjerojatnosti da će pročitati poruke
    • 50% vjerojatnosti slanja poruke
    • Sljedeći korisnik:
      • Ima 20% šanse za stvaranje nove rasprave
      • Nasumično odabire bilo koju od svojih rasprava
      • Uđe unutra
      • Poruke zahtjeva, korisnički profili
      • Iz ove rasprave stvara pet poruka upućenih nasumičnim korisnicima
      • Napušta raspravu
      • Ponavlja se 20 puta
      • Odjavljuje se, vraća se na početak skripte

    • Chatbot ulazi u sustav (emulira slanje poruka iz koda aplikacije)
      • Ima 50% šanse za stvaranje novog kanala za razmjenu podataka (posebna rasprava)
      • 50% vjerojatnosti da će napisati poruku na bilo koji od postojećih kanala

Scenarij "Samo veze" pojavio se s razlogom. Postoji situacija: korisnici su spojili sustav, ali se još nisu uključili. Svaki korisnik uključuje računalo u 09:00 ujutro, uspostavlja vezu sa serverom i šuti. Ovi tipovi su opasni, ima ih mnogo - jedini paketi koje imaju su PING/PONG, ali održavaju vezu sa serverom (ne mogu je održati - što ako dođe nova poruka). Test reproducira situaciju u kojoj se velik broj takvih korisnika pokušava ulogirati u sustav u pola sata. Sličan je stres testu, ali mu je fokus upravo na ovom prvom inputu - da ne bude kvarova (čovjek ne koristi sustav, a on već padne - teško je smisliti nešto gore).

Skripta za registraciju pretplatnika počinje od prvog pokretanja. Proveli smo stres test i bili smo sigurni da sustav nije usporio tijekom dopisivanja. Ali korisnici su došli i registracija je počela propadati zbog isteka vremena. Prilikom registracije koristili smo / Dev / random, što je povezano s entropijom sustava. Poslužitelj nije imao vremena akumulirati dovoljno entropije i kada je zatražen novi SecureRandom, zamrznuo se na desetke sekundi. Postoji mnogo izlaza iz ove situacije, na primjer: prijeđite na manje siguran /dev/urandom, instalirajte posebnu ploču koja generira entropiju, generirajte nasumične brojeve unaprijed i pohranite ih u bazen. Privremeno smo zatvorili problem s bazenom, ali od tada provodimo poseban test za registraciju novih pretplatnika.

Koristimo kao generator opterećenja JMeter. Ne zna raditi s websocketom; treba mu dodatak. Prvi u rezultatima pretraživanja za upit “jmeter websocket” su: članci iz BlazeMetera, koji preporučuju dodatak Maciej Zaleski.

Tu smo odlučili početi.

Gotovo odmah nakon početka ozbiljnog testiranja, otkrili smo da je JMeter počeo gubiti memoriju.

Dodatak je posebna velika priča; sa 176 zvjezdica ima 132 forka na githubu. Sam autor nije se obvezao na to od 2015. (preuzeli smo ga 2015., tada nije izazvalo sumnju), nekoliko problema s githubom u vezi s curenjem memorije, 7 nezatvorenih zahtjeva za povlačenjem.
Ako odlučite izvršiti testiranje opterećenja pomoću ovog dodatka, obratite pozornost na sljedeće rasprave:

  1. U okruženju s više niti korišten je obični LinkedList, a rezultat je bio NPE u vremenu izvođenja. To se može riješiti prebacivanjem na ConcurrentLinkedDeque ili sinkroniziranim blokovima. Za sebe smo odabrali prvu opciju (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. Curenje memorije; prilikom prekida veze informacije o vezi se ne brišu (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. U načinu strujanja (kada websocket nije zatvoren na kraju uzorka, nego se koristi kasnije u planu), obrasci odgovora ne rade (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

Ovo je jedan od onih na githubu. Što smo učinili:

  1. uzeti vilica Elyran Kogan (@elyrank) – rješava probleme 1 i 3
  2. Riješen problem 2
  3. Ažurirano pristanište od 9.2.14. do 9.3.12
  4. Zamotan SimpleDateFormat u ThreadLocal; SimpleDateFormat nije siguran za niti, što je dovelo do NPE-a tijekom izvođenja
  5. Popravljeno još jedno curenje memorije (veza je bila pogrešno zatvorena kada je prekinuta)

A ipak teče!

Memorija je počela nestati za dan, već za dva. Vremena više nije bilo, pa smo odlučili pokrenuti manje tema, ali na četiri agenta. Ovo je trebalo biti dovoljno za barem tjedan dana.

Prošla su dva dana...

Sada Hazelcastu ponestaje memorije. Dnevnici su pokazali da se nakon nekoliko dana testiranja Hazelcast počeo žaliti na nedostatak memorije, a nakon nekog vremena klaster se raspao, a čvorovi su nastavili umirati jedan po jedan. Spojili smo JVisualVM na hazelcast i vidjeli "uspinjuću pilu" - redovito je pozivao GC, ali nije mogao očistiti memoriju.

Kako i zašto smo napisali skalabilnu uslugu visokog opterećenja za 1C: Enterprise: Java, PostgreSQL, Hazelcast

Ispostavilo se da u hazelcastu 3.4, prilikom brisanja karte / multiMap (map.destroy()), memorija nije potpuno oslobođena:

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

Greška je sada ispravljena u verziji 3.5, ali tada je to bio problem. Stvorili smo nove multikarte s dinamičkim imenima i izbrisali ih prema našoj logici. Kod je izgledao otprilike ovako:

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

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

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

Vyzov:

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

multiMap je stvoren za svaku pretplatu i izbrisan kada nije bio potreban. Odlučili smo da ćemo pokrenuti Map , ključ će biti naziv pretplate, a vrijednosti će biti identifikatori sesije (iz kojih zatim možete dobiti korisničke identifikatore, ako je potrebno).

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

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

Karte su se poboljšale.

Kako i zašto smo napisali skalabilnu uslugu visokog opterećenja za 1C: Enterprise: Java, PostgreSQL, Hazelcast

Što smo još naučili o testiranju opterećenja?

  1. JSR223 treba biti napisan u groovyju i uključiti predmemoriju kompilacije - mnogo je brži. Link.
  2. Jmeter-Plugins grafikone lakše je razumjeti od standardnih. Link.

O našem iskustvu s Hazelcastom

Hazelcast je za nas bio novi proizvod, počeli smo raditi s njim od verzije 3.4.1, sada naš produkcijski poslužitelj pokreće verziju 3.9.2 (u vrijeme pisanja, najnovija verzija Hazelcasta je 3.10).

ID generacija

Počeli smo s cjelobrojnim identifikatorima. Zamislimo da nam treba još jedan Long za novi entitet. Redoslijed u bazi podataka nije prikladan, tablice su uključene u dijeljenje - ispada da postoji poruka ID=1 u DB1 i poruka ID=1 u DB2, ne možete staviti ovaj ID u Elasticsearch, niti u Hazelcast , ali najgore je ako podatke iz dvije baze želite spojiti u jednu (npr. odlučiti da je tim pretplatnicima dovoljna jedna baza). Možete dodati nekoliko AtomicLongova u Hazelcast i tamo držati brojač, tada je izvedba dobivanja novog ID-a incrementAndGet plus vrijeme za zahtjev Hazelcastu. Ali Hazelcast ima nešto optimalnije - FlakeIdGenerator. Prilikom kontaktiranja svakog klijenta dobivaju raspon ID-a, na primjer, prvi – od 1 do 10, drugi – od 000 do 10 i tako dalje. Sada klijent može samostalno izdavati nove identifikatore dok ne završi raspon koji mu je dodan. Radi brzo, ali kada ponovno pokrenete aplikaciju (i Hazelcast klijent) počinje novi niz - otud preskakanja itd. Osim toga, programeri zapravo ne razumiju zašto su ID-ovi cijeli brojevi, ali su toliko nedosljedni. Sve smo izvagali i prešli na UUID-ove.

Usput, za one koji žele biti poput Twittera, postoji takva biblioteka Snowcast - ovo je implementacija Snowflake na vrhu Hazelcasta. Možete ga pogledati ovdje:

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

Ali više nismo stigli do toga.

TransactionalMap.zamijeniti

Još jedno iznenađenje: TransactionalMap.replace ne radi. Evo testa:

@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

Morao sam napisati vlastitu zamjenu koristeći 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);
}

Testirajte ne samo uobičajene strukture podataka, već i njihove transakcijske verzije. Događa se da IMap radi, ali TransactionalMap više ne postoji.

Umetnite novi JAR bez zastoja

Prvo smo odlučili snimiti objekte naših razreda u Hazelcastu. Na primjer, imamo klasu Application, želimo je spremiti i pročitati. Uštedjeti:

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

Čitamo:

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

Sve radi. Zatim smo odlučili napraviti indeks u Hazelcastu za pretraživanje prema:

map.addIndex("subscriberId", false);

I kada su pisali novi entitet, počeli su dobivati ​​ClassNotFoundException. Hazelcast je pokušao dodati u indeks, ali nije znao ništa o našoj klasi i želio je da mu se dostavi JAR s ovom klasom. Upravo smo to i učinili, sve je radilo, ali pojavio se novi problem: kako ažurirati JAR bez potpunog zaustavljanja klastera? Hazelcast ne preuzima novi JAR tijekom ažuriranja čvor po čvor. U ovom smo trenutku odlučili da možemo živjeti bez pretraživanja indeksa. Uostalom, ako koristite Hazelcast kao ključ-vrijednost pohranu, onda će sve raditi? Ne baš. I ovdje se ponašanje IMap-a i TransactionalMapa razlikuje. Gdje IMap nije briga, TransactionalMap izbacuje pogrešku.

IMap. Napišemo 5000 objekata, pročitamo ih. Sve je očekivano.

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

Ali to ne radi u transakciji, dobivamo 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();
        }
    });
}

U 3.8 se pojavio mehanizam za implementaciju korisničke klase. Možete odrediti jedan glavni čvor i ažurirati JAR datoteku na njemu.

Sada smo potpuno promijenili pristup: sami ga serijaliziramo u JSON i spremamo u Hazelcast. Hazelcast ne mora znati strukturu naših klasa i možemo ažurirati bez prekida. Određivanje verzija objekata domene kontrolira aplikacija. Različite verzije aplikacije mogu raditi istovremeno, a moguća je situacija da nova aplikacija ispisuje objekte s novim poljima, a stara još ne zna za ta polja. U isto vrijeme, nova aplikacija čita objekte koje je napisala stara aplikacija i koji nemaju nova polja. Takve situacije rješavamo unutar aplikacije, ali radi jednostavnosti ne mijenjamo niti brišemo polja, već samo proširujemo klase dodavanjem novih polja.

Kako osiguravamo visok učinak

Četiri putovanja u Hazelcast - dobro, dva u bazu podataka - loše

Odlazak u predmemoriju po podatke uvijek je bolji od odlaska u bazu podataka, ali ne želite ni pohranjivati ​​neiskorištene zapise. Odluku o tome što predmemorirati ostavljamo za posljednju fazu razvoja. Kada je nova funkcionalnost kodirana, uključujemo bilježenje svih upita u PostgreSQL (log_min_duration_statement na 0) i pokrećemo testiranje opterećenja na 20 minuta. Koristeći prikupljene zapise, pomoćni programi kao što su pgFouine i pgBadger mogu izraditi analitička izvješća. U izvješćima prvenstveno tražimo spore i česte upite. Za spore upite gradimo plan izvršenja (OBJAŠNIMO) i procjenjujemo može li se takav upit ubrzati. Česti zahtjevi za istim ulaznim podacima dobro se uklapaju u predmemoriju. Trudimo se da upiti budu "ravni", jedna tablica po upitu.

Eksploatacija

SV kao online servis pušten je u rad u proljeće 2017., a kao zaseban proizvod SV je pušten u prodaju u studenom 2017. (tada u statusu beta verzije).

U više od godinu dana rada nije bilo ozbiljnijih problema u radu CB online usluge. Pratimo online uslugu putem Zabbix, prikupiti i rasporediti iz Bambus.

Distribucija SV poslužitelja isporučuje se u obliku izvornih paketa: RPM, DEB, MSI. Plus za Windows nudimo jedan instalacijski program u obliku jednog EXE koji instalira poslužitelj, Hazelcast i Elasticsearch na jednom stroju. U početku smo ovu verziju instalacije nazivali "demo" verzijom, ali sada je postalo jasno da je to najpopularnija opcija implementacije.

Izvor: www.habr.com

Dodajte komentar