Berücksichtigen Sie bei der Optimierung der Leistung einer Anwendung die Verwendung von NDB. Wenn eine Anwendung beispielsweise einen Wert liest, der sich nicht im Cache befindet, dauert der Lesevorgang länger. Sie können Ihre Anwendung möglicherweise beschleunigen, indem Sie Datenspeicheraktionen parallel zu anderen Aufgaben oder anderen Datenspeicheraktionen ausführen.
Die NDB-Clientbibliothek bietet viele asynchrone ("async") Funktionen.
Mit jeder dieser Funktionen kann eine Anwendung eine Anfrage an den Datenspeicher senden. Die Funktion gibt sofort ein Future
-Objekt zurück. Während der Datenspeicher die Anfrage verarbeitet, kann die Anwendung andere Aufgaben ausführen.
Im Anschluss an die Verarbeitung der Anfrage durch den Datenspeicher können die Ergebnisse durch die Anwendung vom Future
-Objekt abgerufen werden.
Einführung
Angenommen, einer der Anfrage-Handler Ihrer Anwendung benötigt NDB für einen Schreibvorgang, beispielsweise zum Aufzeichnen der Anfrage. Darüber hinaus hat er weitere NDB-Vorgänge auszuführen, wie etwa das Abrufen von Daten.
Wenn der Aufruf von put()
durch einen Aufruf des asynchronen Äquivalents put_async()
ersetzt wird, kann die Anwendung andere Aufgaben sofort ausführen und wird nicht durch put()
blockiert.
Auf diese Weise lassen sich die anderen NDB-Funktionen und das Vorlagen-Rendering ausführen, während der Datenspeicher die Daten schreibt. Die Anwendung blockiert den Datenspeicher erst, wenn sie Daten daraus abruft.
In diesem Beispiel ist es nicht sinnvoll, future.get_result
aufzurufen, da die Anwendung nie das NDB-Ergebnis verwendet. Dieser Code sorgt lediglich dafür, dass der Anfrage-Handler erst beendet wird, nachdem der NDB-Vorgang put
beendet wurde. Andernfalls kann "put" unter Umständen nicht ausgeführt werden. Sie können den Anfrage-Handler der Einfachheit halber @ndb.toplevel
hinzufügen. Dadurch wird der Handler angewiesen, die Aktion erst nach Abschluss der asynchronen Anfragen zu beenden. Sie können somit die Anfrage senden, ohne sich Gedanken um das Ergebnis machen zu müssen.
Sie können eine vollständige WSGIApplication
als ndb.toplevel
angeben. Dadurch können alle Handler der WSGIApplication
vor der Rückgabe auf den Abschluss aller asynchronen Anfragen warten.
(Die Handler der WSGIApplication
werden dadurch nicht als "toplevel" behandelt.)
Die Verwendung einer toplevel
-Anwendung ist unkomplizierter als all ihre Handler-Funktionen. Wenn eine Handler-Methode jedoch yield
verwendet, muss diese Methode trotzdem in einen anderen Decorator @ndb.synctasklet
eingebunden werden. Andernfalls wird die Ausführung bei yield
gestoppt und nicht beendet.
Asynchrone APIs und Future-Objekte verwenden
Fast jede synchrone NDB-Funktion hat ein _async
-Gegenstück. Bei put()
ist dies beispielsweise put_async()
.
Die Argumente der asynchronen Funktion sind immer mit denen der synchronen Version identisch.
Der Rückgabewert einer asynchronen Methode ist immer entweder ein Future
-Objekt oder (für "Multi"-Funktionen) eine Liste von Future
-Objekten.
Ein Future ist ein Objekt, das den Status während eines gestarteten, jedoch noch nicht abgeschlossenen Vorgangs beibehält. Alle asynchronen APIs geben mindestens ein Objekt vom Typ Futures
zurück.
Sie können die Funktion get_result()
des Future
-Objekts aufrufen, um das Ergebnis des Vorgangs abzufragen. Das Future-Objekt blockiert daraufhin bei Bedarf den Vorgang, bis das Ergebnis verfügbar ist, und gibt dieses anschließend an Sie zurück.
get_result()
gibt den Wert zurück, der von der synchronen Version der API zurückgegeben würde.
Hinweis: Wenn Sie Future-Objekte bereits aus bestimmten anderen Programmiersprachen kennen, sind Sie es möglicherweise gewohnt, ein Future-Objekt direkt als Ergebnis verwenden zu können. Dies funktioniert hier nicht.
Diese Sprachen verwenden implizite Future-Objekte. NDB nutzt hingegen explizite Future-Objekte.
Rufen Sie get_result()
auf, um das Ergebnis eines NDB-Future
-Objekts abzurufen.
Was passiert, wenn der Vorgang eine Ausnahme auslöst? Das hängt davon ab, wann die Ausnahme auftritt. Wenn NDB während der Erstellung einer Anfrage ein Problem feststellt (vielleicht ein Argument vom falschen Typ), löst die Methode _async()
eine Ausnahme aus. Wenn die Ausnahme jedoch vom Datenspeicherserver erkannt wird, gibt die Methode _async()
ein Future
-Objekt zurück und die Ausnahme wird ausgelöst, wenn Ihre Anwendung get_result()
aufruft. Sie müssen sich damit nicht näher befassen, da letztendlich alles recht selbsterklärend ist. Der vielleicht größte Unterschied besteht darin, dass beim Drucken von Rückverfolgungsinformationen Teile der asynchronen Low-Level-Maschinen angezeigt werden.
Angenommen, Sie schreiben eine Gästebuchanwendung. Wenn Nutzer angemeldet sind, soll eine Seite mit den neuesten Gästebuchbeiträgen angezeigt werden. Auch der Alias des Nutzers soll auf der Seite angegeben sein. Die Anwendung benötigt zwei Arten von Informationen: die Kontoinformationen des angemeldeten Nutzers sowie die Inhalte der Gästebuchbeiträge. Die "synchrone" Version dieser Anwendung könnte folgendermaßen aussehen:
Hier gibt es zwei unabhängige E/A-Aktionen: das Abrufen der Account
-Entität und das Abrufen der letzten Guestbook
-Entitäten. Mit der synchronen API werden diese nacheinander ausgeführt. Wir warten zuerst auf den Empfang der Kontoinformationen und rufen anschließend die Gästebuchentitäten ab. Die Anwendung benötigt die Kontoinformationen jedoch nicht sofort. Daher können wir asynchrone APIs nutzen:
Diese Version des Codes erstellt zuerst zwei Futures
(acct_future
und recent_entries_future
) und wartet dann auf diese. Der Server verarbeitet beide Anfragen parallel.
Jeder Aufruf einer _async()
-Funktion erstellt ein Future-Objekt und sendet eine Anfrage an den Datenspeicherserver. Der Server kann sofort mit der Verarbeitung der Anfrage beginnen. Die Serverantworten können in beliebiger Reihenfolge zurückgegeben werden. Die Future-Objektverknüpfung antwortet auf die entsprechenden Anfragen.
Die gesamte (reale) Zeit, die in der asynchronen Version aufgewendet wird, entspricht in etwa der maximalen Zeit für die Vorgänge. Die Gesamtzeit in der synchronen Version übersteigt die Summe der Betriebszeiten. Wenn Sie mehrere Vorgänge parallel ausführen können, sind asynchrone Vorgänge hilfreicher.
Durch die Verwendung von Appstats können Sie ermitteln, wie lange die Abfragen Ihrer Anwendung dauern oder wie viele E/A-Vorgänge pro Anfrage ausgeführt werden. Dieses Tool kann Diagramme ähnlich der obigen Darstellung basierend auf der Instrumentierung einer Live-App anzeigen.
Tasklets verwenden
Ein NDB-Tasklet ist ein Code, der gleichzeitig mit anderem Code ausgeführt werden kann. Wenn Sie ein Tasklet schreiben, kann Ihre Anwendung es ähnlich wie eine asynchrone NDB-Funktion verwenden: Sie ruft das Tasklet auf, das ein Future
-Objekt zurückgibt. Später wird durch Aufrufen der Methode get_result()
des Future
-Objekts das Ergebnis ausgegeben.
Tasklets ermöglichen die gleichzeitige Ausführung von Funktionen, ohne Threads schreiben zu müssen. Tasklets werden von einer Ereignisschleife ausgeführt und können sich selbst mithilfe einer Yield-Anweisung sperren und damit E/A- oder verschiedene andere Vorgänge blockieren. Das Konzept eines blockierenden Vorgangs wird mit der Klasse Future
realisiert. Allerdings kann ein Tasklet auch den yield
eines RPC erzeugen und dann auf dessen Abschluss warten.
Wenn das Tasklet ein Ergebnis aufweist, wird raise
für eine Ausnahme vom Typ ndb.Return
ausgeführt. NDB verknüpft dann das Ergebnis mit dem zuvor für das Future
-Objekt ausgeführten yield
.
Wenn Sie ein NDB-Tasklet schreiben, verwenden Sie yield
und raise
auf ungewöhnliche Weise. Wenn Sie nach Beispielen suchen, wie diese verwendet werden, finden Sie somit möglicherweise keinen Code wie ein NDB-Tasklet.
So wandeln Sie eine Funktion in ein NDB-Tasklet um:
- Dekorieren Sie die Funktion mit
@ndb.tasklet
. - Ersetzen Sie alle synchronen Datenspeicheraufrufe durch
yield
-Objekte asynchroner Datenspeicheranrufe. - Führen Sie
raise ndb.Return(retval)
aus, damit die Funktion ihren Rückgabewert zurückgibt. Dies ist nicht notwendig, wenn die Funktion nichts zurückgibt.
Eine Anwendung kann Tasklets für eine genauere Kontrolle über asynchrone APIs verwenden. Ein Beispiel ist folgendes Schema:
...
Beim Anzeigen einer Nachricht ist es sinnvoll, den Alias des Autors aufzurufen. Die "synchrone" Abrufmethode für die Anzeige einer Liste von Nachrichten könnte folgendermaßen aussehen:
Diese Herangehensweise ist leider ineffizient. Bei der Betrachtung in Appstats sehen Sie, dass die "Get"-Anfragen als Serie vorliegen. Möglicherweise wird das folgende "Treppen"-Muster angezeigt.
Dieser Teil des Programms wäre schneller, wenn diese "Gets" sich überlappen würden.
Sie können den Code neu schreiben, um get_async
zu verwenden. Es ist jedoch schwierig zu verfolgen, welche asynchronen Anfragen und Nachrichten zusammengehören.
Die Anwendung kann ihre eigene "async"-Funktion definieren, indem sie sie in ein Tasklet umwandelt. Dadurch können Sie den Code auf eine weniger verwirrende Weise organisieren.
Außerdem sollte die Funktion acct = yield key.get_async()
anstelle von acct = key.get()
oder acct = key.get_async().get_result()
verwenden.
yield
teilt NDB mit, dass an dieser Stelle dieses Tasklet am besten gesperrt werden soll und dass andere Tasklets ausgeführt werden sollen.
Durch das Dekorieren einer Generatorfunktion mit @ndb.tasklet
wird anstelle eines Generatorobjekts ein Future
-Objekt zurückgegeben. Innerhalb des Tasklets wird bei jedem yield
eines Future
-Objekts auf das Ergebnis des Future
-Objekts gewartet und dieses dann zurückgegeben.
Beispiel:
Beachten Sie, dass das Tasklet-Framework bewirkt, dass der yield
-Ausdruck das Future
-Ergebnis in der Variablen acct
zurückgibt, obwohl get_async()
ein Future
-Objekt zurückgibt.
Die Funktion map()
ruft callback()
mehrmals auf.
Aber yield ..._async()
in callback()
veranlasst den NDB-Planer, viele asynchrone Anfragen zu senden, ohne darauf zu warten, dass eine von ihnen beendet wird.
Bei der Betrachtung in Appstats werden Sie vielleicht überrascht sein, dass sich diese verschiedenen "Gets" nicht nur überlappen, sondern auch alle in derselben Anfrage verarbeitet werden. NDB implementiert einen Autobatcher. Der Autobatcher bündelt mehrere Requests in einem einzelnen Batch-RPC an den Server. Dabei wird so vorgegangen, dass Schlüssel gesammelt werden, solange weitere Aufgaben (möglicherweise ein anderer Callback) ausgeführt werden müssen. Sobald eines der Ergebnisse benötigt wird, sendet der Autobatcher den Batch-RPC. Im Gegensatz zu den meisten Anfragen werden Abfragen nicht "zu Batches zusammengefasst".
Wenn ein Tasklet ausgeführt wird, wird ihm der bei der Erstellung des Tasklets verwendete Standard-Namespace oder der bei der Ausführung des Tasklets geänderte Namespace zugewiesen. Der Standard-Namespace ist somit weder mit dem Kontext verknüpft noch wird er darin gespeichert. Außerdem wirkt sich eine Änderung des Standard-Namespace in einem Tasklet nicht auf den Standard-Namespace in anderen Tasklets aus, ausgenommen diejenigen, die von ihm erzeugt wurden.
Tasklets, parallele Abfragen, paralleles Ergebnis
Sie können Tasklets verwenden, sodass mehrere Abfragen Datensätze gleichzeitig abrufen. Angenommen, Ihre Anwendung verfügt über eine Seite, die den Inhalt eines Einkaufswagens und eine Liste mit Sonderangeboten anzeigt. Das Schema könnte folgendermaßen aussehen:
Eine "synchrone" Funktion, die Einkaufswagenartikel und Sonderangebote abruft, könnte in etwa folgendermaßen aussehen:
In diesem Beispiel werden Abfragen zum Abrufen von Listen mit Einkaufswagenartikeln und Angeboten verwendet. Anschließend werden Details zu Inventarelementen mit get_multi()
abgerufen.
(Diese Funktion verwendet den Rückgabewert von get_multi()
nicht direkt. Sie ruft get_multi()
auf, um alle Inventardetails in den Cache abzurufen, damit sie später schnell gelesen werden können.) get_multi
kombiniert viele "Gets" zu einer Anfrage. Die Abrufvorgänge für Abfragen werden jedoch nacheinander ausgeführt. Überlappen Sie die beiden Abfragen, damit diese Abrufvorgänge gleichzeitig ausgeführt werden:
Der get_multi()
-Aufruf erfolgt weiterhin getrennt: Er hängt von den Abfrageergebnissen ab, sodass Sie ihn nicht mit den Abfragen kombinieren können.
Angenommen, diese Anwendung benötigt manchmal den Einkaufswagen, manchmal die Angebote und manchmal beides. Sie möchten Ihren Code so organisieren, dass eine Funktion zum Abrufen des Einkaufswagens und eine Funktion zum Abrufen der Angebote verfügbar ist. Wenn Ihre Anwendung diese Funktionen zusammen aufruft, könnten sich ihre Abfragen idealerweise "überlappen". Wandeln Sie dazu Ihre Funktionen in Tasklets um:
yield x, y
ist wichtig, aber leicht zu übersehen. Wenn es sich um zwei getrennte yield
-Anweisungen gehandelt hätte, würden sie in Serie ausgeführt. Aber die yield
-Anweisung eines Tupels von Tasklets ist eine parallele Ausgabe: Die Tasklets können parallel ausgeführt werden. yield
wartet, bis alle von ihnen beendet wurden und gibt die Ergebnisse zurück. (In einigen Programmiersprachen wird dies als Barriere bezeichnet.)
Wenn Sie einen Code in ein Tasklet umwandeln, ist es wahrscheinlich, dass Sie diesen Vorgang bald häufiger ausführen möchten. Wenn Sie "synchronen" Code bemerken, der parallel zu einem Tasklet ausgeführt werden könnte, ist es wahrscheinlich eine gute Idee, diesen ebenfalls in ein Tasklet umzuwandeln.
Dann können Sie es mit einem parallelen yield
parallelisieren.
Wenn Sie eine Anfragefunktion (eine Webapp2-Anfragefunktion, eine Django-Ansichtsfunktion usw.) als Tasklet schreiben, verhält es sich nicht entsprechend Ihren Erwartungen: Es liefert Ergebnisse, beendet dann jedoch die Ausführung. In diesem Fall dekorieren Sie die Funktion mit @ndb.synctasklet
.
@ndb.synctasklet
ähnelt @ndb.tasklet
, ruft aber get_result()
für das Tasklet auf.
Dadurch wird Ihr Tasklet zu einer Funktion, die das Ergebnis wie gewohnt zurückgibt.
Abfrage-Iteratoren in Tasklets
Verwenden Sie das folgende Muster, um Abfrageergebnisse in einem Tasklet zu durchlaufen:
Dies ist das Tasklet-freundliche Äquivalent zu Folgendem:
Die drei fett gedruckten Zeilen in der ersten Version sind das Tasklet-freundliche Äquivalent der einzelnen fett gedruckten Zeile in der zweiten Version.
Tasklets können nur an einem yield
-Schlüsselwort gesperrt werden.
Die for-Schleife ohne yield
verhindert die Ausführung anderer Tasklets.
Sie werden sich vielleicht fragen, warum dieser Code überhaupt einen Abfrage-Iterator verwendet, anstatt alle Entitäten mit qry.fetch_async()
abzurufen.
Die Anwendung enthält möglicherweise so viele Entitäten, dass sie nicht in den Arbeitsspeicher passen.
Vielleicht suchen Sie nach einer Entität und können die Iteration beenden, sobald Sie sie gefunden haben. Sie können Ihre Suchkriterien aber nicht nur mit der Abfragesprache ausdrücken. Sie könnten einen Iterator verwenden, um zu überprüfende Entitäten zu laden, und dann aus der Schleife ausbrechen, wenn Sie gefunden haben, wonach Sie suchen.
Asynchrones Urlfetch mit NDB
Ein NDB-Context
hat eine asynchrone urlfetch()
-Funktion, die gut mit NDB-Tasklets parallelisiert werden kann, z. B.:
Der URL-Abrufdienst verfügt über eine eigene asynchrone Anfrage-API. Er ist hilfreich, kann jedoch nicht immer einfach mit NDB-Tasklets verwendet werden.
Asynchrone Transaktionen verwenden
Transaktionen können auch asynchron durchgeführt werden. Sie können eine vorhandene Funktion an ndb.transaction_async()
übergeben oder den Dekorator @ndb.transactional_async
verwenden.
Wie bei den anderen asynchronen Funktionen wird hierbei ein NDB-Future
zurückgegeben:
Transaktionen funktionieren auch mit Tasklets. Beispielsweise könnten Sie den update_counter
-Code in yield
ändern, während Sie auf das Sperren von RPCs warten:
Future.wait_any() verwenden
Manchmal möchten Sie vielleicht mehrere asynchrone Anfragen erstellen und immer dann zurückgeben, wenn die erste abgeschlossen ist.
Dazu können Sie die Klassenmethode ndb.Future.wait_any()
verwenden:
Leider gibt es keine einfache Möglichkeit, dies in ein Tasklet umzuwandeln. Eine parallele yield
-Anweisung wartet, bis alle Future
-Objekte abgeschlossen sind – darunter auch jene, auf die Sie nicht warten möchten.