Com i per què vam escriure un servei escalable molt carregat per a 1C: Enterprise: Java, PostgreSQL, Hazelcast

En aquest article parlarem de com i per què ens hem desenvolupat Sistema d'interacció - un mecanisme que transfereix informació entre les aplicacions de client i els servidors 1C: Enterprise - des de la configuració d'una tasca fins a pensar en l'arquitectura i els detalls de la implementació.

El Sistema d'Interacció (d'ara endavant - CB) és un sistema de missatgeria distribuït tolerant a fallades amb lliurament garantit. CB està dissenyat com un servei d'alta càrrega i alta escalabilitat, disponible tant com a servei en línia (prestat per 1C) com com a producte de producció massiva que es pot desplegar a les seves pròpies instal·lacions de servidor.

SW utilitza emmagatzematge distribuït avellana i motor de cerca Elasticsearch. També parlarem de Java i de com escalem horitzontalment PostgreSQL.
Com i per què vam escriure un servei escalable molt carregat per a 1C: Enterprise: Java, PostgreSQL, Hazelcast

Declaració de problemes

Per deixar clar per què hem creat el Sistema d'interacció, us explicaré una mica com funciona el desenvolupament d'aplicacions empresarials a 1C.

Primer, una mica de nosaltres per a aquells que encara no saben què fem :) Estem desenvolupant la plataforma tecnològica 1C:Enterprise. La plataforma inclou una eina de desenvolupament d'aplicacions empresarials, així com un temps d'execució que permet que les aplicacions empresarials funcionin en un entorn multiplataforma.

Paradigma de desenvolupament client-servidor

Les aplicacions empresarials creades a 1C:Enterprise funcionen en tres nivells client-servidor arquitectura "DBMS - servidor d'aplicacions - client". Codi de l'aplicació escrit llenguatge integrat 1C, es pot executar al servidor d'aplicacions o al client. Tot el treball amb objectes d'aplicació (directoris, documents, etc.), així com la lectura i l'escriptura de la base de dades, es realitza només al servidor. La funcionalitat de formularis i interfície d'ordres també s'implementa al servidor. Al client, es reben, s'obren i es mostren formularis, "comunicació" amb l'usuari (avisos, preguntes...), petits càlculs en formularis que requereixen una resposta ràpida (per exemple, multiplicant el preu per la quantitat), treballant amb fitxers locals, treballant amb equips.

En el codi de l'aplicació, les capçaleres de procediments i funcions han d'indicar explícitament on s'executarà el codi, utilitzant les directives &AtClient / &AtServer (&AtClient / &AtServer en la versió anglesa de l'idioma). Els desenvolupadors 1C ara em corregiran dient que les directives són realment més que, però per a nosaltres ara no és important.

Podeu trucar al codi del servidor des del codi del client, però no podeu trucar al codi del client des del codi del servidor. Aquesta és una limitació fonamental, feta per una sèrie de raons. En particular, perquè el codi del servidor s'ha d'escriure de tal manera que s'executi de la mateixa manera, sense importar des d'on es cridi: des del client o des del servidor. I en el cas d'una trucada al codi del servidor des d'un altre codi del servidor, no hi ha client com a tal. I perquè durant l'execució del codi del servidor, el client que l'ha cridat podria tancar, sortir de l'aplicació i el servidor no tindria a qui trucar.

Com i per què vam escriure un servei escalable molt carregat per a 1C: Enterprise: Java, PostgreSQL, Hazelcast
Codi que gestiona un clic de botó: cridar a un procediment de servidor des del client funcionarà, cridar a un procediment de client des del servidor no

Això vol dir que si volem enviar algun missatge des del servidor a l'aplicació client, per exemple, que la formació d'un informe "de llarga durada" ha acabat i l'informe es pot veure, no tenim aquesta manera. Heu d'utilitzar trucs, per exemple, enquestar periòdicament el servidor des del codi del client. Però aquest enfocament carrega el sistema amb trucades innecessàries i, en general, no sembla gaire elegant.

I també hi ha una necessitat, per exemple, quan un telèfon SIP-truca, notificar-ho a l'aplicació client perquè la trobi a la base de dades de la contrapart pel número de la persona que truca i mostri a l'usuari la informació sobre la contrapart que truca. O, per exemple, quan arriba una comanda al magatzem, aviseu-ho a l'aplicació client del client. En general, hi ha molts casos en què aquest mecanisme seria útil.

En realitat configurant

Crear un mecanisme de missatgeria. Ràpid, fiable, amb lliurament garantit, amb possibilitat de cerca flexible de missatges. En funció del mecanisme, implementeu un missatger (missatges, videotrucades) que funcioni dins d'aplicacions 1C.

Dissenyar el sistema horitzontalment escalable. Una càrrega creixent hauria de ser coberta per un augment del nombre de nodes.

Implementació

Vam decidir no incrustar la part del servidor del CB directament a la plataforma 1C:Enterprise, sinó implementar-la com a producte independent, l'API del qual es pot cridar des del codi de les solucions d'aplicació 1C. Això es va fer per diversos motius, el principal dels quals va ser per fer possible l'intercanvi de missatges entre diferents aplicacions 1C (per exemple, entre el Departament de Comerç i Comptabilitat). Diferents aplicacions 1C es poden executar en diferents versions de la plataforma 1C:Enterprise, estar ubicades en diferents servidors, etc. En aquestes condicions, la implementació de CB com a producte independent, situat "al costat" de les instal·lacions 1C, és la solució òptima.

Per tant, vam decidir fer CB com un producte independent. Per a les empreses més petites, recomanem utilitzar el servidor CB que hem instal·lat al nostre núvol (wss://1cdialog.com) per evitar la sobrecàrrega associada a la instal·lació i configuració del servidor local. Els grans clients, però, poden considerar convenient instal·lar el seu propi servidor CB a les seves instal·lacions. Hem utilitzat un enfocament similar al nostre producte SaaS al núvol. 1cFresca – es llança com a producte de producció per a la instal·lació dels clients i també es desplega al nostre núvol https://1cfresh.com/.

Aplicació

Per a la distribució de càrrega i la tolerància a errors, no desplegarem una aplicació Java, sinó diverses, posarem un equilibrador de càrrega davant d'elles. Si necessiteu enviar un missatge d'un node a un altre, feu servir la publicació/subscriure's a Hazelcast.

Comunicació entre el client i el servidor - mitjançant websocket. És molt adequat per a sistemes en temps real.

Memòria cau distribuïda

Trieu entre Redis, Hazelcast i Ehcache. Fora el 2015. Redis acaba de llançar un nou clúster (massa nou, espantós), hi ha un Sentinel amb moltes restriccions. Ehcache no sap com agrupar (aquesta funcionalitat va aparèixer més tard). Vam decidir provar amb Hazelcast 3.4.
Hazelcast està agrupat fora de la caixa. En el mode de node únic, no és molt útil i només pot cabre com a memòria cau: no sap com bolcar dades al disc, si es perd l'únic node, les dades es perden. Despleguem diversos Hazelcasts, entre els quals fem una còpia de seguretat de dades crítiques. No fem una còpia de seguretat de la memòria cau; no ens sap greu.

Per a nosaltres, Hazelcast és:

  • Emmagatzematge de sessions d'usuari. Es triga molt de temps a anar a la base de dades per a una sessió, així que posem totes les sessions a Hazelcast.
  • Memòria cau. Buscant un perfil d'usuari: comproveu la memòria cau. Va escriure un missatge nou: posar-lo a la memòria cau.
  • Temes per a la comunicació d'instàncies d'aplicació. El node genera un esdeveniment i el col·loca en un tema Hazelcast. Altres nodes d'aplicació subscrits a aquest tema reben i processen l'esdeveniment.
  • bloquejos de clúster. Per exemple, creem una discussió mitjançant una clau única (discussion-singleton en el marc de la base 1C):

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

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

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

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

Hem comprovat que no hi ha cap canal. Van agafar el pany, el van tornar a comprovar, el van crear. Si no comproveu després de prendre el bloqueig, hi ha la possibilitat que un altre fil també s'hagi comprovat en aquell moment i ara intentarà crear la mateixa discussió, i ja existeix. És impossible fer un bloqueig mitjançant el bloqueig java sincronitzat o normal. A través de la base, lentament, i la base és una llàstima, a través de Hazelcast, el que necessiteu.

Selecció d'un SGBD

Tenim una àmplia i exitosa experiència amb PostgreSQL i la cooperació amb els desenvolupadors d'aquest SGBD.

Amb un clúster, PostgreSQL no és fàcil; n'hi ha XL, XC, Citus, però, en general, no és noSQL el que s'escala fora de la caixa. NoSQL no es considerava com l'emmagatzematge principal, n'hi havia prou que agafem Hazelcast, amb el qual no havíem treballat abans.

Com que necessiteu escalar una base de dades relacional, vol dir fragmentació. Com ja sabeu, quan es divideix, dividim la base de dades en parts separades perquè cadascuna d'elles es pugui col·locar en un servidor independent.

La primera versió del nostre sharding suposava la capacitat de difondre cadascuna de les taules de la nostra aplicació a diferents servidors en diferents proporcions. Molts missatges al servidor A; si us plau, traslladem part d'aquesta taula al servidor B. Aquesta decisió només va cridar sobre una optimització prematura, així que vam decidir limitar-nos a un enfocament multi-inquilí.

Podeu llegir sobre multi-inquilí, per exemple, al lloc web Dades Citus.

A SV hi ha conceptes de l'aplicació i l'abonat. Una aplicació és una instal·lació específica d'una aplicació empresarial, com ERP o Comptabilitat, amb els seus usuaris i dades empresarials. Un subscriptor és una organització o una persona en nom de la qual l'aplicació està registrada al servidor CB. Un subscriptor pot tenir diverses aplicacions registrades i aquestes aplicacions poden intercanviar missatges entre elles. L'abonat es va convertir en llogater del nostre sistema. Els missatges de diversos subscriptors es poden localitzar en una base física; si veiem que algun subscriptor ha començat a generar molt trànsit, el traslladem a una base de dades física independent (o fins i tot a un servidor de bases de dades independent).

Tenim una base de dades principal on s'emmagatzema la taula d'encaminament amb informació sobre la ubicació de totes les bases de dades de subscriptors.

Com i per què vam escriure un servei escalable molt carregat per a 1C: Enterprise: Java, PostgreSQL, Hazelcast

Per evitar que la base de dades principal sigui un coll d'ampolla, mantenim la taula d'encaminament (i altres dades sol·licitades amb freqüència) a la memòria cau.

Si la base de dades de l'abonat comença a alentir-se, la tallarem en particions a l'interior. En altres projectes, utilitzem per particionar taules grans pg_pathman.

Com que perdre missatges d'usuari és dolent, fem una còpia de seguretat de les nostres bases de dades amb rèpliques. La combinació de rèpliques síncrones i asíncrones us permet assegurar-vos contra la pèrdua de la base de dades principal. La pèrdua de missatge només es produirà en cas de fallada simultània de la base de dades principal i la seva rèplica síncrona.

Si es perd la rèplica síncrona, la rèplica asíncrona esdevé sincrònica.
Si es perd la base de dades principal, la rèplica síncrona es converteix en la base de dades principal, la rèplica asíncrona es converteix en una rèplica síncrona.

Elasticsearch per a la cerca

Com que, entre altres coses, el CB també és un missatger, aquí necessitem una cerca ràpida, còmoda i flexible, tenint en compte la morfologia, per coincidències inexactes. Vam decidir no reinventar la roda i utilitzar el cercador gratuït Elasticsearch, creat a partir de la biblioteca Lucene. També despleguem Elasticsearch en un clúster (mestre - dades - dades) per eliminar problemes en cas de fallada dels nodes de l'aplicació.

A github hem trobat Connector de morfologia russa per a Elasticsearch i utilitzar-lo. A l'índex Elasticsearch, emmagatzemem arrels de paraules (que defineix el connector) i N-grams. A mesura que l'usuari introdueix text per cercar, cerquem el text escrit entre N-grams. Quan es deseu a l'índex, la paraula "textos" es dividirà en els N-grams següents:

[te, tech, tex, text, texts, ek, eks, ext, exts, ks, kst, ksty, st, sty, you],

I també es desarà l'arrel de la paraula "text". Aquest enfocament us permet cercar al principi, al mig i al final de la paraula.

Imatge general

Com i per què vam escriure un servei escalable molt carregat per a 1C: Enterprise: Java, PostgreSQL, Hazelcast
Repetint la imatge del principi de l'article, però amb explicacions:

  • Balancer exposat a Internet; tenim nginx, pot ser qualsevol.
  • Les instàncies d'aplicacions Java es comuniquen entre elles mitjançant Hazelcast.
  • Per treballar amb un endoll web, fem servir Netty.
  • L'aplicació Java escrita en Java 8, consta de paquets OSGi. Els plans són migrar a Java 10 i canviar a mòduls.

Desenvolupament i proves

Durant el desenvolupament i les proves del CB, vam trobar una sèrie de característiques interessants dels productes que utilitzem.

Proves de càrrega i fuites de memòria

El llançament de cada llançament CB és una prova de càrrega. Va passar amb èxit quan:

  • La prova va funcionar durant diversos dies i no hi va haver denegacions de servei
  • El temps de resposta per a les operacions clau no va superar un llindar còmode
  • La degradació del rendiment en comparació amb la versió anterior no supera el 10%

Omplim la base de dades de proves amb dades: per a això obtenim informació sobre el subscriptor més actiu del servidor de producció, multipliquem els seus números per 5 (el nombre de missatges, discussions, usuaris) i així ho fem.

Realitzem proves de càrrega del sistema d'interacció en tres configuracions:

  1. prova d'esforç
  2. Només connexions
  3. Registre de subscriptor

Durant una prova d'esforç, iniciem diversos centenars de fils, i carreguen el sistema sense parar: escriuen missatges, creen discussions, reben una llista de missatges. Simulem les accions dels usuaris normals (aconseguir una llista dels meus missatges no llegits, escriure a algú) i les decisions del programa (transferir un paquet a una altra configuració, processar una alerta).

Per exemple, així és com es veu una part de la prova d'estrès:

  • L'usuari inicia sessió
    • Demana els vostres fils no llegits
    • 50% de possibilitats de llegir missatges
    • 50% de possibilitats d'escriure missatges
    • Següent usuari:
      • 20% de possibilitats de crear un fil nou
      • Selecciona aleatòriament qualsevol de les seves discussions
      • Entra dins
      • Missatges de sol·licituds, perfils d'usuari
      • Crea cinc missatges dirigits a usuaris aleatoris d'aquest fil
      • Fora de discussió
      • Es repeteix 20 vegades
      • Tanca la sessió, torna al principi de l'script

    • Un chatbot entra al sistema (emula la missatgeria del codi de solucions aplicades)
      • 50% de possibilitats de crear un nou canal de dades (debat especial)
      • 50% de possibilitats d'escriure un missatge en qualsevol dels canals existents

L'escenari "Només connexions" va aparèixer per un motiu. Hi ha una situació: els usuaris han connectat el sistema, però encara no hi han participat. Cada usuari al matí a les 09:00 encén l'ordinador, estableix una connexió amb el servidor i guarda silenci. Aquests nois són perillosos, n'hi ha molts: només tenen PING / PONG fora dels paquets, però mantenen la connexió amb el servidor (no poden mantenir-la, i de sobte un missatge nou). La prova reprodueix la situació quan un gran nombre d'aquests usuaris intenten iniciar sessió al sistema en mitja hora. Sembla una prova d'estrès, però el seu enfocament se centra precisament en aquesta primera entrada, perquè no hi hagi errors (una persona no fa servir el sistema, però ja s'està caient, és difícil trobar alguna cosa pitjor).

L'escenari de registre de subscriptor s'origina des del primer llançament. Vam fer una prova d'esforç i vam estar segurs que el sistema no s'alentirà en la correspondència. Però els usuaris van anar i el registre va començar a caure en el temps d'espera. En registrar-nos, hem utilitzat / Dev / random, que està lligada a l'entropia del sistema. El servidor no va tenir temps per acumular prou entropia i, quan es va demanar un nou SecureRandom, es va congelar durant desenes de segons. Hi ha moltes maneres de sortir d'aquesta situació, per exemple: canviar a un /dev/urandom menys segur, instal·lar un tauler especial que generi entropia, generar números aleatoris per endavant i emmagatzemar-los a la piscina. Vam tancar temporalment el problema amb la piscina, però des de llavors hem fet una prova independent per registrar nous subscriptors.

Com a generador de càrrega fem servir jmetre. No sap com treballar amb un websocket, cal un connector. Els primers dels resultats de la cerca per a la consulta "jmeter websocket" són articles amb BlazeMeteren què recomanen connector de Maciej Zaleski.

És per aquí on vam decidir començar.

Gairebé immediatament després de l'inici de les proves serioses, vam descobrir que les fuites de memòria van començar a JMeter.

El connector és una gran història a part, amb 176 estrelles, té 132 forquilles a github. El mateix autor no s'hi ha compromès des del 2015 (el vam prendre el 2015, després no va despertar sospita), diversos problemes de github sobre fuites de memòria, 7 sol·licituds d'extracció no tancades.
Si trieu carregar la prova amb aquest connector, tingueu en compte les discussions següents:

  1. En un entorn multiprocés, es va utilitzar l'habitual LinkedList, com a resultat, vam obtenir NPE en temps d'execució. Es resol canviant a ConcurrentLinkedDeque o mitjançant blocs sincronitzats. Hem escollit la primera opció per nosaltres mateixoshttps://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. Fuga de memòria, la informació de connexió no s'elimina en desconnectar (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. En mode de transmissió (quan el websocket no es tanca al final de la mostra, però s'utilitza més en el pla), els patrons de resposta no funcionen (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

Aquest és un dels que hi ha a github. Què vam fer:

  1. Han pres forquilla d'Elyran Kogan (@elyrank): soluciona els problemes 1 i 3
  2. Problema resolt 2
  3. Moll actualitzat del 9.2.14 al 9.3.12
  4. SimpleDateFormat embolicat a ThreadLocal; SimpleDateFormat no és segur per a fils que condueix a NPE en temps d'execució
  5. S'ha solucionat una altra fuga de memòria (la connexió es va tancar incorrectament en desconnectar)

I tanmateix flueix!

La memòria va començar a acabar no en un dia, sinó en dos. No hi va haver temps, vam decidir executar menys fils, però en quatre agents. Això hauria d'haver estat suficient durant almenys una setmana.

Han passat dos dies...

Ara Hazelcast s'està quedant sense memòria. Els registres van mostrar que després d'un parell de dies de proves, Hazelcast comença a queixar-se de la manca de memòria i, al cap d'un temps, el clúster es desfà i els nodes continuen morint un a un. Vam connectar JVisualVM a hazelcast i vam veure la "serra cap amunt": habitualment anomenava GC, però no podia esborrar la memòria de cap manera.

Com i per què vam escriure un servei escalable molt carregat per a 1C: Enterprise: Java, PostgreSQL, Hazelcast

