Wie und warum wir einen hoch ausgelasteten skalierbaren Dienst für 1C geschrieben haben: Enterprise: Java, PostgreSQL, Hazelcast

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

Das Interaktionssystem (im Folgenden: CB) ist ein verteiltes fehlertolerantes Nachrichtensystem mit garantierter Zustellung. CB ist als Hochlastdienst mit hoher Skalierbarkeit konzipiert, der sowohl als Onlinedienst (bereitgestellt von 1C) als auch als Massenproduktionsprodukt verfügbar ist, das auf eigenen Serveranlagen bereitgestellt werden kann.

SW verwendet verteilten Speicher Hazelcast und Suchmaschine Elasticsearch. Wir werden auch über Java sprechen und wie wir PostgreSQL horizontal skalieren.
Wie und warum wir einen hoch ausgelasteten skalierbaren Dienst für 1C geschrieben haben: Enterprise: Java, PostgreSQL, Hazelcast

Formulierung des Problems

Um zu verdeutlichen, warum wir das Interaction System entwickelt haben, erzähle ich Ihnen ein wenig darüber, wie die Entwicklung von Geschäftsanwendungen in 1C funktioniert.

Zunächst ein wenig über uns für diejenigen, die noch nicht wissen, was wir tun :) Wir entwickeln die Technologieplattform 1C:Enterprise. Die Plattform umfasst ein Entwicklungstool für Geschäftsanwendungen sowie eine Laufzeit, die es Geschäftsanwendungen ermöglicht, in einer plattformübergreifenden Umgebung zu arbeiten.

Client-Server-Entwicklungsparadigma

Auf 1C:Enterprise erstellte Geschäftsanwendungen funktionieren auf drei Ebenen Kundenserver Architektur „DBMS – Anwendungsserver – Client“. Eingeschriebener Anwendungscode integrierte Sprache 1C, kann auf dem Anwendungsserver oder auf dem Client ausgeführt werden. Sämtliche Arbeiten mit Anwendungsobjekten (Verzeichnisse, Dokumente etc.) sowie das Lesen und Schreiben der Datenbank erfolgen ausschließlich auf dem Server. Auch Formulare und Befehlsschnittstellenfunktionen sind auf dem Server implementiert. Auf dem Client werden Formulare empfangen, geöffnet und angezeigt, „Kommunikation“ mit dem Benutzer (Warnungen, Fragen ...), kleine Berechnungen in Formularen, die eine schnelle Reaktion erfordern (z. B. Preis mit Menge multiplizieren), Arbeiten mit lokale Dateien, Arbeiten mit Geräten.

Im Anwendungscode müssen die Header von Prozeduren und Funktionen explizit angeben, wo der Code ausgeführt wird – mithilfe der Anweisungen &AtClient / &AtServer (&AtClient / &AtServer in der englischen Version der Sprache). 1C-Entwickler werden mich jetzt korrigieren, indem sie sagen, dass die Anweisungen tatsächlich vorhanden sind besser, aber für uns ist es jetzt nicht wichtig.

Sie können Servercode über Clientcode aufrufen, Sie können Clientcode jedoch nicht über Servercode aufrufen. Dies ist eine grundlegende Einschränkung, die wir aus mehreren Gründen vorgenommen haben. Insbesondere, weil der Servercode so geschrieben sein muss, dass er gleich ausgeführt wird, egal von wo er aufgerufen wird – vom Client oder vom Server. Und im Falle eines Aufrufs des Servercodes von einem anderen Servercode gibt es keinen Client als solchen. Und weil während der Ausführung des Servercodes der Client, der ihn aufruft, die Anwendung schließen und beenden könnte und der Server niemanden hätte, den er anrufen könnte.

Wie und warum wir einen hoch ausgelasteten skalierbaren Dienst für 1C geschrieben haben: Enterprise: Java, PostgreSQL, Hazelcast
Code, der einen Tastenklick verarbeitet: Der Aufruf einer Serverprozedur vom Client aus funktioniert, der Aufruf einer Clientprozedur vom Server nicht

Das heißt, wenn wir beispielsweise eine Nachricht vom Server an die Client-Anwendung senden möchten, dass die Erstellung eines „Langzeitberichts“ abgeschlossen ist und der Bericht angezeigt werden kann, haben wir keine solche Möglichkeit. Sie müssen Tricks anwenden, zum Beispiel den Server regelmäßig vom Client-Code abfragen. Dieser Ansatz belastet das System jedoch mit unnötigen Aufrufen und sieht im Allgemeinen nicht sehr elegant aus.

Und es besteht auch Bedarf, wenn beispielsweise ein Telefon vorhanden ist SIP-call, benachrichtigen Sie die Client-Anwendung darüber, damit sie sie in der Gegenpartei-Datenbank anhand der Nummer des Anrufers finden und dem Benutzer Informationen über die anrufende Gegenpartei anzeigen kann. Oder benachrichtigen Sie beispielsweise die Client-Anwendung des Kunden, wenn eine Bestellung im Lager eintrifft. Im Allgemeinen gibt es viele Fälle, in denen ein solcher Mechanismus nützlich wäre.

Eigentlich Einstellung

Erstellen Sie einen Nachrichtenmechanismus. Schnell, zuverlässig, mit garantierter Zustellung, mit der Möglichkeit der flexiblen Suche nach Nachrichten. Implementieren Sie basierend auf dem Mechanismus einen Messenger (Nachrichten, Videoanrufe), der in 1C-Anwendungen funktioniert.

Gestalten Sie das System horizontal skalierbar. Eine steigende Belastung soll durch eine Erhöhung der Knotenzahl abgedeckt werden.

Implementierung

Wir haben uns entschieden, den Serverteil des CB nicht direkt in die 1C:Enterprise-Plattform einzubetten, sondern ihn als separates Produkt zu implementieren, dessen API aus dem Code der 1C-Anwendungslösungen aufgerufen werden kann. Dies geschah aus mehreren Gründen, vor allem um den Austausch von Nachrichten zwischen verschiedenen 1C-Anwendungen (z. B. zwischen dem Ministerium für Handel und Rechnungswesen) zu ermöglichen. Verschiedene 1C-Anwendungen können auf unterschiedlichen Versionen der 1C:Enterprise-Plattform laufen, sich auf unterschiedlichen Servern befinden usw. Unter solchen Bedingungen ist die Implementierung von CB als separates Produkt, das sich „neben“ von 1C-Installationen befindet, die optimale Lösung.

Deshalb haben wir uns entschieden, CB als separates Produkt herzustellen. Für kleinere Unternehmen empfehlen wir die Verwendung des CB-Servers, den wir in unserer Cloud (wss://1cdialog.com) installiert haben, um den mit der lokalen Serverinstallation und -konfiguration verbundenen Aufwand zu vermeiden. Großkunden können es jedoch für sinnvoll halten, einen eigenen CB-Server an ihrem Standort zu installieren. Einen ähnlichen Ansatz haben wir bei unserem Cloud-SaaS-Produkt verwendet. 1cFrisch – Es wird als Produktionsprodukt zur Installation durch Kunden freigegeben und auch in unserer Cloud bereitgestellt https://1cfresh.com/.

Anwendung

Zur Lastverteilung und Fehlertoleranz werden wir nicht eine, sondern mehrere Java-Anwendungen bereitstellen und ihnen einen Load Balancer voranstellen. Wenn Sie eine Nachricht von Knoten zu Knoten senden müssen, verwenden Sie Publish/Subscribe in Hazelcast.

Kommunikation zwischen Client und Server – über Websocket. Es eignet sich gut für Echtzeitsysteme.

Verteilter Cache

Wählen Sie zwischen Redis, Hazelcast und Ehcache. Draußen im Jahr 2015. Redis hat gerade einen neuen Cluster veröffentlicht (zu neu, beängstigend), es gibt einen Sentinel mit vielen Einschränkungen. Ehcache kann nicht gruppieren (diese Funktionalität erschien später). Wir haben uns entschieden, es mit Hazelcast 3.4 zu versuchen.
Hazelcast ist sofort einsatzbereit. Im Einzelknotenmodus ist es nicht sehr nützlich und kann nur als Cache verwendet werden. Es weiß nicht, wie Daten auf die Festplatte geschrieben werden sollen. Wenn der einzige Knoten verloren geht, gehen die Daten verloren. Wir setzen mehrere Hazelcasts ein, zwischen denen wir kritische Daten sichern. Wir sichern den Cache nicht – er tut uns nicht leid.

Für uns ist Hazelcast:

  • Speicherung von Benutzersitzungen. Es dauert lange, die Datenbank für eine Sitzung aufzurufen, daher haben wir alle Sitzungen in Hazelcast gespeichert.
  • Zwischenspeicher. Auf der Suche nach einem Benutzerprofil – schauen Sie im Cache nach. Habe eine neue Nachricht geschrieben – lege sie in den Cache.
  • Themen zur Kommunikation von Anwendungsinstanzen. Der Knoten generiert ein Ereignis und platziert es in einem Hazelcast-Thema. Andere Anwendungsknoten, die dieses Thema abonniert haben, empfangen und verarbeiten das Ereignis.
  • Cluster-Sperren. Zum Beispiel erstellen wir eine Diskussion mit einem eindeutigen Schlüssel (Diskussions-Singleton im Rahmen der 1C-Basis):

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

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

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

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

Wir haben überprüft, dass kein Kanal vorhanden ist. Sie nahmen das Schloss, überprüften es erneut und erstellten es. Wenn Sie nach dem Aufheben der Sperre nicht nachsehen, besteht die Möglichkeit, dass in diesem Moment auch ein anderer Thread nachgesehen hat und nun versucht, dieselbe Diskussion zu erstellen – und sie existiert bereits. Es ist unmöglich, eine Sperre durch eine synchronisierte oder reguläre Java-Sperre vorzunehmen. Durch die Basis – langsam, und die Basis ist schade, durch Hazelcast – was Sie brauchen.

Auswahl eines DBMS

Wir verfügen über umfangreiche und erfolgreiche Erfahrungen mit PostgreSQL und Zusammenarbeit mit den Entwicklern dieses DBMS.

Mit einem Cluster ist PostgreSQL nicht einfach – das gibt es XL, XC, Citus, aber im Allgemeinen ist es nicht noSQL, das sofort skaliert werden kann. Als Hauptspeicher kam NoSQL nicht in Betracht, es reichte uns, Hazelcast zu nehmen, mit dem wir vorher noch nicht gearbeitet hatten.

Da Sie eine relationale Datenbank skalieren müssen, bedeutet dies Scherben. Wie Sie wissen, teilen wir beim Sharding die Datenbank in separate Teile auf, sodass jeder von ihnen auf einem separaten Server platziert werden kann.

Die erste Version unseres Shardings ging von der Fähigkeit aus, jede der Tabellen unserer Anwendung in unterschiedlichen Anteilen auf verschiedene Server zu verteilen. Viele Nachrichten auf Server A – bitte verschieben wir einen Teil dieser Tabelle auf Server B. Diese Entscheidung schreit nur nach vorzeitiger Optimierung, daher haben wir uns entschieden, uns auf einen mandantenfähigen Ansatz zu beschränken.

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

Im SV gibt es Konzepte der Anwendung und des Abonnenten. Eine Anwendung ist eine spezifische Installation einer Geschäftsanwendung wie ERP oder Buchhaltung mit ihren Benutzern und Geschäftsdaten. Ein Abonnent ist eine Organisation oder Einzelperson, in deren Namen die Anwendung auf dem CB-Server registriert wird. Ein Abonnent kann mehrere Anwendungen registrieren lassen und diese Anwendungen können untereinander Nachrichten austauschen. Der Abonnent wurde Mieter in unserem System. Nachrichten mehrerer Abonnenten können sich in einer physischen Basis befinden; Wenn wir feststellen, dass ein Abonnent begonnen hat, viel Datenverkehr zu generieren, verschieben wir ihn in eine separate physische Datenbank (oder sogar auf einen separaten Datenbankserver).

Wir verfügen über eine Hauptdatenbank, in der die Routing-Tabelle mit Informationen über den Standort aller Abonnentendatenbanken gespeichert ist.

Wie und warum wir einen hoch ausgelasteten skalierbaren Dienst für 1C geschrieben haben: Enterprise: Java, PostgreSQL, Hazelcast

Um zu verhindern, dass die Hauptdatenbank einen Engpass darstellt, behalten wir die Routing-Tabelle (und andere häufig angeforderte Daten) im Cache.

Wenn die Datenbank des Abonnenten langsamer wird, teilen wir sie in Partitionen auf. Bei anderen Projekten verwenden wir zum Partitionieren großer Tabellen pg_pathman.

Da es schlimm ist, Benutzernachrichten zu verlieren, sichern wir unsere Datenbanken mit Replikaten. Durch die Kombination aus synchronen und asynchronen Replikaten können Sie sich gegen den Verlust der Hauptdatenbank absichern. Ein Nachrichtenverlust tritt nur auf, wenn gleichzeitig die Hauptdatenbank und ihr synchrones Replikat ausfallen.

Wenn das synchrone Replikat verloren geht, wird das asynchrone Replikat synchron.
Wenn die Hauptdatenbank verloren geht, wird das synchrone Replikat zur Hauptdatenbank, das asynchrone Replikat wird zu einem synchronen Replikat.

Elasticsearch für die Suche

Da CB unter anderem auch ein Messenger ist, benötigen wir hier eine schnelle, komfortable und flexible Suche unter Berücksichtigung der Morphologie nach ungenauen Übereinstimmungen. Wir haben uns entschieden, das Rad nicht neu zu erfinden und die kostenlose Suchmaschine Elasticsearch zu verwenden, die auf Basis der Bibliothek erstellt wurde Lucene. Wir implementieren Elasticsearch auch in einem Cluster (Stamm – Daten – Daten), um Probleme im Falle eines Ausfalls von Anwendungsknoten zu beseitigen.

Auf Github haben wir gefunden Russisches Morphologie-Plugin für Elasticsearch und verwenden Sie es. Im Elasticsearch-Index speichern wir Wortwurzeln (die das Plugin definiert) und N-Gramm. Wenn der Benutzer den zu durchsuchenden Text eingibt, suchen wir nach dem eingegebenen Text unter N-Grammen. Beim Speichern im Index wird das Wort „Texte“ in die folgenden N-Gramme unterteilt:

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

Außerdem wird der Wortstamm „Text“ gespeichert. Mit diesem Ansatz können Sie am Anfang, in der Mitte und am Ende des Wortes suchen.

Großes Bild

Wie und warum wir einen hoch ausgelasteten skalierbaren Dienst für 1C geschrieben haben: Enterprise: Java, PostgreSQL, Hazelcast
Wiederholung des Bildes vom Anfang des Artikels, jedoch mit Erläuterungen:

  • Balancer dem Internet ausgesetzt; Wir haben Nginx, es kann jedes sein.
  • Java-Anwendungsinstanzen kommunizieren über Hazelcast miteinander.
  • Um mit einem Web-Socket zu arbeiten, verwenden wir Netty.
  • Eine in Java 8 geschriebene Java-Anwendung besteht aus Bundles OSGi. Geplant ist die Migration auf Java 10 und die Umstellung auf Module.

Entwicklung und Tests

Während der Entwicklung und Erprobung des CB sind wir auf eine Reihe interessanter Eigenschaften der von uns verwendeten Produkte gestoßen.

Lasttests und Speicherlecks

Die Veröffentlichung jedes CB-Releases ist ein Belastungstest. Es wurde erfolgreich bestanden, wenn:

  • Der Test funktionierte mehrere Tage lang und es gab keine Dienstverweigerungen
  • Die Reaktionszeit bei Tastenbetätigungen überschritt nicht den angenehmen Schwellenwert
  • Der Leistungsabfall im Vergleich zur Vorgängerversion beträgt nicht mehr als 10 %

Wir füllen die Testdatenbank mit Daten – dazu erhalten wir Informationen über den aktivsten Abonnenten vom Produktionsserver, multiplizieren seine Zahlen mit 5 (die Anzahl der Nachrichten, Diskussionen, Benutzer) und testen so.

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

  1. Stresstest
  2. Nur Verbindungen
  3. Abonnentenregistrierung

Während eines Stresstests starten wir mehrere hundert Threads, die das System ohne Unterbrechung belasten: Nachrichten schreiben, Diskussionen erstellen, eine Liste mit Nachrichten empfangen. Wir simulieren die Aktionen normaler Benutzer (eine Liste meiner ungelesenen Nachrichten abrufen, jemandem schreiben) und Programmentscheidungen (ein Paket in eine andere Konfiguration übertragen, eine Warnung verarbeiten).

So sieht beispielsweise ein Teil des Stresstests aus:

  • Benutzer meldet sich an
    • Fordert Ihre ungelesenen Threads an
    • 50 % Chance, Nachrichten zu lesen
    • 50 % Chance, Nachrichten zu schreiben
    • Nächster Benutzer:
      • 20 % Chance, einen neuen Thread zu erstellen
      • Wählt zufällig eine seiner Diskussionen aus
      • Kommt rein
      • Fordert Nachrichten, Benutzerprofile an
      • Erstellt fünf Nachrichten, die an zufällige Benutzer dieses Threads gerichtet sind
      • Außerhalb der Diskussion
      • 20 Mal wiederholt
      • Meldet sich ab und kehrt zum Anfang des Skripts zurück

    • Ein Chatbot dringt in das System ein (emuliert Nachrichten aus dem Code der angewandten Lösungen)
      • 50 % Chance, einen neuen Datenkanal zu erstellen (spezielle Diskussion)
      • 50 % Chance, eine Nachricht in einem der vorhandenen Kanäle zu schreiben

Das Szenario „Nur Verbindungen“ erschien aus einem bestimmten Grund. Es gibt eine Situation: Benutzer haben das System angeschlossen, waren aber noch nicht beteiligt. Jeder Benutzer schaltet morgens um 09:00 Uhr den Computer ein, stellt eine Verbindung zum Server her und schweigt. Diese Typen sind gefährlich, es gibt viele von ihnen – sie haben nur PING/PONG aus den Paketen, aber sie halten die Verbindung zum Server aufrecht (sie können sie nicht aufrechterhalten – und plötzlich eine neue Nachricht). Der Test reproduziert die Situation, wenn eine große Anzahl solcher Benutzer versucht, sich innerhalb einer halben Stunde am System anzumelden. Es sieht aus wie ein Stresstest, aber der Fokus liegt genau auf dieser ersten Eingabe – damit es nicht zu Ausfällen kommt (man nutzt das System nicht, aber es stürzt bereits ab – etwas Schlimmeres kann man sich kaum vorstellen).

Das Abonnentenregistrierungsszenario stammt aus dem ersten Start. Wir haben einen Stresstest durchgeführt und waren sicher, dass das System bei der Korrespondenz nicht langsamer wurde. Aber die Benutzer gingen weg und die Registrierung begann aufgrund einer Zeitüberschreitung abzufallen. Bei der Registrierung haben wir verwendet / dev / random, die an die Entropie des Systems gebunden ist. Der Server hatte keine Zeit, genügend Entropie anzusammeln, und als ein neues SecureRandom angefordert wurde, fror er mehrere zehn Sekunden lang ein. Es gibt viele Auswege aus dieser Situation, zum Beispiel: Wechseln Sie zu einem weniger sicheren /dev/urandom, installieren Sie ein spezielles Board, das Entropie erzeugt, generieren Sie im Voraus Zufallszahlen und speichern Sie diese im Pool. Wir haben das Problem mit dem Pool vorübergehend behoben, führen seitdem jedoch einen separaten Test für die Registrierung neuer Abonnenten durch.

Als Lastgenerator verwenden wir JMeter. Er weiß nicht, wie man mit einem Websocket arbeitet, es wird ein Plugin benötigt. Die ersten in den Suchergebnissen für die Suchanfrage „jmeter websocket“ sind Artikel von BlazeMeterin dem sie empfehlen Plugin von Maciej Zaleski.

Dort haben wir beschlossen, anzufangen.

Fast unmittelbar nach Beginn ernsthafter Tests stellten wir fest, dass es in JMeter zu Speicherlecks kam.

Das Plugin ist eine eigene große Geschichte, mit 176 Sternen hat es 132 Forks auf Github. Der Autor selbst hat sich seit 2015 nicht dazu verpflichtet (wir haben es 2015 aufgenommen, damals hat es keinen Verdacht erregt), mehrere Github-Probleme zu Speicherlecks, 7 nicht geschlossene Pull-Anfragen.
Wenn Sie sich für einen Lasttest mit diesem Plugin entscheiden, beachten Sie bitte die folgenden Diskussionen:

  1. In einer Multithread-Umgebung wurde eine reguläre LinkedList verwendet, als Ergebnis erhielten wir NPE zur Laufzeit. Das Problem wird entweder durch den Wechsel zu ConcurrentLinkedDeque oder durch synchronisierte Blöcke gelöst. Wir haben uns für die erste Option entschiedenhttps://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. Speicherverlust, Verbindungsinformationen werden beim Trennen nicht gelöscht (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. Im Streaming-Modus (wenn der Websocket am Ende des Beispiels nicht geschlossen wird, sondern weiter im Plan verwendet wird) funktionieren Antwortmuster nicht (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

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

  1. Hat genommen Gabel von Elyran Kogan (@elyrank) – es behebt die Probleme 1 und 3
  2. Problem 2 gelöst
  3. Anlegestelle von 9.2.14 auf 9.3.12 aktualisiert
  4. SimpleDateFormat in ThreadLocal eingebunden; SimpleDateFormat ist nicht threadsicher und führt zur Laufzeit zu NPE
  5. Ein weiteres Speicherleck wurde behoben (Verbindung wurde beim Trennen falsch geschlossen)

Und doch fließt es!

Die Erinnerung begann nicht nach einem, sondern nach zwei Tagen zu enden. Wir hatten überhaupt keine Zeit und beschlossen, weniger Threads auszuführen, dafür aber vier Agenten. Das hätte für mindestens eine Woche reichen sollen.

Es sind zwei Tage vergangen...

Jetzt geht Hazelcast der Speicher aus. Die Protokolle zeigten, dass Hazelcast nach ein paar Testtagen anfängt, sich über den Mangel an Speicher zu beschweren, und nach einer Weile fällt der Cluster auseinander und die Knoten sterben weiterhin einer nach dem anderen ab. Wir haben JVisualVM mit Hazelcast verbunden und sahen die „Säge nach oben“ – es rief regelmäßig den GC auf, konnte den Speicher jedoch in keiner Weise löschen.

Wie und warum wir einen hoch ausgelasteten skalierbaren Dienst für 1C geschrieben haben: Enterprise: Java, PostgreSQL, Hazelcast

Es stellte sich heraus, dass in Hazelcast 3.4 beim Löschen einer Karte / MultiMap (map.destroy()) der Speicher nicht vollständig freigegeben wird:

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

Der Fehler ist jetzt in 3.5 behoben, damals gab es jedoch ein Problem. Wir haben eine neue MultiMap mit dynamischen Namen erstellt und entsprechend unserer Logik gelöscht. Der Code sah ungefähr so ​​aus:

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

Bewertungen:

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

multiMap wurde für jedes Abonnement erstellt und entfernt, wenn es nicht benötigt wurde. Wir beschlossen, eine Karte zu starten , der Schlüssel ist der Name des Abonnements und die Werte sind Sitzungskennungen (über die Sie dann bei Bedarf Benutzerkennungen erhalten können).

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

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

Die Diagramme haben sich verbessert.

Wie und warum wir einen hoch ausgelasteten skalierbaren Dienst für 1C geschrieben haben: Enterprise: Java, PostgreSQL, Hazelcast

Was haben wir sonst noch über Lasttests gelernt?

  1. JSR223 muss in Groovy geschrieben sein und einen Kompilierungscache enthalten – es ist viel schneller. Link.
  2. Jmeter-Plugins-Diagramme sind einfacher zu verstehen als Standarddiagramme. Link.

Über unsere Erfahrungen mit Hazelcast

Hazelcast war für uns ein neues Produkt, wir haben ab Version 3.4.1 damit begonnen, damit zu arbeiten, jetzt haben wir Version 3.9.2 auf unserem Produktionsserver (zum Zeitpunkt des Verfassens dieses Artikels ist die neueste Version von Hazelcast 3.10).

ID-Generierung

Wir haben mit ganzzahligen Bezeichnern begonnen. Stellen wir uns vor, wir benötigen ein weiteres Long für eine neue Entität. Die Reihenfolge in der Datenbank ist nicht geeignet, Tabellen sind am Sharding beteiligt – es stellt sich heraus, dass es in DB1 eine Nachrichten-ID=1 und in DB1 eine Nachrichten-ID=2 gibt. Sie können diese ID nicht in Elasticsearch eingeben, auch nicht in Hazelcast. Aber das Schlimmste ist, wenn Sie Daten aus zwei Datenbanken auf eine reduzieren möchten (z. B. entscheiden, dass eine Datenbank für diese Abonnenten ausreicht). Sie können mehrere AtomicLongs in Hazelcast haben und den Zähler dort belassen, dann beträgt die Leistung beim Abrufen einer neuen ID incrementAndGet plus die Zeit für die Abfrage in Hazelcast. Aber Hazelcast hat etwas Optimaleres – FlakeIdGenerator. Bei der Kontaktaufnahme erhält jeder Kunde eine Reihe von IDs, zum Beispiel die erste – von 1 bis 10, die zweite – von 000 bis 10 und so weiter. Jetzt kann der Client selbst neue Identifikatoren vergeben, bis der ihm zugewiesene Bereich endet. Funktioniert schnell, aber ein Neustart der App (und des Hazelcast-Clients) startet eine neue Sequenz – daher die Sprünge usw. Darüber hinaus ist es für Entwickler nicht ganz klar, warum IDs Ganzzahlen sind, aber sie sind so widersprüchlich. Wir haben alles abgewogen und sind auf UUIDs umgestiegen.

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

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

Aber wir sind noch nicht dazu gekommen.

TransactionalMap.replace

Eine weitere Überraschung: TransactionalMap.replace funktioniert nicht. Hier ist ein 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

Ich musste meinen eigenen Ersatz mit getForUpdate schreiben:

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

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

Schließen Sie ein neues JAR ohne Ausfallzeit an

Zuerst haben wir beschlossen, Objekte unserer Klassen in Hazelcast zu schreiben. Wir haben zum Beispiel eine Anwendungsklasse, die wir speichern und lesen möchten. Speichern:

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

Lesen:

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

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

map.addIndex("subscriberId", false);

Und als sie eine neue Entität schrieben, erhielten sie eine ClassNotFoundException. Hazelcast hat versucht, den Index zu ergänzen, wusste aber nichts über unsere Klasse und wollte ein JAR mit dieser Klasse darin einfügen. Wir haben genau das getan, alles hat funktioniert, aber ein neues Problem ist aufgetaucht: Wie kann man die JAR aktualisieren, ohne den Cluster vollständig zu stoppen? Hazelcast ruft bei einem Update pro Knoten kein neues JAR ab. An diesem Punkt entschieden wir, dass wir ohne Indexsuche leben könnten. Wenn Sie Hazelcast als Schlüsselwertspeicher verwenden, funktioniert dann alles? Nicht wirklich. Auch hier unterschiedliches Verhalten von IMap und TransactionalMap. Wenn es IMap egal ist, gibt TransactionalMap einen Fehler aus.

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

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

Aber es funktioniert nicht in einer Transaktion, wir bekommen eine 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 erschien der User Class Deployment-Mechanismus. Sie können einen Masterknoten festlegen und die JAR-Datei darauf aktualisieren.

Jetzt haben wir unseren Ansatz komplett geändert: Wir selbst serialisieren in JSON und speichern in Hazelcast. Hazelcast muss die Struktur unserer Klassen nicht kennen und wir können ohne Ausfallzeiten aktualisieren. Die Versionierung von Domänenobjekten wird von der Anwendung gesteuert. Verschiedene Versionen der Anwendung können gleichzeitig gestartet werden, und es ist möglich, dass eine neue Anwendung Objekte mit neuen Feldern schreibt, während die alte Anwendung diese Felder noch nicht kennt. Gleichzeitig liest die neue Anwendung die von der alten Anwendung geschriebenen Objekte, die keine neuen Felder haben. Wir behandeln solche Situationen innerhalb der Anwendung, aber der Einfachheit halber ändern oder entfernen wir die Felder nicht, sondern erweitern die Klassen nur durch das Hinzufügen neuer Felder.

Wie wir Höchstleistungen erbringen

Vier Fahrten nach Hazelcast sind gut, zwei Fahrten zur Datenbank sind schlecht

Die Suche nach Daten im Cache ist immer besser als in der Datenbank, aber Sie möchten auch keine nicht beanspruchten Datensätze speichern. Die Entscheidung, was zwischengespeichert werden soll, bleibt der letzten Entwicklungsstufe überlassen. Wenn die neue Funktionalität codiert ist, aktivieren wir die Protokollierung aller Abfragen in PostgreSQL (log_min_duration_statement auf 0) und führen Lasttests für 20 Minuten durch. Dienstprogramme wie pgFouine und pgBadger können Analyseberichte basierend auf den gesammelten Protokollen erstellen. In Berichten achten wir vor allem auf langsame und häufige Abfragen. Für langsame Abfragen erstellen wir einen Ausführungsplan (EXPLAIN) und bewerten, ob eine solche Abfrage beschleunigt werden kann. Häufige Anfragen nach derselben Eingabe passen gut in den Cache. Wir versuchen, Abfragen „flach“ zu halten, eine Tabelle pro Abfrage.

Ausbeutung

CB als Online-Dienst wurde im Frühjahr 2017 gestartet, da im November 2017 ein separates CB-Produkt veröffentlicht wurde (damals in der Beta-Version).

Seit mehr als einem Betriebsjahr sind beim Betrieb des CB-Onlinedienstes keine gravierenden Probleme aufgetreten. Wir überwachen das Online-Angebot durch Zabbix, sammeln und bereitstellen von Bambus.

Die CB-Server-Distribution liegt in Form nativer Pakete vor: RPM, DEB, MSI. Darüber hinaus stellen wir für Windows ein einziges Installationsprogramm in Form einer einzelnen EXE-Datei bereit, das den Server, Hazelcast und Elasticsearch auf einem Computer installiert. Zuerst nannten wir diese Version der Installation „Demo“, aber jetzt ist klar, dass dies die beliebteste Bereitstellungsoption ist.

Source: habr.com

Kommentar hinzufügen