Cum și de ce am scris un serviciu scalabil de mare încărcare pentru 1C: Enterprise: Java, PostgreSQL, Hazelcast

În acest articol vom vorbi despre cum și de ce ne-am dezvoltat Sistem de interacțiune – un mecanism care transferă informații între aplicațiile client și serverele 1C:Enterprise - de la stabilirea unei sarcini până la gândirea la detaliile arhitecturii și implementării.

Sistemul de interacțiune (denumit în continuare SV) este un sistem de mesagerie distribuit, tolerant la erori, cu livrare garantată. SV este conceput ca un serviciu de mare sarcină cu scalabilitate ridicată, disponibil atât ca serviciu online (furnizat de 1C), cât și ca produs produs în masă, care poate fi implementat pe propriile instalații de server.

SV folosește stocarea distribuită hazelcast si motorul de cautare Elasticsearch. Vom vorbi, de asemenea, despre Java și despre modul în care scalam pe orizontală PostgreSQL.
Cum și de ce am scris un serviciu scalabil de mare încărcare pentru 1C: Enterprise: Java, PostgreSQL, Hazelcast

Declarație de problemă

Pentru a clarifica de ce am creat Sistemul de interacțiune, vă voi spune puțin despre cum funcționează dezvoltarea aplicațiilor de afaceri în 1C.

Pentru început, puțin despre noi pentru cei care încă nu știu ce facem :) Facem platforma tehnologică 1C:Enterprise. Platforma include un instrument de dezvoltare a aplicațiilor de afaceri, precum și un timp de execuție care permite aplicațiilor de afaceri să ruleze într-un mediu multiplatform.

Paradigma de dezvoltare client-server

Aplicațiile de afaceri create pe 1C:Enterprise funcționează pe trei niveluri client server arhitectura „DBMS – server de aplicații – client”. Codul aplicației scris în limbaj 1C încorporat, poate fi executat pe serverul de aplicații sau pe client. Toate lucrările cu obiectele aplicației (directoare, documente etc.), precum și citirea și scrierea bazei de date, se efectuează numai pe server. Pe server sunt implementate și funcționalitatea formularelor și a interfeței de comandă. Clientul efectuează primirea, deschiderea și afișarea formularelor, „comunicarea” cu utilizatorul (avertismente, întrebări...), mici calcule în formulare care necesită un răspuns rapid (de exemplu, înmulțirea prețului cu cantitatea), lucrul cu fișiere locale, lucrul cu echipamente.

În codul aplicației, anteturile de proceduri și funcții trebuie să indice în mod explicit unde va fi executat codul - folosind directivele &AtClient / &AtServer (&AtClient / &AtServer în versiunea engleză a limbii). Dezvoltatorii 1C mă vor corecta acum spunând că directivele sunt de fapt mai mult decât, dar pentru noi acest lucru nu este important acum.

Puteți apela codul de server din codul de client, dar nu puteți apela codul de client din codul de server. Aceasta este o limitare fundamentală pe care am făcut-o din mai multe motive. În special, deoarece codul serverului trebuie scris în așa fel încât să se execute în același mod, indiferent de unde este apelat - de la client sau de pe server. Și în cazul apelării codului de server de la un alt cod de server, nu există client ca atare. Și pentru că în timpul execuției codului de server, clientul care l-a sunat se putea închide, ieși din aplicație, iar serverul nu ar mai avea pe cine să sune.

Cum și de ce am scris un serviciu scalabil de mare încărcare pentru 1C: Enterprise: Java, PostgreSQL, Hazelcast
Cod care gestionează un clic pe buton: apelarea unei proceduri de server de la client va funcționa, apelarea unei proceduri client de pe server nu va funcționa

Aceasta înseamnă că dacă vrem să trimitem un mesaj de pe server către aplicația client, de exemplu, că generarea unui raport „de lungă durată” s-a terminat și raportul poate fi vizualizat, nu avem o astfel de metodă. Trebuie să folosiți trucuri, de exemplu, să interogeți periodic serverul din codul clientului. Dar această abordare încarcă sistemul cu apeluri inutile și, în general, nu arată foarte elegant.

