Skalierungsspiel! zu Tausenden von gleichzeitigen Anfragen

Veröffentlicht: 2022-03-11

Scala-Webentwickler berücksichtigen oft nicht die Folgen, wenn Tausende von Benutzern gleichzeitig auf unsere Anwendungen zugreifen. Vielleicht liegt es daran, dass wir gerne schnell Prototypen erstellen; Vielleicht liegt es daran, dass das Testen solcher Szenarien einfach schwierig ist .

Unabhängig davon werde ich argumentieren, dass das Ignorieren der Skalierbarkeit nicht so schlimm ist, wie es klingt – wenn Sie die richtigen Tools verwenden und gute Entwicklungspraktiken befolgen.

Skalierbarkeit zu ignorieren ist nicht so schlimm, wie es sich anhört – wenn Sie die richtigen Tools verwenden.

Lojinha und das Spiel! Rahmen

Vor einiger Zeit startete ich ein Projekt namens Lojinha (was auf Portugiesisch „kleiner Laden“ bedeutet), mein Versuch, eine Auktionsseite aufzubauen. (Dieses Projekt ist übrigens Open Source). Meine Beweggründe waren folgende:

  • Ich wollte wirklich ein paar alte Sachen verkaufen, die ich nicht mehr benutze.
  • Ich mag keine traditionellen Auktionsseiten, besonders nicht die, die wir hier in Brasilien haben.
  • Ich wollte mit dem Play! Framework 2 (Wortspiel beabsichtigt).

Also entschied ich mich, wie oben erwähnt, offensichtlich für die Verwendung des Play! Rahmen. Ich weiß nicht genau, wie lange der Aufbau gedauert hat, aber es dauerte sicherlich nicht lange, bis ich meine Website mit dem einfachen System unter http://lojinha.jcranky.com zum Laufen gebracht hatte. Tatsächlich verbrachte ich mindestens die Hälfte der Entwicklungszeit mit dem Design, das Twitter Bootstrap verwendet (zur Erinnerung: Ich bin kein Designer…).

Der obige Absatz sollte zumindest eines klarstellen: Ich habe mir bei der Erstellung von Lojinha nicht allzu viele Gedanken über die Leistung gemacht, wenn überhaupt.

Und genau das ist mein Punkt: Es liegt in der Verwendung der richtigen Tools – Tools, die Sie auf dem richtigen Weg halten, Tools, die Sie ermutigen, die besten Entwicklungspraktiken durch ihre Konstruktion zu befolgen.

In diesem Fall sind diese Tools Play! Framework und der Scala-Sprache, wobei Akka einige „Gastauftritte“ absolvierte.

Lassen Sie mich Ihnen zeigen, was ich meine.

Unveränderlichkeit und Caching

Es ist allgemein anerkannt, dass die Minimierung von Mutabilität eine gute Praxis ist. Kurz gesagt, Veränderlichkeit macht es schwieriger, über Ihren Code nachzudenken, insbesondere wenn Sie versuchen, Parallelität oder Nebenläufigkeit einzuführen.

Das Spiel! Das Scala-Framework sorgt dafür, dass Sie einen Großteil der Zeit Unveränderlichkeit verwenden, ebenso wie die Scala-Sprache selbst. Beispielsweise ist das von einem Controller generierte Ergebnis unveränderlich. Manchmal mag man diese Unveränderlichkeit als „lästig“ oder „ärgerlich“ empfinden, aber diese „guten Praktiken“ sind aus einem bestimmten Grund „gut“.

In diesem Fall war die Unveränderlichkeit des Controllers absolut entscheidend, als ich mich schließlich entschied, einige Leistungstests durchzuführen: Ich entdeckte einen Engpass und um ihn zu beheben, habe ich diese unveränderliche Antwort einfach zwischengespeichert.

Mit Caching meine ich das Speichern des Response-Objekts und das Bereitstellen einer identischen Instanz für alle neuen Clients. Dies befreit den Server davon, das Ergebnis noch einmal neu berechnen zu müssen. Es wäre nicht möglich, dieselbe Antwort mehreren Clients zu übermitteln, wenn dieses Ergebnis veränderlich wäre.

Der Nachteil: Für einen kurzen Zeitraum (die Ablaufzeit des Caches) können Clients veraltete Informationen erhalten. Dies ist nur in Szenarien ein Problem, in denen der Client unbedingt auf die neuesten Daten zugreifen muss, ohne Verzögerungen zu tolerieren.

Als Referenz hier der Scala-Code zum Laden der Startseite mit einer Produktliste ohne Caching:

 def index = Action { implicit request => Ok(html.index(body = html.body(Items.itemsHigherBids(itemDAO.all(false))), menu = mainMenu)) }

Jetzt den Cache hinzufügen:

 def index = Cached("index", 5) { Action { implicit request => Ok(html.index(body = html.body(Items.itemsHigherBids(itemDAO.all(false))), menu = mainMenu)) } }

Ganz einfach, oder? Hier ist „Index“ der Schlüssel, der im Cache-System verwendet werden soll, und 5 ist die Ablaufzeit in Sekunden.

Nach dem Caching stieg der Durchsatz auf 800 Anfragen pro Sekunde. Das ist eine mehr als vierfache Verbesserung für weniger als zwei Codezeilen.

