Caches de Aplicação

Artigo que explica o conceito de cache de aplicação e alguns exemplos de uso em PHP.

Introdução

É muito comum (e útil) aplicações complexas armazenarem algumas informações em cache para diversas finalidades. A mais importante é o rápido acesso a uma informação que tem um custo relativamente alto para ser obtido, seja pelo tempo gasto, consumo de recursos computacionais para se realizar a consulta ou pela quantidade de vezes que se acessa a mesma informação.

Neste artigo, vou mostrar o conceito básico para se montar uma estratégia de cache e algumas exemplos de utilização.

Como montar um cache para aplicação

A coisa mais importante para se levar em conta ao adotar uma estratégia de cache sobre alguma informação é que a informação precisa estar sempre acessível a partir de sua fonte original, ou seja, o cache deve ser descartável a qualquer momento. Outra questão importante para se considerar é a confiabilidade da informação vinda de um cache, afinal, é possível adotar um tempo de vida do cache e assumir que o valor obtido dentro deste tempo é razoavelmente válido, ainda que a informação real possa ter sofrido alguma alteração.

A lógica básica para se montar qualquer cache é a seguinte:

$informacao = obter_informacao_do_cache($id);
if (!$informacao) {
    $informacao = obter_informacao_da_fonte_original();
    salvar_informacao_em_cache($id, $informacao);
}

Ou seja, primeiro se tenta obter a informação desejada do cache através de algum código de identificação única. Depois, caso não tenha encontrado no cache ou a informação não é mais válida (por exemplo passou da validade), então se obtém a informação de sua fonte original e se guarda essa informação no cache associada ao código de identificação única. Note que, mesmo que a informação não seja obtida do cache, ela ainda assim será obtida por outra via confiável.

APC

A extensão do PHP APC é capaz de cachear código intermediário do PHP (opcode), mas também oferece funções para se armazenar dados da aplicação (variáveis) em cache de memória.

As vantagens dele são a simplicidade de uso, possibilidade de especificação de tempo de vida (validade), excelente desempenho, já que utiliza a memória RAM, e possibilidade de armazenar qualquer valor do PHP que não seja do tipo resource.

Por outro lado, é importante estar atento à quantidade de dados colocados em cache, afinal, a memória RAM do servidor é limitada. Além disso, quando a aplicação é provida por um cluster de vários servidores, cada um manterá um controle próprio de seu próprio cache, portanto há um desperdício de recursos de certo modo. Embora isso possa ser implementado de forma peculiar onde apenas um servidor cuida do cache e provê um mecanismo para acesso pelos outros, adotar essa estratégia acaba prejudicando um pouco a performance (já que haverá troca de dados entre servidores).

Para utilizar o APC como cache é muito simples:

$informacao = apc_fetch($id);
if (!$informacao) {
    $informacao = obter_informacao_da_fonte_original();
    apc_store($id, $informacao, $tempo_de_vida);
}

Resumidamente: apc_fetch serve para obter uma informação do cache (retornando false caso não seja encontrada ou não esteja mais disponível/válida) e apc_store serve para guardar uma informação no cache, com possibilidade de passar o tempo de vida (em segundos), que pode ser 0 (zero) para não ter uma validade específica.

Algumas outras funções úteis são:

  • apc_exits: verifica se determinado cache existe no APC a partir de um código de identificação.
  • apc_delete: apaga uma informação do APC a partir de um código de identificação.
  • apc_clear_cache: passando 'user' como parâmetro, ela limpa todos os caches de dados do APC.

Uma observação é que também existe uma extensão chamada APCu, que é similar à extensão APC, só que sem o suporte ao cache de código intermediário. Ou seja, ela apenas provê as funções necessárias para se guardar/recuperar dados em cache. A diferença dela é que as funções tem o prefixo apcu_ ao invés de apc_, por exemplo apcu_fetch e apcu_store. Talvez isso foi feito para que uma instância do PHP que tenha as extensões APC e APCu não causem conflitos entre si.

Memcache

A extensão do PHP Memcache é capaz de se comunicar com um servidor que esteja provendo o serviço de Memcache, que é um cache em memória baseado em códigos de identificação (hash). A extensão provê duas utilidade: utilizar memcache como forma de armazenamento de sessões e utilizar o memcache como cache de aplicação.