Va resultar que a hazelcast 3.4, en suprimir un mapa / multiMap (map.destroy()), la memòria no s'allibera completament:

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

L'error s'ha corregit ara a la 3.5, però aleshores era un problema. Hem creat un nou multiMap amb noms dinàmics i esborrat segons la nostra lògica. El codi semblava a això:

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

Comentari:

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

multiMap es va crear per a cada subscripció i es va eliminar quan no era necessari. Vam decidir que començaríem un mapa , la clau serà el nom de la subscripció, i els valors seran identificadors de sessió (a través dels quals podeu obtenir identificadors d'usuari, si cal).

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

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

Els gràfics han millorat.

Com i per què vam escriure un servei escalable molt carregat per a 1C: Enterprise: Java, PostgreSQL, Hazelcast

Què més hem après sobre les proves de càrrega

  1. JSR223 s'ha d'escriure en groovy i incloure memòria cau de compilació: és molt més ràpid. Enllaç.
  2. Els gràfics Jmeter-Plugins són més fàcils d'entendre que els estàndards. Enllaç.

Sobre la nostra experiència amb Hazelcast

Hazelcast era un producte nou per a nosaltres, vam començar a treballar amb ell des de la versió 3.4.1, ara tenim la versió 3.9.2 al nostre servidor de producció (en el moment d'escriure aquest article, l'última versió d'Hazelcast és la 3.10).

Generació d'identificació

Hem començat amb identificadors enters. Imaginem que necessitem un altre Long per a una nova entitat. La seqüència de la base de dades no és adequada, les taules estan implicades en la fragmentació: resulta que hi ha un ID de missatge=1 a DB1 i un ID de missatge=1 a DB2, no podeu posar aquest ID a Elasticsearch, tampoc a Hazelcast, però el pitjor és si voleu reduir les dades de dues bases de dades a una (per exemple, decidir que una base de dades és suficient per a aquests subscriptors). Podeu tenir diversos AtomicLongs a Hazelcast i mantenir el comptador allà, llavors el rendiment d'obtenir una nova identificació és incrementAndGet més el temps per consultar a Hazelcast. Però Hazelcast té quelcom més òptim: FlakeIdGenerator. Quan es posa en contacte, a cada client se li dóna una sèrie d'identificacions, per exemple, la primera, d'1 a 10, la segona, de 000 a 10, etc. Ara el client pot emetre nous identificadors per si mateix fins que acabi l'interval que se li ha emès. Funciona ràpidament, però en reiniciar l'aplicació (i el client Hazelcast) s'inicia una nova seqüència, d'aquí els salts, etc. A més, els desenvolupadors no tenen molt clar per què els identificadors són nombres enters, però van molt en desacord. Ho vam pesar tot i vam canviar als UUID.

Per cert, per a aquells que volen ser com Twitter, hi ha una biblioteca de Snowcast: aquesta és una implementació de Snowflake a la part superior de Hazelcast. Podeu veure aquí:

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

Però encara no hi hem arribat.

TransactionalMap.replace

Una altra sorpresa: TransactionalMap.replace no funciona. Aquí teniu 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

Vaig haver d'escriure la meva pròpia substitució amb 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);
}

Proveu no només les estructures de dades habituals, sinó també les seves versions transaccionals. Passa que IMap funciona, però TransactionalMap ja no existeix.

Connecteu un JAR nou sense temps d'inactivitat

Primer, vam decidir escriure objectes de les nostres classes a Hazelcast. Per exemple, tenim una classe d'aplicació, la volem emmagatzemar i llegir. Desa:

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

Llegim:

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

Tot funciona. Aleshores vam decidir crear un índex a Hazelcast per cercar-lo:

map.addIndex("subscriberId", false);

I en escriure una nova entitat, van començar a rebre una ClassNotFoundException. Hazelcast va intentar afegir a l'índex, però no sabia res de la nostra classe i volia posar-hi un JAR amb aquesta classe. Vam fer això, tot va funcionar, però va aparèixer un nou problema: com actualitzar el JAR sense aturar completament el clúster? Hazelcast no recull un nou JAR en una actualització per node. En aquest punt, vam decidir que podríem viure sense cerques d'índex. Al cap i a la fi, si utilitzeu Hazelcast com a botiga de valor-clau, tot funcionarà? No realment. Aquí de nou un comportament diferent d'IMap i TransactionalMap. Quan IMap no li importa, TransactionalMap genera un error.

IMap. Escrivim 5000 objectes, llegim. S'espera de tot.

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

Però no funciona en una transacció, obtenim 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();
        }
    });
}

