Memcached
 sql >> Datenbank >  >> NoSQL >> Memcached

Python + Memcached:Effizientes Caching in verteilten Anwendungen

Beim Schreiben von Python-Anwendungen ist Caching wichtig. Die Verwendung eines Caches, um die Neuberechnung von Daten oder den Zugriff auf eine langsame Datenbank zu vermeiden, kann Ihnen einen großen Leistungsschub verschaffen.

Python bietet eingebaute Caching-Möglichkeiten, von einem einfachen Wörterbuch bis hin zu einer vollständigeren Datenstruktur wie functools.lru_cache . Letzteres kann jedes Element unter Verwendung eines Least-Recently Used-Algorithmus zwischenspeichern, um die Cache-Größe zu begrenzen.

Diese Datenstrukturen sind jedoch per Definition lokal zu Ihrem Python-Prozess. Wenn mehrere Kopien Ihrer Anwendung auf einer großen Plattform ausgeführt werden, lässt die Verwendung einer In-Memory-Datenstruktur die gemeinsame Nutzung des zwischengespeicherten Inhalts nicht zu. Dies kann bei großen und verteilten Anwendungen ein Problem darstellen.

Wenn ein System über ein Netzwerk verteilt ist, benötigt es daher auch einen Cache, der über ein Netzwerk verteilt ist. Heutzutage gibt es viele Netzwerkserver, die Caching-Funktionen bieten – wir haben bereits behandelt, wie man Redis für das Caching mit Django verwendet.

Wie Sie in diesem Tutorial sehen werden, ist Memcached eine weitere großartige Option für das verteilte Caching. Nach einer kurzen Einführung in die grundlegende Memcache-Nutzung lernen Sie fortgeschrittene Muster wie „Cache and Set“ und die Verwendung von Fallback-Caches kennen, um Leistungsprobleme im Cold-Cache zu vermeiden.


Memcache installieren

Memcache ist für viele Plattformen verfügbar:

  • Wenn Sie Linux ausführen , können Sie es mit apt-get install memcached installieren oder yum install memcached . Dadurch wird Memcached aus einem vorgefertigten Paket installiert, aber Sie können Memcached auch aus dem Quellcode erstellen, wie hier erklärt.
  • Für macOS , die Verwendung von Homebrew ist die einfachste Option. Führen Sie einfach brew install memcached aus nachdem Sie den Homebrew-Paketmanager installiert haben.
  • Unter Windows , müssten Sie memcached selbst kompilieren oder vorkompilierte Binärdateien finden.

Einmal installiert, memcached kann einfach durch den Aufruf von memcached gestartet werden Befehl:

$ memcached

Bevor Sie mit Memcached aus Python-Land interagieren können, müssen Sie einen Memcached-Client installieren Bücherei. Wie das geht, erfahren Sie im nächsten Abschnitt, zusammen mit einigen grundlegenden Cache-Zugriffsvorgängen.



Speichern und Abrufen von zwischengespeicherten Werten mit Python

Wenn Sie memcached noch nie verwendet haben , es ist ziemlich einfach zu verstehen. Es bietet im Grunde ein riesiges netzwerkverfügbares Wörterbuch. Dieses Wörterbuch hat einige Eigenschaften, die sich von einem klassischen Python-Wörterbuch unterscheiden, hauptsächlich:

  • Schlüssel und Werte müssen Bytes sein
  • Schlüssel und Werte werden nach einer Ablaufzeit automatisch gelöscht

Daher sind die beiden grundlegenden Operationen für die Interaktion mit memcached sind set und get . Wie Sie vielleicht schon erraten haben, werden sie verwendet, um einem Schlüssel einen Wert zuzuweisen bzw. um einen Wert von einem Schlüssel zu erhalten.

Meine bevorzugte Python-Bibliothek für die Interaktion mit memcached ist pymemcache – Ich empfehle die Verwendung. Sie können es einfach mit pip:

installieren
$ pip install pymemcache

Der folgende Code zeigt, wie Sie sich mit memcached verbinden können und verwenden Sie ihn als über das Netzwerk verteilten Cache in Ihren Python-Anwendungen:

>>> from pymemcache.client import base

# Don't forget to run `memcached' before running this next line:
>>> client = base.Client(('localhost', 11211))

# Once the client is instantiated, you can access the cache:
>>> client.set('some_key', 'some value')

# Retrieve previously set data again:
>>> client.get('some_key')
'some value'

speichern Das Netzwerkprotokoll ist wirklich einfach und seine Implementierung extrem schnell, was es nützlich macht, Daten zu speichern, die sonst nur langsam aus der kanonischen Datenquelle abgerufen oder erneut berechnet werden könnten:

Dieses Beispiel ist zwar einfach genug, ermöglicht jedoch das Speichern von Schlüssel/Wert-Tupeln im gesamten Netzwerk und den Zugriff darauf über mehrere verteilte, laufende Kopien Ihrer Anwendung. Dies ist simpel, aber dennoch mächtig. Und es ist ein großartiger erster Schritt zur Optimierung Ihrer Anwendung.