A principal diferença para o APC é que normalmente o servidor de Memcache não é o mesmo servidor da aplicação, ou seja, pode ser um servidor dedicado ao propósito de cache ou um servidor externo que, dentre outras funções, oferece o serviço de memcache. Embora essa estratégia perca um pouco em performance para trafegar dados entre o servidor de memcache e o servidor da aplicação que requisitou um dado, existem algumas benefícios importantes de serem citados, tais como: centralização dos dados de cache (poupa a utilização de recursos, já que não permanece com dados duplicados entre vários servidores desnecessariamente), a possibilidade de montar uma arquitetura com vários servidores memcache com replicação de dados, provendo escalabilidade, e a possibilidade de consumir o servidor de memcache por aplicações feitas em outras linguagens de programação, provendo o intercâmbio de dados.

Uma observação é que dados inteiros e strings são armazenados de forma literal no memcache, já os outros tipos são armazenados de forma serializada (com serialize), e que são desserializadas automaticamente ao serem obtidas (com unserialize). Variáveis do tipo resource também não podem ser armazenados.

Segue um exemplo de como utilizar a extensão Memcache do PHP para recuperar/armazenar dados de cache em um servidor de memcache:

$memcache = new Memcache();
if ($memcache->connect('localhost', 11211)) {
    $informacao = $memcache->get($id);
    if (!$informacao) {
        $informacao = obter_informacao_da_fonte_original();
        $memcache->set($id, $informacao, $flag, $time_expiracao);
    }
    $memcache->close();
} else {
    throw new RuntimeException('Falha ao conectar no servidor de Memcache');
}

Note que primeiro é necessário conectar a um servidor de memcache (no exemplo representado por "localhost") em determinada porta. Em seguinda usa-se o método get para tentar obter a informação do cache e, caso não encontre ou tenha expirado, obtém da fonte original e guarda em cache pelo método set. O método set aceita uma flag binária que pode ser a constante MEMCACHE_COMPRESSED, que faz com que a informação seja armazena de forma comprimida (utilizando zlib). Já o último parâmetro do método set indica o timestamp de quando a informação irá expirar, ou pode ser algum número menor ou igual a 2592000, e que representa o tempo de vida do cache (assim como o APC). O valor 2592000 é o número de segundos de 30 dias.

Note também que, ao invés de soltar uma exception, caso não consiga conectar ao memcache, seria possível simplesmente obter a informação da sua fonte original e apenas gerar um log, melhorando a disponibilidade da aplicação.

Outros métodos úteis da classe Memcache:

  • flush: para marcar todos os dados em cache como inválidos (expirados). Ele não chega a limpar exatamente a memória, mas só de marcar os dados como inválidos, eles não serão obtidos com o get, e o processo de invalidação é bem mais rápido que limpar de fato os dados da memória.
  • pconnect: similar a connect, mas se conecta de forma persistente. Isso significa que a conexão não irá fechar ao encerrar o script ou mesmo que o método close seja chamado. Assim, ao abrir uma conexão persistente novamente, é possível que o PHP utilize uma conexão persistente existente, poupando custos de abertura/fechamento de conexão.
  • addServer: é usado para especificar vários servidores de memcache que formam um pool. Neste caso, o método connect (ou pconnect) não são usados. A própria classe fica responsável por obter ou armazenar a informação de algum dos servidores de memcache especificados no pool e que esteja disponível.

Para mais informações, acesse a lista completa de métodos da classe Memcache

Memcached

A extensão do PHP Memcached é bem parecida com a Memcache. Ela também oferece a possibilidade de usar um servidor de memcache para armazenar as sessões e a possibilidade de usar um servidor de memcache para fazer cache da aplicação, mas ela tem um "D" a mais que vamos ver a seguir. A diferença é que ela provê a classe Memcached ao invés da classe Memcache e possui algumas pequenas diferenças de como os métodos são usados. Em geral, ela oferece bem mais métodos que a classe Memcache, porém, a utilização básica é a seguinte:

$memcache = new Memcached();

$memcache->addServer('memcache1.exemplo.com', 11211, 70);
$memcache->addServer('memcache2.exemplo.com', 11211, 30);

$informacao = $memcache->get($id);
if (!$informacao) {
    $informacao = obter_informacao_da_fonte_original();
    $memcache->set($id, $informacao, $time_expiracao);
    $memcache->quit();
}

Note as diferenças com a classe Memcache: ao invés de connect, são especificados os servidores do pool que provêem memcache através do método addServer, que aceita o host, porta e o peso do servidor (probabilidade de ele ser escolhido para que haja balanceamento de carga controlado). Ao realizar o set, o terceiro parâmetro é o timestamp de expiração da informação. Ao invés de close, é usado o método quit para fechar as possíveis conexões abertas.

