Memcached
 sql >> Database >  >> NoSQL >> Memcached

Python + Memcached:Caching efficiente nelle applicazioni distribuite

Quando si scrivono applicazioni Python, la memorizzazione nella cache è importante. L'utilizzo di una cache per evitare il ricalcolo dei dati o l'accesso a un database lento può fornire un notevole aumento delle prestazioni.

Python offre possibilità integrate per la memorizzazione nella cache, da un semplice dizionario a una struttura dati più completa come functools.lru_cache . Quest'ultimo può memorizzare nella cache qualsiasi elemento utilizzando un algoritmo utilizzato meno di recente per limitare la dimensione della cache.

Tali strutture di dati sono, tuttavia, per definizione locali al tuo processo Python. Quando più copie dell'applicazione vengono eseguite su una piattaforma di grandi dimensioni, l'utilizzo di una struttura dati in memoria non consente la condivisione del contenuto memorizzato nella cache. Questo può essere un problema per le applicazioni distribuite e su larga scala.

Pertanto, quando un sistema è distribuito su una rete, necessita anche di una cache distribuita su una rete. Al giorno d'oggi, ci sono molti server di rete che offrono capacità di memorizzazione nella cache:abbiamo già spiegato come utilizzare Redis per la memorizzazione nella cache con Django.

Come vedrai in questo tutorial, memcached è un'altra ottima opzione per la memorizzazione nella cache distribuita. Dopo una rapida introduzione all'utilizzo di base di memcached, imparerai i modelli avanzati come "cache and set" e l'utilizzo delle cache di fallback per evitare problemi di prestazioni della cold cache.


Installazione di memcached

Memcached è disponibile per molte piattaforme:

  • Se esegui Linux , puoi installarlo usando apt-get install memcached o yum install memcached . Questo installerà memcached da un pacchetto precompilato ma puoi anche compilare memcached dal sorgente, come spiegato qui.
  • Per macOS , l'utilizzo di Homebrew è l'opzione più semplice. Basta eseguire brew install memcached dopo aver installato il gestore di pacchetti Homebrew.
  • Su Windows , dovresti compilare tu stesso memcached o trovare binari precompilati.

Una volta installato, memcached può essere avviato semplicemente chiamando il memcached comando:

$ memcached

Prima di poter interagire con memcached da Python-land, dovrai installare un client memcached biblioteca. Vedrai come farlo nella prossima sezione, insieme ad alcune operazioni di base per l'accesso alla cache.



Memorizzazione e recupero di valori memorizzati nella cache utilizzando Python

Se non hai mai usato memcached , è abbastanza facile da capire. Fondamentalmente fornisce un gigantesco dizionario disponibile in rete. Questo dizionario ha alcune proprietà diverse da un dizionario Python classico, principalmente:

  • Chiavi e valori devono essere byte
  • Chiavi e valori vengono eliminati automaticamente dopo un periodo di scadenza

Pertanto, le due operazioni di base per interagire con memcached sono set e gets . Come avrai intuito, vengono utilizzati rispettivamente per assegnare un valore a una chiave o per ottenere un valore da una chiave.

La mia libreria Python preferita per interagire con memcached è pymemcache — Consiglio di usarlo. Puoi semplicemente installarlo usando pip:

$ pip install pymemcache

Il codice seguente mostra come puoi connetterti a memcached e usalo come cache distribuita in rete nelle tue applicazioni Python:

>>> 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'

memcached protocollo di rete è davvero semplice e la sua implementazione estremamente veloce, il che lo rende utile per memorizzare dati che altrimenti sarebbero lenti da recuperare dalla fonte canonica dei dati o da rielaborare:

Sebbene sia abbastanza semplice, questo esempio consente di archiviare tuple chiave/valore attraverso la rete e di accedervi tramite copie multiple, distribuite e in esecuzione dell'applicazione. Questo è semplicistico, ma potente. Ed è un ottimo primo passo verso l'ottimizzazione della tua applicazione.



Dati memorizzati nella cache con scadenza automatica

Quando si archiviano i dati in memcached , puoi impostare una scadenza, un numero massimo di secondi per memcached per mantenere la chiave e il valore in giro. Dopo quel ritardo, memcached rimuove automaticamente la chiave dalla sua cache.

A cosa dovresti impostare questo tempo di cache? Non esiste un numero magico per questo ritardo e dipenderà interamente dal tipo di dati e dall'applicazione con cui stai lavorando. Potrebbero essere necessari alcuni secondi o alcune ore.

Invalidamento della cache , che definisce quando rimuovere la cache perché non è sincronizzata con i dati correnti, è anche qualcosa che l'applicazione dovrà gestire. Soprattutto se si presentano dati troppo vecchi o obsoleti è da evitare.

Anche in questo caso, non esiste una ricetta magica; dipende dal tipo di applicazione che stai creando. Tuttavia, ci sono diversi casi periferici che dovrebbero essere gestiti, che non abbiamo ancora trattato nell'esempio precedente.

Un server di cache non può crescere all'infinito:la memoria è una risorsa finita. Pertanto, le chiavi verranno svuotate dal server di memorizzazione nella cache non appena avrà bisogno di più spazio per archiviare altre cose.

Alcune chiavi potrebbero anche essere scadute perché hanno raggiunto il loro tempo di scadenza (a volte chiamato anche "time-to-live" o TTL). In questi casi i dati vengono persi e l'origine dati canonica deve essere nuovamente interrogata.

