Wie und warum haben wir einen hochskalierbaren Service für 1C geschrieben: Unternehmen: Java, PostgreSQL, Hazelcast

    In diesem Artikel werden wir darüber sprechen, wie und warum wir das Interaktionssystem entwickelt haben - ein Mechanismus, der Informationen zwischen Clientanwendungen und 1C: Enterprise-Servern überträgt - von der Aufgabenstellung bis zum Nachdenken über die Architektur und Implementierungsdetails.

    Das Interaktionssystem (im Folgenden: CB) ist ein verteiltes, fehlertolerantes Nachrichtensystem mit garantierter Zustellung. SV ist ein hoch belasteter Dienst mit hoher Skalierbarkeit, ist als Onlinedienst (von 1C bereitgestellt) und als Lotterieprodukt verfügbar, das in seinen Serverkapazitäten eingesetzt werden kann.

    ST verwendet ein verteiltes Speicherhazelcast und Suchmaschinen - Elasticsearch. Wir werden auch Java besprechen und wie wir PostgreSQL horizontal skalieren.
    Bild


    Problemstellung


    Um deutlich zu machen, warum wir das Interaktionssystem erstellt haben, erzähle ich Ihnen ein wenig darüber, wie die Entwicklung von Geschäftsanwendungen in 1C gestaltet wird.

    Zunächst einmal ein wenig über uns für diejenigen, die noch nicht wissen, was wir tun :) Wir machen die Technologieplattform 1C: Enterprise. Die Plattform umfasst ein Entwicklungstool für Geschäftsanwendungen sowie eine Laufzeitumgebung, mit der Geschäftsanwendungen in einer plattformübergreifenden Umgebung arbeiten können.

    Client-Server-Entwicklungsparadigma


    Mit 1C erstellte Geschäftsanwendungen: Enterprise arbeitet in einer dreischichtigen Client / Server- Architektur „DBMS - Application Server - Client“. Der in der eingebetteten Sprache 1C geschriebene Anwendungscode kann auf dem Anwendungsserver oder auf dem Client ausgeführt werden. Alle Arbeiten mit Anwendungsobjekten (Verzeichnisse, Dokumente usw.) sowie das Lesen und Schreiben der Datenbank werden nur auf dem Server ausgeführt. Die Funktionalität der Formulare und der Befehlsschnittstelle ist auch auf dem Server implementiert. Der Client empfängt, öffnet und zeigt Formulare an, "kommuniziert" mit dem Benutzer (Warnungen, Fragen ...), führt kleine Berechnungen in Formularen durch, die eine schnelle Reaktion erfordern (z. B. Preise nach Menge multiplizieren), mit lokalen Dateien arbeiten, mit Geräten arbeiten.

    Im Anwendungscode müssen die Überschriften der Prozeduren und Funktionen explizit angeben, wo der Code ausgeführt wird - mithilfe der Anweisungen & OnClient / & OnServer (& AtClient / & AtServer in der englischen Version). Die 1C-Entwickler werden mich jetzt korrigieren und sagen, dass es tatsächlich mehr Direktiven gibt , aber für uns ist das jetzt nicht wesentlich.

    Sie können den Servercode über den Clientcode aufrufen, den Clientcode jedoch nicht über den Servercode. Dies ist eine grundlegende Einschränkung, die wir aus verschiedenen Gründen vorgenommen haben. Insbesondere weil der Servercode so geschrieben werden muss, dass er auf dieselbe Weise ausgeführt werden kann, unabhängig davon, wo er aufgerufen wird - vom Client oder vom Server. Wenn der Servercode von einem anderen Servercode aufgerufen wird, fehlt der Client als solcher. Und weil während der Ausführung des Servercodes der Client, der ihn anrief, die Anwendung schließen konnte, konnte die Anwendung beendet werden, und der Server hat keinen Anrufer.

    Bild
    Der Code, mit dem Tastendruck ausgeführt wird: Ein Aufruf der Serverprozedur vom Client funktioniert, ein Aufruf der Clientprozedur vom Server - Nein

    Das heißt, wenn wir eine Nachricht vom Server an die Client-Anwendung senden möchten, zum Beispiel, dass die Bildung eines "Langzeit" -Rechts abgeschlossen ist und der Bericht angezeigt werden kann - wir haben keine solche Methode. Wir müssen Tricks suchen, beispielsweise den Server regelmäßig vom Client-Code abfragen. Dieser Ansatz belastet das System jedoch mit unnötigen Anrufen und sieht im Allgemeinen nicht sehr elegant aus.

    Und es besteht auch ein Bedarf, zum Beispiel, wenn ein empfangener SIP- Telefonanruf eingeht, die Client-Anwendung zu benachrichtigen, damit diese die Nummer des Anrufers in der Datenbank des Gegenpartners findet und dem Benutzer Informationen über den anrufenden Partner anzeigt. Oder zum Beispiel, wenn Sie im Lager der Bestellung ankommen, um die Kundenanwendung des Kunden zu benachrichtigen. Im Allgemeinen gibt es viele Fälle, in denen ein solcher Mechanismus nützlich wäre.

    Eigentlich Inszenierung


    Erstellen Sie einen Nachrichtenmechanismus. Schnell, zuverlässig, mit garantierter Zustellung, mit der Möglichkeit einer flexiblen Nachrichtensuche. Auf der Grundlage des Mechanismus zum Implementieren eines Instant Messenger (Nachrichten, Videoanrufe), der in 1C-Anwendungen ausgeführt wird.

    Gestalten Sie das System horizontal skalierbar. Steigende Last muss durch Erhöhen der Knotenanzahl geschlossen werden.

    Umsetzung


    Wir haben uns entschieden, den Serverteil von SV nicht in die 1C: Enterprise-Plattform zu integrieren, sondern als separates Produkt zu implementieren, dessen API vom 1C-Anwendungslösungscode aus aufgerufen werden kann. Dies geschah aus verschiedenen Gründen. Der Hauptgrund bestand darin, den Austausch von Nachrichten zwischen verschiedenen 1C-Anwendungen (z. B. zwischen dem Amt für Handel und Rechnungswesen) zu ermöglichen. Verschiedene 1C-Anwendungen können mit verschiedenen Versionen des 1C arbeiten: Unternehmensplattform, auf verschiedenen Servern usw. Unter diesen Bedingungen ist die Implementierung des SV als separates Produkt, das sich „auf der Seite“ von 1C-Installationen befindet, die optimale Lösung.

    Also haben wir uns entschieden, SV als separates Produkt zu machen. Wir empfehlen kleinen Unternehmen, den CB-Server zu verwenden, den wir in unserer Cloud installiert haben (wss: //1cdialog.com), um den mit der lokalen Installation und Konfiguration des Servers verbundenen Overhead zu vermeiden. Große Kunden halten es möglicherweise für angebracht, einen eigenen CB-Server in ihren Einrichtungen zu installieren. In unserem Cloud-SaaS-Produkt 1cFresh haben wir einen ähnlichen Ansatz angewandt - es wird als Lotterieprodukt zur Installation bei Kunden veröffentlicht und auch in unserer Cloud ( https://1cfresh.com/) eingesetzt .

    Anwendung


    Für den Lastausgleich und die Fehlertoleranz werden wir nicht eine, sondern mehrere Java-Anwendungen bereitstellen, und wir werden einen Lastausgleich vor ihnen platzieren. Wenn Sie eine Nachricht von Knoten zu Knoten senden müssen, verwenden Sie Publish / Subscribe in Hazelcast.

    Client-Kommunikation mit dem Server - über Websocket. Es ist gut für Echtzeitsysteme geeignet.

    Verteilter Cache


    Wählen Sie zwischen Redis, Hazelcast und Ehcache. Auf dem Hof ​​im Jahr 2015. Redis hat gerade einen neuen Cluster veröffentlicht (zu neu, unheimlich), es gibt einen Sentinel mit einigen Einschränkungen. Ehcache weiß nicht, wie er zu einem Cluster zusammengefügt werden soll (diese Funktionalität erschien später). Wir haben uns für Hazelcast 3.4 entschieden.
    Hazelcast wird aus dem Kasten heraus bündeln. Im Einzelknotenmodus ist es nicht sehr nützlich und kann nur als Cache verwendet werden - er weiß nicht, wie er Daten auf die Festplatte speichert, einen einzelnen Knoten verloren hat und Daten verloren hat. Wir erweitern mehrere Hazelcasts, zwischen denen wir wichtige Daten sichern. Der Cache wird nicht gesichert - es ist nicht schade.

    Für uns ist Hazelcast:

    • Benutzersitzungs-Repository Es ist jedes Mal eine lange Zeit, zur Basis für die Sitzung zu gehen, also setzen wir alle Sitzungen in den Hazelcast.
    • Cache Suchen nach einem Benutzerprofil - checken Sie den Cache. Schreibe eine neue Nachricht - in den Cache legen.
    • Themen für Kommunikationsinstanzen der Anwendung. Der Knoten generiert ein Ereignis und fügt es in das Hazelcast-Thema ein. Andere Anwendungsknoten, die dieses Thema abonniert haben, empfangen und verarbeiten das Ereignis.
    • Clustersperre Zum Beispiel erstellen wir eine Diskussion mit einem eindeutigen Schlüssel (einer Singleton-Diskussion in der 1C-Datenbank):

    conversationKeyChecker.check("БЕНЗОКОЛОНКА");
          doInClusterLock("БЕНЗОКОЛОНКА", () -> {
              conversationKeyChecker.check("БЕНЗОКОЛОНКА");
              createChannel("БЕНЗОКОЛОНКА");
          });

    Überprüft, dass es keinen Kanal gibt. Sie nahmen das Schloss wieder auf, erstellt und geprüft. Wenn Sie nach dem Sperren nicht überprüfen, besteht die Möglichkeit, dass ein anderer Thread in diesem Moment ebenfalls geprüft wird und nun versucht, die gleiche Diskussion zu erstellen - und diese ist bereits vorhanden. Es ist nicht möglich, über synchronisierte oder reguläre Java-Sperre eine Sperre auszuführen. Durch die Basis - langsam und die Basis ist erbärmlich, durch den Hazelcast - was wir brauchen.

    Auswahl eines DBMS


    Wir haben großartige und erfolgreiche Erfahrungen mit PostgreSQL und der Zusammenarbeit mit den Entwicklern dieses DBMS.

    Mit einem Cluster ist PostgreSQL nicht einfach - es gibt XL , XC , Citus , aber im Allgemeinen ist NoSQL kein Skalierensatz. NoSQL als Haupt-Repository wurde nicht berücksichtigt, es war genug, dass wir Hazelcast nehmen, mit dem wir vorher noch nicht gearbeitet haben.

    Wenn Sie die relationale Datenbank skalieren müssen, bedeutet dies ein Sharding . Wie Sie wissen, teilen wir die Datenbank beim Sharding in separate Teile auf, so dass jeder von ihnen auf einen separaten Server übertragen werden kann.

    In der ersten Version unseres Shards wurde die Möglichkeit vorgeschlagen, jede unserer Anwendungstabellen auf verschiedene Server mit unterschiedlichen Anteilen zu verteilen. Viele Nachrichten auf Server A - Bitte übertragen Sie einen Teil dieser Tabelle auf Server B. Bei einer solchen Lösung wurde nur über vorzeitige Optimierung gebrüllt. Daher haben wir uns auf einen Multi-Tenant-Ansatz beschränkt.

    Über Multi-Tenant können Sie beispielsweise auf der Citus Data- Website nachlesen .

    In NE gibt es Konzepte der Anwendung und des Abonnenten. Eine Anwendung ist eine spezifische Installation einer Geschäftsanwendung, z. B. ERP oder Buchhaltung, mit ihren Benutzern und Geschäftsdaten. Ein Abonnent ist eine Organisation oder eine Einzelperson, in deren Auftrag die Registrierung der Anwendung auf dem CB-Server erfolgt. Ein Teilnehmer kann mehrere Anwendungen registriert haben, und diese Anwendungen können Nachrichten miteinander austauschen. Abonnent und wurde Mieter in unserem System. Nachrichten mehrerer Abonnenten können sich in derselben physischen Datenbank befinden. Wenn wir feststellen, dass ein Abonnent anfing, viel Datenverkehr zu generieren, bringen wir ihn in eine separate physische Datenbank (oder sogar einen separaten Datenbankserver).

    Wir haben die Hauptdatenbank, in der die Routing-Tabelle mit Informationen zum Standort aller Teilnehmerdatenbanken gespeichert ist.

    Bild

    Damit die Hauptdatenbank kein Engpass ist, behalten wir die Routing-Tabelle (und andere häufig angeforderte Daten) im Cache.

    Wenn sich die Datenbank des Abonnenten verlangsamt, schneiden wir die Partitionen darin ab. Bei anderen Projekten für die Partitionierung von großen Tabellen mit pg_pathman .

    Da der Verlust von Benutzernachrichten schlecht ist, werden unsere Replikate in unseren Datenbanken gespeichert. Durch die Kombination synchroner und asynchroner Replikate können Sie sich gegen den Verlust der Hauptdatenbank absichern. Die Nachricht geht nur bei gleichzeitigem Ausfall der Hauptdatenbank und ihrer synchronen Replikate verloren.

    Wenn ein synchrones Replikat verloren geht, wird das asynchrone Replikat synchron.
    Wenn die Primärdatenbank verloren geht, wird das synchrone Replikat zur Primärdatenbank und das asynchrone Replikat zum synchronen Replikat.

    Elasticsuche für die Suche


    Da CB unter anderem auch ein Messenger ist, benötigen Sie eine schnelle, bequeme und flexible Suche unter Berücksichtigung der Morphologie durch ungenaue Korrespondenz. Wir beschlossen, das Rad nicht neu zu erfinden und die kostenlose Suchmaschine Elasticsearch zu verwenden, die auf der Lucene- Bibliothek basiert . Wir implementieren Elasticsearch auch in einem Cluster (Stammdaten - Daten), um Probleme bei einem Ausfall von Anwendungsknoten zu beseitigen.

    Auf github haben wir ein russisches Morphologie-Plugin für Elasticsearch gefunden und verwenden es. Im Elasticsearch-Index speichern wir die Wurzeln der Wörter (die das Plugin definiert) und die N-Gramme. Während der Benutzer den Text für die Suche eingibt, suchen wir nach eingetipptem Text unter N-Gramm. Wenn Sie im Index speichern, wird das Wort "Texte" in die folgenden N-Gramme unterteilt:

    [diese, tech, tex, text, texte, ek, ex, exts, exts, exs, ks, ksts, cpsy, kunst, smy, du],

    und die Wurzel des Wortes "text" wird ebenfalls gerettet. Dieser Ansatz ermöglicht es Ihnen, am Anfang und in der Mitte und am Ende des Wortes zu suchen.

    Gesamtbild


    Bild
    Wiederholen Sie Bilder vom Anfang des Artikels, jedoch mit Erläuterungen:

    • Online Balancer; wir haben nginx, vielleicht auch welche.
    • Java-Anwendungsinstanzen kommunizieren über Hazelcast miteinander.
    • Für die Arbeit mit einem Web-Socket verwenden wir Netty .
    • Java-Anwendung, die in Java 8 geschrieben ist, besteht aus OSGi- Paketen . Die Pläne - Migration nach Java 10 und der Übergang zu Modulen.

    Entwicklung und Test


    Bei der Entwicklung und Erprobung von CB stießen wir auf eine Reihe interessanter Merkmale der von uns verwendeten Produkte.

    Laden von Tests und Speicherlecks


    Die Version jeder CB-Version ist Belastungstest. Es war erfolgreich, als:

    • Der Test hat mehrere Tage gedauert und es gab keine Servicefehler.
    • Die Reaktionszeit für wichtige Operationen überschritt nicht den komfortablen Schwellenwert
    • Der Leistungsabfall im Vergleich zur Vorgängerversion beträgt nicht mehr als 10%.

    Wir füllen die Testdatenbank mit Daten - dazu erhalten wir vom Produktionsserver Informationen über den aktiven Teilnehmer, multiplizieren seine Ziffern mit 5 (Anzahl der Nachrichten, Diskussionen, Benutzer) und testen so.

    Wir führen Lasttests des Interaktionssystems in drei Konfigurationen durch:

    1. Belastungstest
    2. Nur Verbindungen
    3. Abonnenten-Registrierung

    Im Stresstest führen wir mehrere Hundert Threads aus, und ohne sie zu stoppen, wird das System geladen: Nachrichten schreiben, Diskussionen erstellen, Liste der Nachrichten abrufen. Wir simulieren die Aktionen normaler Benutzer (erhalten eine Liste meiner ungelesenen Nachrichten, schreiben an jemanden) und Softwarelösungen (senden Sie ein Paket einer anderen Konfiguration, behandeln Sie die Warnung).

    Zum Beispiel sieht ein Teil des Stresstests so aus:

    • Benutzer meldet sich an
      • Fordert seine ungelesenen Diskussionen
      • Mit einer 50% igen Wahrscheinlichkeit, Nachrichten zu lesen
      • 50% schreiben wahrscheinlich Nachrichten
      • Nächster Benutzer:
        • Mit einer Chance von 20%, eine neue Diskussion zu erstellen.
        • Wählt zufällig eine seiner Diskussionen aus.
        • Geht hinein
        • Fordert Nachrichten, Benutzerprofile an
        • Erstellt fünf Nachrichten, die an zufällige Benutzer aus diesem Thread gerichtet sind.
        • Aus der Diskussion heraus
        • Wiederholt sich 20 mal
        • Melden Sie sich ab, kehren Sie zum Anfang des Skripts zurück.

      • Ein Chat-Bot tritt in das System ein (emuliert den Austausch von Nachrichten aus dem Anwendungscode)

        • Mit einer 50% igen Chance, einen neuen Kanal für den Datenaustausch einzurichten (spezielle Diskussion)
        • Mit einer 50% igen Chance, eine Nachricht in einen der vorhandenen Kanäle zu schreiben



    Das Szenario „Nur Verbindungen“ wurde nicht umsonst angezeigt. Es gibt eine Situation: Benutzer haben das System angeschlossen, waren aber noch nicht daran beteiligt. Jeder Benutzer um 09:00 Uhr morgens schaltet den Computer ein, stellt eine Verbindung mit dem Server her und ist stumm. Diese Jungs sind gefährlich, es gibt viele - sie haben nur PING / PONG von den Paketen, aber sie behalten die Verbindung zum Server (sie können es nicht behalten - und wenn es eine neue Nachricht gibt). Der Test reproduziert die Situation, wenn sich eine große Anzahl solcher Benutzer innerhalb einer halben Stunde anmeldet. Es sieht aus wie ein Stresstest, aber der Fokus liegt auf diesem ersten Eintrag - so dass es keine Ausfälle gibt (die Person nutzt das System nicht und fällt bereits ab - es fällt schwer, sich etwas Schlimmeres vorzustellen).

    Das Abonnentenregistrierungsskript stammt vom ersten Start. Wir haben einen Stresstest durchgeführt und waren zuversichtlich, dass das System die Korrespondenz nicht verlangsamt. Die Benutzer gingen jedoch nach und nach von der Registrierung ab. Bei der Registrierung haben wir / dev / random verwendet , das an die Entropie des Systems gebunden ist. Der Server hatte keine Zeit, um ausreichend Entropie zu speichern, und als er ein neues SecureRandom anforderte, wurde er für einige Sekunden einfrieren. Es gibt viele Wege aus dieser Situation, zum Beispiel: Wechseln Sie zu einem weniger sicheren / dev / urandom, legen Sie eine spezielle Gebühr für Entropie fest, generieren Sie im Voraus Zufallszahlen und speichern Sie sie in einem Pool. Wir haben das Problem mit einem Pool vorübergehend geschlossen, aber seitdem haben wir einen separaten Test für die Registrierung neuer Abonnenten durchgeführt.

    Als Lastgenerator verwenden wir JMeter. Er kann nicht mit einer Webanwendung arbeiten, Sie benötigen ein Plugin Die ersten Suchergebnisse in der Suchanfrage "jmeter websocket" sind Artikel von BlazeMeter , die ein Plugin von Maciej Zaleski empfehlen .

    Wir haben uns entschieden, damit zu beginnen.

    Unmittelbar nach dem Beginn der ernsthaften Tests stellten wir fest, dass in JMeter Speicherlecks begannen.

    Das Plugin ist eine separate große Geschichte, mit 176 Sternen hat es 132 Gabeln auf Github. Der Autor selbst bekennt sich nicht seit 2015 (wir haben es 2015 genommen, dann hat es keinen Verdacht geweckt), mehrere Github-Probleme über Speicherlecks, 7 nicht geschlossene Pull-Anfragen.
    Wenn Sie sich für das Testen mit diesem Plugin entscheiden, beachten Sie die folgenden Diskussionen:

    1. In einer Multithread-Umgebung wurde die übliche LinkedList verwendet. Daher wurden NPEs zur Laufzeit abgerufen . Es wird entweder durch Umschalten auf ConcurrentLinkedDeque oder durch synchronisierte Blöcke gelöst. Wir haben die erste Option für uns ausgewählt ( https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43 ).
    2. Speicherverlust, Trennen der Verbindung entfernt keine Verbindungsinformationen ( https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44 ).
    3. Im Streaming-Modus (wenn der Web-Socket nicht am Ende des Beispiels geschlossen wird, sondern im Plan weiter verwendet wird) Antwortmuster funktionieren nicht ( https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19 ).

    Dies ist einer von denen auf Github. Was wir gemacht haben:

    1. Sie nahmen die Gabel von Elyran Kogan (@elyrank) - die Probleme 1 und 3 wurden behoben
    2. Gelöstes Problem 2
    3. Anlegestelle von 9.2.14 auf 9.3.12 aktualisiert
    4. SimpleDateFormat in ThreadLocal verpackt; SimpleDateFormat ist nicht threadsicher, was zur Laufzeit von NPE führt
    5. Beseitigen Sie ein weiteres Speicherverlust (Verbindung wurde beim Trennen der Verbindung falsch geschlossen)

    Und doch fließt es!

    Die Erinnerung endete nicht an einem Tag, sondern an zwei Tagen. Wir hatten keine Zeit mehr und beschlossen, weniger Threads auszuführen, sondern vier Agenten. Das hätte mindestens eine Woche reichen sollen.

    Zwei Tage vergingen ...

    Nun endete die Erinnerung mit Hazelcast. In den Protokollen war klar, dass sich Hazelcast nach ein paar Tagen des Tests über den Mangel an Speicher beschwert, und nach einiger Zeit fällt der Cluster auseinander und die Knoten sterben nach und nach weiter. Wir haben JVisualVM an den Hazelcast angeschlossen und sahen die "aufsteigende Säge" - er rief regelmäßig den GC an, konnte aber den Speicher nicht löschen.

    Bild

    Es stellte sich heraus, dass in hazelcast 3.4 beim Löschen von map / multiMap (map.destroy ()) der Speicher nicht vollständig freigegeben wird:

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

    Nun ist der Fehler in 3.5 behoben, aber dann war es ein Problem. Wir haben neue MultiMap mit dynamischen Namen erstellt und nach unserer Logik gelöscht. Der Code sah so aus:

    publicvoidjoin(Authentication auth, String sub){
        MultiMap<UUID, Authentication> sessions = instance.getMultiMap(sub);
        sessions.put(auth.getUserId(), auth);
    }
    publicvoidleave(Authentication auth, String sub){
        MultiMap<UUID, Authentication> sessions = instance.getMultiMap(sub);
        sessions.remove(auth.getUserId(), auth);
        if (sessions.size() == 0) {
            sessions.destroy();
        }
    }

    Anrufen:

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

    MultiMap wurde für jedes Abonnement erstellt und gelöscht, wenn es nicht benötigt wurde. Wir haben beschlossen, Map <String, Set> zu starten, der Schlüssel ist der Name des Abonnements und die Bezeichner der Sitzungen als Werte (für die Sie später ggf. Benutzer-IDs erhalten können).

    publicvoidjoin(Authentication auth, String sub){
        addValueToMap(sub, auth.getSessionId());
    }
    publicvoidleave(Authentication auth, String sub){ 
        removeValueFromMap(sub, auth.getSessionId());
    }

    Die Karten richteten sich auf.

    Bild

    Was haben wir noch über Lasttests gelernt?


    1. JSR223 muss auf Groovy geschrieben werden und Compilierungscache enthalten - es ist viel schneller. Link .
    2. Jmeter-Plugins-Grafiken sind einfacher zu verstehen als Standard-Grafiken. Link .


    Über unsere Erfahrungen mit Hazelcast


    Hazelcast war für uns ein neues Produkt. Wir haben damit begonnen, ab Version 3.4.1 damit zu arbeiten. Die Version 3.9.2 befindet sich nun auf unserem Produktionsserver (zum jetzigen Zeitpunkt ist dies die neueste Version von Hazelcast 3.10).

    ID-Generierung


    Wir haben mit ganzzahligen IDs begonnen. Stellen wir uns vor, wir brauchen ein neues Long für eine neue Entität. Die Reihenfolge in DB ist nicht geeignet, Tabellen beteiligen sich am Sharding. Es stellt sich heraus, dass es in DB1 eine Nachrichten-ID = 1 und in DB2 eine Nachrichten-ID = 1 gibt. In Elasticsearch können Sie diese ID nicht in Hazelcast angeben von zwei Datenbanken zu einer (z. B. die Entscheidung, dass eine Basis für diese Abonnenten ausreicht). Sie können mehrere AtomicLongs in Hazelcast starten und den Zähler dort belassen. Anschließend erhalten Sie eine neue ID incrementAndGet sowie die Zeit für eine Abfrage in Hazelcast. In Hazelcast gibt es jedoch etwas Optimaleres - den FlakeIdGenerator. Bei der Kontaktaufnahme mit jedem Kunden wird ein ID-Bereich angegeben, z. B. für den ersten - von 1 bis 10.000, für den zweiten - von 10.001 bis 20.000 und so weiter. Nun kann der Client neue Identifikatoren unabhängig vergeben. bis die ihm gegebene Reichweite endet. Es funktioniert schnell, aber wenn die Anwendung neu gestartet wird (und der Hazelcast-Client), beginnt eine neue Sequenz - daher die Auslassungen usw. Außerdem ist den Entwicklern nicht klar, warum die ID eine Ganzzahl ist, aber sie sind so sehr uneinig. Wir haben alle gewogen und zu UUIDs gewechselt.

    Übrigens, für diejenigen, die wie Twitter sein wollen, gibt es eine solche Snowcast-Bibliothek - dies ist die Implementierung von Snowflake auf Hazelcast. Sie können es hier sehen:

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

    Aber wir haben unsere Hände noch nicht erreicht.

    TransactionalMap.replace


    Eine weitere Überraschung: TransactionalMap.replace funktioniert nicht. Hier ist ein Test:

    @TestpublicvoidreplaceInMap_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);
                returnnull;
            } finally {
                HazelcastTransactionContextHolder.clearContext();
            }        
        });
    }
    Expected : newValue
    Actual : oldValue

    Ich musste meinen Ersatz mit getForUpdate schreiben:

    protected <K,V> booleanreplaceInMap(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);
                returntrue;
            }
            returnfalse;
        }
        log.trace("[CACHE] Replacing value in a not transactional map");
        IMap<K, V> map = hazelcastInstance.getMap(mapName);
        return map.replace(key, oldValue, newValue);
    }

    Testen Sie nicht nur reguläre Datenstrukturen, sondern auch deren Transaktionsversionen. Es kommt vor, dass IMap funktioniert und TransactionalMap nicht mehr vorhanden ist.

    Setzen Sie ein neues JAR ohne Ausfallzeiten ein


    Zuerst entschieden wir uns, Objekte unserer Klassen in Hazelcast aufzunehmen. Zum Beispiel haben wir eine Application-Klasse, die wir speichern und lesen möchten. Speichern:

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

    Wir lesen:

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

    Alles arbeitet. Dann haben wir beschlossen, einen Index in Hazelcast zu erstellen, um danach zu suchen:

    map.addIndex("subscriberId", false);

    Beim Schreiben einer neuen Entität haben wir angefangen, ClassNotFoundException zu erhalten. Hazelcast versuchte den Index zu ergänzen, wusste aber nichts über unsere Klasse und wollte einen JAR mit dieser Klasse haben. Wir haben es geschafft, alles hat funktioniert, aber es ist ein neues Problem aufgetreten: Wie kann man die JAR aktualisieren, ohne den Cluster vollständig zu stoppen? Hazelcast nimmt mit einem neuen Update kein neues JAR auf. An diesem Punkt haben wir uns entschieden, dass wir ohne Indexsuche leben können. Wenn Sie Hazelcast als Schlüsselwertspeicher verwenden, wird dann alles funktionieren? Nicht wirklich. Hier noch einmal das unterschiedliche Verhalten von IMap und TransactionalMap. Wenn es IMap egal ist, wirft TransactionalMap einen Fehler aus.

    IMap. Wir schreiben 5000 Objekte auf, wir lesen. Alles wird erwartet.

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

    Und wenn die Transaktion nicht funktioniert, erhalten wir eine ClassNotFoundException:

    @Testvoidget_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());
                returnnull;
            } finally {
                HazelcastTransactionContextHolder.clearContext();
            }
        });
    }

    In 3.8 wurde der Mechanismus zur Bereitstellung der Benutzerklassen veröffentlicht. Sie können einen Master-Knoten zuweisen und die JAR-Datei darauf aktualisieren.

    Jetzt haben wir den Ansatz komplett geändert: Wir serialisieren uns in JSON und speichern in Hazelcast. Hazelcast muss die Struktur unserer Klassen nicht kennen und wir können ohne Ausfallzeiten aktualisiert werden. Die Versionierung von Domänenobjekten wird von der Anwendung gesteuert. Gleichzeitig können verschiedene Versionen der Anwendung gestartet werden. Möglicherweise schreibt eine neue Anwendung Objekte mit neuen Feldern, und die alte kennt diese Felder nicht. Gleichzeitig liest die neue Anwendung Objekte, die von der alten Anwendung geschrieben wurden und in denen keine neuen Felder vorhanden sind. Wir behandeln solche Situationen innerhalb der Anwendung, aber der Einfachheit halber ändern oder löschen Sie keine Felder, wir erweitern Klassen nur durch das Hinzufügen neuer Felder.

    Wie wir hohe Leistung erbringen


    Vier Fahrten nach Hazelcast - gut, zwei Fahrten in der Datenbank - schlecht.


    Das Abrufen von Daten im Cache ist immer besser als in der Datenbank. Sie möchten jedoch auch nicht beanspruchte Datensätze nicht speichern. Die Entscheidung, den Cache zu verschieben, verschieben wir für die letzte Entwicklungsstufe. Wenn die neue Funktion verschlüsselt ist, aktivieren wir die Protokollierung aller Anforderungen (log_min_duration_statement auf 0) in PostgreSQL und führen Lasttests für 20 Minuten aus. In den Berichten suchen wir hauptsächlich nach langsamen und häufigen Anfragen. Für langsame Abfragen erstellen wir einen Ausführungsplan (EXPLAIN) und prüfen, ob eine solche Abfrage beschleunigt werden kann. Häufige Abfragen für dieselben Eingabedaten passen gut in den Cache. Wir versuchen, die Anfragen "flach" zu halten, einen Tisch in der Anfrage.

    Bedienung


    SV als Onlinedienst wurde im Frühjahr 2017 gestartet, da im November 2017 ein separates SV-Produkt (damals im Betastatus) veröffentlicht wurde.

    Während eines Betriebs von mehr als einem Jahr gab es keine ernsthaften Probleme mit dem Betrieb des SV-Onlinedienstes. Der Online-Service wird über Zabbix überwacht , aus Bamboo zusammengestellt und bereitgestellt .

    Das CB-Distributionspaket wird in Form von nativen Paketen geliefert: RPM, DEB, MSI. Für Windows bieten wir ein einzelnes Installationsprogramm in Form einer einzigen EXE-Datei an, mit der Server, Hazelcast und Elasticsearch auf einem Computer installiert werden. Zuerst haben wir diese Version der Installation als "Demo" bezeichnet. Nun ist jedoch klar, dass dies die am häufigsten verwendete Implementierungsoption ist.

    Jetzt auch beliebt: