Come e perché abbiamo sviluppato un servizio scalabile ad alta richiesta per 1C: Enterprise: Java, PostgreSQL, Hazelcast

In questo articolo parleremo di come e perché abbiamo sviluppato il Sistema di Interazione – un meccanismo che trasmette informazioni tra le applicazioni client e i server di 1C:Enterprise – dalla definizione del compito alla progettazione dell'architettura e ai dettagli di implementazione.

Il Sistema di Interazione (di seguito – SI) è un sistema distribuito di messaggistica tollerante ai guasti con consegna garantita. Il SI è progettato come un servizio ad alta richiesta con alta scalabilità, disponibile sia come servizio online (fornito da 1C), sia come prodotto di licenza, che può essere distribuito sui propri server.

Il SI utilizza un archivio distribuito Hazelcast e un motore di ricerca Elasticsearch. Parleremo anche di Java e di come scaldiamo orizzontalmente PostgreSQL.
Come e perché abbiamo sviluppato un servizio scalabile ad alta richiesta per 1C: Enterprise: Java, PostgreSQL, Hazelcast

Definizione del compito

Per comprendere meglio perché abbiamo creato il Sistema di Interazione, parlerò un po' di come si svolge lo sviluppo di applicazioni aziendali in 1C.

Per iniziare, ecco un po' di noi per chi non sa ancora cosa facciamo :) Creiamo la piattaforma tecnologica "1C:Enterprise". La piattaforma include uno strumento per lo sviluppo di applicazioni aziendali, oltre a un runtime che consente alle applicazioni aziendali di funzionare in un ambiente multi-piattaforma.

Paradigma di sviluppo client-server

Le applicazioni aziendali create su "1C:Enterprise" operano in un'architettura a tre livelli client-server con la "DBMS – server delle applicazioni – client". Il codice applicativo, scritto nel linguaggio incorporato 1C, può essere eseguito sul server delle applicazioni o sul client. Tutto il lavoro con gli oggetti applicativi (riferimenti, documenti, ecc.), così come la lettura e la scrittura del database, avviene solo sul server. La funzionalità dei moduli e dell'interfaccia a comando è anch'essa implementata sul server. Sul client vengono eseguite operazioni come il recupero, l'apertura e la visualizzazione dei moduli, le comunicazioni con l'utente (avvisi, domande...), alcuni calcoli veloci nei moduli che richiedono una reazione immediata (come moltiplicare il prezzo per la quantità), e l'interazione con file locali e hardware.

Nel codice applicativo, nelle intestazioni di procedure e funzioni, è necessario specificare esplicitamente dove verrà eseguito il codice — utilizzando le direttive &НаКлиенте / &НаСервере (&AtClient / &AtServer nella variante inglese del linguaggio). Gli sviluppatori di 1C ora potrebbero correggermi, dicendo che in realtà non ci sono direttive, maggiore, ma per noi questo non è rilevante ora.

Dal codice client si può chiamare il codice server, ma non viceversa. Questo è un vincolo fondamentale che abbiamo adottato per vari motivi. In particolare, perché il codice server deve essere scritto in modo da eseguire in modo identico, indipendentemente da dove venga chiamato — sia dal client che dal server. Nel caso di una chiamata al codice server da un altro codice server, il client non esiste. Inoltre, durante l'esecuzione del codice server, il client che lo ha chiamato potrebbe essersi chiuso o aver lasciato l'applicazione, e non ci sarebbe più nessuno da chiamare dal server.

Come e perché abbiamo sviluppato un servizio scalabile ad alta richiesta per 1C: Enterprise: Java, PostgreSQL, Hazelcast
Il codice che gestisce il clic del pulsante: la chiamata alla procedura server dal client funzionerà, ma la chiamata alla procedura client dal server — no.

Questo significa che se vogliamo inviare un messaggio dall server all'applicazione client, ad esempio che è terminata la generazione di un rapporto "a lungo termine" e il rapporto è disponibile per la consultazione, non abbiamo un modo per farlo. Ci tocca ricorrere a espedienti, come far polling periodico dal codice cliente al server. Ma questo approccio carica il sistema con chiamate superflue e, in generale, non appare molto elegante.