Și există și o nevoie, de exemplu, când sosește un apel telefonic SIP- la efectuarea unui apel, notificați aplicația client despre acest lucru, astfel încât să poată utiliza numărul apelantului pentru a-l găsi în baza de date a contrapartei și a arăta utilizatorului informații despre contrapartea care apelează. Sau, de exemplu, când o comandă ajunge la depozit, notificați aplicația client a clientului despre acest lucru. În general, există multe cazuri în care un astfel de mecanism ar fi util.

Producția în sine

Creați un mecanism de mesagerie. Rapid, de încredere, cu livrare garantată, cu capacitatea de a căuta în mod flexibil mesajele. Pe baza mecanismului, implementați un messenger (mesaje, apeluri video) care rulează în cadrul aplicațiilor 1C.

Proiectați sistemul pentru a fi scalabil pe orizontală. Sarcina în creștere trebuie acoperită prin creșterea numărului de noduri.

punerea în aplicare

Am decis să nu integrăm partea de server a SV direct în platforma 1C:Enterprise, ci să o implementăm ca produs separat, al cărui API poate fi apelat din codul soluțiilor de aplicație 1C. Acest lucru a fost făcut din mai multe motive, dintre care principalul a fost că am vrut să fac posibil schimbul de mesaje între diferite aplicații 1C (de exemplu, între Managementul Comerțului și Contabilitate). Diferite aplicații 1C pot rula pe diferite versiuni ale platformei 1C:Enterprise, pot fi localizate pe servere diferite etc. În astfel de condiții, implementarea SV ca produs separat situat „pe partea” instalațiilor 1C este soluția optimă.