Outros método úteis da classe Memcached

  • __construct: no construtor da classe é possível passar um identificador único para realizar conexões persistentes baseadas no identificador.
  • delete: apaga um dado do memcache a partir de seu código de identificação.
  • getDelayed: permite consultar vários dados pelos códigos de identificação e iterar sobre eles com o método fetch.
  • setMulti: armazena vários dados de uma única vez através de um array associativo indexado pelo código de identificação do dado a ser armazenado.
  • getMulti: consulta vários dados de uma única vez através da especificação de um array com os códigos de identificação dos dados a serem obtidos. O método devolve um array associativo indexado pelos códigos de identificação dos dados que puderam ser obtidos.
  • getResultCode: é usado após uma operação para saber o resultado exato dela. Por exemplo, após chamar o método get, utilizamos este método para saber se o dado foi encontrado, se ele não foi encontrado, se ele expirou, etc.
  • setSaslAuthData: é usado para especificar as credenciais de acesso a servidores memcache que habilitaram a autenticação.
  • touch: muda a data de expiração de um dado a partir de seu código de identificação.

Para mais informações, acesse a lista completa de métodos da classe Memcached

Cache em Arquivo

Armazenar cache em arquivo também pode ser uma boa opção dependendo do cenário. Ele possui uma abordagem similar ao APC, onde cada servidor de aplicação terá seu próprio cache, havendo duplicação de dados, mas não ocupará dados em memória RAM, mas sim em seu dispositivo de armazenamento não volátil (HD, SSD, etc).

É possível montar uma arquitetura em que uma particão virtual é utilizada pelos servidores de aplicação para acessar arquivos (com dados de cache) armazenados em um servidor externo. Isso torna a arquitetura mais parecida com a apresentada no Memcache/Memcached.

Outra possibilidade é utilizar um diretório montado como partição especial em memória no sistema de arquivos. Neste caso, acessamos um arquivo de forma convencional, mas na verdade ele está disponível em memória, o que torna mais rápido, porém, com um limite de espaço provavelmente menor.

Para montar um cache em arquivo, basta usar algumas funções básicas do PHP:

$informacao = obter_informacao_do_cache_arquivo($id);
if (!$informacao) {
    $informacao = obter_informacao_da_fonte_original();
    salvar_informacao_em_cache_arquivo($id, $informacao, $time_expiracao);
}

/// Implementacao das funcoes usadas

function obter_informacao_do_cache_arquivo($id) {
    $arquivo = sprintf('/tmp/%s.cache', md5($id));

    // Se nao encontrar o arquivo com o cache
    if (!file_exists($arquivo)) {
        return false;
    }

    $cache = unserialize(file_get_contents($arquivo));

    // Se o cache ja expirou
    if ($cache['expiracao'] && time() > $cache['expiracao']) {
        unlink($arquivo);
        return false;
    }

    // Se o cache eh valido
    return $cache['valor'];
}

function salvar_informacao_em_cache_arquivo($id, $informacao, $time_expiracao = 0) {
    $arquivo = sprintf('/tmp/%s.cache', md5($id));

    $cache = array(
        'expiracao' => $time_expiracao,
        'valor'     => $informacao
    );

    file_put_contents($arquivo, serialize($cache), LOCK_EX);
}

Observe que tanto a função que obtém uma informação quanto a que salva a informação no cache compartilham a mesma forma de se determinar o caminho do arquivo. No caso, foi utilizado o diretório "/tmp" para o propósito.

Um outro detalhe importante a ser considerado nesta implementação é que, dependendo do tipo de partição do sistema de arquivos, manter o diretório de cache com milhares de arquivinhos pequenos pode prejudicar a performance para encontrar o arquivo. Uma solução para este inconveniente é manter uma quantidade limitada de arquivos de cache, ou então agrupá-los em sub-diretórios. Uma forma de fazer isso seria usar os primeiros caracteres do md5 do código de identificação como sendo nomes de sub-diretórios. Por exemplo, se um md5 obtido foi "6c716b189babf179a712570d31560673", então o arquivo deve ser armazenado ou obtido em "/tmp/6/c/7/6c716b189babf179a712570d31560673.cache". Neste caso utilizamos 3 níveis de sub-diretórios utilizando os 3 primeiros caracteres retornados do md5 (como o md5 sempre devolve 32 caracteres, é seguro obter um número fixo de caracteres do início). Como resultado, cada nível de diretório conterá menos arquivos e, portanto, a velocidade de encontrar o arquivo será maior. Esta é a mesma estratégia usada pela sessão quando se utiliza o armazenamento em arquivo.

Outras formas de cache

Além dos exemplos mostrados no artigo, também existem outras formas de cache, tais como:

  • redis: banco de dados em memória que pode ser usado como cache.
  • Wincache: Cache para PHP no Windows.

1 comentário