Wie kann die Java-Leistung signifikant verbessert werden?

23

Das Team von LMAX hat eine Präsentation darüber, wie es gelungen ist, 100.000 TPS bei einer Latenz von weniger als 1 ms zu erreichen . Sie haben diese Präsentation mit einem Blog , einem technischen Dokument (PDF) und dem Quellcode selbst unterlegt .

Martin Fowler hat kürzlich ein hervorragendes Papier zur LMAX-Architektur veröffentlicht und erwähnt, dass sie jetzt sechs Millionen Bestellungen pro Sekunde verarbeiten können, und zeigt einige der Schritte auf, die das Team unternommen hat, um die Leistung um eine Größenordnung zu steigern.

Bisher habe ich erklärt, dass der Schlüssel zur Geschwindigkeit des Business Logic Processors darin besteht, alles sequentiell im Arbeitsspeicher auszuführen. Wenn Sie dies tun (und nichts wirklich Dummes), können Entwickler Code schreiben, der 10K TPS verarbeiten kann.

Sie fanden dann heraus, dass die Konzentration auf die einfachen Elemente eines guten Codes dies in den Bereich von 100.000 TPS bringen könnte. Hierfür sind nur gut durchdachter Code und kleine Methoden erforderlich. Dies ermöglicht Hotspot im Wesentlichen eine bessere Optimierung und eine effizientere Zwischenspeicherung des ausgeführten Codes durch die CPUs.

Es bedurfte etwas mehr Geschicklichkeit, um eine weitere Größenordnung aufzusteigen. Es gibt mehrere Dinge, die das LMAX-Team hilfreich fand, um dorthin zu gelangen. Eine bestand darin, benutzerdefinierte Implementierungen der Java-Auflistungen zu schreiben, die cachefreundlich und müllschonend gestaltet wurden.

Eine andere Technik, um dieses Höchstmaß an Leistung zu erreichen, besteht darin, den Leistungstests Aufmerksamkeit zu widmen. Ich habe lange bemerkt, dass die Leute viel über Techniken zur Verbesserung der Leistung sprechen, aber das einzige, was wirklich einen Unterschied macht, ist es, sie zu testen

Fowler erwähnte, dass es mehrere Dinge gibt, die gefunden wurden, aber er erwähnte nur ein paar.

Gibt es andere Architekturen, Bibliotheken, Techniken oder "Dinge", die hilfreich sind, um ein solches Leistungsniveau zu erreichen?

Dakotah North
quelle
11
"Welche anderen Architekturen, Bibliotheken, Techniken oder" Dinge "sind hilfreich, um ein solches Leistungsniveau zu erreichen?" Warum fragst du? Dieses Zitat ist die endgültige Liste. Es gibt viele, viele andere Dinge, von denen keines die Art der Elemente in dieser Liste auswirkt. Alles andere, was jemand benennen kann, ist nicht so hilfreich wie diese Liste. Warum nach schlechten Ideen fragen, wenn Sie eine der besten Optimierungslisten zitiert haben, die jemals erstellt wurden?
S.Lott
Es wäre schön zu erfahren, mit welchen Tools der generierte Code auf dem System ausgeführt wurde.
1
Ich habe von Leuten gehört, die auf alle möglichen Techniken schwören. Was ich als am effektivsten empfunden habe, ist die Profilerstellung auf Systemebene. Es kann Ihnen Engpässe in der Art und Weise aufzeigen, wie Ihr Programm und Ihre Arbeitsbelastung das System ausüben. Ich würde empfehlen, die bekannten Richtlinien zur Leistung einzuhalten und modularen Code zu schreiben, damit Sie ihn später problemlos optimieren können. Ich glaube nicht, dass Sie mit der Systemprofilerstellung etwas falsch machen können.
Ritesh

Antworten:

21

Es gibt alle Arten von Techniken für die Hochleistungstransaktionsverarbeitung, und die in Fowlers Artikel beschriebene ist nur eine von vielen, die derzeit auf dem neuesten Stand sind. Anstatt eine Reihe von Techniken aufzulisten, die auf die Situation eines Menschen anwendbar sind oder nicht, ist es meines Erachtens besser, die Grundprinzipien und die Art und Weise, wie LMAX eine große Anzahl von Techniken anspricht, zu diskutieren.