Deci, am decis să facem SV ca produs separat. Recomandăm companiilor mici să folosească serverul CB pe care l-am instalat în cloud (wss://1cdialog.com) pentru a evita costurile generale asociate instalării și configurației locale a serverului. Clienții mari pot considera că este recomandabil să-și instaleze propriul server CB la unitățile lor. Am folosit o abordare similară în produsul nostru cloud SaaS 1cProaspăt – este produs ca produs de serie pentru instalare pe site-urile clienților și este, de asemenea, implementat în cloud-ul nostru https://1cfresh.com/.

App

Pentru a distribui toleranța la încărcare și la eroare, vom implementa nu o aplicație Java, ci mai multe, cu un echilibrator de încărcare în fața lor. Dacă trebuie să transferați un mesaj de la nod la nod, utilizați publish/subscribe în Hazelcast.

Comunicarea dintre client și server se face prin websocket. Este foarte potrivit pentru sistemele în timp real.

Cache distribuită

Am ales între Redis, Hazelcast și Ehcache. Este 2015. Redis tocmai a lansat un nou cluster (prea nou, înfricoșător), există Sentinel cu o mulțime de restricții. Ehcache nu știe cum să se asambleze într-un cluster (această funcționalitate a apărut mai târziu). Am decis să-l încercăm cu Hazelcast 3.4.
Hazelcast este asamblat într-un grup din cutie. În modul cu un singur nod, nu este foarte util și poate fi folosit doar ca cache - nu știe cum să dump datele pe disc, dacă pierzi singurul nod, pierzi datele. Implementăm mai multe Hazelcast, între care facem backup pentru datele critice. Nu facem copii de rezervă ale memoriei cache - nu ne deranjează.

Pentru noi, Hazelcast este:

  • Stocarea sesiunilor utilizator. Este nevoie de mult timp pentru a merge la baza de date pentru o sesiune de fiecare dată, așa că punem toate sesiunile în Hazelcast.
  • Cache. Dacă sunteți în căutarea unui profil de utilizator, verificați memoria cache. Am scris un mesaj nou - puneți-l în cache.
  • Subiecte pentru comunicarea între instanțe de aplicație. Nodul generează un eveniment și îl plasează în subiectul Hazelcast. Alte noduri de aplicație abonate la acest subiect primesc și procesează evenimentul.
  • Blocări de grup. De exemplu, creăm o discuție folosind o cheie unică (discuție unică în baza de date 1C):

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

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

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

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

Am verificat că nu există canal. Am luat lacătul, l-am verificat din nou și l-am creat. Dacă nu verificați blocarea după ce ați luat blocarea, atunci există șansa ca un alt thread să fi verificat și în acel moment și să încerce acum să creeze aceeași discuție - dar există deja. Nu puteți bloca folosind blocarea java sincronizată sau obișnuită. Prin baza de date - este lent și este păcat pentru baza de date; prin Hazelcast - de asta aveți nevoie.

Alegerea unui SGBD

Avem o experiență vastă și de succes de lucru cu PostgreSQL și de colaborare cu dezvoltatorii acestui DBMS.

Nu este ușor cu un cluster PostgreSQL - există XL, XC, Citus, dar în general acestea nu sunt NoSQL-uri care se extind din cutie. Nu am considerat NoSQL ca stocare principală; a fost suficient să luăm Hazelcast, cu care nu lucrasem înainte.

Dacă trebuie să scalați o bază de date relațională, asta înseamnă fragmentare. După cum știți, cu sharding împărțim baza de date în părți separate, astfel încât fiecare dintre ele să poată fi plasată pe un server separat.

Prima versiune a fragmentării noastre presupunea capacitatea de a distribui fiecare dintre tabelele aplicației noastre pe servere diferite în proporții diferite. Există o mulțime de mesaje pe serverul A - vă rog, să mutăm o parte din acest tabel pe serverul B. Această decizie pur și simplu a țipat despre optimizarea prematură, așa că am decis să ne limităm la o abordare multi-chiriași.

Puteți citi despre multi-chiriaș, de exemplu, pe site Citus Data.

SV are conceptele de aplicație și abonat. O aplicație este o instalare specifică a unei aplicații de afaceri, cum ar fi ERP sau Contabilitate, cu utilizatorii și datele sale de afaceri. Un abonat este o organizație sau persoană fizică în numele căreia aplicația este înregistrată pe serverul SV. Un abonat poate avea mai multe aplicații înregistrate, iar aceste aplicații pot face schimb de mesaje între ele. Abonatul a devenit chiriaș în sistemul nostru. Mesajele de la mai mulți abonați pot fi localizate într-o bază de date fizică; dacă vedem că un abonat a început să genereze mult trafic, îl mutăm într-o bază de date fizică separată (sau chiar un server de bază de date separat).

Avem o bază de date principală în care este stocat un tabel de rutare cu informații despre locația tuturor bazelor de date abonaților.

Cum și de ce am scris un serviciu scalabil de mare încărcare pentru 1C: Enterprise: Java, PostgreSQL, Hazelcast

Pentru a preveni ca baza de date principală să fie un blocaj, păstrăm tabelul de rutare (și alte date necesare frecvent) într-un cache.

Dacă baza de date a abonatului începe să încetinească, o vom tăia în partiții în interior. În alte proiecte pe care le folosim pg_pathman.

Deoarece pierderea mesajelor utilizatorilor este rău, ne menținem bazele de date cu replici. Combinația de replici sincrone și asincrone vă permite să vă asigurați în caz de pierdere a bazei de date principale. Pierderea mesajului va avea loc numai dacă baza de date primară și replica sa sincronă eșuează simultan.

Dacă o replică sincronă este pierdută, replica asincronă devine sincronă.
Dacă baza de date principală este pierdută, replica sincronă devine baza de date principală, iar replica asincronă devine o replică sincronă.

Elasticsearch pentru căutare

Întrucât, printre altele, SV este și un mesager, necesită o căutare rapidă, comodă și flexibilă, ținând cont de morfologie, folosind potriviri imprecise. Am decis să nu reinventăm roata și să folosim motorul de căutare gratuit Elasticsearch, creat pe baza bibliotecii Lucene. De asemenea, implementăm Elasticsearch într-un cluster (master – date – date) pentru a elimina problemele în cazul eșecului nodurilor aplicației.

Pe github am găsit Plugin de morfologie rusă pentru Elasticsearch și folosiți-l. În indexul Elasticsearch stocăm rădăcinile cuvintelor (pe care pluginul le determină) și N-grame. Pe măsură ce utilizatorul introduce text pentru a căuta, căutăm textul tastat printre N-grame. Când este salvat în index, cuvântul „texte” va fi împărțit în următoarele N-grame:

[cele, tek, tex, text, textes, ek, ex, ext, texts, ks, kst, ksty, st, sty, you],

Și rădăcina cuvântului „text” va fi, de asemenea, păstrată. Această abordare vă permite să căutați la începutul, la mijloc și la sfârșitul cuvântului.

Imaginea de ansamblu

Cum și de ce am scris un serviciu scalabil de mare încărcare pentru 1C: Enterprise: Java, PostgreSQL, Hazelcast
Repetă imaginea de la începutul articolului, dar cu explicații:

  • Balancer expus pe internet; avem nginx, poate fi oricare.
  • Instanțele aplicației Java comunică între ele prin Hazelcast.
  • Pentru a lucra cu un socket web pe care îl folosim Netty.
  • Aplicația Java este scrisă în Java 8 și constă din pachete OSGi. Planurile includ migrarea la Java 10 și tranziția la module.

Dezvoltare și testare

În procesul de dezvoltare și testare a SV, am întâlnit o serie de caracteristici interesante ale produselor pe care le folosim.

Testare de încărcare și scurgeri de memorie

Lansarea fiecărei lansări SV implică testarea sarcinii. Are succes atunci când:

  • Testul a funcționat timp de câteva zile și nu au existat erori de service
  • Timpul de răspuns pentru operațiunile cheie nu a depășit un prag confortabil
  • Deteriorarea performanței în comparație cu versiunea anterioară nu depășește 10%

Umplem baza de date de testare cu date - pentru a face acest lucru, primim informații despre cel mai activ abonat de la serverul de producție, înmulțim numerele acestuia cu 5 (numărul de mesaje, discuții, utilizatori) și îl testăm astfel.

Efectuăm testarea de încărcare a sistemului de interacțiune în trei configurații:

  1. test de stres
  2. Doar conexiuni
  3. Înregistrarea abonatului

În timpul testului de stres, lansăm câteva sute de fire, iar acestea încarcă sistemul fără oprire: scrierea mesajelor, crearea discuțiilor, primirea unei liste de mesaje. Simulăm acțiunile utilizatorilor obișnuiți (obține o listă cu mesajele mele necitite, scriem cuiva) și soluții software (transmite un pachet cu o altă configurație, procesează o alertă).

De exemplu, așa arată o parte a testului de stres:

  • Utilizatorul se conectează
    • Solicită discuțiile dvs. necitite
    • 50% probabilitatea de a citi mesaje
    • 50% probabilitate să trimită mesaje
    • Următorul utilizator:
      • Are 20% șanse de a crea o nouă discuție
      • Selectează aleatoriu oricare dintre discuțiile sale
      • Intră înăuntru
      • Solicitari mesaje, profiluri de utilizator
      • Creează cinci mesaje adresate unor utilizatori aleatori din această discuție
      • Părăsește discuția
      • Se repetă de 20 de ori
      • Se deconectează, se întoarce la începutul scriptului

    • Un chatbot intră în sistem (emulează mesajele din codul aplicației)
      • Are 50% șanse de a crea un nou canal pentru schimbul de date (discuție specială)
      • 50% probabil să scrie un mesaj pe oricare dintre canalele existente

Scenariul „Numai conexiuni” a apărut cu un motiv. Există o situație: utilizatorii au conectat sistemul, dar nu s-au implicat încă. Fiecare utilizator pornește computerul la ora 09:00 dimineața, stabilește o conexiune la server și rămâne tăcut. Acești tipi sunt periculoși, sunt mulți dintre ei - singurele pachete pe care le au sunt PING/PONG, dar păstrează conexiunea cu serverul (nu o pot menține - ce se întâmplă dacă există un mesaj nou). Testul reproduce o situație în care un număr mare de astfel de utilizatori încearcă să se conecteze la sistem într-o jumătate de oră. Este similar cu un test de stres, dar se concentrează tocmai pe această primă intrare - astfel încât să nu existe eșecuri (o persoană nu folosește sistemul și deja cade - este dificil să te gândești la ceva mai rău).

Scriptul de înregistrare a abonaților începe de la prima lansare. Am efectuat un test de stres și am fost siguri că sistemul nu încetinește în timpul corespondenței. Dar utilizatorii au venit și înregistrarea a început să eșueze din cauza unui timeout. La înregistrare am folosit / dev / random, care este legat de entropia sistemului. Serverul nu a avut timp să acumuleze suficientă entropie și când a fost solicitat un nou SecureRandom, a înghețat zeci de secunde. Există multe modalități de ieșire din această situație, de exemplu: treceți la /dev/urandom mai puțin sigur, instalați o placă specială care generează entropie, generați numere aleatorii în avans și stocați-le într-un pool. Am închis temporar problema cu pool-ul, dar de atunci am făcut un test separat pentru înregistrarea de noi abonați.

Folosim ca generator de sarcină JMeter. Nu știe cum să lucreze cu websocket; are nevoie de un plugin. Primele rezultate ale căutării pentru interogarea „jmeter websocket” sunt: articole de la BlazeMeter, care recomanda plugin de Maciej Zaleski.

De acolo am decis să începem.

Aproape imediat după ce am început testele serioase, am descoperit că JMeter a început să scurgă memorie.

Pluginul este o poveste mare separată; cu 176 de stele, are 132 de furcături pe github. Autorul însuși nu s-a angajat la el din 2015 (am luat-o în 2015, apoi nu a ridicat suspiciuni), mai multe probleme github privind scurgerile de memorie, 7 cereri de pull neînchise.
Dacă decideți să efectuați testarea încărcării folosind acest plugin, vă rugăm să acordați atenție următoarelor discuții:

  1. Într-un mediu cu mai multe fire, a fost folosită o listă LinkedList obișnuită, iar rezultatul a fost NPE în timpul de execuție. Acest lucru poate fi rezolvat fie prin trecerea la ConcurrentLinkedDeque, fie prin blocuri sincronizate. Am ales prima variantă pentru noi înșine (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. Scurgere de memorie; la deconectare, informațiile de conectare nu sunt șterse (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. În modul de streaming (când socket-ul web nu este închis la sfârșitul eșantionului, dar este folosit mai târziu în plan), modelele de răspuns nu funcționează (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

Acesta este unul dintre cei de pe github. Ce am făcut:

  1. Am luat furcă Elyran Kogan (@elyrank) – rezolvă problemele 1 și 3
  2. Problema rezolvata 2
  3. Debarcader actualizat de la 9.2.14 la 9.3.12
  4. Înfășurat SimpleDateFormat în ThreadLocal; SimpleDateFormat nu este sigur pentru fire, ceea ce a condus la NPE în timpul execuției
  5. S-a remediat o altă scurgere de memorie (conexiunea a fost închisă incorect când a fost deconectată)

Și totuși curge!

Memoria a început să se epuizeze nu într-o zi, ci în două. Nu mai era deloc timp, așa că am decis să lansăm mai puține fire, dar pe patru agenți. Ar fi trebuit să fie suficient pentru cel puțin o săptămână.

Au trecut doua zile...

Acum Hazelcast rămâne fără memorie. Jurnalele au arătat că, după câteva zile de testare, Hazelcast a început să se plângă de lipsa memoriei, iar după un timp clusterul s-a destrămat, iar nodurile au continuat să moară unul câte unul. Am conectat JVisualVM la hazelcast și am văzut un „ferăstrău în creștere” - a numit în mod regulat GC, dar nu a putut șterge memoria.

Cum și de ce am scris un serviciu scalabil de mare încărcare pentru 1C: Enterprise: Java, PostgreSQL, Hazelcast

S-a dovedit că în hazelcast 3.4, la ștergerea unei hărți / multiMap (map.destroy()), memoria nu este complet eliberată:

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

Bug-ul este acum remediat în 3.5, dar atunci era o problemă. Am creat noi multiHărți cu nume dinamice și le-am șters conform logicii noastre. Codul arăta cam așa:

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

Apel:

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

multiMap a fost creat pentru fiecare abonament și șters atunci când nu a fost necesar. Am decis că vom începe Map , cheia va fi numele abonamentului, iar valorile vor fi identificatori de sesiune (de la care puteți obține apoi identificatori de utilizator, dacă este necesar).

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

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

Graficele s-au îmbunătățit.

Cum și de ce am scris un serviciu scalabil de mare încărcare pentru 1C: Enterprise: Java, PostgreSQL, Hazelcast

Ce altceva am învățat despre testarea sarcinii?

  1. JSR223 trebuie să fie scris în groovy și să includă memoria cache de compilare - este mult mai rapid. Legătură.
  2. Graficele Jmeter-Plugins sunt mai ușor de înțeles decât cele standard. Legătură.

Despre experiența noastră cu Hazelcast

Hazelcast a fost un produs nou pentru noi, am început să lucrăm cu el de la versiunea 3.4.1, acum serverul nostru de producție rulează versiunea 3.9.2 (la momentul scrierii, cea mai recentă versiune de Hazelcast este 3.10).

generarea ID-ului

Am început cu identificatori întregi. Să ne imaginăm că avem nevoie de un alt Long pentru o nouă entitate. Secvența din baza de date nu este potrivită, tabelele sunt implicate în sharding - se dovedește că există un mesaj ID=1 în DB1 și un mesaj ID=1 în DB2, nu poți pune acest ID în Elasticsearch, nici în Hazelcast , dar cel mai rău lucru este dacă doriți să combinați datele din două baze de date într-una singură (de exemplu, să decideți că o singură bază de date este suficientă pentru acești abonați). Puteți adăuga mai multe AtomicLongs la Hazelcast și păstrați contorul acolo, apoi performanța obținerii unui nou ID este incrementAndGet plus timpul pentru o solicitare către Hazelcast. Dar Hazelcast are ceva mai optim - FlakeIdGenerator. Când contactează fiecare client, i se oferă un interval de identificare, de exemplu, primul – de la 1 la 10, al doilea – de la 000 la 10 și așa mai departe. Acum clientul poate emite noi identificatori pe cont propriu până la sfârșitul intervalului care i-a fost eliberat. Funcționează rapid, dar când reporniți aplicația (și clientul Hazelcast), începe o nouă secvență - de unde săriturile etc. În plus, dezvoltatorii nu înțeleg cu adevărat de ce ID-urile sunt întregi, dar sunt atât de inconsecvente. Am cântărit totul și am trecut la UUID-uri.

Apropo, pentru cei care vor să fie ca Twitter, există o astfel de bibliotecă Snowcast - aceasta este o implementare a Snowflake pe partea de sus a Hazelcast. Il puteti vizualiza aici:

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

Dar nu ne-am mai apucat de asta.

TransactionalMap.replace

O altă surpriză: TransactionalMap.replace nu funcționează. Iată un test:

@Test
public void replaceInMap_putsAndGetsInsideTransaction() {

    hazelcastInstance.executeTransaction(context -> {
        HazelcastTransactionContextHolder.setContext(context);
        try {
            context.getMap("map").put("key", "oldValue");
            context.getMap("map").replace("key", "oldValue", "newValue");
            
            String value = (String) context.getMap("map").get("key");
            assertEquals("newValue", value);

            return null;
        } finally {
            HazelcastTransactionContextHolder.clearContext();
        }        
    });
}

Expected : newValue
Actual : oldValue

A trebuit să scriu propria mea înlocuire folosind 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);
}

Testați nu numai structurile obișnuite de date, ci și versiunile lor tranzacționale. Se întâmplă ca IMap să funcționeze, dar TransactionalMap nu mai există.

Introduceți un nou JAR fără timp de nefuncționare

În primul rând, am decis să înregistrăm obiectele claselor noastre în Hazelcast. De exemplu, avem o clasă Application, vrem să o salvăm și să o citim. Salva:

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

Citim:

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

Totul merge. Apoi am decis să construim un index în Hazelcast pentru a căuta după:

map.addIndex("subscriberId", false);

Și când scriu o nouă entitate, au început să primească ClassNotFoundException. Hazelcast a încercat să adauge la index, dar nu știa nimic despre clasa noastră și dorea să îi fie furnizat un JAR cu această clasă. Am făcut exact asta, totul a funcționat, dar a apărut o nouă problemă: cum să actualizez JAR-ul fără a opri complet cluster-ul? Hazelcast nu preia noul JAR în timpul unei actualizări nod cu nod. În acest moment am decis că putem trăi fără căutarea indexului. La urma urmei, dacă utilizați Hazelcast ca magazin cheie-valoare, atunci totul va funcționa? Nu chiar. Din nou, comportamentul IMap și TransactionalMap este diferit. Acolo unde IMap nu-i pasă, TransactionalMap afișează o eroare.

IMap. Scriem 5000 de obiecte, le citim. Totul este de așteptat.

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

Dar nu funcționează într-o tranzacție, obținem o 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();
        }
    });
}