Questo suona più complicato di quanto non sia in realtà. In genere puoi lavorare con il seguente modello quando lavori con memcached in Python:

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)

Nota: La gestione delle chiavi mancanti è obbligatoria a causa delle normali operazioni di svuotamento. È inoltre obbligatorio gestire lo scenario della cache a freddo, ovvero quando memcached è appena stato avviato. In tal caso, la cache sarà completamente vuota e la cache dovrà essere completamente ripopolata, una richiesta alla volta.

Ciò significa che dovresti visualizzare tutti i dati memorizzati nella cache come effimeri. E non dovresti mai aspettarti che la cache contenga un valore che hai scritto in precedenza.



Riscaldare una riserva fredda

Alcuni degli scenari di cold cache non possono essere prevenuti, ad esempio un memcached incidente. Ma alcuni possono, ad esempio migrare a un nuovo memcached server.

Quando è possibile prevedere che si verificherà uno scenario di cold cache, è meglio evitarlo. Una cache che deve essere ricaricata significa che, all'improvviso, la memorizzazione canonica dei dati memorizzati nella cache verrà massicciamente colpita da tutti gli utenti della cache che non dispongono di una cache di dati (noto anche come il problema della mandria tonante.)

pymemcache fornisce una classe denominata FallbackClient che aiuta nell'implementazione di questo scenario, come dimostrato qui:

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)

Il FallbackClient interroga la vecchia cache passata al suo costruttore, rispettando l'ordine. In questo caso, verrà sempre interrogato per primo il nuovo server cache e, in caso di mancanza di cache, verrà interrogato quello vecchio, evitando un possibile viaggio di ritorno all'origine dati primaria.

Se viene impostata una chiave, verrà impostata solo sulla nuova cache. Dopo qualche tempo, la vecchia cache può essere rimossa e il FallbackClient può essere sostituito diretto con new_cache cliente.



Verifica e imposta

Quando si comunica con una cache remota, si ripresenta il solito problema di concorrenza:potrebbero esserci diversi client che tentano di accedere alla stessa chiave contemporaneamente. memcached fornisce un controlla e imposta operazione, abbreviato in CAS , che aiuta a risolvere questo problema.

L'esempio più semplice è un'applicazione che vuole contare il numero di utenti che ha. Ogni volta che un visitatore si connette, un contatore viene incrementato di 1. Utilizzo di memcached , una semplice implementazione sarebbe:

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

Tuttavia, cosa succede se due istanze dell'applicazione tentano di aggiornare questo contatore contemporaneamente?

La prima chiamata client.get('visitors') restituirà lo stesso numero di visitatori per entrambi, diciamo che è 42. Quindi entrambi aggiungeranno 1, calcoleranno 43 e imposteranno il numero di visitatori a 43. Quel numero è sbagliato e il risultato dovrebbe essere 44, ovvero 42 + 1 + 1.

Per risolvere questo problema di concorrenza, l'operazione CAS di memcached è a portata di mano. Il frammento di codice seguente implementa una soluzione corretta:

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

Il gets il metodo restituisce il valore, proprio come gets metodo, ma restituisce anche un valore CAS .

Il contenuto di questo valore non è rilevante, ma viene utilizzato per il metodo successivo cas chiamata. Questo metodo è equivalente al set operazione, tranne per il fatto che fallisce se il valore è cambiato da quando gets operazione. In caso di successo, il ciclo viene interrotto. In caso contrario, l'operazione viene riavviata dall'inizio.

Nello scenario in cui due istanze dell'applicazione tentano di aggiornare il contatore contemporaneamente, solo una riesce a spostare il contatore da 42 a 43. La seconda istanza ottiene un False valore restituito da client.cas chiamare e riprovare il ciclo. Questa volta recupererà 43 come valore, lo incrementerà a 44 e il suo cas la chiamata avrà successo, risolvendo così il nostro problema.

L'incremento di un contatore è interessante come esempio per spiegare come funziona il CAS perché è semplicistico. Tuttavia, memcached fornisce anche incr e decr metodi per incrementare o decrementare un intero in una singola richiesta, invece di eseguire più gets /cas chiamate. Nelle applicazioni del mondo reale gets e cas vengono utilizzati per tipi di dati o operazioni più complessi

La maggior parte dei server di memorizzazione nella cache remoti e dell'archivio dati forniscono un tale meccanismo per prevenire problemi di concorrenza. È fondamentale essere a conoscenza di questi casi per fare un uso corretto delle loro funzionalità.



Oltre alla memorizzazione nella cache

Le semplici tecniche illustrate in questo articolo ti hanno mostrato quanto sia facile sfruttare memcached per velocizzare le prestazioni della tua applicazione Python.

Semplicemente utilizzando le due operazioni di base "set" e "get" è spesso possibile accelerare il recupero dei dati o evitare di ricalcolare i risultati più e più volte. Con memcached puoi condividere la cache su un gran numero di nodi distribuiti.

Altri modelli più avanzati che hai visto in questo tutorial, come Check And Set (CAS) l'operazione consente di aggiornare i dati archiviati nella cache contemporaneamente su più thread o processi Python evitando il danneggiamento dei dati.

Se sei interessato a saperne di più sulle tecniche avanzate per scrivere applicazioni Python più veloci e scalabili, dai un'occhiata a Scaling Python. Copre molti argomenti avanzati come la distribuzione di rete, i sistemi di accodamento, l'hashing distribuito e la profilazione del codice.