C'è anche la necessità, ad esempio, di notificare all'applicazione cliente al ricevimento di una telefonata SIP- per avvisare l'applicazione cliente di questo, affinché possa identificare il contatto nel database in base al numero del chiamante e mostrare all'utente informazioni relative al contatto. Oppure, per esempio, notificare all'applicazione cliente dell'acquirente al ricevimento di un ordine in magazzino. In generale, ci sono molti casi in cui un meccanismo del genere sarebbe utile.

Fondamentalmente, la richiesta è

Creare un meccanismo di scambio di messaggi. Veloce, affidabile, con consegna garantita, con la possibilità di ricerca flessibile dei messaggi. Basare su questo meccanismo un messenger (messaggi, videochiamate) funzionante all'interno delle applicazioni 1C.

Progettare un sistema scalabile orizzontalmente. L'aumento del carico deve essere affrontato aumentando il numero di nodi.

Implementazione

Abbiamo deciso di non integrare la parte server di SV direttamente nella piattaforma 1C:Enterprise, ma di realizzarla come prodotto separato, il cui API può essere chiamato dal codice delle soluzioni applicative 1C. Questo è stato fatto per vari motivi, il principale dei quali è stato quello di consentire lo scambio di messaggi tra diverse applicazioni 1C (ad esempio, tra la Gestione delle Vendite e la Contabilità). Diverse applicazioni 1C possono funzionare su versioni diverse della piattaforma 1C:Enterprise, trovarsi su server diversi, ecc. In tali condizioni, l'implementazione di SV come prodotto separato, situato "a lato" delle installazioni 1C, è la soluzione ottimale.

Abbiamo deciso di rendere il SV un prodotto separato. Per le piccole aziende, consigliamo di utilizzare il server SV che abbiamo installato nel nostro cloud (wss://1cdialog.com) per evitare le spese generali legate all'installazione e configurazione locale del server. I clienti più grandi potrebbero ritenere utile installare il proprio server SV nei loro datacenter. Abbiamo adottato un approccio simile nel nostro prodotto SaaS cloud. 1cFresh – viene rilasciato come prodotto di massa per l'installazione presso i clienti ed è anche distribuito nel nostro cloud. https://1cfresh.com/.

Applicazione

Per distribuire il carico e garantire la resilienza, non installeremo una sola applicazione Java, ma più di una, e un bilanciatore di carico sarà posto davanti a esse. Se è necessario inviare un messaggio da un nodo all'altro, utilizzeremo il publish/subscribe in Hazelcast.

La comunicazione tra il cliente e il server avviene tramite websocket. Questa tecnologia è particolarmente adatta per i sistemi in tempo reale.

Cache distribuita

Ci siamo trovati a scegliere tra Redis, Hazelcast e Ehcache. Siamo nel 2015. Redis ha appena rilasciato un nuovo cluster (troppo nuovo, spaventoso), c'è Sentinel con molte limitazioni. Ehcache non supporta il clustering (questa funzionalità è arrivata più tardi). Abbiamo deciso di provare Hazelcast 3.4.
Hazelcast si aggrega in cluster 'out of the box'. In modalità single node non è molto utile e può essere utilizzato solo come cache – non può scrivere i dati su disco, se perdi l'unico nodo – perdi i dati. Distribuiamo più istanze di Hazelcast tra cui facciamo backup dei dati critici. Non facciamo backup della cache – non ci fa male.

Per noi Hazelcast è:

  • Un archivio delle sessioni utente. Andare ogni volta a cercare la sessione nel database è troppo lungo, quindi mettiamo tutte le sessioni in Hazelcast.
  • Cache. Cerchi il profilo utente – controlla nella cache. Hai scritto un nuovo messaggio – metti nella cache.
  • Topic per la comunicazione tra istanze dell'applicazione. Un nodo genera un evento e lo inserisce nel topic di Hazelcast. Gli altri nodi dell'applicazione, iscritti a quel topic, ricevono e trattano l'evento.
  • Locks di cluster. Ad esempio, creiamo una discussione su una chiave unica (discussione singleton all'interno del database 1C):

conversationKeyChecker.check("BENCOLONNA");

      doInClusterLock("BENCOLONNA", () -> {

          conversationKeyChecker.check("BENCOLONNA");

          createChannel("BENCOLONNA");
      });

Abbiamo verificato che il canale non esiste. Abbiamo preso il blocco, ricontrollato e creato. Se dopo aver preso il blocco non si verifica, c'è la possibilità che un altro thread abbia controllato nel frattempo e ora provi a creare la stessa discussione, che già esiste. Non si può fare il blocco tramite synchronized o un normale Lock di Java. Tramite database – lento e, inoltre, non si vuole utilizzare il database, tramite Hazelcast – è proprio ciò che serve.

Scegliamo il DBMS

Abbiamo una vasta e positiva esperienza con PostgreSQL e collaboriamo con gli sviluppatori di questo DBMS.

Il clustering con PostgreSQL non è semplice – ci sono XL, XC, Citus, ma, in generale, non è un noSQL, che si scalano facilmente. Non abbiamo considerato noSQL come principale archiviazione, bastava e avanzava utilizzare Hazelcast, con cui non avevamo mai lavorato in precedenza.

Poiché è necessario scalare il database relazionale, significa che sharding. Come sapete, durante lo sharding dividiamo il database in parti separate in modo tale che ognuna di esse possa essere spostata su un server separato.

La prima versione del nostro sharding prevedeva la possibilità di suddividere ciascuna delle tabelle della nostra applicazione su diversi server in proporzioni variabili. Se arrivavano molte richieste sul server A, trasferivamo parte di quella tabella sul server B. Questa soluzione sembrava davvero un caso di ottimizzazione prematura, quindi abbiamo deciso di limitarci all'approccio multi-tenant.

Puoi leggere di più sul multi-tenant, ad esempio, sul sito Citus Data.

Nel nostro sistema ci sono i concetti di applicazione e abbonato. L'applicazione è un'installazione specifica di un'applicazione aziendale, come un ERP o un software di contabilità, con i propri utenti e dati aziendali. L'abbonato è un'organizzazione o una persona fisica sotto cui viene registrata l'applicazione nel server. Un abbonato può registrare diverse applicazioni e queste possono scambiarsi messaggi tra loro. L'abbonato diventa quindi un residente (tenant) nel nostro sistema. I messaggi di diversi abbonati possono trovarsi in un'unica base dati fisica; se osserviamo che un certo abbonato inizia a generare molto traffico, lo trasferiamo in una base dati fisica separata (o addirittura in un server DB separato).

Abbiamo un database principale in cui memorizziamo una tabella di routing con informazioni sulla posizione di tutti i database degli abbonati.

Come e perché abbiamo sviluppato un servizio scalabile ad alta richiesta per 1C: Enterprise: Java, PostgreSQL, Hazelcast

Per evitare che il database principale diventi un collo di bottiglia, teniamo in cache la tabella di routing (e altri dati frequentemente richiesti).

Se il database dell'abbonato inizia a rallentare, suddivideremo internamente in partizioni. Per la partizionamento di grandi tabelle utilizziamo pg_pathman.

Poiché perdere i messaggi degli utenti è un problema, manteniamo i nostri database con repliche. La combinazione di repliche sincrone e asincrone ci consente di proteggerci in caso di perdita del database principale. La perdita del messaggio avverrà solo in caso di guasto simultaneo del database principale e della sua replica sincrona.

Se si perde la replica sincrona, la replica asincrona diventa sincrona.
Se si perde il database principale, la replica sincrona diventa il database principale e la replica asincrona diventa la replica sincrona.

Elasticsearch per le ricerche

Poiché, oltre a essere un messaggero, il SV ha bisogno di una ricerca veloce, comoda e flessibile, tenendo conto della morfologia e delle corrispondenze imprecise. Abbiamo deciso di non reinventare la ruota e di utilizzare il sistema di ricerca open source Elasticsearch, basato sulla libreria Lucene. Utilizziamo anche Elasticsearch in cluster (master – data – data) per escludere problemi nel caso in cui nodi dell'applicazione si guastino.

Su GitHub abbiamo trovato un plugin per la morfologia russa per Elasticsearch e lo utilizziamo. Nell'indice di Elasticsearch conserviamo le radici delle parole (definite dal plugin) e gli N-grammi. Man mano che l'utente inserisce testo per la ricerca, cerchiamo il testo digitato tra gli N-grammi. Quando salvato nell'indice, la parola «testi» sarà scomposta nei seguenti N-grammi:

[те, тек, текс, текст, тексты, ек, екс, екст, ексты, кс, кст, ксты, ст, сты, ты],

E verrà anche salvata la radice della parola «testo». Questo approccio consente di cercare all'inizio, in mezzo e alla fine della parola.

Panoramica generale

Come e perché abbiamo sviluppato un servizio scalabile ad alta richiesta per 1C: Enterprise: Java, PostgreSQL, Hazelcast
Ripetizione dell'immagine all'inizio dell'articolo, ma con spiegazioni:

  • Il bilanciatore, esposto su Internet; noi utilizziamo nginx, ma potrebbe essere qualsiasi altro.
  • Le istanze dell'applicazione Java comunicano tra loro tramite Hazelcast.
  • Per lavorare con i websocket utilizziamo Netty.
  • L'applicazione Java è scritta in Java 8 ed è composta da bundle OSGi. Nei piani c'è la migrazione a Java 10 e il passaggio ai moduli.

Sviluppo e testing

Durante lo sviluppo e il testing del sistema abbiamo riscontrato una serie di caratteristiche interessanti dei prodotti utilizzati.

Stress test e perdite di memoria

L'uscita di ogni rilascio del sistema è uno stress test. Si considera riuscita quando:

  • Il test è durato diversi giorni senza interruzioni del servizio.
  • Il tempo di risposta per le operazioni chiave non ha superato una soglia confortevole.
  • Il degrado delle prestazioni rispetto alla versione precedente non è stato superiore al 10%.

Riempiamo il database di test con i dati: per questo otteniamo dal server di produzione informazioni sul cliente più attivo, moltiplichiamo i suoi dati per 5 (numero di messaggi, discussioni, utenti) e testiamo in questo modo.

Effettuiamo lo stress test del sistema di interazione in tre configurazioni:

  1. Stress test
  2. Solo connessioni
  3. Registrazione degli utenti

Durante il test di stress, avviamo centinaia di flussi che sovraccaricano costantemente il sistema: inviano messaggi, creano discussioni, recuperano elenchi di messaggi. Simuliamo le azioni degli utenti comuni (ottenere un elenco dei miei messaggi non letti, scrivere a qualcuno) e delle soluzioni software (inviare un pacchetto con una configurazione diversa, elaborare una notifica).

Ad esempio, ecco come appare una parte del test di stress:

  • Un utente accede al sistema
    • Richiede le sue discussioni non lette
    • Con il 50% di probabilità legge i messaggi
    • Con il 50% di probabilità scrive messaggi
    • Successivamente, l'utente:
      • Con il 20% di probabilità crea una nuova discussione
      • Sceglie casualmente una delle sue discussioni
      • Accede al suo interno
      • Richiede messaggi, profili degli utenti
      • Crea cinque messaggi indirizzati a utenti casuali di questa discussione
      • Esce dalla discussione
      • Ripete 20 volte
      • Esce dal sistema e torna all'inizio dello scenario

    • Un chatbot accede al sistema (emula lo scambio di messaggi dal codice delle soluzioni applicative)
      • Con il 50% di probabilità crea un nuovo canale per lo scambio di dati (una discussione speciale)
      • Con una probabilità del 50% invia un messaggio in uno dei canali esistenti.

Lo scenario 'Solo connessioni' non è stato introdotto a caso. Ci possono essere situazioni in cui: gli utenti hanno connesso il sistema, ma non si sono ancora immersi. Ogni mattina alle 09:00, ogni utente accende il computer, stabilisce una connessione con il server e rimane in silenzio. Questi ragazzi sono pericolosi, sono tanti: dai pacchetti ricevono solo PING/PONG, ma mantengono la connessione al server (non possono permettersi di non tenerla – e se arriva un nuovo messaggio?). Il test riproduce la situazione in cui, in mezz'ora, un gran numero di tali utenti cerca di autenticarsi nel sistema. È simile a un test di stress, ma l'attenzione è focalizzata proprio su questo primo accesso – per evitare errori (una persona non utilizza il sistema, ma questo già si disconnette – è difficile immaginare qualcosa di peggio).

Lo scenario di registrazione degli abbonati inizia con il primo avvio. Abbiamo effettuato un test di stress ed eravamo certi che nel flusso di messaggi il sistema non rallentasse. Ma gli utenti sono arrivati e la registrazione ha cominciato a disconnettersi a causa del timeout. Durante la registrazione, abbiamo utilizzato /dev/random, che è legato all'entropia del sistema. Il server non riusciva a raccogliere abbastanza entropia e si bloccava per decine di secondi quando veniva richiesto un nuovo SecureRandom. Ci sono molte soluzioni a questo problema, ad esempio: passare a un dispositivo meno sicuro \/dev\/urandom, installare una scheda speciale che genera entropia, generare numeri casuali in anticipo e conservarli nel pool. Abbiamo temporaneamente risolto il problema con un pool, ma da allora eseguiamo un test separato per la registrazione di nuovi abbonati.

Come generatore di carico utilizziamo JMeter. Non sa lavorare con WebSocket, è necessario un plugin. I primi risultati di ricerca per la query «jmeter websocket» sono articoli di BlazeMeter, nei quali raccomandano il plugin di Maciej Zaleski.

Da lì abbiamo deciso di iniziare.

Quasi subito dopo l'inizio dei test seri abbiamo scoperto che in JMeter si verificavano perdite di memoria.

Il plugin è una storia a parte; con 176 stelle ha 132 fork su github. L'autore stesso non committa dal 2015 (lo abbiamo preso nel 2015, allora non ha suscitato sospetti), ci sono alcuni problemi su github riguardo alle perdite di memoria, 7 pull request non chiusi.
Se decidete di eseguire test di carico utilizzando questo plugin, prendete in considerazione le seguenti discussioni:

  1. In un ambiente multithreading, è stato utilizzato un normale LinkedList, ottenendo alla fine NPE a runtime. Questo si risolve passando a ConcurrentLinkedDeque o utilizzando blocchi synchronized. Noi abbiamo scelto la prima opzione (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. Perdita di memoria; le informazioni sulla connessione non vengono eliminate durante la disconnessione (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. In modalità streaming (quando il websocket non si chiude alla fine del campione, ma viene utilizzato ulteriormente nel piano) i pattern di risposta non funzionano (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

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

  1. Abbiamo preso il fork di Elyran Kogan (@elyrank) – in esso sono stati risolti i problemi 1 e 3.
  2. Abbiamo risolto il problema 2.
  3. Abbiamo aggiornato Jetty da 9.2.14 a 9.3.12.
  4. Abbiamo avvolto SimpleDateFormat in ThreadLocal; SimpleDateFormat non è thread-safe, il che portava a NPE a runtime.
  5. Abbiamo risolto un'altra perdita di memoria (la connessione non veniva chiusa correttamente durante la disconnessione).

Eppure continua a perdere!

La memoria non finiva in un giorno, ma in due. Non c'era tempo, abbiamo deciso di avviare meno thread, ma su quattro agenti. Questo avrebbe dovuto bastare, almeno, per una settimana.

Sono passati due giorni…

La memoria di Hazelcast sta esaurendosi. Nei log si poteva vedere che dopo alcuni giorni di test, Hazelcast cominciava a lamentarsi per la mancanza di memoria, e dopo un po' il cluster si sgretolava, con i nodi che continuavano a morire uno dopo l'altro. Abbiamo collegato JVisualVM a Hazelcast e abbiamo visto una «sawtooth» — chiamava regolarmente il GC, ma non riusciva a liberare la memoria.

Come e perché abbiamo sviluppato un servizio scalabile ad alta richiesta per 1C: Enterprise: Java, PostgreSQL, Hazelcast

Si è scoperto che in Hazelcast 3.4, durante l'eliminazione di map / multiMap (map.destroy()), la memoria non veniva completamente liberata:

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

Ora l'errore è stato corretto nella versione 3.5, ma all'epoca era un problema. Creavamo nuove multiMap con nomi dinamici e le eliminavamo secondo la nostra logica. Il codice sembrava più o meno così:

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

Chiamata:

service.join(auth1, "NUOVE_MESSAGGI_IN_DISCUSSIONE_UUID1");
service.join(auth2, "NUOVE_MESSAGGI_IN_DISCUSSIONE_UUID1");

multiMap è stato creato per ogni abbonamento ed è stato rimosso quando non era più necessario. Abbiamo deciso di utilizzare una Map<String,Set>, con il nome dell'abbonamento come chiave e gli identificativi delle sessioni come valori (che poi possono essere utilizzati per ottenere gli identificativi degli utenti, se necessario).

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

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

I grafici si sono sistemati.

Come e perché abbiamo sviluppato un servizio scalabile ad alta richiesta per 1C: Enterprise: Java, PostgreSQL, Hazelcast

Cosa abbiamo imparato sul testing delle prestazioni?

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

La nostra esperienza con Hazelcast

Hazelcast era un prodotto nuovo per noi, abbiamo iniziato a lavorare con esso dalla versione 3.4.1, attualmente sul nostro server di produzione c'è la versione 3.9.2 (al momento della scrittura dell'articolo, l'ultima versione di Hazelcast è la 3.10).

Generazione ID

Iniziammo con identificatori interi. Immaginiamo ora di necessitare di un Long per una nuova entità. La sequenza nel database non è adatta, le tabelle partecipano allo sharding – risulterebbe che ci sia un messaggio con ID=1 nel DB1 e un messaggio con ID=1 nel DB2, non si può memorizzare un ID simile in Elasticsearch, nemmeno in Hazelcast. Ma la cosa peggiore è se desiderate unire i dati da due database in uno solo (ad esempio, decidendo che un solo database è sufficiente per questi abbonati). Si potrebbe creare in Hazelcast diversi AtomicLong e mantenere il contatore lì, quindi le prestazioni per ottenere un nuovo ID sarebbero – incrementAndGet più il tempo per la richiesta in Hazelcast. Ma in Hazelcast esiste qualcosa di più ottimale – FlakeIdGenerator. Ad ogni cliente che richiede, viene assegnato un intervallo di ID, per esempio, al primo – da 1 a 10.000, al secondo – da 10.001 a 20.000 e così via. Ora il cliente può generare nuovi identificatori autonomamente, finché non termina l'intervallo assegnatogli. Funziona velocemente, ma al riavvio dell'applicazione (e del cliente Hazelcast) inizia una nuova sequenza – da qui le mancanze, ecc. Inoltre, agli sviluppatori non è chiaro perché gli ID siano interi, ma siano così tanto disparati. Abbiamo ponderato il tutto e siamo passati a UUID.

A proposito, per coloro che vogliono essere come Twitter, c'è una libreria chiamata Snowcast – è un'implementazione di Snowflake su Hazelcast. Puoi darci un'occhiata qui:

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

Ma noi non ci siamo ancora arrivati.

TransactionalMap.replace

Un'altra sorpresa: TransactionalMap.replace non funziona. Ecco 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();
        }
    });
}

Atteso : newValue
Reale : oldValue

Ho dovuto scrivere il mio replace, utilizzando getForUpdate:

protected  boolean replaceInMap(String mapName, K key, V oldValue, V newValue) {
    TransactionalTaskContext context = HazelcastTransactionContextHolder.getContext();
    if (context != null) {
        log.trace("[CACHE] Sostituzione del valore in una mappa transazionale");
        TransactionalMap map = context.getMap(mapName);
        V value = map.getForUpdate(key);
        if (oldValue.equals(value)) {
            map.put(key, newValue);
            return true;
        }

        return false;
    }
    log.trace("[CACHE] Sostituzione del valore in una mappa non transazionale");
    IMap map = hazelcastInstance.getMap(mapName);
    return map.replace(key, oldValue, newValue);
}

Testa non solo le strutture dati comuni, ma anche le loro versioni transazionali. A volte, IMap funziona, ma TransactionalMap no.

Sostituisci il nuovo JAR senza downtime

Inizialmente abbiamo deciso di registrare in Hazelcast gli oggetti delle nostre classi. Ad esempio, abbiamo una classe Application, vogliamo salvarla e leggerla. Salviamo:

IMap map = hazelcastInstance.getMap("application");
map.set(id, application);

Leggiamo:

IMap map = hazelcastInstance.getMap("application");
return map.get(id);

Tutto funziona. Poi abbiamo deciso di costruire un indice in Hazelcast per facilitare la ricerca:

map.addIndex("subscriberId", false);

E durante la registrazione di una nuova entità abbiamo iniziato a ricevere ClassNotFoundException. Hazelcast stava cercando di aggiornare l'indice, ma non sapeva nulla della nostra classe e voleva che gli fornessimo un JAR con essa. Lo abbiamo fatto, tutto ha funzionato, ma è emersa un nuovo problema: come aggiornare il JAR senza fermare completamente il cluster? Hazelcast non riconosce il nuovo JAR durante l'aggiornamento dei nodi. A quel punto abbiamo deciso che potevamo tranquillamente vivere senza la ricerca nell'indice. Infatti, se usiamo Hazelcast come archivio di tipo chiave-valore, funzionerà? Non proprio. Qui si manifesta nuovamente un comportamento diverso tra IMap e TransactionalMap. Mentre a IMap non importa, TransactionalMap genera un errore.

IMap. Registriamo 5000 oggetti, leggiamo. Tutto come previsto.

@Test
void get5000() {
    IMap 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());
    }
}

