In diesem Artikel erzĂ€hlen wir, wie und warum wir entwickelt haben â einen Mechanismus, der Informationen zwischen Client-Anwendungen und 1C:Unternehmensservern ĂŒbertrĂ€gt â von der Aufgabenstellung bis hin zur Ausarbeitung der Architektur und der Implementierungsdetails.
Das Interaktionssystem (im Folgenden â IS) ist ein verteiltes, fehlertolerantes System zum Nachrichtenaustausch mit garantierter Zustellung. IS wurde als hochbelasteter Dienst mit hoher Skalierbarkeit konzipiert und ist sowohl als Online-Service (angeboten von der Firma 1C) als auch als Serienprodukt erhĂ€ltlich, das auf eigenen Serverressourcen bereitgestellt werden kann.
IS nutzt ein verteiltes Speicher und ein Suchsystem . AuĂerdem wird es um Java gehen und darum, wie wir PostgreSQL horizontal skalieren.
Aufgabenstellung
Um zu verstehen, warum wir das Interaktionssystem entwickelt haben, möchte ich kurz erzÀhlen, wie die Entwicklung von GeschÀftsanwendungen bei 1C organisiert ist.
ZunĂ€chst einmal â ein wenig ĂŒber uns fĂŒr diejenigen, die noch nicht wissen, womit wir uns beschĂ€ftigen:) Wir entwickeln die technische Plattform â1C:Unternehmenâ. Die Plattform umfasst ein Entwicklungswerkzeug fĂŒr GeschĂ€ftsanwendungen sowie einen Runtime, der es GeschĂ€ftsanwendungen ermöglicht, in plattformĂŒbergreifenden Umgebungen zu funktionieren.
Die Client-Server-Entwicklungsparadigma
GeschĂ€ftsanwendungen, die auf â1C:Unternehmenâ erstellt wurden, arbeiten in einer dreischichtigen der eingebetteten 1C-Sprache Im Anwendungscode mĂŒssen in den Ăberschriften von Prozeduren und Funktionen ausdrĂŒcklich angegeben werden, wo der Code ausgefĂŒhrt werden soll â mithilfe der Direktiven &AufClient / &AufServer (&AtClient / &AtServer in der englischen Variante der Sprache). Die Entwickler bei 1C werden mich jetzt korrigieren und sagen, dass es mit den Direktiven tatsĂ€chlich
Im Anwendungs-Code mĂŒssen in den Kopfzeilen von Prozeduren und Funktionen explizit angegeben werden, wo der Code ausgefĂŒhrt wird â mithilfe der Direktiven &AmClienten / &AmServer (&AtClient / &AtServer in der englischen Version der Sprache). Die Entwickler bei 1C werden mir jetzt sagen, dass es tatsĂ€chlich Direktiven gibt. , aber das ist fĂŒr uns momentan nicht wesentlich.
Aus dem Client-Code kann Server-Code aufgerufen werden, aber aus dem Server-Code kann kein Client-Code aufgerufen werden. Dies ist eine fundamentale EinschrĂ€nkung, die wir aus verschiedenen GrĂŒnden eingefĂŒhrt haben. Unter anderem, weil der Server-Code so geschrieben sein muss, dass er unabhĂ€ngig von seinem Aufrufort â sei es vom Client oder vom Server â gleich ausgefĂŒhrt wird. Und im Falle des Aufrufs von Server-Code durch einen anderen Server-Code fehlt der Client als solcher. Und weil der Client, der den Server-Code aufgerufen hat, wĂ€hrend der AusfĂŒhrung des Server-Codes geschlossen oder die Anwendung verlassen haben könnte, hat der Server niemanden mehr, den er anrufen kann.
Code, der den Druck der SchaltflÀche verarbeitet: Der Aufruf der Serverprozedur vom Client funktioniert, der Aufruf der Clientprozedur vom Server hingegen nicht.
Das bedeutet, dass wir, wenn wir vom Server aus eine Nachricht in die Client-Anwendung senden möchten, beispielsweise um mitzuteilen, dass die Erstellung eines âlangwierigenâ Berichts abgeschlossen ist und dieser angesehen werden kann â wir keinen solchen Weg haben. Wir mĂŒssen auf Umwege zurĂŒckgreifen, wie beispielsweise, dass der Client-Code den Server regelmĂ€Ăig abfragt. Aber dieser Ansatz belastet das System mit ĂŒberflĂŒssigen Aufrufen und sieht auĂerdem nicht sehr elegant aus.
Und es gibt auch die Notwendigkeit, beispielsweise bei einem eingehenden Telefonanruf die Client-Anwendung darĂŒber zu informieren, damit diese anhand der Nummer des Anrufers den Kontakt in der Datenbank findet und dem Benutzer Informationen ĂŒber den anrufenden Partner anzeigt. Oder um beispielsweise den Kunden ĂŒber einen eingegangenen Auftrag im Lager zu informieren. Es gibt viele FĂ€lle, in denen ein solcher Mechanismus von Nutzen wĂ€re. Eigentlich die Aufgabe
Einen Mechanismus fĂŒr den Nachrichtenaustausch zu schaffen. Schnell, zuverlĂ€ssig, mit garantierter Zustellung und der Möglichkeit einer flexiblen Suche nach Nachrichten. Auf Basis dieses Mechanismus einen Messenger (Nachrichten, Videoanrufe) zu implementieren, der in 1C-Anwendungen funktioniert.
Das System horizontal skalierbar zu entwerfen. Eine steigende Last sollte durch die Erhöhung der Anzahl der Nodes abgedeckt werden.
Ein horizontal skalierbares System zu entwerfen. Die steigende Last sollte durch eine Erhöhung der Anzahl der Knoten gedeckt werden.
Implementierung
Wir haben beschlossen, die Server-Seite des SV nicht direkt in die 1C:Enterprise-Plattform einzubinden, sondern als eigenstĂ€ndiges Produkt zu implementieren, dessen API aus dem Code der 1C-Anwendungslösungen aufgerufen werden kann. Dies wurde aus mehreren GrĂŒnden getan, der wichtigste davon war, den Nachrichtenaustausch zwischen verschiedenen 1C-Anwendungen (z.B. zwischen Handelsmanagement und Buchhaltung) zu ermöglichen. Verschiedene 1C-Anwendungen können auf unterschiedlichen Versionen der 1C:Enterprise-Plattform laufen, auf unterschiedlichen Servern tĂ€tig sein usw. Unter diesen UmstĂ€nden ist die Implementierung des SV als eigenstĂ€ndiges Produkt, das ânebenâ den 1C-Installationen steht, die optimale Lösung.
Also haben wir beschlossen, SV als eigenstĂ€ndiges Produkt zu entwickeln. FĂŒr kleine Unternehmen empfehlen wir die Nutzung des SV-Servers, den wir in unserer Cloud installiert haben (wss://1cdialog.com), um die mit der lokalen Installation und Konfiguration des Servers verbundenen Kosten zu vermeiden. GröĂere Kunden könnten es jedoch fĂŒr sinnvoll erachten, einen eigenen SV-Server auf ihren eigenen Ressourcen zu installieren. Einen Ă€hnlichen Ansatz haben wir in unserem Cloud-SaaS-Produkt verwendet. â es wird als Serienprodukt zur Installation bei Kunden herausgegeben und ist auch in unserer Cloud bereitgestellt. .
Anwendung
Um die Last zu verteilen und die Ausfallsicherheit zu gewĂ€hrleisten, werden wir nicht eine, sondern mehrere Java-Anwendungen bereitstellen und ein Lastenausgleichssystem davor setzen. Wenn es erforderlich ist, eine Nachricht von Knoten zu Knoten zu ĂŒbertragen, verwenden wir publish/subscribe in Hazelcast.
Die Kommunikation zwischen dem Client und dem Server erfolgt ĂŒber WebSocket. Dies eignet sich gut fĂŒr Echtzeitsysteme.
Verteiltes Cache
Wir haben zwischen Redis, Hazelcast und Ehcache gewÀhlt. Es ist das Jahr 2015. Von Redis wurde gerade ein neues Cluster veröffentlicht (zu neu, zu riskant), es gibt Sentinel mit vielen EinschrÀnkungen. Ehcache kann nicht in ein Cluster integriert werden (diese FunktionalitÀt kam spÀter hinzu). Wir haben uns entschieden, mit Hazelcast 3.4 zu experimentieren.
Hazelcast kann âout of the boxâ in ein Cluster integriert werden. Im Modus eines Knotens ist es nicht sehr nĂŒtzlich und kann nur als Cache verwendet werden â es kann keine Daten auf der Festplatte ablegen, wenn wir den einzigen Knoten verlieren, verlieren wir die Daten. Wir setzen mehrere Hazelcast-Knoten ein, zwischen denen wir kritische Daten sichern. Den Cache sichern wir nicht â da tut es nicht weh.
FĂŒr uns ist Hazelcast:
- Ein Speicher fĂŒr Benutzersitzungen. Jedes Mal, wenn wir fĂŒr eine Sitzung in die Datenbank gehen, dauert es lange, daher speichern wir alle Sitzungen in Hazelcast.
- Cache. Wenn du das Benutzerprofil suchst, ĂŒberprĂŒfe den Cache. Hast du eine neue Nachricht geschrieben, lege sie im Cache ab.
- Themen fĂŒr die Kommunikation von Anwendungsinstanzen. Der Knoten generiert ein Ereignis und platziert es im Hazelcast-Topic. Andere Knoten der Anwendung, die auf dieses Topic abonniert sind, empfangen und verarbeiten das Ereignis.
- Cluster-Sperren. Beispielsweise erstellen wir eine Diskussion mit einem eindeutigen SchlĂŒssel (Singleton-Diskussion im Rahmen der 1C-Datenbank):
conversationKeyChecker.check("BENSINPUMPE");
doInClusterLock("BENSINPUMPE", () -> {
conversationKeyChecker.check("BENSINPUMPE");
createChannel("BENSINPUMPE");
});Wir haben ĂŒberprĂŒft, dass es keinen Kanal gibt. Wir haben eine Sperre ĂŒbernommen, erneut ĂŒberprĂŒft und erstellt. Wenn man nach der Ăbernahme der Sperre nicht ĂŒberprĂŒft, besteht die Chance, dass ein anderer Thread in diesem Moment ebenfalls ĂŒberprĂŒft und versucht, dieselbe Diskussion zu erstellen â und diese existiert bereits. Die Sperre ĂŒber synchronized oder herkömmliche Java Locks kann nicht erfolgen. Ăber die Datenbank â langsam, und schade um die Datenbank, ĂŒber Hazelcast â genau das, was wir brauchen.
Wir wÀhlen das DBMS aus.
Wir haben umfangreiche und erfolgreiche Erfahrungen mit PostgreSQL und der Zusammenarbeit mit den Entwicklern dieser DBMS.
Bei PostgreSQL ist es kompliziert mit Clustern â es gibt , , , aber im Allgemeinen handelt es sich nicht um NoSQL, die von Haus aus skalierbar sind. NoSQL als primĂ€res Speichersystem wurde nicht betrachtet, es genĂŒgte, dass wir Hazelcast verwenden, mit dem wir zuvor nicht gearbeitet hatten.
Da wir eine relationale Datenbank skalieren mĂŒssen â bedeutet das, . Wie Sie wissen, teilen wir beim Sharding die Datenbank in einzelne Teile, sodass jeder von ihnen auf einen separaten Server ausgelagert werden kann.
Die erste Variante unseres Shardings sah vor, jede Tabelle unserer Anwendung auf verschiedene Server in unterschiedlichen VerhĂ€ltnissen zu verteilen. Viele Nachrichten auf Server A â bitte, lassen Sie uns einen Teil dieser Tabelle auf Server B verschieben. Diese Lösung schrie förmlich nach verfrĂŒhter Optimierung, also entschieden wir uns, es auf den Multi-Tenant-Ansatz zu beschrĂ€nken.
Mehr ĂŒber Multi-Tenant kann man beispielsweise auf der Website lesen .
Im SV gibt es die Begriffe Anwendung und Abonnent. Eine Anwendung ist eine spezifische Installation einer GeschĂ€ftsanwendung, wie zum Beispiel ERP oder Buchhaltung, mit ihren eigenen Benutzern und GeschĂ€ftsdaten. Ein Abonnent ist eine Organisation oder eine natĂŒrliche Person, im Namen derer die Registrierung der Anwendung auf dem SV-Server erfolgt. Ein Abonnent kann mehrere Anwendungen registriert haben, und diese Anwendungen können Nachrichten untereinander austauschen. Der Abonnent wurde also zum Mieter (tenant) in unserem System. Nachrichten mehrerer Abonnenten können sich in einer physischen Datenbank befinden; wenn wir feststellen, dass ein Abonnent viel Traffic generiert, bringen wir ihn in eine separate physische Datenbank (oder sogar auf einen separaten Datenbankserver).
Wir haben eine Hauptdatenbank, in der die Routingtabelle mit Informationen ĂŒber den Standort aller Abonnentendatenbanken gespeichert ist.
Um zu verhindern, dass die Hauptdatenbank zum Engpass wird, halten wir die Routingtabelle (und andere hÀufig benötigte Daten) im Cache.
Wenn die Datenbank des Abonnenten langsamer wird, partitionieren wir sie intern. In anderen Projekten verwenden wir fĂŒr die Partitionierung groĂer Tabellen .
Da es schlecht ist, Nachrichten von Benutzern zu verlieren, halten wir unsere Datenbanken mit Replikaten bereit. Die Kombination aus synchronen und asynchronen Replikaten ermöglicht es, sich abzusichern, falls die Hauptdatenbank verloren geht. Der Verlust einer Nachricht tritt nur auf, wenn die Hauptdatenbank und ihr synchrones Replica gleichzeitig ausfallen.
Wenn das synchrone Replica verloren geht, wird das asynchrone Replica synchron.
Wenn die Hauptdatenbank verloren geht, wird das synchrone Replica zur Hauptdatenbank, das asynchrone Replica wird zum synchronen Replica.
Elasticsearch fĂŒr die Suche
Da der SV unter anderem auch ein Messenger ist, ist eine schnelle, benutzerfreundliche und flexible Suche unter BerĂŒcksichtigung der Morphologie und ungenauer Ăbereinstimmungen erforderlich. Wir haben beschlossen, das Rad nicht neu zu erfinden und das freie Suchsystem Elasticsearch zu verwenden, das auf der Bibliothek basiert . Elasticsearch setzen wir ebenfalls in einem Cluster (Master â Daten â Daten) ein, um Probleme im Falle eines Ausfalls von Anwendungsknoten zu vermeiden.
Auf github haben wir gefunden fĂŒr Elasticsearch und verwenden es. Im Elasticsearch-Index speichern wir die Wurzeln von Wörtern (die vom Plugin definiert werden) und N-Gramme. WĂ€hrend der Benutzer Text fĂŒr die Suche eingibt, suchen wir den eingegebenen Text unter den N-Grammen. Beim Speichern im Index wird das Wort âtexteâ in folgende N-Gramme zerlegt:
[the, tek, tekst, texte, textes, ek, eks, ekst, eks, ks, kst, ksty, st, sty, ty],
Und auch die Wurzel des Wortes âtextâ wird gespeichert. Dieser Ansatz ermöglicht es, sowohl den Anfang als auch die Mitte und das Ende eines Wortes zu durchsuchen.
Das groĂe Ganze
Wiederholung des Bildes zu Beginn des Artikels, aber bereits mit ErklÀrungen:
- Der Lastenausgleich, der ins Internet gestellt wird; wir haben â nginx, kann beliebig sein.
- Die Instanzen der Java-Anwendung kommunizieren ĂŒber Hazelcast miteinander.
- FĂŒr die Arbeit mit Websockets verwenden wir .
- Die Java-Anwendung ist in Java 8 geschrieben und besteht aus Bundles . Geplant ist eine Migration auf Java 10 und der Umstieg auf Module.
Entwicklung und Testen
WĂ€hrend der Entwicklung und Testung der SV sind wir auf eine Reihe interessanter Besonderheiten der von uns genutzten Produkte gestoĂen.
Lasttests und Speicherlecks
Die Veröffentlichung jeder SV-Version ist ein Lasttest. Er gilt als erfolgreich, wenn:
- Der Test lief mehrere Tage und es gab keine AusfÀlle beim Service.
- Die Reaktionszeit fĂŒr SchlĂŒsselfunktionen ĂŒberschritt nicht die komfortable Grenze.
- Die LeistungseinbuĂen im Vergleich zur vorherigen Version betragen nicht mehr als 10%.
Wir fĂŒllen die Testdatenbank mit Daten â dazu erhalten wir vom Produktionsserver Informationen ĂŒber den aktivsten Abonnenten, multiplizieren seine Zahlen mit 5 (Anzahl der Nachrichten, Diskussionen, Benutzer) und testen so.
Die Lasttests des Interaktionssystems fĂŒhren wir in drei Konfigurationen durch:
- Stresstest
- Nur Verbindungen
- Registrierung von Abonnenten
Beim Stresstest starten wir mehrere Hundert Threads, die ohne Unterbrechung das System belasten: Sie senden Nachrichten, erstellen Diskussionen, erhalten die Liste der Nachrichten. Wir simulieren die Aktionen gewöhnlicher Benutzer (meine ungelesenen Nachrichten abrufen, jemandem schreiben) und Softwarelösungen (ein Paket an eine andere Konfiguration ĂŒbermitteln, eine Benachrichtigung verarbeiten).
So sieht beispielsweise ein Teil des Stresstests aus:
- Ein Benutzer loggt sich ins System ein
- Fragt seine ungelesenen Diskussionen an
- Liest mit 50% Wahrscheinlichkeit Nachrichten
- Schreibt mit 50% Wahrscheinlichkeit Nachrichten
- Weiterer Benutzer:
- Mit 20% Wahrscheinlichkeit wird ein neues GesprÀch erstellt
- WÀhlt zufÀllig eines seiner GesprÀche aus
- Geht hinein
- Fordert Nachrichten, Benutzerprofile an
- Erstellt fĂŒnf Nachrichten, die an zufĂ€llige Benutzer aus diesem GesprĂ€ch gerichtet sind
- VerlÀsst das GesprÀch
- Wiederholt dies 20 Mal
- Meldet sich ab und kehrt zum Beginn des Szenarios zurĂŒck
- Der Chatbot meldet sich im System an (simuliert den Nachrichtenaustausch aus dem Code der AnwendungsfÀlle)
- Mit 50% Wahrscheinlichkeit wird ein neuer Kanal fĂŒr den Datenaustausch erstellt (ein spezielles GesprĂ€ch)
- Mit 50% Wahrscheinlichkeit wird eine Nachricht in einen der bestehenden KanÀle geschrieben
Das Szenario âNur Verbindungenâ entstand nicht einfach so. Es gibt Situationen: Benutzer haben das System angeschlossen, aber noch nicht begonnen, es aktiv zu nutzen. Jeder Benutzer schaltet morgens um 09:00 seinen Computer ein, stellt die Verbindung zum Server her und bleibt still. Diese Benutzer sind gefĂ€hrlich, es sind viele â aus den Paketen kommt nur PING/PONG, aber die Verbindung zum Server bleibt bestehen (sie können sie nicht trennen â was ist, wenn eine neue Nachricht kommt). Der Test reproduziert eine Situation, in der sich innerhalb von einer halben Stunde viele solcher Benutzer im System anmelden wollen. Es Ă€hnelt einem Stresstest, aber der Fokus liegt auf diesem ersten Login â um Fehlermeldungen zu vermeiden (der Mensch nutzt das System nicht, und es fĂ€llt bereits aus â schwer, etwas Schlimmeres zu erfinden).
Das Szenario zur Registrierung von Teilnehmern beginnt mit dem ersten Start. Wir haben einen Stresstest durchgefĂŒhrt und waren ĂŒberzeugt, dass das System beim Austausch nicht langsamer wird. Doch dann kamen die Benutzer, und die Registrierung begann, aufgrund von Timeouts Probleme zu bekommen. Bei der Registrierung haben wir verwendet , der mit der Entropie des Systems gekoppelt ist. Der Server konnte nicht genug Entropie ansammeln, und beim Anfordern eines neuen SecureRandom blieb er fĂŒr Dutzende von Sekunden stehen. Es gibt viele Wege aus dieser Situation, zum Beispiel: Wechsel zu einem weniger sicheren /dev/urandom, eine spezielle Hardware hinzufĂŒgen, die Entropie erzeugt, oder vorher zufĂ€llige Zahlen generieren und im Pool speichern. Wir haben das Problem vorĂŒbergehend mit einem Pool gelöst, fĂŒhren aber seitdem einen separaten Test zur Registrierung neuer Teilnehmer durch.
Als Lastgenerator verwenden wir . Es kann nicht mit Websockets arbeiten, dafĂŒr ist ein Plugin erforderlich. Die ersten Suchergebnisse fĂŒr âjmeter websocketâ sind , die empfehlen .
Damit haben wir begonnen.
Fast nach Beginn der ernsthaften Tests entdeckten wir, dass es in JMeter zu Speicherlecks kam.
Das Plugin ist eine ganz andere Geschichte; es hat 176 Sterne und 132 Forks auf GitHub. Der ursprĂŒngliche Autor hat seit 2015 nicht mehr daran committen (wir haben es 2015 ĂŒbernommen, was damals keine Verdachtsmomente aufwarf), es gibt einige GitHub-Issues bezĂŒglich von Speicherlecks und 7 offene Pull-Requests.
Wenn Sie sich entscheiden, Lasttests mit diesem Plugin durchzufĂŒhren, beachten Sie bitte die folgenden Diskussionen:
- In der Multithreading-Umgebung wurde ein gewöhnlicher LinkedList verwendet, was dazu fĂŒhrte, dass wir zur Laufzeit erhielten. Dies kann entweder durch den Wechsel zu ConcurrentLinkedDeque oder durch synchronisierte Blöcke gelöst werden. Wir haben uns fĂŒr die erste Option entschieden ().
- Speicherleck, beim Disconect wird die Verbindungsinformation nicht gelöscht ().
- Im Streaming-Modus (wenn die WebSocket-Verbindung am Ende der Probe nicht geschlossen, sondern weiter im Plan verwendet wird) funktionieren die Antwortmuster nicht ().
Das ist eines von denen, die auf GitHub sind. Was wir gemacht haben:
- Wir haben (@elyrank) genommen â darin wurden die Probleme 1 und 3 behoben
- Das Problem 2 wurde gelöst
- Wir haben Jetty von 9.2.14 auf 9.3.12 aktualisiert
- Wir haben SimpleDateFormat in ThreadLocal gekapselt; SimpleDateFormat ist nicht thread-sicher, was zu NPE zur Laufzeit fĂŒhrte
- Wir haben ein weiteres Speicherleck beseitigt (die Verbindung wurde beim Disconect nicht korrekt geschlossen)
Und trotzdem leckt es!
Der Speicher ging nicht mehr nach einem Tag aus, sondern nach zwei. Es blieb ĂŒberhaupt keine Zeit mehr, also beschlossen wir, weniger Threads, aber auf vier Agenten zu starten. Das sollte mindestens eine Woche ausreichen.
Zwei Tage sind vergangen...
Jetzt ging der Speicher bei Hazelcast aus. In den Logs war zu sehen, dass Hazelcast nach ein paar Tagen Tests mit Speichermangel zu kĂ€mpfen hatte und nach einiger Zeit der Cluster zusammenbrach und die Knoten einer nach dem anderen ausfielen. Wir haben JVisualVM mit Hazelcast verbunden und die âaufsteigende SĂ€geâ gesehen â es rief regelmĂ€Ăig GC, konnte aber den Speicher nicht freigeben.
Es stellte sich heraus, dass in Hazelcast 3.4 der Speicher bei der Löschung von map / multiMap (map.destroy()) nicht vollstÀndig freigegeben wird:
Jetzt ist der Fehler in 3.5 behoben, aber damals war das ein Problem. Wir haben neue multiMaps mit dynamischen Namen erstellt und nach unserer Logik gelöscht. Der Code sah ungefÀhr so aus:
public void join(Authentication auth, String sub) {
MultiMap sessions = instance.getMultiMap(sub);
sessions.put(auth.getUserId(), auth);
}
public void leave(Authentication auth, String sub) {
MultiMap sessions = instance.getMultiMap(sub);
sessions.remove(auth.getUserId(), auth);
if (sessions.size() == 0) {
sessions.destroy();
}
}Aufruf:
service.join(auth1, "NEUE_NACHRICHTEN_IM_DISKUSSIONS_UUID1");
service.join(auth2, "NEUE_NACHRICHTEN_IM_DISKUSSIONS_UUID1");multiMap wurde fĂŒr jedes Abonnement erstellt und entfernt, wenn es nicht mehr benötigt wurde. Wir haben entschieden, dass wir eine Map einfĂŒhren, wobei die SchlĂŒssel die Bezeichnungen der Abonnements und die Werte die Session-IDs sind (auf deren Basis man spĂ€ter die Benutzer-IDs abrufen kann, falls erforderlich).
public void join(Authentication auth, String sub) {
addValueToMap(sub, auth.getSessionId());
}
public void leave(Authentication auth, String sub) {
removeValueFromMap(sub, auth.getSessionId());
}Die Grafiken haben sich stabilisiert.
Was wir sonst noch ĂŒber Lasttests gelernt haben
- JSR223 muss in Groovy geschrieben und die Kompilierungscache aktiviert werden â das ist wesentlich schneller. .
- Die Grafiken der JMeter-Plugins sind einfacher zu verstehen als die Standardgrafiken. .
Unsere Erfahrungen mit Hazelcast
Hazelcast war fĂŒr uns ein neues Produkt, wir haben mit Version 3.4.1 begonnen, jetzt lĂ€uft auf unserem Produktionsserver Version 3.9.2 (zum Zeitpunkt des Schreibens war die letzte Version von Hazelcast 3.10).
ID-Generierung
Wir haben mit ganzzahligen Identifikatoren begonnen. Angenommen, wir benötigen eine neue Long-ID fĂŒr eine neue EntitĂ€t. Sequenzen in der Datenbank sind nicht geeignet, da die Tabellen am Sharding beteiligt sind â das wĂŒrde bedeuten, dass es eine Nachricht mit ID=1 in DB1 und eine Nachricht mit ID=1 in DB2 gibt. In Elasticsearch kann man keinen solch ID speichern, und in Hazelcast auch nicht. Das Schlimmste ist, wenn Sie die Daten aus zwei Datenbanken in einer zusammenfĂŒhren möchten (zum Beispiel, wenn Sie beschlossen haben, dass eine Datenbank fĂŒr diese Abonnenten ausreicht). Man könnte in Hazelcast mehrere AtomicLong einrichten und dort einen ZĂ€hler fĂŒhren, dann wĂ€re die Leistung zur Erzeugung einer neuen ID â incrementAndGet plus die Zeit fĂŒr die Anfrage an Hazelcast. Aber Hazelcast bietet etwas Optimales â FlakeIdGenerator. Jeder Client erhĂ€lt bei Anfragen einen ID-Bereich, zum Beispiel der erste â von 1 bis 10.000, der zweite â von 10.001 bis 20.000 und so weiter. Jetzt kann der Client selbstĂ€ndig neue Identifikatoren ausgeben, bis sein zugewiesener Bereich erschöpft ist. Es funktioniert schnell, aber beim Neustart der Anwendung (und des Hazelcast-Clients) beginnt eine neue Sequenz â daher entstehen LĂŒcken usw. AuĂerdem ist es fĂŒr die Entwickler nicht klar, warum die IDs ganzzahlig sind, aber so durcheinander erscheinen. Wir haben alles abgewogen und sind auf UUIDs umgestiegen.
Ăbrigens, fĂŒr diejenigen, die wie Twitter sein wollen, gibt es die Bibliothek Snowcast â das ist eine Implementierung von Snowflake auf Hazelcast. Hier kann man es ansehen:
Aber wir haben es bisher nicht geschafft.
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();
}
});
}
Erwartet: newValue
TatsÀchlich: oldValueIch musste mein eigenes replace schreiben, indem ich getForUpdate verwendet habe:
protected boolean replaceInMap(String mapName, K key, V oldValue, V newValue) {
TransactionalTaskContext context = HazelcastTransactionContextHolder.getContext();
if (context != null) {
log.trace("[CACHE] Wert in einer transaktionalen Karte ersetzen");
TransactionalMap map = context.getMap(mapName);
V value = map.getForUpdate(key);
if (oldValue.equals(value)) {
map.put(key, newValue);
return true;
}
return false;
}
log.trace("[CACHE] Wert in einer nicht transaktionalen Karte ersetzen");
IMap map = hazelcastInstance.getMap(mapName);
return map.replace(key, oldValue, newValue);
}Testen Sie nicht nur gewöhnliche Datenstrukturen, sondern auch deren transaktionale Versionen. Manchmal funktioniert IMap, wÀhrend TransactionalMap nicht funktioniert.
Neues JAR ohne Downtime bereitstellen
ZunÀchst haben wir beschlossen, Objekte unserer Klassen in Hazelcast zu speichern. Zum Beispiel haben wir die Klasse Application, die wir speichern und lesen wollen. Wir speichern:
IMap map = hazelcastInstance.getMap("application");
map.set(id, application);Wir lesen:
IMap map = hazelcastInstance.getMap("application");
return map.get(id);Es funktioniert alles. Dann haben wir beschlossen, einen Index in Hazelcast zu erstellen, um danach zu suchen:
map.addIndex("subscriberId", false);Und beim Speichern einer neuen EntitĂ€t begannen wir, ClassNotFoundException zu erhalten. Hazelcast versuchte, den Index zu ergĂ€nzen, wusste aber nichts ĂŒber unsere Klasse und wollte, dass wir ihm das JAR mit dieser Klasse bereitstellen. Das haben wir getan, es hat funktioniert, aber ein neues Problem tauchte auf: Wie aktualisiert man das JAR ohne vollstĂ€ndigen Cluster-Stopp? Hazelcast erkennt das neue JAR bei einem Sonic-Update nicht. In diesem Moment beschlossen wir, dass wir gut ohne die Suche nach Indizes leben können. Wenn man Hazelcast als SchlĂŒssel-Wert-Speicher verwendet, funktioniert das doch? Nicht ganz. Hier zeigt IMap ein anderes Verhalten als TransactionalMap. Wo es IMap egal ist, wirft TransactionalMap einen Fehler.
IMap. Wir zeichnen 5000 Objekte auf, lesen sie aus. Alles wie erwartet.
@Test
void get5000() {
IMap map = hazelcastInstance.getMap("application");
UUID subscriberId = UUID.randomUUID();
for (int i = 0; i < 5000; i++) {
UUID id = UUID.randomUUID();
String title = RandomStringUtils.random(5);
Application application = new Application(id, title, subscriberId);
map.set(id, application);
Application retrieved = map.get(id);
assertEquals(id, retrieved.getId());
}
}In der Transaktion funktioniert es nicht, wir erhalten ClassNotFoundException:
@Test
void get_transaction() {
IMap map = hazelcastInstance.getMap("application_t");
UUID subscriberId = UUID.randomUUID();
UUID id = UUID.randomUUID();
Application application = new Application(id, "qwer", subscriberId);
map.set(id, application);
Application retrievedOutside = map.get(id);
assertEquals(id, retrievedOutside.getId());
hazelcastInstance.executeTransaction(context -> {
HazelcastTransactionContextHolder.setContext(context);
try {
TransactionalMap transactionalMap = context.getMap("application_t");
Application retrievedInside = transactionalMap.get(id);
assertEquals(id, retrievedInside.getId());
return null;
} finally {
HazelcastTransactionContextHolder.clearContext();
}
});
}In 3.8 wurde der Mechanismus User Class Deployment eingefĂŒhrt. Sie können einen Hauptknoten zuweisen und die JAR-Datei darauf aktualisieren.
Derzeit haben wir unseren Ansatz komplett geĂ€ndert: Wir serialisieren selbst in JSON und speichern dies in Hazelcast. Hazelcast muss die Struktur unserer Klassen nicht kennen und wir können uns ohne Ausfallzeiten aktualisieren. Die Versionierung der Domain-Objekte wird von der Anwendung verwaltet. Verschiedene Versionen der Anwendung können gleichzeitig laufen, und es kann vorkommen, dass die neue Anwendung Objekte mit neuen Feldern schreibt, wĂ€hrend die alte ĂŒber diese Felder noch nichts weiĂ. Gleichzeitig liest die neue Anwendung Objekte, die von der alten Anwendung geschrieben wurden und keine neuen Felder enthalten. Solche Situationen handhaben wir innerhalb der Anwendung, Ă€ndern oder löschen jedoch zu Einfachheitszwecken keine Felder, sondern erweitern die Klassen durch HinzufĂŒgen neuer Felder.
Wie wir hohe Leistung sicherstellen
Vier Zugriffe auf Hazelcast â gut, zwei auf die DB â schlecht
Es ist immer besser, Daten aus dem Cache abzurufen als aus der Datenbank, aber es ist auch nicht wĂŒnschenswert, nicht genutzte DatensĂ€tze zu speichern. Die Entscheidung, was zu cachen ist, verschieben wir auf die letzte Phase der Entwicklung. Wenn die neue FunktionalitĂ€t codiert ist, aktivieren wir die Protokollierung aller Anfragen in PostgreSQL (log_min_duration_statement auf 0) und starten einen Lasttest von etwa 20 Minuten. Mit den gesammelten Logs können Tools wie pgFouine und pgBadger analytische Berichte erstellen. In den Berichten suchen wir zunĂ€chst nach langsamen und hĂ€ufigen Anfragen. FĂŒr langsame Anfragen erstellen wir einen AusfĂŒhrungsplan (EXPLAIN) und bewerten, ob sich diese Anfrage beschleunigen lĂ€sst. HĂ€ufige Anfragen mit denselben Eingabewerten lassen sich gut im Cache speichern. Wir versuchen, die Anfragen âflachâ zu halten, indem wir in jeder Anfrage nur eine Tabelle verwenden.
Betrieb
Der Service SV wurde im FrĂŒhjahr 2017 in Betrieb genommen und das separate Produkt SV kam im November 2017 (damals im Status einer Beta-Version) auf den Markt.
In ĂŒber einem Jahr Betrieb gab es keine schwerwiegenden Probleme mit dem Online-Service SV. Wir ĂŒberwachen den Online-Service ĂŒber , sammeln und implementieren aus .
Das Server-Distributionspaket von SV wird in Form von nativen Paketen geliefert: RPM, DEB, MSI. ZusĂ€tzlich bieten wir fĂŒr Windows einen einzigen Installer in Form einer EXE an, der den Server, Hazelcast und Elasticsearch auf einer Maschine installiert. ZunĂ€chst haben wir diese Installationsversion âDemoversionâ genannt, aber nun ist klar, dass dies die beliebteste Bereitstellungsart ist.
Quelle: habr.com