Um die Auswirkung dieser Änderung zu testen, habe ich einige JMeter-Tests (im GitHub-Repo enthalten) lokal ausgeführt. Vor dem Hinzufügen des Caches erreichte ich einen Durchsatz von ungefähr 180 Anfragen pro Sekunde. Nach dem Caching stieg der Durchsatz auf 800 Anfragen pro Sekunde. Das ist eine mehr als vierfache Verbesserung für weniger als zwei Codezeilen.

So habe ich Play! Cache, um die Leistung auf meiner Scala-Auktionsseite zu verbessern.

Speicherverbrauch

Ein weiterer Bereich, in dem die richtigen Scala-Tools einen großen Unterschied machen können, ist der Speicherverbrauch. Auch hier wieder Spielen! bringt Sie in die richtige (skalierbare) Richtung. In der Java-Welt ist es für eine „normale“ Webanwendung, die mit der Servlet-API geschrieben wurde (dh fast jedes Java- oder Scala-Framework da draußen), sehr verlockend, viel Müll in die Benutzersitzung zu stecken, weil die API einfach zu bedienende Rufen Sie Methoden auf, die Ihnen dies ermöglichen:

 session.setAttribute("attrName", attrValue);

Da es so einfach ist, Informationen zur Benutzersitzung hinzuzufügen, wird dies häufig missbraucht. Dementsprechend hoch ist das Risiko, möglicherweise ohne triftigen Grund zu viel Speicherplatz zu verbrauchen.

Mit dem Spiel! Framework ist dies keine Option – das Framework hat einfach keinen serverseitigen Sitzungsraum. Das Spiel! Framework-Benutzersitzung wird in einem Browser-Cookie gespeichert, und Sie müssen damit leben. Das bedeutet, dass der Sitzungsraum in Größe und Typ begrenzt ist: Sie können nur Zeichenfolgen speichern. Wenn Sie Objekte speichern müssen, müssen Sie den zuvor besprochenen Caching-Mechanismus verwenden. Beispielsweise möchten Sie möglicherweise die E-Mail-Adresse oder den Benutzernamen des aktuellen Benutzers in der Sitzung speichern, aber Sie müssen den Cache verwenden, wenn Sie ein vollständiges Benutzerobjekt aus Ihrem Domänenmodell speichern müssen.

Spiel! hält Sie auf dem richtigen Weg und zwingt Sie, Ihre Speichernutzung sorgfältig zu prüfen, was einen First-Pass-Code erzeugt, der praktisch Cluster-fähig ist.

Auch dies mag zunächst wie ein Schmerz erscheinen, aber in Wahrheit ist Play! hält Sie auf dem richtigen Weg und zwingt Sie dazu, Ihre Speichernutzung sorgfältig zu prüfen, was zu einem First-Pass-Code führt, der praktisch Cluster-fähig ist – insbesondere angesichts der Tatsache, dass es keine serverseitige Sitzung gibt, die über Ihren gesamten Cluster weitergegeben werden müsste, was das Leben erleichtert unendlich einfacher.

Async-Unterstützung

Weiter in diesem Spiel! Framework Review, werden wir untersuchen, wie Play! glänzt auch in der asynchronen (chronous) Unterstützung. Und über seine nativen Funktionen hinaus bietet Play! ermöglicht Ihnen die Einbettung von Akka, einem leistungsstarken Tool für die asynchrone Verarbeitung.

Obwohl Lojinha noch nicht alle Vorteile von Akka ausschöpft, ist die einfache Integration mit Play! ganz einfach gemacht:

  1. Planen Sie einen asynchronen E-Mail-Dienst.
  2. Bearbeiten Sie Angebote für verschiedene Produkte gleichzeitig.

Kurz gesagt, Akka ist eine Implementierung des Akteurmodells, das durch Erlang berühmt wurde. Wenn Sie mit dem Akka Actor Model nicht vertraut sind, stellen Sie es sich einfach als eine kleine Einheit vor, die nur durch Nachrichten kommuniziert.

Um eine E-Mail asynchron zu versenden, erstelle ich zuerst die richtige Nachricht und den richtigen Akteur. Dann muss ich nur noch so etwas tun:

 EMail.actor ! BidToppedMessage(item.name, itemUrl, bidderEmail)

Die E-Mail-Sendelogik ist im Akteur implementiert, und die Nachricht teilt dem Akteur mit, welche E-Mail wir senden möchten. Dies geschieht in einem Fire-and-Forget-Schema, was bedeutet, dass die obige Zeile die Anfrage sendet und dann mit der Ausführung fortfährt, was auch immer wir danach haben (dh sie blockiert nicht).

Weitere Informationen zum nativen Async von Play! finden Sie in der offiziellen Dokumentation.

Fazit

Zusammenfassend: Ich habe schnell eine kleine Anwendung entwickelt, Lojinha, die sich sehr gut skalieren lässt. Wenn ich auf Probleme stieß oder Engpässe entdeckte, waren die Behebungen schnell und einfach, mit viel Anerkennung aufgrund der von mir verwendeten Tools (Play!, Scala, Akka usw.), was mich dazu veranlasste, Best Practices in Bezug auf Effizienz und Effizienz zu befolgen Skalierbarkeit. Ohne Rücksicht auf die Leistung konnte ich auf Tausende gleichzeitige Anforderungen skalieren.

Berücksichtigen Sie bei der Entwicklung Ihrer nächsten Anwendung Ihre Tools sorgfältig.

Siehe auch : Boilerplate-Code mit Scala-Makros und Quasiquotes reduzieren