E nella transazione non funziona, otteniamo ClassNotFoundException:

@Test
void get_transaction() {
    IMap 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 transactionalMap = context.getMap("application_t");
            Application retrievedInside = transactionalMap.get(id);

            assertEquals(id, retrievedInside.getId());
            return null;
        } finally {
            HazelcastTransactionContextHolder.clearContext();
        }
    });
}

Nella versione 3.8, è stato introdotto il meccanismo di User Class Deployment. Puoi assegnare un nodo principale e aggiornare il file JAR su di esso.

Ora abbiamo completamente cambiato approccio: serializziamo noi stessi in JSON e salviamo in Hazelcast. Hazelcast non deve conoscere la struttura delle nostre classi, mentre noi possiamo aggiornare senza downtime. La gestione delle versioni degli oggetti di dominio è a carico dell'applicazione. Diverse versioni dell'applicazione possono essere eseguite simultaneamente, e può succedere che una nuova applicazione scriva oggetti con nuovi campi, mentre la vecchia non li conosce ancora. Contemporaneamente, la nuova applicazione legge oggetti scritti dalla vecchia applicazione che non hanno i nuovi campi. Queste situazioni vengono gestite internamente all'applicazione, ma per semplicità non cambiamo e non eliminiamo i campi, ma espandiamo le classi aggiungendo nuovi campi.

Come garantiamo alte prestazioni

Quattro chiamate a Hazelcast – bene, due al DB – male

Ходить за данными в кэш всегда лучше, чем в БД, но и хранить невостребованные записи не хочется. Решение о том, что кэшировать, мы откладываем на последний этап разработки. Когда новая функциональность закодирована, мы включаем в PostgreSQL логгирование всех запросов (log_min_duration_statement в 0) и запускаем нагрузочное тестирование минут на 20. По собранным логам утилиты типа pgFouine и pgBadger умеют строить аналитические отчеты. В отчетах мы в первую очередь ищем медленные и частые запросы. Для медленных запросов строим план выполнения (EXPLAIN) и оцениваем, можно ли такой запрос ускорить. Частые запросы по одним и тем же входным данным хорошо ложатся в кэш. Запросы стараемся держать «плоскими», по одной таблице в запросе.

Operatività

СВ как онлайн-сервис была запущена в эксплуатацию весной 2017 года, как отдельный продукт СВ вышел в ноябре 2017 (на тот момент в статусе бета-версии).

Более чем за год эксплуатации серьезных проблем в работе онлайн-сервиса СВ не случалось. Онлайн-сервис мониторим через Zabbix, собираем и деплоим из Bamboo.

La distribuzione del server SV è fornita in pacchetti nativi: RPM, DEB, MSI. Inoltre, per Windows offriamo un unico installer in formato EXE, che installa il server, Hazelcast ed Elasticsearch su una macchina. Inizialmente avevamo chiamato questa versione di installazione "dimostrativa", ma ora è chiaro che è l'opzione di distribuzione più popolare.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster