Hoe en waarom we een schaalbare service met hoge belasting schreven voor 1C: Enterprise: Java, PostgreSQL, Hazelcast

In dit artikel zullen we praten over hoe en waarom we ons ontwikkelden Interactie Systeem – een mechanisme dat informatie overdraagt ​​tussen clientapplicaties en 1C:Enterprise-servers - van het instellen van een taak tot het doordenken van de architectuur en implementatiedetails.

Het interactiesysteem (hierna SV genoemd) is een gedistribueerd, fouttolerant berichtensysteem met gegarandeerde bezorging. SV is ontworpen als een dienst met hoge belasting en hoge schaalbaarheid, die zowel beschikbaar is als een online dienst (geleverd door 1C) als als een massaproduct dat op uw eigen serverfaciliteiten kan worden ingezet.

SV maakt gebruik van gedistribueerde opslag Hazelcast en zoekmachine Elasticsearch. We zullen ook praten over Java en hoe we PostgreSQL horizontaal schalen.
Hoe en waarom we een schaalbare service met hoge belasting schreven voor 1C: Enterprise: Java, PostgreSQL, Hazelcast

Formulering van het probleem

Om duidelijk te maken waarom we het interactiesysteem hebben gemaakt, zal ik je iets vertellen over hoe de ontwikkeling van bedrijfsapplicaties in 1C werkt.

Om te beginnen iets over ons voor degenen die nog niet weten wat we doen :) We maken het 1C:Enterprise technologieplatform. Het platform omvat een ontwikkelingstool voor bedrijfsapplicaties, evenals een runtime waarmee bedrijfsapplicaties in een platformonafhankelijke omgeving kunnen worden uitgevoerd.

Client-server ontwikkelingsparadigma

Bedrijfsapplicaties die op 1C:Enterprise zijn gemaakt, werken op drie niveaus client server architectuur “DBMS – applicatieserver – client”. Applicatiecode geschreven ingebouwde 1C-taal, kan worden uitgevoerd op de applicatieserver of op de client. Al het werk met applicatieobjecten (directories, documenten, enz.), evenals het lezen en schrijven van de database, wordt alleen op de server uitgevoerd. De functionaliteit van formulieren en opdrachtinterface is ook op de server geïmplementeerd. De klant voert het ontvangen, openen en weergeven van formulieren uit, “communiceert” met de gebruiker (waarschuwingen, vragen...), kleine berekeningen in formulieren die een snelle reactie vereisen (bijvoorbeeld het vermenigvuldigen van de prijs met het aantal), het werken met lokale bestanden, werken met apparatuur.

In applicatiecode moeten de headers van procedures en functies expliciet aangeven waar de code zal worden uitgevoerd - met behulp van de &AtClient / &AtServer-richtlijnen (&AtClient / &AtServer in de Engelse versie van de taal). 1C-ontwikkelaars zullen mij nu corrigeren door te zeggen dat richtlijnen dat wel zijn больше, maar voor ons is dit nu niet belangrijk.

U kunt de servercode aanroepen vanuit de clientcode, maar u kunt de clientcode niet aanroepen vanuit de servercode. Dit is een fundamentele beperking die we om een ​​aantal redenen hebben gemaakt. In het bijzonder omdat servercode zo moet worden geschreven dat deze op dezelfde manier wordt uitgevoerd, ongeacht waar deze wordt aangeroepen: vanaf de client of vanaf de server. En in het geval dat servercode vanuit een andere servercode wordt aangeroepen, is er geen client als zodanig. En omdat tijdens de uitvoering van de servercode de client die de code had opgeroepen, kon sluiten en de applicatie kon verlaten, en de server niemand meer zou hebben om te bellen.

Hoe en waarom we een schaalbare service met hoge belasting schreven voor 1C: Enterprise: Java, PostgreSQL, Hazelcast
Code die een klik op een knop afhandelt: het aanroepen van een serverprocedure vanaf de client werkt, het aanroepen van een clientprocedure vanaf de server niet

Dit betekent dat als we een bericht van de server naar de clientapplicatie willen sturen, bijvoorbeeld dat het genereren van een “langlopend” rapport is voltooid en het rapport kan worden bekeken, we zo’n methode niet hebben. Je moet trucs gebruiken, bijvoorbeeld om de server periodiek te ondervragen via de clientcode. Maar deze aanpak belast het systeem met onnodige oproepen en ziet er over het algemeen niet erg elegant uit.

En er is ook behoefte aan, bijvoorbeeld als er een telefoontje binnenkomt SIP- breng de clientapplicatie hiervan bij het bellen op de hoogte, zodat deze het nummer van de beller kan gebruiken om dit op te zoeken in de database van de tegenpartij en de gebruikersinformatie over de bellende tegenpartij kan tonen. Of geef dit bijvoorbeeld door aan de klantapplicatie van de klant als er een bestelling in het magazijn arriveert. Over het algemeen zijn er veel gevallen waarin een dergelijk mechanisme nuttig zou zijn.

De productie zelf

Creëer een berichtenmechanisme. Snel, betrouwbaar, met gegarandeerde bezorging, met de mogelijkheid om flexibel naar berichten te zoeken. Implementeer op basis van het mechanisme een messenger (berichten, videogesprekken) die binnen 1C-applicaties draait.

Ontwerp het systeem zo dat het horizontaal schaalbaar is. De toenemende belasting moet worden opgevangen door het aantal knooppunten te vergroten.

uitvoering

We hebben besloten om het servergedeelte van SV niet rechtstreeks in het 1C:Enterprise-platform te integreren, maar als een afzonderlijk product te implementeren, waarvan de API kan worden aangeroepen vanuit de code van 1C-applicatieoplossingen. Dit werd gedaan om een ​​aantal redenen, waarvan de belangrijkste was dat ik het mogelijk wilde maken om berichten uit te wisselen tussen verschillende 1C-applicaties (bijvoorbeeld tussen Handelsmanagement en Boekhouding). Verschillende 1C-applicaties kunnen op verschillende versies van het 1C:Enterprise-platform draaien, zich op verschillende servers bevinden, enz. In dergelijke omstandigheden is de implementatie van SV als een afzonderlijk product dat zich “aan de zijkant” van 1C-installaties bevindt, de optimale oplossing.

Daarom hebben we besloten om SV als een afzonderlijk product te maken. We raden kleine bedrijven aan de CB-server te gebruiken die we in onze cloud hebben geïnstalleerd (wss://1cdialog.com) om de overheadkosten te vermijden die gepaard gaan met de lokale installatie en configuratie van de server. Grote klanten kunnen het raadzaam vinden om hun eigen CB-server op hun locatie te installeren. We hebben een vergelijkbare aanpak gebruikt in ons cloud-SaaS-product 1cVers – het wordt geproduceerd als een massaproduct voor installatie op de locatie van de klant, en wordt ook ingezet in onze cloud https://1cfresh.com/.

toepassing

Om de belasting- en fouttolerantie te verdelen, zullen we niet één Java-applicatie inzetten, maar meerdere, met een load balancer ervoor. Als u een bericht van knooppunt naar knooppunt moet overbrengen, gebruikt u publiceren/abonneren in Hazelcast.

De communicatie tussen de client en de server verloopt via websocket. Het is zeer geschikt voor real-time systemen.

Gedistribueerde cache

We kozen tussen Redis, Hazelcast en Ehcache. Het is 2015. Redis heeft zojuist een nieuw cluster uitgebracht (te nieuw, eng), er is Sentinel met veel beperkingen. Ehcache weet niet hoe hij tot een cluster moet assembleren (deze functionaliteit verscheen later). We besloten het te proberen met Hazelcast 3.4.
Hazelcast wordt kant-en-klaar tot een cluster samengesteld. In de modus met één knooppunt is het niet erg handig en kan het alleen als cache worden gebruikt. Het weet niet hoe gegevens naar schijf moeten worden gedumpt. Als u het enige knooppunt verliest, verliest u de gegevens. Wij zetten meerdere Hazelcasts in, waartussen wij een back-up maken van kritische data. We maken geen back-up van de cache – dat vinden we niet erg.

Voor ons is Hazelcast:

  • Opslag van gebruikerssessies. Het duurt elke keer lang om voor een sessie naar de database te gaan, daarom plaatsen we alle sessies in Hazelcast.
  • Cache. Als u op zoek bent naar een gebruikersprofiel, controleer dan de cache. Een nieuw bericht geschreven - plaats het in de cache.
  • Onderwerpen voor communicatie tussen toepassingsinstanties. Het knooppunt genereert een gebeurtenis en plaatst deze in het Hazelcast-onderwerp. Andere toepassingsknooppunten die op dit onderwerp zijn geabonneerd, ontvangen en verwerken de gebeurtenis.
  • Clustersloten. We maken bijvoorbeeld een discussie aan met behulp van een unieke sleutel (singleton-discussie binnen de 1C-database):

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

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

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

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

We hebben gecontroleerd of er geen kanaal is. We hebben het slot gepakt, opnieuw gecontroleerd en gemaakt. Als je de lock niet controleert nadat je de lock hebt genomen, dan bestaat de kans dat een andere thread op dat moment ook heeft gecontroleerd en nu zal proberen dezelfde discussie te creëren - maar deze bestaat al. U kunt niet vergrendelen met gesynchroniseerde of gewone Java Lock. Via de database - het is traag, en het is jammer voor de database; via Hazelcast - dat is wat je nodig hebt.

Een DBMS kiezen

We hebben uitgebreide en succesvolle ervaring met het werken met PostgreSQL en het samenwerken met de ontwikkelaars van dit DBMS.

Het is niet eenvoudig met een PostgreSQL-cluster: dat is zo XL, XC, citus, maar over het algemeen zijn dit geen NoSQL's die out-of-the-box schalen. We beschouwden NoSQL niet als de hoofdopslag; het was voldoende dat we Hazelcast namen, waar we nog niet eerder mee hadden gewerkt.

Als u een relationele database moet schalen, betekent dit: scherven. Zoals u weet verdelen we met sharding de database in afzonderlijke delen, zodat ze elk op een aparte server kunnen worden geplaatst.

De eerste versie van onze sharding ging ervan uit dat elk van de tabellen van onze applicatie in verschillende verhoudingen over verschillende servers kon worden verdeeld. Er staan ​​veel berichten op server A. Laten we alstublieft een deel van deze tabel naar server B verplaatsen. Deze beslissing schreeuwde simpelweg om voortijdige optimalisatie, dus besloten we ons te beperken tot een aanpak met meerdere tenants.

Over multitenant leest u bijvoorbeeld op de website Citus-gegevens.

SV heeft de concepten applicatie en abonnee. Een applicatie is een specifieke installatie van een bedrijfsapplicatie, zoals ERP of Boekhouding, met zijn gebruikers en bedrijfsgegevens. Een abonnee is een organisatie of individu namens wie de applicatie op de SV-server is geregistreerd. Een abonnee kan meerdere applicaties geregistreerd hebben, en deze applicaties kunnen berichten met elkaar uitwisselen. De abonnee is huurder geworden in ons systeem. Berichten van meerdere abonnees kunnen in één fysieke database worden opgeslagen; als we zien dat een abonnee veel verkeer begint te genereren, verplaatsen we deze naar een aparte fysieke database (of zelfs een aparte databaseserver).

We hebben een hoofddatabase waarin een routeringstabel is opgeslagen met informatie over de locatie van alle abonneedatabases.

Hoe en waarom we een schaalbare service met hoge belasting schreven voor 1C: Enterprise: Java, PostgreSQL, Hazelcast

Om te voorkomen dat de hoofddatabase een knelpunt wordt, bewaren we de routeringstabel (en andere vaak benodigde gegevens) in een cache.

Als de database van de abonnee langzamer begint te werken, zullen we deze binnenin in partities opdelen. Bij andere projecten gebruiken we pg_pathman.

Omdat het verliezen van gebruikersberichten slecht is, onderhouden we onze databases met replica's. Door de combinatie van synchrone en asynchrone replica's kunt u zich verzekeren tegen verlies van de hoofddatabase. Berichtverlies treedt alleen op als de primaire database en de synchrone replica ervan tegelijkertijd mislukken.

Als een synchrone replica verloren gaat, wordt de asynchrone replica synchroon.
Als de hoofddatabase verloren gaat, wordt de synchrone replica de hoofddatabase en wordt de asynchrone replica een synchrone replica.

Elastisch zoeken voor zoeken

Omdat SV onder andere ook een boodschapper is, vereist het een snelle, gemakkelijke en flexibele zoekactie, rekening houdend met de morfologie, met behulp van onnauwkeurige overeenkomsten. We hebben besloten het wiel niet opnieuw uit te vinden en de gratis zoekmachine Elasticsearch te gebruiken, gemaakt op basis van de bibliotheek Luceen. Ook zetten wij Elasticsearch in een cluster (master – data – data) in om problemen bij uitval van applicatienodes te elimineren.

Op github vonden we Russische morfologieplug-in voor Elasticsearch en gebruik het. In de Elasticsearch-index slaan we woordwortels (die de plug-in bepaalt) en N-grammen op. Terwijl de gebruiker tekst invoert om te zoeken, zoeken we naar de getypte tekst tussen N-grammen. Wanneer het woord “teksten” in de index wordt opgeslagen, wordt het opgesplitst in de volgende N-grammen:

[die, tek, tex, tekst, teksten, ek, ex, ext, teksten, ks, kst, ksty, st, stal, jij],

En de wortel van het woord ‘tekst’ blijft ook behouden. Met deze aanpak kunt u aan het begin, in het midden en aan het einde van het woord zoeken.

De grote afbeelding

Hoe en waarom we een schaalbare service met hoge belasting schreven voor 1C: Enterprise: Java, PostgreSQL, Hazelcast
Herhaling van de afbeelding uit het begin van het artikel, maar met uitleg:

  • Balancer getoond op internet; we hebben nginx, het kan elk zijn.
  • Java-applicatie-instances communiceren met elkaar via Hazelcast.
  • Om te werken met een websocket gebruiken wij Netty.
  • De Java-applicatie is geschreven in Java 8 en bestaat uit bundels OSGi. De plannen omvatten migratie naar Java 10 en transitie naar modules.

Ontwikkeling en testen

Tijdens het ontwikkelen en testen van de SV kwamen we een aantal interessante kenmerken tegen van de producten die we gebruiken.

Belastingtesten en geheugenlekken

De release van elke SV-release omvat belastingtests. Het is succesvol wanneer:

  • De test werkte meerdere dagen en er waren geen servicestoringen
  • De responstijd voor belangrijke handelingen overschreed een comfortabele drempel niet
  • Prestatieverslechtering ten opzichte van de vorige versie bedraagt ​​maximaal 10%

We vullen de testdatabase met gegevens - hiervoor ontvangen we informatie over de meest actieve abonnee van de productieserver, vermenigvuldigen we de cijfers met 5 (het aantal berichten, discussies, gebruikers) en testen we deze op die manier.

We voeren belastingtesten uit van het interactiesysteem in drie configuraties:

  1. stresstest
  2. Alleen verbindingen
  3. Registratie van abonnees

Tijdens de stresstest lanceren we honderden threads, en deze laden het systeem zonder te stoppen: berichten schrijven, discussies maken, een lijst met berichten ontvangen. We simuleren de acties van gewone gebruikers (een lijst krijgen van mijn ongelezen berichten, naar iemand schrijven) en softwareoplossingen (een pakket met een andere configuratie verzenden, een waarschuwing verwerken).

Zo ziet een deel van de stresstest er bijvoorbeeld uit:

  • Gebruiker logt in
    • Vraagt ​​uw ongelezen discussies op
    • 50% waarschijnlijk dat hij berichten leest
    • 50% kans dat je sms't
    • Volgende gebruiker:
      • Heeft een kans van 20% om een ​​nieuwe discussie te creëren
      • Selecteert willekeurig een van de discussies
      • Gaat naar binnen
      • Verzoekt berichten, gebruikersprofielen
      • Creëert vijf berichten gericht aan willekeurige gebruikers uit deze discussie
      • Verlaat de discussie
      • Herhaalt 20 keer
      • Logt uit, gaat terug naar het begin van het script

    • Een chatbot komt het systeem binnen (emuleert berichten uit applicatiecode)
      • Heeft 50% kans op het creëren van een nieuw kanaal voor gegevensuitwisseling (speciale discussie)
      • 50% zal waarschijnlijk een bericht schrijven naar een van de bestaande kanalen

Het scenario 'Alleen verbindingen' verscheen met een reden. Er is een situatie: gebruikers hebben het systeem aangesloten, maar zijn er nog niet mee aan de slag gegaan. Elke gebruiker zet de computer om 09 uur 's ochtends aan, brengt verbinding met de server tot stand en blijft stil. Deze jongens zijn gevaarlijk, er zijn er veel - de enige pakketten die ze hebben zijn PING/PONG, maar ze behouden de verbinding met de server (ze kunnen het niet volhouden - wat als er een nieuw bericht is). De test reproduceert een situatie waarin een groot aantal van dergelijke gebruikers binnen een half uur probeert in te loggen op het systeem. Het lijkt op een stresstest, maar de focus ligt precies op deze eerste input - zodat er geen mislukkingen zijn (een persoon gebruikt het systeem niet en het valt er al af - het is moeilijk om iets ergers te bedenken).

Het abonneeregistratiescript start vanaf de eerste lancering. We voerden een stresstest uit en waren er zeker van dat het systeem tijdens de correspondentie niet vertraagde. Maar er kwamen gebruikers en de registratie begon te mislukken vanwege een time-out. Bij het registreren gebruikten we / dev / willekeurig, wat gerelateerd is aan de entropie van het systeem. De server had geen tijd om voldoende entropie te verzamelen en toen een nieuwe SecureRandom werd aangevraagd, bevroor deze tientallen seconden. Er zijn veel manieren om uit deze situatie te komen, bijvoorbeeld: schakel over naar het minder veilige /dev/urandom, installeer een speciaal bord dat entropie genereert, genereer vooraf willekeurige getallen en sla ze op in een pool. We hebben het probleem met het zwembad tijdelijk afgesloten, maar sindsdien draaien we een aparte test voor het registreren van nieuwe abonnees.

Wij gebruiken als belastinggenerator JMeter. Het weet niet hoe het met websocket moet werken; het heeft een plug-in nodig. De eerste in de zoekresultaten voor de zoekopdracht “jmeter websocket” zijn: artikelen van BlazeMeter, die aanbevelen plug-in van Maciej Zaleski.

Dat is waar we besloten om te beginnen.

Vrijwel onmiddellijk na het starten van serieuze tests ontdekten we dat JMeter geheugen begon te lekken.

De plug-in is een apart groot verhaal; met 176 sterren heeft hij 132 forks op github. De auteur zelf heeft zich er sinds 2015 niet meer aan verbonden (we hebben het in 2015 genomen, toen wekte het geen verdenkingen op), verschillende github-problemen met betrekking tot geheugenlekken, 7 niet-gesloten pull-verzoeken.
Als u besluit een belastingtest uit te voeren met deze plug-in, let dan op de volgende discussies:

  1. In een omgeving met meerdere threads werd een gewone LinkedList gebruikt, en het resultaat was NPE in looptijd. Dit kan worden opgelost door over te schakelen naar ConcurrentLinkedDeque of door gesynchroniseerde blokken. Wij kozen voor onszelf de eerste optie (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. Geheugenlek; bij het verbreken van de verbinding wordt de verbindingsinformatie niet verwijderd (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. In de streamingmodus (wanneer de websocket niet wordt gesloten aan het einde van het voorbeeld, maar later in het plan wordt gebruikt), werken responspatronen niet (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

Dit is er eentje op github. Wat we gedaan hebben:

  1. Heb genomen vork Elyran Kogan (@elyrank) – het lost problemen 1 en 3 op
  2. Opgelost probleem 2
  3. Steiger bijgewerkt van 9.2.14 naar 9.3.12
  4. SimpleDateFormat verpakt in ThreadLocal; SimpleDateFormat is niet thread-safe, wat tijdens runtime tot NPE leidde
  5. Nog een geheugenlek opgelost (de verbinding werd verkeerd gesloten toen de verbinding werd verbroken)

En toch stroomt het!

Het geheugen begon niet binnen een dag op te raken, maar binnen twee dagen. Er was absoluut geen tijd meer, dus besloten we minder threads te lanceren, maar op vier agenten. Dit zou genoeg moeten zijn voor minimaal een week.

Er zijn twee dagen verstreken...

Nu heeft Hazelcast bijna geen geheugen meer. Uit de logboeken bleek dat Hazelcast na een paar dagen testen begon te klagen over een gebrek aan geheugen, en dat na enige tijd het cluster uit elkaar viel en de knooppunten één voor één bleven sterven. We hebben JVisualVM verbonden met hazelcast en zagen een "stijgende zaag" - deze belde regelmatig de GC, maar kon het geheugen niet wissen.

Hoe en waarom we een schaalbare service met hoge belasting schreven voor 1C: Enterprise: Java, PostgreSQL, Hazelcast

Het bleek dat in hazelcast 3.4, bij het verwijderen van een kaart / multiMap (map.destroy()), het geheugen niet volledig wordt vrijgemaakt:

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

De bug is nu opgelost in 3.5, maar toen was het nog een probleem. We hebben nieuwe multiMaps met dynamische namen gemaakt en deze volgens onze logica verwijderd. De code zag er ongeveer zo uit:

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

Telefoongesprek:

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

multiMap is voor elk abonnement gemaakt en verwijderd wanneer het niet nodig was. We besloten dat we Map zouden starten , de sleutel is de naam van het abonnement en de waarden zijn sessie-ID's (waarvan u vervolgens, indien nodig, gebruikers-ID's kunt verkrijgen).

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

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

De grafieken zijn verbeterd.

Hoe en waarom we een schaalbare service met hoge belasting schreven voor 1C: Enterprise: Java, PostgreSQL, Hazelcast

Wat hebben we nog meer geleerd over belastingtesten?

  1. JSR223 moet groovy worden geschreven en een compilatiecache bevatten - het is veel sneller. Link.
  2. Jmeter-Plugins-grafieken zijn gemakkelijker te begrijpen dan standaardgrafieken. Link.

Over onze ervaring met Hazelcast

Hazelcast was een nieuw product voor ons, we zijn ermee begonnen te werken vanaf versie 3.4.1, nu draait onze productieserver versie 3.9.2 (op het moment van schrijven is de nieuwste versie van Hazelcast 3.10).

ID-generatie

We zijn begonnen met integer-ID's. Laten we ons voorstellen dat we nog een Long nodig hebben voor een nieuwe entiteit. Volgorde in de database is niet geschikt, de tabellen zijn betrokken bij sharding - het blijkt dat er een bericht-ID=1 is in DB1 en een bericht-ID=1 in DB2, je kunt deze ID niet in Elasticsearch plaatsen, noch in Hazelcast , maar het ergste is als je de gegevens uit twee databases in één wilt combineren (bijvoorbeeld door te besluiten dat één database voldoende is voor deze abonnees). U kunt verschillende AtomicLongs aan Hazelcast toevoegen en de teller daar houden, waarna de prestatie van het verkrijgen van een nieuwe ID incrementAndGet is plus de tijd voor een verzoek aan Hazelcast. Maar Hazelcast heeft iets optimalers: FlakeIdGenerator. Wanneer ze contact opnemen met elke klant, krijgen ze een ID-bereik, bijvoorbeeld de eerste – van 1 tot 10, de tweede – van 000 tot 10, enzovoort. Nu kan de klant zelf nieuwe identificatiegegevens uitgeven totdat het bereik dat hem wordt toegekend, eindigt. Het werkt snel, maar wanneer u de applicatie (en de Hazelcast-client) opnieuw start, begint een nieuwe reeks - vandaar het overslaan, enz. Bovendien begrijpen ontwikkelaars niet echt waarom de ID's geheeltallig zijn, maar zo inconsistent. We hebben alles gewogen en zijn overgestapt op UUID's.

Trouwens, voor degenen die op Twitter willen lijken, is er zo'n Snowcast-bibliotheek - dit is een implementatie van Snowflake bovenop Hazelcast. Je kunt het hier bekijken:

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

Maar wij zijn er niet meer aan toegekomen.

TransactioneleMap.replace

Nog een verrassing: TransactionalMap.replace werkt niet. Hier is een test:

@Test
public void replaceInMap_putsAndGetsInsideTransaction() {

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

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

Expected : newValue
Actual : oldValue

Ik moest mijn eigen vervanging schrijven met 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);
}

Test niet alleen reguliere datastructuren, maar ook hun transactionele versies. Het komt voor dat IMap werkt, maar TransactionalMap bestaat niet meer.

Voeg een nieuwe JAR in zonder downtime

Eerst hebben we besloten om objecten van onze klassen in Hazelcast op te nemen. We hebben bijvoorbeeld een klasse Application, we willen deze opslaan en lezen. Redden:

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

We lezen:

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

Alles werkt. Vervolgens hebben we besloten om een ​​index in Hazelcast te bouwen om te zoeken op:

map.addIndex("subscriberId", false);

En toen ze een nieuwe entiteit schreven, begonnen ze ClassNotFoundException te ontvangen. Hazelcast probeerde iets toe te voegen aan de index, maar wist niets over onze klasse en wilde dat er een JAR met deze klasse aan zou worden geleverd. Dat hebben we gedaan, alles werkte, maar er verscheen een nieuw probleem: hoe kon ik de JAR updaten zonder het cluster volledig te stoppen? Hazelcast haalt de nieuwe JAR niet op tijdens een knooppunt-voor-knooppunt-update. Op dit punt besloten we dat we zonder indexzoeken konden leven. Als je Hazelcast als sleutelwaardewinkel gebruikt, werkt alles immers? Niet echt. Ook hier is het gedrag van IMap en TransactionalMap anders. Waar IMap er niets om geeft, genereert TransactionalMap een foutmelding.

IMap. We schrijven 5000 objecten, lezen ze. Er wordt van alles verwacht.

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

Maar het werkt niet in een transactie, we krijgen een 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();
        }
    });
}

In 3.8 verscheen het User Class Deployment-mechanisme. U kunt één hoofdknooppunt aanwijzen en het JAR-bestand daarop bijwerken.

Nu hebben we onze aanpak compleet veranderd: we serialiseren het zelf in JSON en slaan het op in Hazelcast. Hazelcast hoeft de structuur van onze lessen niet te kennen en we kunnen updaten zonder downtime. Het versiebeheer van domeinobjecten wordt beheerd door de applicatie. Verschillende versies van de applicatie kunnen tegelijkertijd draaien, en er is een situatie mogelijk waarin de nieuwe applicatie objecten met nieuwe velden schrijft, maar de oude nog niet op de hoogte is van deze velden. En tegelijkertijd leest de nieuwe applicatie objecten die door de oude applicatie zijn geschreven en die geen nieuwe velden hebben. Dergelijke situaties behandelen we binnen de applicatie, maar voor de eenvoud wijzigen of verwijderen we geen velden, we breiden de klassen alleen uit door nieuwe velden toe te voegen.

Hoe wij zorgen voor hoge prestaties

Vier uitstapjes naar Hazelcast - goed, twee naar de database - slecht

Naar de cache gaan voor gegevens is altijd beter dan naar de database gaan, maar u wilt ook geen ongebruikte records opslaan. We laten de beslissing over wat we gaan cachen tot de laatste ontwikkelingsfase. Wanneer de nieuwe functionaliteit is gecodeerd, schakelen we het loggen van alle queries in PostgreSQL in (log_min_duration_statement naar 0) en voeren we loadtests uit gedurende 20 minuten. Met behulp van de verzamelde logs kunnen hulpprogramma's zoals pgFouine en pgBadger analytische rapporten bouwen. In rapporten kijken we vooral naar langzame en frequente zoekopdrachten. Voor langzame queries bouwen we een uitvoeringsplan (EXPLAIN) en evalueren we of zo’n query versneld kan worden. Frequente verzoeken om dezelfde invoergegevens passen goed in de cache. We proberen zoekopdrachten “plat” te houden, één tabel per zoekopdracht.

Exploitatie

SV is als online dienst in het voorjaar van 2017 in gebruik genomen en als afzonderlijk product is SV in november 2017 uitgebracht (destijds in bètaversie).

In de ruim een ​​jaar dat het bedrijf actief was, hebben zich geen ernstige problemen voorgedaan bij de werking van de online service van CB. Wij monitoren de online dienstverlening via Zabbix, verzamelen en implementeren van Bamboo.

De SV-serverdistributie wordt geleverd in de vorm van native pakketten: RPM, DEB, MSI. Bovendien bieden we voor Windows één enkel installatieprogramma in de vorm van één enkele EXE die de server, Hazelcast en Elasticsearch op één machine installeert. We noemden deze versie van de installatie aanvankelijk de “demoversie”, maar het is nu duidelijk geworden dat dit de meest populaire implementatieoptie is.

Bron: www.habr.com

Voeg een reactie