Für ein umfangreiches Transaktionsverarbeitungssystem möchten Sie so viel wie möglich tun:

  1. Minimieren Sie den Zeitaufwand in den langsamsten Speicherebenen. Von der schnellsten zur langsamsten auf einem modernen Server haben Sie: CPU / L1 -> L2 -> L3 -> RAM -> Festplatte / LAN -> WAN. Der Sprung von der schnellsten modernen Magnetplatte zum langsamsten Arbeitsspeicher beträgt mehr als das 1000-fache für sequentiellen Zugriff. Zufallszugriff ist noch schlimmer.

  2. Minimieren oder eliminieren Sie Wartezeiten . Dies bedeutet, dass Sie so wenig Status wie möglich freigeben und explizite Sperren nach Möglichkeit vermeiden , wenn der Status freigegeben werden muss .

  3. Verteilen Sie die Arbeitslast. CPUs sind in den letzten Jahren nicht viel schneller geworden, aber sie sind kleiner geworden, und 8 Kerne sind auf einem Server ziemlich verbreitet. Darüber hinaus können Sie die Arbeit sogar auf mehrere Computer verteilen. Dies ist der Ansatz von Google. Das Tolle daran ist, dass es alles skaliert , einschließlich I / O.

Laut Fowler verfolgt LMAX bei jedem dieser Punkte den folgenden Ansatz:

  1. Halten Sie alle Zustand in Erinnerung an allen Zeiten. Die meisten Datenbank-Engines tun dies ohnehin, wenn die gesamte Datenbank in den Arbeitsspeicher passt, aber sie möchten nichts dem Zufall überlassen, was auf einer Echtzeit-Handelsplattform verständlich ist. Um dies zu erreichen, ohne ein großes Risiko einzugehen, mussten sie eine Reihe leichter Backup- und Failover-Infrastrukturen aufbauen.

  2. Verwenden Sie eine sperrfreie Warteschlange ("Disruptor") für den Stream von Eingabeereignissen. Im Gegensatz zu herkömmlichen dauerhaften Nachrichtenwarteschlangen, die definitiv nicht frei von Sperren sind und in der Regel schmerzhaft langsam verteilte Transaktionen beinhalten .

  3. Nicht viel. LMAX wirft dieses unter den Bus auf der Grundlage, dass die Workloads voneinander abhängig sind. Das Ergebnis des einen ändert die Parameter für die anderen. Dies ist eine kritische Einschränkung, auf die Fowler ausdrücklich hinweist. Sie machen einige Verwendung von Parallelität , um Failover - Funktionen zur Verfügung zu stellen, aber alle der Business - Logik auf einem verarbeiteten einzigen Thread .

LMAX ist nicht der einzige Ansatz für hochskaliertes OLTP. Und obwohl es in seinem eigenen Recht ziemlich brillant, man nicht bleeding-edge - Techniken , um dieses Niveau der Leistung abziehen verwenden müssen.

Von allen oben genannten Prinzipien ist # 3 wahrscheinlich das wichtigste und effektivste, da Hardware ehrlich gesagt billig ist. Wenn Sie die Arbeitslast ordnungsgemäß auf ein halbes Dutzend Kerne und mehrere Dutzend Computer verteilen können, ist der Himmel die Grenze für konventionelle Parallel-Computing- Techniken. Sie wären überrascht, wie viel Durchsatz Sie mit nur ein paar Nachrichtenwarteschlangen und einem Round-Robin-Verteiler erzielen können. Es ist offensichtlich nicht so effizient wie LMAX - eigentlich nicht einmal in der Nähe -, aber Durchsatz, Latenz und Kosteneffizienz sind getrennte Aspekte, und hier geht es speziell um den Durchsatz.