În 3.8, a apărut mecanismul de implementare a clasei de utilizator. Puteți desemna un nod principal și puteți actualiza fișierul JAR pe acesta.

Acum ne-am schimbat complet abordarea: o serializăm noi înșine în JSON și o salvăm în Hazelcast. Hazelcast nu are nevoie să cunoască structura cursurilor noastre și ne putem actualiza fără timpi de nefuncționare. Versiunea obiectelor de domeniu este controlată de aplicație. Diferite versiuni ale aplicației pot rula în același timp și este posibilă o situație când noua aplicație scrie obiecte cu câmpuri noi, dar cea veche nu știe încă despre aceste câmpuri. Și în același timp, noua aplicație citește obiecte scrise de vechea aplicație care nu au câmpuri noi. Gestionăm astfel de situații în cadrul aplicației, dar pentru simplitate nu modificăm sau ștergem câmpuri, extindem doar clasele adăugând noi câmpuri.

Cum asigurăm performanță ridicată

Patru călătorii la Hazelcast - bune, două la baza de date - proaste

Mersul în memoria cache pentru date este întotdeauna mai bine decât accesul la baza de date, dar nici nu doriți să stocați înregistrările neutilizate. Lăsăm decizia despre ce să memorăm în cache până la ultima etapă de dezvoltare. Când noua funcționalitate este codificată, activăm înregistrarea tuturor interogărilor în PostgreSQL (log_min_duration_statement la 0) și rulăm testarea încărcării timp de 20 de minute. Folosind jurnalele colectate, utilitare precum pgFouine și pgBadger pot crea rapoarte analitice. În rapoarte, căutăm în primul rând interogări lente și frecvente. Pentru interogările lente, construim un plan de execuție (EXPLAIN) și evaluăm dacă o astfel de interogare poate fi accelerată. Solicitările frecvente pentru aceleași date de intrare se potrivesc bine în cache. Încercăm să păstrăm interogările „plate”, câte un tabel per interogare.

exploatare

SV ca serviciu online a fost pus în funcțiune în primăvara anului 2017, iar ca produs separat, SV a fost lansat în noiembrie 2017 (la acel moment în stare de versiune beta).

În mai bine de un an de funcționare, nu au existat probleme serioase în funcționarea serviciului online CB. Monitorizăm serviciul online prin Zabbix, colectați și implementați de la Bambus.

Distribuția serverului SV este furnizată sub formă de pachete native: RPM, DEB, MSI. În plus, pentru Windows oferim un singur program de instalare sub forma unui singur EXE care instalează serverul, Hazelcast și Elasticsearch pe o singură mașină. Inițial ne-am referit la această versiune a instalării drept versiunea „demo”, dar acum a devenit clar că aceasta este cea mai populară opțiune de implementare.

Sursa: www.habr.com

Adauga un comentariu