Come e perché abbiamo scritto un servizio scalabile ad alto carico per 1C: Enterprise: Java, PostgreSQL, Hazelcast

In questo articolo parleremo di come e perché ci siamo sviluppati Sistema di interazione – un meccanismo che trasferisce le informazioni tra le applicazioni client e i server 1C:Enterprise, dall'impostazione di un'attività alla riflessione sull'architettura e sui dettagli di implementazione.

L'Interaction System (di seguito denominato SV) è un sistema di messaggistica distribuito, tollerante ai guasti con consegna garantita. SV è concepito come un servizio ad alto carico con elevata scalabilità, disponibile sia come servizio online (fornito da 1C) sia come prodotto prodotto in serie che può essere distribuito sulle proprie strutture server.

SV utilizza l'archiviazione distribuita Nocciola e motore di ricerca elasticsearch. Parleremo anche di Java e di come scalare orizzontalmente PostgreSQL.
Come e perché abbiamo scritto un servizio scalabile ad alto carico per 1C: Enterprise: Java, PostgreSQL, Hazelcast

Formulazione del problema

Per chiarire perché abbiamo creato il sistema di interazione, ti dirò qualcosa su come funziona lo sviluppo di applicazioni aziendali in 1C.

Per cominciare, qualcosa su di noi per coloro che non sanno ancora cosa facciamo :) Stiamo realizzando la piattaforma tecnologica 1C:Enterprise. La piattaforma include uno strumento di sviluppo di applicazioni aziendali, nonché un runtime che consente l'esecuzione delle applicazioni aziendali in un ambiente multipiattaforma.

Paradigma di sviluppo client-server

Le applicazioni aziendali create su 1C:Enterprise operano su tre livelli client-server architettura “DBMS – application server – client”. Codice dell'applicazione scritto linguaggio 1C integrato, può essere eseguito sul server delle applicazioni o sul client. Tutto il lavoro con gli oggetti dell'applicazione (directory, documenti, ecc.), nonché la lettura e la scrittura del database, viene eseguito solo sul server. Sul server è implementata anche la funzionalità dei moduli e dell'interfaccia di comando. Il cliente esegue la ricezione, l'apertura e la visualizzazione di moduli, la “comunicazione” con l'utente (avvisi, domande...), piccoli calcoli nei moduli che richiedono una risposta rapida (ad esempio, moltiplicando il prezzo per la quantità), lavorando con file locali, lavorare con le attrezzature.

Nel codice dell'applicazione, le intestazioni delle procedure e delle funzioni devono indicare esplicitamente dove verrà eseguito il codice, utilizzando le direttive &AtClient / &AtServer (&AtClient / &AtServer nella versione inglese del linguaggio). Gli sviluppatori 1C ora mi correggeranno dicendo che le direttive lo sono in realtà più, ma per noi questo non è importante adesso.

È possibile chiamare il codice server dal codice client, ma non è possibile chiamare il codice client dal codice server. Questa è una limitazione fondamentale che abbiamo imposto per una serie di motivi. In particolare perché il codice del server deve essere scritto in modo tale da essere eseguito allo stesso modo, indipendentemente da dove viene chiamato, dal client o dal server. E nel caso di chiamata del codice server da un altro codice server, non esiste alcun client in quanto tale. E perché durante l'esecuzione del codice del server, il client che lo ha chiamato potrebbe chiudersi, uscire dall'applicazione e il server non avrebbe più nessuno da chiamare.

Come e perché abbiamo scritto un servizio scalabile ad alto carico per 1C: Enterprise: Java, PostgreSQL, Hazelcast
Codice che gestisce il clic di un pulsante: la chiamata a una procedura server dal client funzionerà, la chiamata a una procedura client dal server no

Ciò significa che se vogliamo inviare qualche messaggio dal server all'applicazione client, ad esempio, che la generazione di un report "di lunga durata" è terminata e il report può essere visualizzato, non disponiamo di un metodo del genere. Devi usare trucchi, ad esempio, interrogare periodicamente il server dal codice client. Ma questo approccio carica il sistema di chiamate non necessarie e generalmente non sembra molto elegante.