Wenn Sie die gleichen speziellen Anforderungen wie LMAX haben, insbesondere einen geteilten Zustand, der einer geschäftlichen Realität entspricht, im Gegensatz zu einer voreiligen Designwahl, dann würde ich vorschlagen, ihre Komponente auszuprobieren, da ich nicht viel gesehen habe sonst passt das zu diesen anforderungen. Aber wenn wir nur über hohe Skalierbarkeit sprechen, dann möchte ich Sie dringend auffordern, mehr über verteilte Systeme zu forschen, da dies der kanonische Ansatz ist, den die meisten Organisationen heutzutage verwenden (Hadoop und verwandte Projekte, ESB und verwandte Architekturen, CQRS, auch Fowler) Erwähnungen und so weiter).

SSDs werden auch zum Game-Changer. wohl schon. Sie können nun einen permanenten Speicher mit ähnlichen Zugriffszeiten auf den Arbeitsspeicher haben, und obwohl SSDs der Server-Klasse immer noch schrecklich teuer sind, werden sie mit zunehmender Adoptionsrate im Preis sinken. Es wurde ausgiebig recherchiert und die Ergebnisse sind ziemlich umwerfend und werden mit der Zeit immer besser, so dass das gesamte Konzept "Alles im Gedächtnis behalten" viel weniger wichtig ist als früher. Daher würde ich versuchen, mich nach Möglichkeit auf die Nebenläufigkeit zu konzentrieren.

Aaronaught
quelle
Die Grundsätze der Diskussion wird zugrunde liegenden Prinzipien ist groß und Ihren Kommentar ist ausgezeichnet und ... es sei denn , Vogelgarn Papier nicht hatte einen Verweis in einer Fußnote zu Cache vergesslich Algorithmen hatte en.wikipedia.org/wiki/Cache-oblivious_algorithm (die schön passt in Kategorie Nummer 1, die Sie oben haben) Ich wäre nie auf sie gestoßen. Also ... kennen Sie in Bezug auf jede Kategorie, die Sie oben haben, die drei wichtigsten Dinge, die eine Person wissen sollte?
Dakotah North
@Dakotah: Ich würde nicht einmal anfangen zu Sorgen über Cache - Lokalität , solange ich völlig Scheibe eliminiert hatte I / O, das ist , wo die überwiegende Mehrheit der Zeit verbracht wird in der überwiegenden Mehrzahl der Anwendungen warten. Abgesehen davon, was meinst du mit "Top 3 Dinge, die eine Person wissen sollte"? Top 3 was, wovon wissen?
Aaronaught
Der Sprung von der RAM-Zugriffslatenz (~ 10 ^ -9s) zur Latenz der Magnetplatte (~ 10 ^ -3s Durchschnittsfall) ist noch einige Größenordnungen größer als das 1000-fache. Selbst bei SSDs werden Zugriffszeiten in Hunderten von Mikrosekunden gemessen.
Sedate Alien
@Sedate: Latenz ja, aber dies ist eher eine Frage des Durchsatzes als der unformatierten Latenz. Sobald die Zugriffszeiten überschritten sind und die Gesamtübertragungsgeschwindigkeit erreicht ist, sind die Festplatten nicht mehr so ​​schlecht. Deshalb habe ich zwischen wahlfreiem und sequentiellem Zugriff unterschieden. Bei Szenarien mit wahlfreiem Zugriff tritt in erster Linie ein Problem mit der Latenz auf.
Aaronaught
@Aaronaught: Beim erneuten Lesen nehme ich an, dass Sie richtig liegen. Vielleicht sollte darauf hingewiesen werden, dass alle Datenzugriffe so sequentiell wie möglich sein sollten; Erhebliche Vorteile ergeben sich auch beim Zugriff auf Daten in auftragsbezogener Reihenfolge aus dem RAM.
Sedate Alien
10

Ich denke, die größte Lektion, die Sie daraus lernen können, ist, dass Sie mit den Grundlagen beginnen müssen:

  • Gute Algorithmen, passende Datenstrukturen und nichts "wirklich dummes" machen
  • Gut faktorisierter Code
  • Leistungstest

Während des Leistungstests profilieren Sie Ihren Code, finden die Engpässe und beheben sie nacheinander.

Zu viele Leute springen direkt zum Teil "Nacheinander reparieren". Sie verbringen eine Menge Zeit damit, "benutzerdefinierte Implementierungen der Java-Sammlungen" zu schreiben, weil sie nur wissen, dass der Grund für die Langsamkeit ihres Systems in Cache-Fehlern liegt. Das mag ein Faktor sein, aber wenn Sie direkt daran arbeiten, Code auf niedriger Ebene so zu optimieren, werden Sie wahrscheinlich das größere Problem der Verwendung einer ArrayList verpassen, wenn Sie eine LinkedList verwenden sollten, oder den wahren Grund, warum Ihr System so arbeitet Langsam ist, dass Ihr ORM faul untergeordnete Elemente einer Entität lädt und somit für jede Anforderung 400 separate Fahrten zur Datenbank durchführt.

Adam Jaskiewicz
quelle
7

Ich werde den LMAX-Code nicht sonderlich kommentieren, da ich denke, dass dies reichlich beschrieben ist. Hier sind jedoch einige Beispiele für Dinge, die ich getan habe und die zu erheblichen messbaren Leistungsverbesserungen geführt haben.

Wie immer sind dies Techniken, die angewendet werden sollten, wenn Sie wissen, dass Sie ein Problem haben und die Leistung verbessern müssen - andernfalls führen Sie wahrscheinlich nur eine vorzeitige Optimierung durch.

  • Verwenden Sie die richtige Datenstruktur und erstellen Sie bei Bedarf eine benutzerdefinierte - das korrekte Design der Datenstruktur stellt die Verbesserung in den Schatten, die Sie jemals durch Mikrooptimierungen erzielen werden. Tun Sie dies also zuerst. Wenn Ihr Algorithmus für die Leistung von vielen schnellen Lesevorgängen mit wahlfreiem Zugriff abhängt, stellen Sie sicher, dass Sie über eine Datenstruktur verfügen, die dies unterstützt! Es lohnt sich, durch einige Rahmen zu springen, um dies zu korrigieren, z. B. um einen Weg zu finden, wie Sie Ihre Daten in einem Array darstellen können, um sehr schnelle O (1) -indizierte Lesevorgänge auszunutzen.
  • Die CPU ist schneller als der Speicherzugriff - Sie können eine ganze Menge Berechnungen in der Zeit durchführen, die erforderlich ist, um einen zufälligen Speicher auszulesen, wenn sich der Speicher nicht im L1 / L2-Cache befindet. In der Regel lohnt es sich, eine Berechnung durchzuführen, wenn Sie einen Speicherauszug ersparen.
  • Helfen Sie den JIT - Compiler mit final - machen Felder, Methoden und Klassen endgültig ermöglicht spezifische Optimierungen , die wirklich den JIT - Compiler helfen. Spezifische Beispiele:

    • Der Compiler kann davon ausgehen, dass eine letzte Klasse keine Unterklassen hat, sodass virtuelle Methodenaufrufe in statische Methodenaufrufe umgewandelt werden können
    • Der Compiler kann ein statisches Endfeld als Konstante behandeln, um die Leistung zu verbessern, insbesondere wenn die Konstante dann in Berechnungen verwendet wird, die zur Kompilierungszeit berechnet werden können.
    • Wenn ein Feld, das ein Java-Objekt enthält, als final initialisiert wird, kann das Optimierungsprogramm sowohl die Nullprüfung als auch den Versand der virtuellen Methode eliminieren. Nett.
  • Ersetzen Sie Auflistungsklassen durch Arrays - dies führt zu weniger lesbarem Code und ist schwieriger zu warten, ist jedoch fast immer schneller, da eine Ebene der Indirektion entfernt wird und viele nützliche Optimierungen für den Arrayzugriff vorgenommen werden. In der Regel eine gute Idee in inneren Schleifen / leistungsempfindlichem Code, nachdem Sie ihn als Engpass identifiziert haben. Vermeiden Sie dies jedoch aus Gründen der Lesbarkeit!

  • Verwenden Sie nach Möglichkeit Grundelemente. Grundelemente sind grundsätzlich schneller als ihre objektbasierten Entsprechungen. Insbesondere das Boxen erhöht den Overhead erheblich und kann zu unangenehmen GC-Pausen führen. Lassen Sie keine Grundelemente in ein Kästchen zu, wenn Sie Wert auf Leistung / Latenz legen.

  • Low-Level-Verriegelung minimieren - Schlösser sind auf niedrigem Niveau sehr teuer. Suchen Sie nach Möglichkeiten, um Sperren entweder vollständig zu vermeiden oder grobkörnig zu sperren, sodass Sie nur selten große Datenblöcke sperren müssen und der Code auf niedriger Ebene fortgesetzt werden kann, ohne sich um Sperren oder Parallelitätsprobleme kümmern zu müssen.

  • Vermeiden Sie das Zuweisen von Speicher - dies kann Sie insgesamt tatsächlich verlangsamen, da die Garbage Collection von JVM unglaublich effizient ist. Dies ist jedoch sehr hilfreich, wenn Sie versuchen, eine extrem niedrige Latenz zu erreichen und GC-Pausen zu minimieren. Es gibt spezielle Datenstrukturen, mit denen Sie Zuordnungen vermeiden können - insbesondere die Bibliothek http://javolution.org/ ist exzellent und für diese bemerkenswert.
mikera
quelle
Ich bin nicht damit einverstanden , Methoden endgültig zu machen . Die JIT kann herausfinden, dass eine Methode niemals überschrieben wird. Wenn eine Unterklasse später geladen wird, kann die Optimierung rückgängig gemacht werden. Beachten Sie auch, dass "Vermeiden Sie die Zuweisung von Speicher" die Arbeit des GC möglicherweise erschwert und Sie dadurch verlangsamt. Gehen Sie daher vorsichtig vor.
Maaartinus
@maaartinus: In Bezug auf finaleinige JITs könnte es herausfinden, andere könnten es nicht. Es hängt von der Implementierung ab (ebenso wie viele Tipps zur Leistungsoptimierung). Vereinbaren Sie die Zuteilungen - Sie müssen dies benchmarken. Normalerweise habe ich festgestellt, dass es besser ist, Zuordnungen zu entfernen, aber YMMV.
Mikera
4

Anders als bereits in einer hervorragenden Antwort von Aaronaught erwähnt, möchte ich darauf hinweisen, dass es schwierig sein kann, solchen Code zu entwickeln, zu verstehen und zu debuggen. "Während es sehr effizient ist ... ist es sehr einfach, Fehler zu machen ...", wie einer ihrer Kollegen im LMAX-Blog erwähnte .

  • Für einen Entwickler, der an traditionelle Abfragen und Sperren gewöhnt ist , könnte sich das Codieren eines neuen Ansatzes wie das Reiten auf einem wilden Pferd anfühlen. Zumindest war das meine eigene Erfahrung, als ich mit Phaser experimentierte, welches Konzept in LMAX Technical Paper erwähnt wird. In diesem Sinne würde ich sagen, dass dieser Ansatz Sperrenkonflikte gegen Entwicklerhirnkonflikte tauscht .

Vor diesem Hintergrund denke ich, dass diejenigen, die Disruptor und ähnliche Ansätze wählen, besser sicherstellen, dass sie über ausreichende Entwicklungsressourcen verfügen, um ihre Lösung aufrechtzuerhalten .

Insgesamt erscheint mir der Disruptor-Ansatz recht vielversprechend. Auch wenn Ihr Unternehmen es sich nicht leisten kann, es beispielsweise aus den oben genannten Gründen zu nutzen, sollten Sie Ihr Management davon überzeugen, einige Anstrengungen in das Studium zu "investieren" (und SEDA im Allgemeinen) - denn wenn dies nicht der Fall ist, besteht die Möglichkeit, dass dies eines Tages geschieht Ihre Kunden werden sich für eine wettbewerbsfähigere Lösung entscheiden, die 4x, 8x usw. weniger Server erfordert.

Mücke
quelle