Automatisch ablaufende zwischengespeicherte Daten

Beim Speichern von Daten in memcached , können Sie eine Ablaufzeit festlegen – eine maximale Anzahl von Sekunden für memcached um den Schlüssel und den Wert zu behalten. Nach dieser Verzögerung memcached entfernt den Schlüssel automatisch aus seinem Cache.

Auf was sollten Sie diese Cache-Zeit einstellen? Es gibt keine magische Zahl für diese Verzögerung, und sie hängt vollständig von der Art der Daten und der Anwendung ab, mit der Sie arbeiten. Das können ein paar Sekunden oder ein paar Stunden sein.

Cache-Invalidierung , die definiert, wann der Cache entfernt werden soll, weil er nicht mit den aktuellen Daten synchron ist, muss Ihre Anwendung ebenfalls verarbeiten. Vor allem, wenn zu alte oder veraltete Daten präsentiert werden ist zu vermeiden.

Auch hier gibt es kein magisches Rezept; es hängt von der Art der Anwendung ab, die Sie erstellen. Es gibt jedoch einige Sonderfälle, die behandelt werden sollten – die wir im obigen Beispiel noch nicht behandelt haben.

Ein Caching-Server kann nicht unendlich wachsen – Arbeitsspeicher ist eine endliche Ressource. Daher werden Schlüssel vom Caching-Server gelöscht, sobald er mehr Platz zum Speichern anderer Dinge benötigt.

Einige Schlüssel sind möglicherweise auch abgelaufen, weil sie ihre Ablaufzeit erreicht haben (manchmal auch als „Lebensdauer“ oder TTL bezeichnet). In diesen Fällen gehen die Daten verloren und die kanonische Datenquelle muss erneut abgefragt werden.

Das hört sich komplizierter an, als es wirklich ist. Sie können im Allgemeinen mit dem folgenden Muster arbeiten, wenn Sie mit memcached in Python arbeiten:

from pymemcache.client import base


def do_some_query():
    # Replace with actual querying code to a database,
    # a remote REST API, etc.
    return 42


# Don't forget to run `memcached' before running this code
client = base.Client(('localhost', 11211))
result = client.get('some_key')

if result is None:
    # The cache is empty, need to get the value
    # from the canonical source:
    result = do_some_query()

    # Cache the result for next time:
    client.set('some_key', result)

# Whether we needed to update the cache or not,
# at this point you can work with the data
# stored in the `result` variable:
print(result)

Hinweis: Der Umgang mit fehlenden Schlüsseln ist wegen normaler Ausspülvorgänge obligatorisch. Es ist auch obligatorisch, das Cold-Cache-Szenario zu handhaben, dh wenn memcached wurde gerade gestartet. In diesem Fall ist der Cache vollständig leer und der Cache muss vollständig neu gefüllt werden, eine Anforderung nach der anderen.

Das bedeutet, dass Sie alle zwischengespeicherten Daten als ephemer betrachten sollten. Und Sie sollten niemals erwarten, dass der Cache einen Wert enthält, den Sie zuvor in ihn geschrieben haben.



Aufwärmen eines kalten Caches

Einige der Cold-Cache-Szenarien können nicht verhindert werden, zum Beispiel ein memcached Absturz. Aber einige können es, zum Beispiel die Migration zu einem neuen memcached Server.

Wenn vorhersehbar ist, dass ein Cold-Cache-Szenario eintritt, ist es besser, es zu vermeiden. Ein Cache, der nachgefüllt werden muss, bedeutet, dass der kanonische Speicher der zwischengespeicherten Daten plötzlich massiv von allen Cache-Benutzern getroffen wird, denen Cache-Daten fehlen (auch bekannt als das Problem der Donnerherde).

pymemcache stellt eine Klasse namens FallbackClient bereit das hilft bei der Implementierung dieses Szenarios, wie hier gezeigt:

from pymemcache.client import base
from pymemcache import fallback


def do_some_query():
    # Replace with actual querying code to a database,
    # a remote REST API, etc.
    return 42


# Set `ignore_exc=True` so it is possible to shut down
# the old cache before removing its usage from 
# the program, if ever necessary.
old_cache = base.Client(('localhost', 11211), ignore_exc=True)
new_cache = base.Client(('localhost', 11212))

client = fallback.FallbackClient((new_cache, old_cache))

result = client.get('some_key')

if result is None:
    # The cache is empty, need to get the value 
    # from the canonical source:
    result = do_some_query()

    # Cache the result for next time:
    client.set('some_key', result)

print(result)

Der FallbackClient fragt den alten Cache ab, der an seinen Konstruktor übergeben wurde, und respektiert die Reihenfolge. In diesem Fall wird der neue Cache-Server immer zuerst abgefragt, und im Falle eines Cache-Miss wird der alte abgefragt – wodurch ein möglicher Rückweg zur primären Datenquelle vermieden wird.

Wenn ein Schlüssel gesetzt ist, wird er nur auf den neuen Cache gesetzt. Nach einiger Zeit kann der alte Cache außer Betrieb genommen und der FallbackClient kann direkt durch new_cache ersetzt werden Kunde.



Überprüfen und einstellen

Bei der Kommunikation mit einem Remote-Cache tritt das übliche Parallelitätsproblem wieder auf:Es können mehrere Clients gleichzeitig versuchen, auf denselben Schlüssel zuzugreifen. speichern bietet ein check and set Operation, abgekürzt zu CAS , was hilft, dieses Problem zu lösen.

Das einfachste Beispiel ist eine Anwendung, die die Anzahl der Benutzer zählen möchte, die sie hat. Jedes Mal, wenn ein Besucher eine Verbindung herstellt, wird ein Zähler um 1 erhöht. Verwendung von memcached , wäre eine einfache Implementierung:

def on_visit(client):
    result = client.get('visitors')
    if result is None:
        result = 1
    else:
        result += 1
    client.set('visitors', result)

Was passiert jedoch, wenn zwei Instanzen der Anwendung versuchen, diesen Zähler gleichzeitig zu aktualisieren?

Der erste Aufruf client.get('visitors') wird für beide die gleiche Anzahl von Besuchern zurückgeben, sagen wir, es sind 42. Dann addieren beide 1, berechnen 43 und setzen die Anzahl der Besucher auf 43. Diese Zahl ist falsch, und das Ergebnis sollte 44 sein, also 42 + 1 + 1.

Um dieses Parallelitätsproblem zu lösen, wird der CAS-Vorgang von memcached ist praktisch. Das folgende Snippet implementiert eine korrekte Lösung:

def on_visit(client):
    while True:
        result, cas = client.gets('visitors')
        if result is None:
            result = 1
        else:
            result += 1
        if client.cas('visitors', result, cas):
            break

Der gets -Methode gibt den Wert zurück, genau wie get -Methode, gibt aber auch einen CAS-Wert zurück .

Was in diesem Wert steht, ist nicht relevant, wird aber für die nächste Methode cas verwendet Forderung. Diese Methode entspricht dem set -Operation, außer dass sie fehlschlägt, wenn sich der Wert seit gets geändert hat Betrieb. Im Erfolgsfall wird die Schleife unterbrochen. Andernfalls wird die Operation von Anfang an neu gestartet.

In dem Szenario, in dem zwei Instanzen der Anwendung gleichzeitig versuchen, den Zähler zu aktualisieren, gelingt es nur einer, den Zähler von 42 auf 43 zu verschieben. Die zweite Instanz erhält ein False Wert, der von client.cas zurückgegeben wird aufrufen und die Schleife wiederholen müssen. Diesmal wird es 43 als Wert abrufen, es auf 44 erhöhen, und seine cas Der Anruf wird erfolgreich sein und somit unser Problem lösen.

Das Erhöhen eines Zählers ist als Beispiel interessant, um zu erklären, wie CAS funktioniert, weil es einfach ist. Allerdings memcached liefert auch den incr und decr Methoden zum Inkrementieren oder Dekrementieren einer Ganzzahl in einer einzigen Anfrage, anstatt mehrere gets auszuführen /cas Anrufe. In realen Anwendungen wird gets und cas werden für komplexere Datentypen oder Operationen verwendet

Die meisten Remote-Caching-Server und -Datenspeicher bieten einen solchen Mechanismus, um Parallelitätsprobleme zu vermeiden. Es ist wichtig, sich dieser Fälle bewusst zu sein, um ihre Funktionen richtig nutzen zu können.



Jenseits von Caching

Die in diesem Artikel veranschaulichten einfachen Techniken haben Ihnen gezeigt, wie einfach es ist, memcached zu nutzen um die Leistung Ihrer Python-Anwendung zu beschleunigen.

Allein durch die Verwendung der beiden grundlegenden „set“- und „get“-Operationen können Sie oft den Datenabruf beschleunigen oder vermeiden, dass Ergebnisse immer wieder neu berechnet werden. Mit memcached können Sie den Cache auf eine große Anzahl verteilter Knoten verteilen.

Andere, fortgeschrittenere Muster, die Sie in diesem Tutorial gesehen haben, wie das Check And Set (CAS) -Operation ermöglichen es Ihnen, im Cache gespeicherte Daten gleichzeitig über mehrere Python-Threads oder -Prozesse hinweg zu aktualisieren und gleichzeitig Datenbeschädigungen zu vermeiden.

Wenn Sie daran interessiert sind, mehr über fortgeschrittene Techniken zum Schreiben schnellerer und besser skalierbarer Python-Anwendungen zu erfahren, sehen Sie sich Skalierung von Python an. Es behandelt viele fortgeschrittene Themen wie Netzwerkverteilung, Warteschlangensysteme, verteiltes Hashing und Code-Profiling.