A la 3.8, va aparèixer el mecanisme de desplegament de classe d'usuari. Podeu designar un node mestre i actualitzar-hi el fitxer JAR.

Ara hem canviat completament el nostre enfocament: nosaltres mateixos serialitzem a JSON i desem a Hazelcast. Hazelcast no necessita conèixer l'estructura de les nostres classes i podem actualitzar-nos sense temps d'inactivitat. La versió dels objectes de domini està controlada per l'aplicació. Es poden llançar diferents versions de l'aplicació al mateix temps, i és possible que una nova aplicació escrigui objectes amb camps nous, mentre que l'antiga encara no coneix aquests camps. I al mateix temps, la nova aplicació llegeix els objectes escrits per l'aplicació antiga que no tenen camps nous. Gestionem aquestes situacions dins de l'aplicació, però per senzillesa no canviem ni eliminem els camps, només ampliem les classes afegint nous camps.

Com oferim un alt rendiment

Quatre viatges a Hazelcast són bo, dos viatges a la base de dades són dolents

Buscar dades a la memòria cau sempre és millor que a la base de dades, però tampoc no voleu emmagatzemar registres no reclamats. La decisió de què s'ha de posar a la memòria cau es deixa a l'última etapa de desenvolupament. Quan es codifica la nova funcionalitat, activem el registre de totes les consultes a PostgreSQL (log_min_duration_statement a 0) i fem proves de càrrega durant 20 minuts. Utilitats com pgFouine i pgBadger poden crear informes analítics basats en els registres recollits. Als informes, busquem principalment consultes lentes i freqüents. Per a consultes lentes, construïm un pla d'execució (EXPLAIN) i avaluem si aquesta consulta es pot accelerar. Les sol·licituds freqüents per a la mateixa entrada encaixen bé a la memòria cau. Intentem mantenir les consultes "planes", una taula per consulta.

Aprofitament

CB com a servei en línia es va llançar a la primavera de 2017, ja que un producte CB independent es va llançar el novembre de 2017 (aleshores en estat beta).

Durant més d'un any de funcionament, no hi ha problemes greus en el funcionament del servei en línia CB. Fem un seguiment del servei en línia a través de Zabbix, recollir i desplegar des de Bambú.

La distribució del servidor CB es presenta en forma de paquets natius: RPM, DEB, MSI. A més, per a Windows, oferim un únic instal·lador en forma d'un únic EXE que instal·la el servidor, Hazelcast i Elasticsearch en una màquina. Al principi vam anomenar aquesta versió de la instal·lació "demo", però ara ha quedat clar que aquesta és l'opció de desplegament més popular.

Font: www.habr.com

Afegeix comentari