sql >> Base de Datos >  >> NoSQL >> Memcached

Python + Memcached:almacenamiento en caché eficiente en aplicaciones distribuidas

Al escribir aplicaciones de Python, el almacenamiento en caché es importante. El uso de una memoria caché para evitar volver a calcular los datos o acceder a una base de datos lenta puede brindarle un gran aumento de rendimiento.

Python ofrece posibilidades integradas para el almacenamiento en caché, desde un diccionario simple hasta una estructura de datos más completa como functools.lru_cache . Este último puede almacenar en caché cualquier elemento utilizando un algoritmo de uso menos reciente para limitar el tamaño del caché.

Esas estructuras de datos son, sin embargo, por definición locales a su proceso de Python. Cuando varias copias de su aplicación se ejecutan en una plataforma grande, el uso de una estructura de datos en memoria no permite compartir el contenido almacenado en caché. Esto puede ser un problema para aplicaciones distribuidas y a gran escala.

Por lo tanto, cuando un sistema se distribuye a través de una red, también necesita un caché que se distribuye a través de una red. Hoy en día, hay muchos servidores de red que ofrecen capacidad de almacenamiento en caché; ya cubrimos cómo usar Redis para el almacenamiento en caché con Django.

Como verá en este tutorial, memcached es otra excelente opción para el almacenamiento en caché distribuido. Después de una introducción rápida al uso básico de Memcached, aprenderá acerca de patrones avanzados como "caché y configuración" y el uso de cachés alternativos para evitar problemas de rendimiento de caché en frío.


Instalando Memcached

Memcached está disponible para muchas plataformas:

  • Si ejecuta Linux , puede instalarlo usando apt-get install memcached o yum install memcached . Esto instalará Memcached desde un paquete preconstruido, pero también puede compilar Memcached desde la fuente, como se explica aquí.
  • Para macOS , usar Homebrew es la opción más sencilla. Simplemente ejecute brew install memcached después de haber instalado el administrador de paquetes Homebrew.
  • En Windows , tendrías que compilar Memcached tú mismo o buscar archivos binarios precompilados.

Una vez instalado, memcached simplemente se puede iniciar llamando a memcached comando:

$ memcached

Antes de poder interactuar con Memcached desde Python-land, deberá instalar un cliente de Memcached biblioteca. Verá cómo hacerlo en la siguiente sección, junto con algunas operaciones básicas de acceso a la memoria caché.



Almacenamiento y recuperación de valores almacenados en caché mediante Python

Si nunca usaste memcached , es bastante fácil de entender. Básicamente proporciona un diccionario gigante disponible en la red. Este diccionario tiene algunas propiedades que son diferentes de un diccionario Python clásico, principalmente:

  • Las claves y los valores deben ser bytes
  • Las claves y los valores se eliminan automáticamente después de un tiempo de vencimiento

Por lo tanto, las dos operaciones básicas para interactuar con memcached están set y get . Como habrás adivinado, se utilizan para asignar un valor a una clave o para obtener un valor de una clave, respectivamente.

Mi biblioteca Python preferida para interactuar con memcached es pymemcache —Recomiendo usarlo. Simplemente puede instalarlo usando pip:

$ pip install pymemcache

El siguiente código muestra cómo puede conectarse a memcached y utilícelo como un caché distribuido en red en sus aplicaciones de 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'

memcaché El protocolo de red es realmente simple y su implementación es extremadamente rápida, lo que lo hace útil para almacenar datos que, de otro modo, serían lentos de recuperar de la fuente canónica de datos o de volver a calcular:

Si bien es bastante sencillo, este ejemplo permite almacenar tuplas clave/valor en toda la red y acceder a ellas a través de múltiples copias distribuidas y en ejecución de su aplicación. Esto es simple, pero poderoso. Y es un gran primer paso hacia la optimización de su aplicación.



Caducidad automática de datos almacenados en caché

Al almacenar datos en memcached , puede establecer un tiempo de caducidad:una cantidad máxima de segundos para memcached para mantener la clave y el valor alrededor. Después de ese retraso, memcached elimina automáticamente la clave de su caché.

¿A qué debe configurar este tiempo de caché? No hay un número mágico para este retraso y dependerá completamente del tipo de datos y la aplicación con la que esté trabajando. Pueden ser unos segundos o pueden ser unas horas.

Invalidación de caché , que define cuándo eliminar la memoria caché porque no está sincronizada con los datos actuales, también es algo que su aplicación tendrá que manejar. Especialmente si presenta datos demasiado antiguos o obsoletos debe evitarse.

Aquí nuevamente, no existe una receta mágica; depende del tipo de aplicación que esté creando. Sin embargo, hay varios casos atípicos que deben manejarse, que aún no hemos cubierto en el ejemplo anterior.

Un servidor de almacenamiento en caché no puede crecer infinitamente:la memoria es un recurso finito. Por lo tanto, el servidor de almacenamiento en caché eliminará las claves tan pronto como necesite más espacio para almacenar otras cosas.

Algunas claves también pueden haber caducado porque alcanzaron su tiempo de caducidad (a veces también llamado "tiempo de vida" o TTL). En esos casos, los datos se pierden y la fuente de datos canónicos debe consultarse nuevamente.

Esto suena más complicado de lo que realmente es. Por lo general, puede trabajar con el siguiente patrón cuando trabaja con Memcached en 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: El manejo de las llaves perdidas es obligatorio debido a las operaciones normales de lavado. También es obligatorio manejar el escenario de caché en frío, es decir, cuando memcached acaba de empezar. En ese caso, la memoria caché estará completamente vacía y la memoria caché debe volver a llenarse por completo, una solicitud a la vez.

Esto significa que debe ver los datos almacenados en caché como efímeros. Y nunca debe esperar que el caché contenga un valor que haya escrito anteriormente.



Calentando un caché frío

Algunos de los escenarios de caché en frío no se pueden evitar, por ejemplo, un memcached choque. Pero algunos pueden, por ejemplo, migrar a un nuevo memcached servidor.

Cuando es posible predecir que ocurrirá un escenario de caché en frío, es mejor evitarlo. Un caché que debe rellenarse significa que, de repente, el almacenamiento canónico de los datos almacenados en caché se verá afectado masivamente por todos los usuarios de caché que carecen de datos de caché (también conocido como el problema del rebaño atronador).

pymemcache proporciona una clase llamada FallbackClient que ayuda en la implementación de este escenario como se demuestra aquí:

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)

El FallbackClient consulta el caché anterior pasado a su constructor, respetando el orden. En este caso, siempre se consultará primero al nuevo servidor de caché y, en caso de que se pierda la caché, se consultará al antiguo, lo que evitará un posible viaje de regreso a la fuente principal de datos.

Si se establece alguna clave, solo se establecerá en la nueva memoria caché. Después de un tiempo, el caché antiguo se puede retirar y el FallbackClient se puede reemplazar dirigido con el new_cache cliente.



Comprobar y configurar

Cuando se comunica con un caché remoto, vuelve el problema habitual de concurrencia:puede haber varios clientes intentando acceder a la misma clave al mismo tiempo. memcaché proporciona un comprobar y configurar operación, abreviado como CAS , que ayuda a resolver este problema.

El ejemplo más simple es una aplicación que quiere contar la cantidad de usuarios que tiene. Cada vez que un visitante se conecta, un contador se incrementa en 1. Uso de memcached , una implementación simple sería:

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

Sin embargo, ¿qué sucede si dos instancias de la aplicación intentan actualizar este contador al mismo tiempo?

La primera llamada client.get('visitors') devolverá el mismo número de visitantes para ambos, digamos que es 42. Luego, ambos sumarán 1, calcularán 43 y establecerán el número de visitantes en 43. Ese número es incorrecto y el resultado debería ser 44, es decir, 42 + 1 + 1.

Para resolver este problema de concurrencia, la operación CAS de memcached es útil El siguiente fragmento implementa una solución correcta:

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

El gets método devuelve el valor, al igual que el get pero también devuelve un valor CAS .

Lo que está en este valor no es relevante, pero se usa para el siguiente método cas llamar. Este método es equivalente al set operación, excepto que falla si el valor ha cambiado desde que gets operación. En caso de éxito, el bucle se rompe. De lo contrario, la operación se reinicia desde el principio.

En el escenario donde dos instancias de la aplicación intentan actualizar el contador al mismo tiempo, solo una logra mover el contador de 42 a 43. La segunda instancia obtiene un False valor devuelto por client.cas llame y tenga que volver a intentar el bucle. Recuperará 43 como valor esta vez, lo incrementará a 44 y su cas la llamada tendrá éxito, resolviendo así nuestro problema.

Incrementar un contador es un ejemplo interesante para explicar cómo funciona CAS porque es simple. Sin embargo, memcached también proporciona el incr y decr métodos para incrementar o disminuir un número entero en una sola solicitud, en lugar de hacer múltiples gets /cas llamadas En aplicaciones del mundo real gets y cas se utilizan para operaciones o tipos de datos más complejos

La mayoría de los servidores de almacenamiento en caché y almacenes de datos remotos proporcionan un mecanismo de este tipo para evitar problemas de concurrencia. Es fundamental conocer esos casos para hacer un uso adecuado de sus funciones.



Más allá del almacenamiento en caché

Las sencillas técnicas ilustradas en este artículo le mostraron lo fácil que es aprovechar memcached para acelerar el rendimiento de su aplicación Python.

Con solo usar las dos operaciones básicas de "establecer" y "obtener", a menudo puede acelerar la recuperación de datos o evitar volver a calcular los resultados una y otra vez. Con memcached, puede compartir el caché entre una gran cantidad de nodos distribuidos.

Otros patrones más avanzados que vio en este tutorial, como Check And Set (CAS) La operación le permite actualizar los datos almacenados en el caché al mismo tiempo a través de múltiples subprocesos o procesos de Python mientras evita la corrupción de datos.

Si está interesado en obtener más información sobre técnicas avanzadas para escribir aplicaciones Python más rápidas y escalables, consulte Scaling Python. Abarca muchos temas avanzados, como distribución de red, sistemas de colas, hashing distribuido y creación de perfiles de código.