E ce n'è bisogno anche, ad esempio, quando arriva una telefonata SIP- quando si effettua una chiamata, avvisare l'applicazione client di ciò in modo che possa utilizzare il numero del chiamante per ritrovarlo nel database della controparte e mostrare all'utente le informazioni sulla controparte chiamante. Oppure, ad esempio, quando un ordine arriva al magazzino, avvisa l'applicazione client del cliente. In generale, ci sono molti casi in cui un simile meccanismo sarebbe utile.

La produzione stessa

Creare un meccanismo di messaggistica. Veloce, affidabile, con consegna garantita, con la possibilità di ricercare i messaggi in modo flessibile. In base al meccanismo, implementare un messenger (messaggi, videochiamate) in esecuzione all'interno delle applicazioni 1C.

Progettare il sistema in modo che sia scalabile orizzontalmente. Il carico crescente deve essere coperto aumentando il numero di nodi.

implementazione

Abbiamo deciso di non integrare la parte server di SV direttamente nella piattaforma 1C:Enterprise, ma di implementarla come prodotto separato, la cui API può essere richiamata dal codice delle soluzioni applicative 1C. Ciò è stato fatto per una serie di ragioni, la principale delle quali era che volevo rendere possibile lo scambio di messaggi tra diverse applicazioni 1C (ad esempio, tra Gestione commerciale e Contabilità). Diverse applicazioni 1C possono essere eseguite su versioni diverse della piattaforma 1C:Enterprise, trovarsi su server diversi, ecc. In tali condizioni, l'implementazione dell'SV come prodotto separato situato "sul lato" delle installazioni 1C è la soluzione ottimale.

Quindi abbiamo deciso di rendere SV un prodotto separato. Consigliamo alle piccole aziende di utilizzare il server CB che abbiamo installato nel nostro cloud (wss://1cdialog.com) per evitare i costi generali associati all'installazione e alla configurazione locale del server. I grandi clienti potrebbero ritenere consigliabile installare un proprio server CB presso le proprie strutture. Abbiamo utilizzato un approccio simile nel nostro prodotto SaaS cloud 1cFresco – viene prodotto come prodotto di serie per l'installazione presso i siti dei clienti e viene anche distribuito nel nostro cloud https://1cfresh.com/.

applicazione

Per distribuire il carico e la tolleranza agli errori, non distribuiremo un'applicazione Java, ma diverse, con un bilanciatore di carico davanti a loro. Se è necessario trasferire un messaggio da un nodo all'altro, utilizzare pubblica/sottoscrivi in ​​Hazelcast.

La comunicazione tra il client e il server avviene tramite websocket. È particolarmente adatto per i sistemi in tempo reale.

Cache distribuita

Abbiamo scelto tra Redis, Hazelcast ed Ehcache. È il 2015. Redis ha appena rilasciato un nuovo cluster (troppo nuovo, spaventoso), c'è Sentinel con molte restrizioni. Ehcache non sa come assemblarsi in un cluster (questa funzionalità è apparsa successivamente). Abbiamo deciso di provarlo con Hazelcast 3.4.
Hazelcast è assemblato in un cluster pronto all'uso. In modalità nodo singolo, non è molto utile e può essere utilizzato solo come cache: non sa come scaricare i dati su disco, se perdi l'unico nodo, perdi i dati. Distribuiamo diversi Hazelcast, tra i quali eseguiamo il backup dei dati critici. Non eseguiamo il backup della cache, non ci importa.

Per noi Hazelcast è:

  • Memorizzazione delle sessioni utente. Ci vuole molto tempo per accedere ogni volta al database per una sessione, quindi inseriamo tutte le sessioni in Hazelcast.
  • Cache. Se stai cercando un profilo utente, controlla la cache. Ho scritto un nuovo messaggio: inseriscilo nella cache.
  • Argomenti per la comunicazione tra istanze dell'applicazione. Il nodo genera un evento e lo inserisce nel topic Hazelcast. Altri nodi dell'applicazione iscritti a questo argomento ricevono ed elaborano l'evento.
  • Serrature a grappolo. Ad esempio, creiamo una discussione utilizzando una chiave univoca (discussione singleton all'interno del database 1C):

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

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

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

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

Abbiamo controllato che non ci sia nessun canale. Abbiamo preso il lucchetto, lo abbiamo ricontrollato e lo abbiamo creato. Se non controlli il blocco dopo averlo preso, c'è la possibilità che anche un altro thread abbia controllato in quel momento e ora proverà a creare la stessa discussione, ma esiste già. Non è possibile bloccare utilizzando il blocco Java sincronizzato o normale. Attraverso il database: è lento ed è un peccato per il database; tramite Hazelcast: questo è ciò di cui hai bisogno.

La scelta di un DBMS

Abbiamo una vasta esperienza di successo lavorando con PostgreSQL e collaborando con gli sviluppatori di questo DBMS.

Non è facile con un cluster PostgreSQL: esiste XL, XC, Cito, ma in generale non si tratta di NoSQL immediatamente scalabili. Non abbiamo considerato NoSQL come storage principale; è stato sufficiente prendere Hazelcast, con cui non avevamo mai lavorato prima.

Se hai bisogno di ridimensionare un database relazionale, ciò significa sharding. Come sai, con lo sharding dividiamo il database in parti separate in modo che ciascuna di esse possa essere posizionata su un server separato.

La prima versione del nostro sharding presupponeva la possibilità di distribuire ciascuna tabella della nostra applicazione su server diversi in proporzioni diverse. Ci sono molti messaggi sul server A: per favore, spostiamo parte di questa tabella sul server B. Questa decisione indicava semplicemente un'ottimizzazione prematura, quindi abbiamo deciso di limitarci a un approccio multi-tenant.

Puoi leggere informazioni sul multi-tenant, ad esempio, sul sito web Dati Cito.

SV ha i concetti di applicazione e abbonato. Un'applicazione è un'installazione specifica di un'applicazione aziendale, ad esempio ERP o Contabilità, con i relativi utenti e dati aziendali. Un abbonato è un'organizzazione o un individuo per conto del quale l'applicazione è registrata nel server SV. Un abbonato può avere diverse applicazioni registrate e queste applicazioni possono scambiarsi messaggi tra loro. L'abbonato è diventato un inquilino nel nostro sistema. I messaggi provenienti da più abbonati possono essere collocati in un database fisico; se vediamo che un abbonato ha iniziato a generare molto traffico, lo spostiamo su un database fisico separato (o anche su un server database separato).

Disponiamo di un database principale in cui è archiviata una tabella di routing con informazioni sulla posizione di tutti i database degli abbonati.

Come e perché abbiamo scritto un servizio scalabile ad alto carico per 1C: Enterprise: Java, PostgreSQL, Hazelcast

Per evitare che il database principale costituisca un collo di bottiglia, manteniamo la tabella di routing (e altri dati frequentemente necessari) in una cache.

Se il database dell'abbonato inizia a rallentare, lo taglieremo in partizioni interne. Su altri progetti che utilizziamo pg_pathman.

Poiché perdere i messaggi degli utenti è negativo, manteniamo i nostri database con repliche. La combinazione di repliche sincrone e asincrone ti consente di assicurarti in caso di perdita del database principale. La perdita dei messaggi si verificherà solo se il database primario e la relativa replica sincrona falliscono contemporaneamente.

Se una replica sincrona viene persa, la replica asincrona diventa sincrona.
Se il database principale viene perso, la replica sincrona diventa il database principale e la replica asincrona diventa una replica sincrona.

Elasticsearch per la ricerca

Poiché, tra le altre cose, SV è anche un messaggero, richiede una ricerca rapida, comoda e flessibile, che tenga conto della morfologia e utilizzi corrispondenze imprecise. Abbiamo deciso di non reinventare la ruota e di utilizzare il motore di ricerca gratuito Elasticsearch, creato sulla base della libreria Luceno. Distribuiamo inoltre Elasticsearch in un cluster (master – dati – dati) per eliminare problemi in caso di guasto dei nodi applicativi.

Su github abbiamo trovato Plugin di morfologia russa per Elasticsearch e usarlo. Nell'indice Elasticsearch memorizziamo le radici delle parole (determinate dal plugin) e gli N-grammi. Quando l'utente inserisce il testo da cercare, cerchiamo il testo digitato tra N-grammi. Una volta salvata nell'indice, la parola "testi" verrà suddivisa nei seguenti N-grammi:

[quelli, tek, tex, testo, testi, ek, ex, ext, testi, ks, kst, ksty, st, sty, tu],

E verrà preservata anche la radice della parola “testo”. Questo approccio ti consente di cercare all'inizio, al centro e alla fine della parola.

РћР � С ‰ Р � СЏ РєР � СЂС,РёРЅР �

Come e perché abbiamo scritto un servizio scalabile ad alto carico per 1C: Enterprise: Java, PostgreSQL, Hazelcast
Ripetizione dell'immagine dall'inizio dell'articolo, ma con spiegazioni:

  • Bilanciatore esposto su Internet; abbiamo nginx, può essere qualsiasi.
  • Le istanze dell'applicazione Java comunicano tra loro tramite Hazelcast.
  • Per lavorare con una presa web utilizziamo Netty.
  • L'applicazione Java è scritta in Java 8 ed è composta da bundle OSGi. I piani includono la migrazione a Java 10 e il passaggio ai moduli.

Sviluppo e test

Durante il processo di sviluppo e test dell'SV, ci siamo imbattuti in una serie di caratteristiche interessanti dei prodotti che utilizziamo.

Test di carico e perdite di memoria

Il rilascio di ogni versione SV comporta test di carico. Ha successo quando:

  • Il test ha funzionato per diversi giorni e non si sono verificati guasti al servizio
  • Il tempo di risposta per le operazioni chiave non ha superato una soglia confortevole
  • Il peggioramento delle prestazioni rispetto alla versione precedente non è superiore al 10%

Riempiamo il database di test con i dati: per fare ciò, riceviamo informazioni sull'abbonato più attivo dal server di produzione, moltiplichiamo i suoi numeri per 5 (il numero di messaggi, discussioni, utenti) e lo testiamo in questo modo.

Effettuiamo test di carico del sistema di interazione in tre configurazioni:

  1. test di resistenza
  2. Solo connessioni
  3. Registrazione dell'abbonato

Durante lo stress test lanciamo diverse centinaia di thread, che caricano il sistema senza fermarsi: scrivere messaggi, creare discussioni, ricevere un elenco di messaggi. Simuliamo le azioni degli utenti ordinari (otteniamo un elenco dei miei messaggi non letti, scriviamo a qualcuno) e soluzioni software (trasmettiamo un pacchetto con una configurazione diversa, elaboriamo un avviso).

Ad esempio, ecco come appare la parte dello stress test:

  • L'utente effettua l'accesso
    • Richiede le tue discussioni non lette
    • Il 50% delle probabilità leggerà i messaggi
    • Il 50% delle probabilità manda messaggi
    • Utente successivo:
      • Ha una probabilità del 20% di creare una nuova discussione
      • Seleziona casualmente una qualsiasi delle sue discussioni
      • Va dentro
      • Richiede messaggi, profili utente
      • Crea cinque messaggi indirizzati a utenti casuali da questa discussione
      • Lascia la discussione
      • Si ripete 20 volte
      • Esce e torna all'inizio dello script

    • Un chatbot entra nel sistema (emula la messaggistica dal codice dell'applicazione)
      • Ha una probabilità del 50% di creare un nuovo canale per lo scambio di dati (discussione speciale)
      • Il 50% delle probabilità scriverà un messaggio su uno qualsiasi dei canali esistenti

Lo scenario “Solo connessioni” è apparso per un motivo. C'è una situazione: gli utenti si sono collegati al sistema, ma non sono ancora stati coinvolti. Ogni utente accende il computer alle 09:00 del mattino, stabilisce una connessione al server e rimane in silenzio. Questi ragazzi sono pericolosi, ce ne sono molti - gli unici pacchetti che hanno sono PING/PONG, ma mantengono la connessione al server (non riescono a mantenerla - e se c'è un nuovo messaggio). Il test riproduce una situazione in cui un gran numero di tali utenti tenta di accedere al sistema in mezz'ora. È simile a uno stress test, ma il suo focus è proprio su questo primo input - in modo che non ci siano fallimenti (una persona non usa il sistema, ma cade già - è difficile pensare a qualcosa di peggio).

Lo script di registrazione dell'abbonato inizia dal primo avvio. Abbiamo condotto uno stress test ed eravamo sicuri che il sistema non rallentasse durante la corrispondenza. Ma gli utenti sono arrivati ​​e la registrazione ha iniziato a fallire a causa di un timeout. Al momento della registrazione abbiamo utilizzato / dev / random, che è legato all'entropia del sistema. Il server non ha avuto il tempo di accumulare abbastanza entropia e quando è stato richiesto un nuovo SecureRandom si è bloccato per decine di secondi. Esistono molti modi per uscire da questa situazione, ad esempio: passare al meno sicuro /dev/urandom, installare una scheda speciale che genera entropia, generare numeri casuali in anticipo e memorizzarli in un pool. Abbiamo temporaneamente risolto il problema con il pool, ma da allora abbiamo eseguito un test separato per la registrazione di nuovi abbonati.

Usiamo come generatore di carico JMeter. Non sa come funzionare con websocket; ha bisogno di un plugin. I primi risultati di ricerca per la query “jmeter websocket” sono: articoli da BlazeMeter, che consiglia plugin di Maciej Zaleski.

È da lì che abbiamo deciso di iniziare.

Quasi subito dopo aver iniziato i test seri, abbiamo scoperto che JMeter iniziava a perdere memoria.

Il plugin è una grande storia a parte; con 176 stelle, ha 132 fork su github. L'autore stesso non si è impegnato dal 2015 (l'abbiamo preso nel 2015, quindi non ha destato sospetti), diversi problemi su GitHub riguardanti perdite di memoria, 7 richieste pull non chiuse.
Se decidi di eseguire test di carico utilizzando questo plugin, presta attenzione alle seguenti discussioni:

  1. In un ambiente multi-thread è stata utilizzata una LinkedList normale e il risultato è stato NPE in fase di esecuzione. Questo può essere risolto passando a ConcurrentLinkedDeque o mediante blocchi sincronizzati. Abbiamo scelto noi stessi la prima opzione (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. Perdita di memoria; durante la disconnessione, le informazioni di connessione non vengono eliminate (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. In modalità streaming (quando il websocket non viene chiuso alla fine del campione, ma viene utilizzato successivamente nel piano), i modelli di risposta non funzionano (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

Questo è uno di quelli su github. Cosa abbiamo fatto:

  1. ha preso forcella Elyran Kogan (@elyrank) – risolve i problemi 1 e 3
  2. Problema risolto 2
  3. Molo aggiornato dal 9.2.14 al 9.3.12
  4. SimpleDateFormat avvolto in ThreadLocal; SimpleDateFormat non è thread-safe, il che ha portato a NPE in fase di esecuzione
  5. Risolta un'altra perdita di memoria (la connessione veniva chiusa in modo errato quando veniva disconnessa)

Eppure scorre!

La memoria cominciò a esaurirsi non in un giorno, ma in due. Non c'era assolutamente tempo, quindi abbiamo deciso di avviare meno thread, ma su quattro agenti. Questo dovrebbe bastare per almeno una settimana.

Sono passati due giorni...

Ora Hazelcast sta esaurendo la memoria. I registri hanno mostrato che dopo un paio di giorni di test, Hazelcast ha iniziato a lamentarsi della mancanza di memoria e dopo un po 'il cluster è andato in pezzi e i nodi hanno continuato a morire uno dopo l'altro. Abbiamo collegato JVisualVM a Hazelcast e abbiamo visto una "sega ascendente": chiamava regolarmente GC, ma non siamo riusciti a cancellare la memoria.

Come e perché abbiamo scritto un servizio scalabile ad alto carico per 1C: Enterprise: Java, PostgreSQL, Hazelcast

Si è scoperto che in hazelcast 3.4, quando si elimina una mappa/multiMap (map.destroy()), la memoria non viene completamente liberata:

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

Il bug è stato corretto nella versione 3.5, ma allora era un problema. Abbiamo creato nuove multiMappe con nomi dinamici e le abbiamo cancellate secondo la nostra logica. Il codice era più o meno questo:

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

Voti:

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

multiMap veniva creato per ogni abbonamento e cancellato quando non era più necessario. Abbiamo deciso di avviare Map , la chiave sarà il nome dell'abbonamento e i valori saranno gli identificatori di sessione (da cui sarà poi possibile ottenere gli identificatori utente, se necessario).

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

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

Le classifiche sono migliorate.

Come e perché abbiamo scritto un servizio scalabile ad alto carico per 1C: Enterprise: Java, PostgreSQL, Hazelcast

Cos'altro abbiamo imparato sui test di carico?

  1. JSR223 deve essere scritto in modo groovy e includere la cache di compilazione: è molto più veloce. Collegamento.
  2. I grafici Jmeter-Plugins sono più facili da capire rispetto a quelli standard. Collegamento.

Sulla nostra esperienza con Hazelcast

Hazelcast era un prodotto nuovo per noi, abbiamo iniziato a lavorarci dalla versione 3.4.1, ora il nostro server di produzione esegue la versione 3.9.2 (al momento in cui scriviamo, l'ultima versione di Hazelcast è la 3.10).

Generazione dell'identità

Abbiamo iniziato con identificatori interi. Immaginiamo di aver bisogno di un altro Long per una nuova entità. La sequenza nel database non è adatta, le tabelle sono coinvolte nello sharding: risulta che esiste un ID messaggio=1 in DB1 e un ID messaggio=1 in DB2, non puoi inserire questo ID in Elasticsearch, né in Hazelcast , ma la cosa peggiore è se si desidera combinare i dati di due database in uno solo (ad esempio, decidendo che un database è sufficiente per questi abbonati). Puoi aggiungere diversi AtomicLong a Hazelcast e mantenere lì il contatore, quindi la prestazione per ottenere un nuovo ID sarà incrementaAndGet più il tempo per una richiesta a Hazelcast. Ma Hazelcast ha qualcosa di più ottimale: FlakeIdGenerator. Quando si contatta ciascun cliente, gli viene assegnato un intervallo ID, ad esempio il primo da 1 a 10, il secondo da 000 a 10 e così via. Ora il client può emettere nuovi identificatori da solo fino al termine dell'intervallo assegnatogli. Funziona velocemente, ma quando riavvii l'applicazione (e il client Hazelcast), inizia una nuova sequenza, da qui i salti, ecc. Inoltre, gli sviluppatori non capiscono veramente perché gli ID siano interi, ma siano così incoerenti. Abbiamo soppesato tutto e siamo passati agli UUID.

A proposito, per coloro che vogliono essere come Twitter, esiste una libreria Snowcast: si tratta di un'implementazione di Snowflake su Hazelcast. Puoi vederlo qui:

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

Ma non ci siamo più riusciti.

TransactionalMap.replace

Un'altra sorpresa: TransactionalMap.replace non funziona. Ecco una prova:

@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

Ho dovuto scrivere la mia sostituzione utilizzando 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 non solo le normali strutture dati, ma anche le loro versioni transazionali. Succede che IMap funziona, ma TransactionalMap non esiste più.

Inserisci un nuovo JAR senza tempi di inattività

Per prima cosa abbiamo deciso di registrare gli oggetti delle nostre lezioni in Hazelcast. Ad esempio, abbiamo una classe Application, vogliamo salvarla e leggerla. Salva:

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

leggiamo:

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

Tutto funziona. Quindi abbiamo deciso di creare un indice in Hazelcast per effettuare ricerche in base a:

map.addIndex("subscriberId", false);

E durante la scrittura di una nuova entità, hanno iniziato a ricevere ClassNotFoundException. Hazelcast ha provato ad aggiungere all'indice, ma non sapeva nulla della nostra classe e voleva che le fosse fornito un JAR con questa classe. Abbiamo fatto proprio così, tutto ha funzionato, ma è apparso un nuovo problema: come aggiornare il JAR senza fermare completamente il cluster? Hazelcast non rileva il nuovo JAR durante un aggiornamento nodo per nodo. A questo punto abbiamo deciso che potevamo vivere senza la ricerca nell'indice. Dopotutto, se usi Hazelcast come negozio di valori-chiave, allora tutto funzionerà? Non proprio. Anche in questo caso il comportamento di IMap e TransactionalMap è diverso. Laddove a IMap non interessa, TransactionalMap genera un errore.

IMap. Scriviamo 5000 oggetti, li leggiamo. Tutto è previsto.

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

Ma non funziona in una transazione, otteniamo una 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();
        }
    });
}

Nella 3.8 è apparso il meccanismo di distribuzione della classe utente. Puoi designare un nodo master e aggiornare il file JAR su di esso.

Ora abbiamo cambiato completamente il nostro approccio: lo serializziamo noi stessi in JSON e lo salviamo in Hazelcast. Hazelcast non ha bisogno di conoscere la struttura delle nostre classi e possiamo aggiornarle senza tempi di inattività. Il controllo delle versioni degli oggetti del dominio è controllato dall'applicazione. Diverse versioni dell'applicazione possono essere eseguite contemporaneamente ed è possibile una situazione in cui la nuova applicazione scrive oggetti con nuovi campi, ma quella vecchia non è ancora a conoscenza di questi campi. E allo stesso tempo, la nuova applicazione legge gli oggetti scritti dalla vecchia applicazione che non hanno nuovi campi. Gestiamo tali situazioni all'interno dell'applicazione, ma per semplicità non modifichiamo o eliminiamo i campi, espandiamo solo le classi aggiungendo nuovi campi.

Come garantiamo prestazioni elevate

Quattro viaggi su Hazelcast: buoni, due nel database: pessimi

Andare alla cache per i dati è sempre meglio che andare al database, ma non vuoi nemmeno archiviare record inutilizzati. Lasciamo la decisione su cosa memorizzare nella cache fino all'ultima fase di sviluppo. Quando la nuova funzionalità viene codificata, attiviamo la registrazione di tutte le query in PostgreSQL (log_min_duration_statement su 0) ed eseguiamo test di carico per 20 minuti. Utilizzando i log raccolti, utility come pgFouine e pgBadger possono creare report analitici. Nei report cerchiamo principalmente query lente e frequenti. Per le query lente, costruiamo un piano di esecuzione (EXPLAIN) e valutiamo se tale query può essere accelerata. Le richieste frequenti per gli stessi dati di input si adattano bene alla cache. Cerchiamo di mantenere le query "piatte", una tabella per query.

Sfruttamento

SV come servizio online è stato messo in funzione nella primavera del 2017 e come prodotto separato SV è stato rilasciato nel novembre 2017 (allora in versione beta).

In più di un anno di attività non si sono verificati problemi seri nel funzionamento del servizio online CB. Monitoriamo il servizio online tramite Zabbix, raccogliere e distribuire da Bambù.

La distribuzione del server SV viene fornita sotto forma di pacchetti nativi: RPM, DEB, MSI. Inoltre per Windows forniamo un singolo programma di installazione sotto forma di un singolo EXE che installa il server, Hazelcast ed Elasticsearch su un computer. Inizialmente ci riferivamo a questa versione dell'installazione come versione “demo”, ma ora è diventato chiaro che questa è l'opzione di distribuzione più popolare.

Fonte: habr.com

Aggiungi un commento