In questo articolo parleremo di come e perché abbiamo sviluppato – un meccanismo che trasmette informazioni tra le applicazioni client e i server 1C:Enterprise – dalla definizione del compito alla progettazione dell'architettura e dei dettagli di implementazione.
Il Sistema di Interazione (di seguito – SI) è un sistema distribuito e tollerante ai guasti per lo scambio di messaggi con consegna garantita. Il SI è progettato come un servizio ad alta capacità di carico, scalabile, disponibile sia come servizio online (fornito dalla società 1C), sia come prodotto di distribuzione che può essere implementato sulle proprie capacità server.
Il SI utilizza uno storage distribuito e un sistema di ricerca . Inoltre, parleremo di Java e di come scalare orizzontalmente PostgreSQL.
Definizione del compito
Per capire perché abbiamo creato il Sistema di Interazione, parlerò un po' di come è organizzato lo sviluppo delle applicazioni aziendali in 1C.
Iniziamo con un po' su di noi per chi non sa ancora cosa facciamo :) Creiamo la piattaforma tecnologica "1C:Enterprise". La piattaforma include uno strumento di sviluppo per applicazioni aziendali, oltre a un runtime che consente alle applicazioni aziendali di funzionare in un ambiente multipiattaforma.
La paridigma client-server dello sviluppo
Le applicazioni aziendali create su "1C:Enterprise" operano in un'architettura a tre livelli sotto forma di "DBMS – server delle applicazioni – client". Il codice applicativo, scritto nel , può essere eseguito sul server delle applicazioni o sul client. Tutte le operazioni con gli oggetti applicativi (riferimenti, documenti, ecc.), così come la lettura e la scrittura nel database, vengono eseguite solo sul server. Anche la funzionalità dei moduli e dell'interfaccia a comando è realizzata sul server. Sul client vengono eseguite la ricezione, l'apertura e la visualizzazione dei moduli, la "comunicazione" con l'utente (avvisi, domande...), piccoli calcoli nei moduli che richiedono una reazione rapida (ad esempio, moltiplicare il prezzo per la quantità), lavoro con file locali, lavoro con hardware.
Nel codice applicativo, le intestazioni delle procedure e delle funzioni devono specificare esplicitamente dove verrà eseguito il codice – usando le direttive &НаКлиенте / &НаСервере (&AtClient / &AtServer nella versione inglese del linguaggio). Gli sviluppatori di 1C ora mi correggeranno dicendo che in realtà le direttive , ma per noi questo non è attualmente rilevante.
Il codice client può chiamare il codice server, mentre dal codice server non è possibile chiamare il codice client. Questa è una limitazione fondamentale, impostata da noi per vari motivi. In particolare, perché il codice server deve essere scritto in modo da funzionare allo stesso modo, indipendentemente da dove venga richiamato – dal client o dal server. E in caso di chiamata del 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 essere uscito dall'applicazione, e il server non avrà più nessuno da chiamare.
Il codice che gestisce la pressione del pulsante: la chiamata a una procedura server dal client funzionerà, ma la chiamata a una procedura client dal server no.
Ciò significa che se dal server vogliamo inviare un messaggio all'applicazione client, ad esempio che la generazione di un rapporto "lungo" è terminata e che il rapporto può essere visualizzato, non abbiamo modo di farlo. Dobbiamo ricorrere a stratagemmi, ad esempio, il codice client deve periodicamente interrogare il server. Ma questo approccio carica il sistema con chiamate superflue e, in generale, non sembra molto elegante.
C'è anche la necessità, ad esempio, di informare l'applicazione client di una chiamata telefonica in arrivo, -per avvisarla che deve cercare il chiamante nel database dei contatti e mostrare all'utente le informazioni sul contatto chiamante. Oppure, ad esempio, nel caso di un ordine ricevuto in magazzino, notificare l'applicazione client del cliente. In generale, ci sono molti casi in cui un meccanismo di questo tipo sarebbe utile.
Fondamentalmente l'obiettivo è
Creare un meccanismo di scambio di messaggi. Veloce, affidabile, con consegna garantita, con la possibilità di ricerca flessibile dei messaggi. Sulla base di questo meccanismo, realizzare un messenger (messaggi, videochiamate) che funzioni all'interno delle applicazioni 1C.
Progettare un sistema orizzontalmente scalabile. L'aumento del carico deve essere gestito aumentando il numero di nodi.
Implementazione
Per la componente server di CV, abbiamo deciso di non integrarla direttamente nella piattaforma 1C:Enterprise, ma di realizzarla come prodotto separato, il cui API può essere richiamato dal codice delle soluzioni applicative 1C. Questo è stato fatto per una serie di motivi, il principale dei quali è rendere possibile lo scambio di messaggi tra diverse applicazioni 1C (ad esempio, tra Gestione Commerciale e Contabilità). Diverse applicazioni 1C possono funzionare su versioni diverse della piattaforma 1C:Enterprise, trovarsi su server diversi, ecc. In queste condizioni, la realizzazione di CV come prodotto separato, posizionato
di lato è rilasciato come prodotto seriale per l'installazione presso i clienti ed è anche distribuito nel nostro cloud .
Applicazione
Per distribuire il carico e garantire la tolleranza ai guasti, distribuirò non una sola applicazione Java, ma diverse, posizionando un bilanciatore di carico davanti a loro. Se è necessario inviare un messaggio da nodo a nodo, utilizziamo publish/subscribe in Hazelcast.
La comunicazione del client con il server avviene tramite websocket. Questo è molto adatto per sistemi in tempo reale.
Cache distribuita
Abbiamo scelto tra Redis, Hazelcast ed Ehcache. Era il 2015. Redis aveva appena rilasciato un nuovo cluster (troppo nuovo, spaventoso), con Sentinel che presentava molte limitazioni. Ehcache non è in grado di formare un cluster (questa funzionalità è stata introdotta successivamente). Abbiamo deciso di provare con Hazelcast 3.4.
Hazelcast si forma in un cluster "out of the box". In modalità nodo singolo non è molto utile e può servire solo come cache – non è in grado di scrivere dati su disco, se perdiamo l'unico nodo, perdiamo i dati. Distribuiamo diversi Hazelcast tra cui facciamo il backup dei dati critici. La cache non viene fatta il backup – non ci dispiace perderla.
Per noi Hazelcast è:
- Un archivio delle sessioni utente. Andare a recuperare la sessione nel database ogni volta è lungo, quindi memorizziamo tutte le sessioni in Hazelcast.
- Cache. Stai cercando il profilo utente? Controlla nella cache. Hai inviato un nuovo messaggio? Mettilo in cache.
- Topic per la comunicazione delle istanze dell'applicazione. Il nodo genera un evento e lo colloca nel topic Hazelcast. Altri nodi dell'applicazione, iscritti a quel topic, ricevono e elaborano l'evento.
- Locking cluster. Ad esempio, creiamo una discussione con una chiave unica (discussione singleton nell'ambito del database 1C):
conversationKeyChecker.check("BENCOLONKA");
doInClusterLock("BENCOLONKA", () -> {
conversationKeyChecker.check("BENCOLONKA");
createChannel("BENCOLONKA");
});Abbiamo verificato che il canale non esiste. Abbiamo preso il lock, controllato di nuovo e creato. Se dopo aver preso il lock non si verifica, c'è la possibilità che un altro thread controlli e provi a creare la stessa discussione—e questa esiste già. Non si può usare il locking con synchronized o il normale lock di Java. Attraverso il database è lento, e sprecare risorse del database non è consigliabile; utilizzare Hazelcast è la soluzione migliore.
Selezioniamo il DBMS
Abbiamo una lunga e proficua esperienza con PostgreSQL e collaboriamo con gli sviluppatori di questo DBMS.
Con il cluster PostgreSQL non è semplice—ci sono , , , ma, in generale, non si tratta di noSQL, che si scalano facilmente. Non abbiamo considerato noSQL come database principale, ci è bastato usare Hazelcast, con cui non avevamo lavorato prima.
Se è necessario scalare un RDB—significa che . Come sapete, nello sharding dividiamo il database in parti separate in modo che ognuna di esse possa essere assemblata su un server separato.
La prima opzione per il nostro sharding prevedeva la possibilità di distribuire ciascuna delle tabelle della nostra applicazione su server diversi in proporzioni diverse. Molti messaggi sul server A? Perfetto, spostiamo parte di questa tabella sul server B. Questa soluzione sembrava una pre-ottimizzazione prematura, quindi abbiamo deciso di limitarci all'approccio multi-tenant.
Puoi leggere di multi-tenant, per esempio, sul sito .
Nel SV ci sono i concetti di applicazione e abbonato. L'applicazione è una specifica installazione di un'applicazione aziendale, ad esempio ERP o Contabilità, con i propri utenti e dati aziendali. L'abbonato è un'organizzazione o una persona fisica che registra l'applicazione nel server SV. Un abbonato può avere registrate più applicazioni, e queste applicazioni possono scambiarsi messaggi tra di loro. L'abbonato è diventato un affittuario (tenant) nel nostro sistema. I messaggi di più abbonati possono trovarsi in un'unica base dati fisica; se vediamo che un abbonato genera molto traffico, lo spostiamo in una base dati fisica separata (o persino in un server DB separato).
Abbiamo un DB principale, dove è memorizzata la tabella di routing con le informazioni sulla posizione di tutti i database abbonati.
Per evitare che il DB principale diventi un collo di bottiglia, manteniamo la tabella di routing (e altri dati frequentemente richiesti) nella cache.
Se il DB dell'abbonato inizia a rallentare, divideremo internamente in partizioni. In altri progetti, per partizionare grandi tabelle utilizziamo .
Poiché perdere messaggi degli utenti è problematico, supportiamo i nostri DB con repliche. La combinazione di repliche sincrone e asincrone permette di coprirsi nel caso si perda il DB principale. La perdita di messaggi avverrà solo in caso di guasto simultaneo del DB principale e della sua replica sincrona.
Se si perde una replica sincrona, la replica asincrona diventa sincrona.
Se si perde il DB principale, la replica sincrona diventa il DB principale, mentre la replica asincrona diventa la replica sincrona.
Elasticsearch per la ricerca
Poiché, oltre ad altro, il SV è anche un messenger, è necessaria una ricerca veloce, conveniente 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, creato sulla base della libreria . Utilizziamo anche Elasticsearch in un cluster (master – data – data) per evitare problemi in caso di guasti dei nodi dell'applicazione.
Su github abbiamo trovato per Elasticsearch e lo utilizziamo. Nell'indice Elasticsearch memorizziamo le radici delle parole (definite dal plugin) e i n-grammi. Man mano che l'utente inserisce testo per la ricerca, cerchiamo il testo digitato tra gli n-grammi. Salvando nell'indice, la parola 'testi' si suddividerà nei seguenti n-grammi:
[te, tek, teks, testo, testi, ek, eks, ekst, ekst, k, kst, ksty, st, sty, ty],
E sarà memorata anche la radice della parola 'testo'. Questo approccio consente di cercare sia all'inizio, che nel mezzo e alla fine della parola.
Panoramica generale
Ripetizione dell'immagine all'inizio dell'articolo, ma con spiegazioni:
- Un bilanciatore impostato su Internet; noi utilizziamo nginx, può essere qualsiasi altro.
- Le istanze dell'applicazione Java comunicano tra loro attraverso Hazelcast.
- Per lavorare con il websocket utilizziamo .
- L'applicazione Java è scritta in Java 8, consiste in bundle . Nei piani c'è la migrazione a Java 10 e il passaggio ai moduli.
Sviluppo e test
Durante il processo di sviluppo e test di SV ci siamo imbattuti in una serie di interessanti caratteristiche dei prodotti utilizzati.
Testing delle prestazioni e perdite di memoria
Il rilascio di ogni versione di SV è un test delle prestazioni. È considerato riuscito quando:
- Il test ha funzionato per diversi giorni senza interruzioni del servizio
- Il tempo di risposta delle operazioni chiave non ha superato una soglia confortevole
- Il degrado delle prestazioni rispetto alla versione precedente non è superiore al 10%
Riempiamo la base di test con dati: per questo otteniamo dal server di produzione informazioni sul cliente più attivo, moltiplichiamo i suoi numeri per 5 (il numero di messaggi, discussioni, utenti) e così testiamo.
Il test delle prestazioni del sistema di interazione viene condotto in tre configurazioni:
- Stress test
- Solo connessioni
- Registrazione degli abbonati
Durante il test di stress lanciamo diverse centinaia di thread, che caricano senza interruzione il sistema: inviano messaggi, creano discussioni, ricevono l'elenco dei messaggi. Imitiamo le azioni degli utenti normali (ottenere l'elenco dei miei messaggi non letti, scrivere a qualcuno) e soluzioni programmatiche (inviare un pacchetto a un'altra configurazione, elaborare una notifica).
Ad esempio, ecco come appare una parte dello stress test:
- 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
- Successivo utente:
- Crea una nuova discussione con probabilità del 20%
- Seleziona casualmente una delle sue discussioni
- Accede all'interno
- Richiede messaggi, profili utenti
- Crea cinque messaggi indirizzati a utenti casuali in questa discussione
- Esce dalla discussione
- Ripete 20 volte
- Esce dal sistema e torna all'inizio dello scenario
- Un chatbot entra nel sistema (emula lo scambio di messaggi dal codice delle soluzioni applicative)
- Crea un nuovo canale per lo scambio di dati con probabilità del 50% (discussione speciale)
- Scrive un messaggio in uno dei canali esistenti con probabilità del 50%
Lo scenario "Solo connessioni" non è apparso per caso. C'è una situazione: gli utenti hanno collegato il sistema, ma non si sono ancora immersi. Ogni utente accende il computer ogni mattina alle 09:00, stabilisce una connessione con il server e rimane in silenzio. Questi ragazzi sono pericolosi, ce ne sono molti - dai pacchetti hanno solo PING/PONG, ma mantengono la connessione al server (non possono non mantenerla - e se arriva un nuovo messaggio). Il test riproduce una situazione in cui un gran numero di tali utenti tenta di autenticarsi nel sistema in mezz'ora. È simile a un test di stress, ma il suo focus è proprio su questo primo accesso - per garantire che non ci siano rifiuti (una persona non utilizza il sistema, ma esso già cade - difficile immaginare qualcosa di peggio).
Lo scenario di registrazione degli abbonati inizia dal primo avvio. Abbiamo fatto un test di stress ed eravamo sicuri che nel messaggistica il sistema non rallentasse. Ma sono iniziati ad arrivare utenti e la registrazione ha cominciato a cadere per timeout. Durante la registrazione abbiamo utilizzato , legato all'entropia del sistema. Il server non riusciva a raccogliere abbastanza entropia e, quando chiedeva un nuovo SecureRandom, si bloccava per decine di secondi. Ci sono molte soluzioni a una situazione del genere, ad esempio: passare a un meno sicuro /dev/urandom, installare una scheda speciale che genera entropia, generare numeri casuali in anticipo e conservarli in un 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 . Non è in grado di lavorare con WebSocket, è necessario un plugin. I primi risultati di ricerca per "jmeter websocket" restituiscono , che raccomandano .
E da lì abbiamo deciso di partire.
Quasi subito dopo l'inizio dei test seri, abbiamo scoperto che in JMeter si erano verificate perdite di memoria.
Il plugin è una storia a parte; con 176 stelle ha 132 fork su github. L'autore non effettua commit dal 2015 (lo abbiamo utilizzato nel 2015, allora non ha sollevato sospetti), ci sono alcuni problemi su github riguardanti perdite di memoria e 7 pull request non chiuse.
Se decidete di eseguire test di carico utilizzando questo plugin, fate attenzione alle seguenti discussioni:
- In un ambiente multithread è stata utilizzata una comune LinkedList, il che ha portato a in runtime. Si risolve passando a ConcurrentLinkedDeque o utilizzando blocchi synchronized. Abbiamo scelto la prima opzione ().
- Perdita di memoria, durante la disconnessione non viene rimossa l'informazione sulla connessione ().
- In modalità streaming (quando il websocket non viene chiuso alla fine del campione, ma viene utilizzato ulteriormente nel piano) non funzionano i pattern di risposta ().
Questo è uno di quelli su github. Cosa abbiamo fatto:
- Abbiamo preso (@elyrank) - in esso sono stati risolti i problemi 1 e 3
- Abbiamo risolto il problema 2
- Abbiamo aggiornato jetty da 9.2.14 a 9.3.12
- Abbiamo avvolto SimpleDateFormat in ThreadLocal; SimpleDateFormat non è thread-safe, il che portava a NPE in runtime
- Abbiamo eliminato un'altra perdita di memoria (non chiudeva correttamente la connessione durante la disconnessione)
Eppure perde!
La memoria non finiva più in un giorno, ma in due. Non ci restava tempo, abbiamo deciso di avviare meno thread, ma su quattro agenti. Questo doveva bastare, almeno, per una settimana.
Sono passati due giorni…
Ora la memoria stava finendo su Hazelcast. Nei log si vedeva che dopo un paio di giorni di test, Hazelcast iniziava a lamentarsi per la mancanza di memoria, e dopo un po' il cluster crollava e i nodi continuavano a morire uno a uno. Abbiamo collegato JVisualVM a hazelcast e abbiamo notato una "sega ascendente" - invocava regolarmente GC ma non riusciva a liberare memoria.
Si è scoperto che in hazelcast 3.4, durante la rimozione di map / multiMap (map.destroy()), la memoria non viene liberata completamente:
Ora il problema è risolto nella 3.5, ma all'epoca era un problema. Creavamo nuovi multiMap con nomi dinamici e li rimuovevamo secondo la nostra logica. Il codice appariva più o meno così:
public void join(Authentication auth, String sub) {
MultiMap sessions = instance.getMultiMap(sub);
sessions.put(auth.getUserId(), auth);
}
public void leave(Authentication auth, String sub) {
MultiMap sessions = instance.getMultiMap(sub);
sessions.remove(auth.getUserId(), auth);
if (sessions.size() == 0) {
sessions.destroy();
}
}Chiamata:
service.join(auth1, "NEW_MESSAGES_IN_DISCUSSION_UUID1");
service.join(auth2, "NEW_MESSAGES_IN_DISCUSSION_UUID1");La multiMap veniva creata per ogni sottoscrizione e cancellata quando non era più necessaria. Abbiamo deciso di creare una Map, utilizzando il nome della sottoscrizione come chiave e come valori gli identificatori delle sessioni (che poi possono essere utilizzati per ottenere gli identificatori 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.
Cosa altro abbiamo imparato sul testing delle prestazioni
- JSR223 deve essere scritto in groovy e includere la cache di compilazione - questo è molto più veloce. .
- I grafici Jmeter-Plugins sono più facili da comprendere rispetto a quelli standard. .
La nostra esperienza con Hazelcast
Hazelcast per noi era un nuovo prodotto, abbiamo iniziato a lavorare con esso dalla versione 3.4.1, attualmente sul nostro server di produzione è installata la versione 3.9.2 (al momento della scrittura dell'articolo, l'ultima versione di Hazelcast era 3.10).
Generazione ID
Inizialmente utilizzavamo identificatori interi. Immaginiamo di aver bisogno di un nuovo Long per una nuova entità. La sequenza nel DB non è adatta, le tabelle partecipano allo sharding - si verificherebbe che c'è un messaggio ID=1 nel DB1 e un messaggio ID=1 nel DB2, in Elasticsearch non puoi utilizzare tale ID, né in Hazelcast, ma la cosa più grave è che se desideri unire i dati di due DB in uno (ad esempio, decidendo che un solo DB è sufficiente per questi abbonati). Puoi creare in Hazelcast diversi AtomicLong e mantenere il contatore lì, quindi le prestazioni per ottenere un nuovo ID sono incrementAndGet più il tempo per la richiesta in Hazelcast. Ma in Hazelcast c'è qualcosa di più ottimale: FlakeIdGenerator. A ogni cliente viene assegnato un intervallo di ID quando si connette, ad esempio, il primo - da 1 a 10.000, il secondo - da 10.001 a 20.000 e così via. Ora il cliente può rilasciare nuovi identificatori autonomamente, fino a quando l'intervallo emesso non si esaurisce. Funziona rapidamente, ma al riavvio dell'applicazione (e del cliente Hazelcast), inizia una nuova sequenza - da qui i salti, ecc. Inoltre, non è molto chiaro agli sviluppatori perché gli ID siano numerici ma siano così disordinati. Abbiamo valutato tutto e siamo passati a UUID.
A proposito, per chi vuole essere come Twitter, c'è una libreria chiamata Snowcast: è un'implementazione di Snowflake sopra Hazelcast. Puoi guardarla qui:
Ma noi non siamo riusciti a raggiungerla.
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 : oldValueHo dovuto scrivere il mio replace, usando 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, mentre TransactionalMap no.
Sostituire un nuovo JAR senza downtime
Inizialmente abbiamo deciso di salvare in Hazelcast oggetti delle nostre classi. Ad esempio, abbiamo una classe Application e 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 cercare attraverso di esso:
map.addIndex("subscriberId", false);E quando abbiamo aggiunto una nuova entità, abbiamo iniziato a ricevere ClassNotFoundException. Hazelcast stava cercando di aggiornare l'indice, ma non sapeva nulla sulla nostra classe e voleva che inserissimo un JAR con quella classe. Così abbiamo fatto, tutto ha funzionato, ma è emerso un nuovo problema: come aggiornare il JAR senza fermare completamente il cluster? Hazelcast non rileva un nuovo JAR durante gli aggiornamenti a caldo. A questo punto, abbiamo deciso che potevamo vivere senza la ricerca per indice. Infatti, se utilizzi Hazelcast come deposito di tipo chiave-valore, funzionerà tutto? Non proprio. Qui si verifica di nuovo un comportamento diverso tra IMap e TransactionalMap. Dove a IMap non importa, TransactionalMap restituisce un errore.
IMap. Registriamo 5000 oggetti, li 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 User Class Deployment. Puoi designare un nod 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 ha bisogno di conoscere la struttura delle nostre classi, e possiamo aggiornarci senza downtime. La gestione della versioning degli oggetti di dominio è effettuata dall'applicazione. Diverse versioni dell'applicazione possono essere eseguite contemporaneamente e può accadere che una nuova applicazione scriva oggetti con nuovi campi, mentre la vecchia non è ancora a conoscenza di questi campi. Allo stesso tempo, la nuova applicazione legge oggetti scritti dalla vecchia applicazione, che non contengono i nuovi campi. Gestiamo queste situazioni all'interno dell'applicazione, ma per semplicità non cambiamo né rimuoviamo i campi, estendiamo solo le classi aggiungendo nuovi campi.
Come garantiamo alte prestazioni
Quattro operazioni in Hazelcast – bene, due nel DB – male
È sempre meglio recuperare i dati dalla cache piuttosto che dal DB, ma non vogliamo nemmeno conservare record non richiesti. La decisione su cosa memorizzare nella cache viene rimandata all'ultima fase dello sviluppo. Una volta che la nuova funzionalità è stata codificata, attiviamo in PostgreSQL il logging di tutte le richieste (log_min_duration_statement impostato a 0) e avviamo test di carico per circa 20 minuti. Dai log raccolti, strumenti come pgFouine e pgBadger possono generare report analitici. Nei report cerchiamo in primo luogo richieste lente e frequenti. Per le richieste lente costruiamo un piano di esecuzione (EXPLAIN) e valutiamo se la richiesta può essere ottimizzata. Le richieste frequenti con gli stessi dati di input si adattano bene alla cache. Cerchiamo di mantenere le richieste "piatte", interrogando una sola tabella per volta.
Gestione
SV come servizio online è stata messa in funzione nella primavera del 2017, mentre come prodotto separato SV è stata rilasciata a novembre 2017 (in quel momento in versione beta).
In oltre un anno di operatività non si sono verificati gravi problemi nel funzionamento del servizio online SV. Monitoriamo il servizio online tramite , raccogliamo e deployiamo da .
Il pacchetto del server SV viene fornito come pacchetti nativi: RPM, DEB, MSI. Inoltre, per Windows forniamo un installatore unico in un unico file EXE, che installa il server, Hazelcast ed Elasticsearch su una sola macchina. Inizialmente chiamavamo questa versione dell'installazione "dimostrativa", ma ora è chiaro che è la soluzione di distribuzione più popolare.
Fonte: habr.com
