Memcached
 sql >> Base de Dados >  >> NoSQL >> Memcached

Python + Memcached:cache eficiente em aplicativos distribuídos


Ao escrever aplicativos Python, o armazenamento em cache é importante. Usar um cache para evitar recalcular dados ou acessar um banco de dados lento pode fornecer um grande aumento de desempenho.

Python oferece possibilidades integradas para armazenamento em cache, desde um dicionário simples até uma estrutura de dados mais completa, como functools.lru_cache . O último pode armazenar em cache qualquer item usando um algoritmo usado menos recentemente para limitar o tamanho do cache.

Essas estruturas de dados são, no entanto, por definição locais ao seu processo Python. Quando várias cópias do seu aplicativo são executadas em uma plataforma grande, o uso de uma estrutura de dados na memória impede o compartilhamento do conteúdo em cache. Isso pode ser um problema para aplicativos distribuídos e de grande escala.

Portanto, quando um sistema é distribuído em uma rede, ele também precisa de um cache distribuído em uma rede. Atualmente, existem muitos servidores de rede que oferecem capacidade de armazenamento em cache – já abordamos como usar o Redis para armazenamento em cache com o Django.

Como você verá neste tutorial, o memcached é outra ótima opção para armazenamento em cache distribuído. Após uma rápida introdução ao uso básico do memcached, você aprenderá sobre padrões avançados, como “cache and set” e como usar caches de fallback para evitar problemas de desempenho de cold cache.

Instalando o memcached


Memcached está disponível para muitas plataformas:
  • Se você executa Linux , você pode instalá-lo usando apt-get install memcached ou yum install memcached . Isso instalará o memcached a partir de um pacote pré-compilado, mas você também pode compilar o memcached a partir da fonte, conforme explicado aqui.
  • Para macOS , usar o Homebrew é a opção mais simples. Basta executar brew install memcached depois de instalar o gerenciador de pacotes Homebrew.
  • No Windows , você teria que compilar o memcached por conta própria ou encontrar binários pré-compilados.

Uma vez instalado, memcached pode simplesmente ser iniciado chamando o memcached comando:
$ memcached

Antes de poder interagir com o memcached do Python-land, você precisará instalar um cliente do memcached biblioteca. Você verá como fazer isso na próxima seção, junto com algumas operações básicas de acesso ao cache.


Armazenando e recuperando valores em cache usando Python


Se você nunca usou o memcached , é bem fácil de entender. Ele basicamente fornece um dicionário gigante disponível na rede. Este dicionário possui algumas propriedades que são diferentes de um dicionário Python clássico, principalmente:
  • Chaves e valores devem ser bytes
  • Chaves e valores são excluídos automaticamente após um tempo de expiração

Portanto, as duas operações básicas para interagir com o memcached estão set e gets . Como você deve ter adivinhado, eles são usados ​​para atribuir um valor a uma chave ou obter um valor de uma chave, respectivamente.

Minha biblioteca Python preferida para interagir com memcached é pymemcache —Recomendo usá-lo. Você pode simplesmente instalá-lo usando pip:
$ pip install pymemcache

O código a seguir mostra como você pode se conectar ao memcached e use-o como um cache distribuído pela rede em seus aplicativos 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 O protocolo de rede é realmente simples e sua implementação extremamente rápida, o que o torna útil para armazenar dados que seriam lentos para recuperar da fonte de dados canônica ou para computar novamente:

Embora bastante simples, este exemplo permite armazenar tuplas de chave/valor na rede e acessá-las por meio de várias cópias distribuídas e em execução de seu aplicativo. Isso é simplista, mas poderoso. E é um ótimo primeiro passo para otimizar seu aplicativo.


Dados em cache com expiração automática


Ao armazenar dados no memcached , você pode definir um tempo de expiração — um número máximo de segundos para memcached para manter a chave e o valor por perto. Após esse atraso, memcached remove automaticamente a chave de seu cache.

Para que você deve definir esse tempo de cache? Não existe um número mágico para esse atraso e dependerá inteiramente do tipo de dados e do aplicativo com o qual você está trabalhando. Pode ser alguns segundos, ou pode ser algumas horas.

Invalidação de cache , que define quando remover o cache porque está fora de sincronia com os dados atuais, também é algo que seu aplicativo terá que lidar. Especialmente se apresentar dados muito antigos ou obsoletos deve ser evitado.

Aqui, novamente, não há receita mágica; depende do tipo de aplicativo que você está construindo. No entanto, existem vários casos periféricos que devem ser tratados - que ainda não abordamos no exemplo acima.

Um servidor de armazenamento em cache não pode crescer infinitamente — a memória é um recurso finito. Portanto, as chaves serão liberadas pelo servidor de cache assim que ele precisar de mais espaço para armazenar outras coisas.

Algumas chaves também podem ter expirado porque atingiram seu tempo de expiração (também chamado de “tempo de vida” ou TTL). Nesses casos, os dados são perdidos e a fonte de dados canônica deve ser consultada novamente.

Isso soa mais complicado do que realmente é. Geralmente, você pode trabalhar com o seguinte padrão ao trabalhar com o memcached em 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)

Observação: O manuseio de chaves ausentes é obrigatório devido às operações normais de liberação. Também é obrigatório lidar com o cenário de cache frio, ou seja, quando memcached acaba de ser iniciado. Nesse caso, o cache estará totalmente vazio e o cache precisará ser totalmente repovoado, uma solicitação por vez.

Isso significa que você deve visualizar todos os dados armazenados em cache como efêmeros. E você nunca deve esperar que o cache contenha um valor que você gravou anteriormente.


Aquecendo um cache frio


Alguns dos cenários de cache frio não podem ser evitados, por exemplo, um memcached colidir. Mas alguns podem, por exemplo, migrar para um novo memcached servidor.

Quando é possível prever que um cenário de cache frio acontecerá, é melhor evitá-lo. Um cache que precisa ser recarregado significa que, de repente, o armazenamento canônico dos dados em cache será massivamente atingido por todos os usuários de cache que não possuem dados de cache (também conhecido como problema do rebanho trovejante).

pymemcache fornece uma classe chamada FallbackClient que ajuda na implementação deste cenário, conforme demonstrado aqui:
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)

O FallbackClient consulta o cache antigo passado para seu construtor, respeitando a ordem. Nesse caso, o novo servidor de cache sempre será consultado primeiro e, em caso de falta de cache, o antigo será consultado, evitando uma possível viagem de retorno à fonte primária de dados.

Se alguma chave for definida, ela será definida apenas para o novo cache. Após algum tempo, o cache antigo pode ser desativado e o FallbackClient pode ser substituído direcionado pelo new_cache cliente.


Verificar e definir


Ao se comunicar com um cache remoto, o problema usual de simultaneidade volta:pode haver vários clientes tentando acessar a mesma chave ao mesmo tempo. memcached fornece um verificar e definir operação, abreviado para CAS , o que ajuda a resolver este problema.

O exemplo mais simples é um aplicativo que deseja contar o número de usuários que possui. Cada vez que um visitante se conecta, um contador é incrementado em 1. Usando o memcached , uma implementação simples seria:
def on_visit(client):
    result = client.get('visitors')
    if result is None:
        result = 1
    else:
        result += 1
    client.set('visitors', result)

No entanto, o que acontece se duas instâncias do aplicativo tentarem atualizar esse contador ao mesmo tempo?

A primeira chamada client.get('visitors') retornará o mesmo número de visitantes para ambos, digamos que seja 42. Então, ambos somarão 1, calcularão 43 e definirão o número de visitantes como 43. Esse número está errado e o resultado deve ser 44, ou seja, 42 + 1 + 1.

Para resolver esse problema de simultaneidade, a operação CAS do memcached é útil. O snippet a seguir implementa uma solução correta:
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

O gets retorna o valor, assim como o gets mas também retorna um valor CAS .

O que está neste valor não é relevante, mas é usado para o próximo método cas ligar. Este método é equivalente ao set operação, exceto que falha se o valor foi alterado desde que o gets Operação. Em caso de sucesso, o loop é quebrado. Caso contrário, a operação é reiniciada desde o início.

No cenário em que duas instâncias do aplicativo tentam atualizar o contador ao mesmo tempo, apenas uma consegue mover o contador de 42 para 43. A segunda instância obtém um False valor retornado pelo client.cas chamar e ter que repetir o loop. Ele recuperará 43 como valor desta vez, aumentará para 44 e seu cas chamada terá sucesso, resolvendo assim o nosso problema.

Incrementar um contador é interessante como exemplo para explicar como o CAS funciona porque é simplista. No entanto, memcached também fornece o incr e decr métodos para incrementar ou decrementar um inteiro em uma única solicitação, em vez de fazer vários gets /cas chamadas. Em aplicativos do mundo real gets e cas são usados ​​para tipos de dados ou operações mais complexas

A maioria dos servidores de cache e armazenamento de dados remotos fornecem esse mecanismo para evitar problemas de simultaneidade. É fundamental estar ciente desses casos para fazer uso adequado de seus recursos.


Além do armazenamento em cache


As técnicas simples ilustradas neste artigo mostraram como é fácil aproveitar o memcached para acelerar o desempenho do seu aplicativo Python.

Apenas usando as duas operações básicas “set” e “get” você pode acelerar a recuperação de dados ou evitar recalcular resultados repetidamente. Com o memcached, você pode compartilhar o cache em um grande número de nós distribuídos.

Outros padrões mais avançados que você viu neste tutorial, como o Check And Set (CAS) A operação permite que você atualize dados armazenados no cache simultaneamente em vários threads ou processos do Python, evitando a corrupção de dados.

Se você estiver interessado em aprender mais sobre técnicas avançadas para escrever aplicativos Python mais rápidos e escaláveis, confira Scaling Python. Abrange muitos tópicos avançados, como distribuição de rede, sistemas de enfileiramento, hashing distribuído e criação de perfil de código.