Implementando caching em feeds RSS/ATOM

Nos últimos meses tenho notado que os feeds RSS/ATOM que publico neste site recebem mais acessos que qualquer outra URL. E acredito que isso aconteça na maioria dos sites que publicam algum tipo de feed.

O problema é que a maior parte do tráfego gerado por esses feeds é, na verdade, desperdiçada.

Para minimizar este problema, estou tirando proveito dos mecanismos de caching adotados por quase todos os user agents que suportam RSS/ATOM.

Vou relatar um pouco da minha experiência recente ao lidar com esse problema e as soluções que encontrei. Espero que seja útil.

O consumo de banda gerado por RSS e ATOM está se tornando realmente preocupante. Os grandes culpados por esse problema são os desktop aggregators, programas que rodam no cliente e geralmente são configurados para baixar os feeds a cada uma hora, ou até menos. Agregadores centralizados, como o Bloglines não têm esse problema, pois baixam cada feed uma vez apenas num determinado período de tempo, independentemente do número de usuários que estejam inscritos.

Felizmente, a maioria dos agregadores — tanto os desktop quanto os baseados em web — usam mecanismos de caching e é sobre isso que vou falar.

Se os feeds do seu site são armazenados em arquivos XML estáticos, você não precisa se preocupar com caching pois o servidor web se encarrega disso pra você. O alvo aqui são os feeds gerados sob demanda, a partir de dados armazenados em um banco de dados, por exemplo.

Para poder tirar proveito do caching é preciso entender os headers HTTP Last-Modified e ETag enviados pelo servidor web quando uma URL é requisitada, e seus respectivos pares If-Modified-Since e If-None-Match enviados pelo user agent.

A dupla Last-Modified e If-Modified-Since faz parte do HTTP 1.0. Last-Modified é a data (GMT) da última atualização. Quando o cliente faz a primeira requisição por um recurso, o servidor envia um código de resposta 200 OK junto com um header Last-Modified e o conteúdo é transferido por completo. O cliente guarda este valor e em uma requisição subseqüente o envia ao servidor em um header If-Modified-Since. Estes valores são comparados e se o conteúdo não tiver sido modificado desde a última requisição, o servidor responde com um 304 Not Modified e nada é transferido — o cliente usa a versão presente no cache.

Desta mesma forma funciona a dupla ETag e If-None-Match, presentes no HTTP 1.1.
A diferença é que a ETag não se baseia necessariamente na data de modificação. O apache, por padrão, dado um recurso estático (um arquivo HTML por exemplo), usa seu inode, tamanho e data de modificação para gerar a ETag.
De acordo com o RFC2616, a ETag pode conter qualquer valor, que precisa ser envolvido por aspas.

Bom, deixando de lado essa minha explicação meia-boca sobre HTTP, vamos ao que interessa: implementar esse mecanismo usando PHP.

Então digamos que você gera seus feeds dinamicamente, através de um script que pega os dados de um banco de dados e gera o resultado no seu formato de feed preferido.

Por ser um recurso dinâmico, é impossível para o servidor saber se ele foi modificado antes de executá-lo, portanto é seu dever verificar, antes de gerar qualquer saída de dados, se houve modificação no conteúdo.
No caso específico dos feeds, o que indica se ele foi ou não modificado é a data de modificação do último post, ou melhor, a data do primeiro <item> no caso do RSS ou da primeira <entry> no ATOM.

Vamos usar para exemplo uma tabela chamada posts que contém, entre outros, dois campos de data: data_post e data_mod (data de postagem e modificação, respectivamente). Vamos ignorar os outros campos, já que eles não interessam para esse exemplo.

Antes de qualquer coisa, vamos ligar o output buffering:

ob_start();

Se você quiser compactar o conteúdo com gzip, use:

ob_start("ob_gzhandler");

Faremos uma consulta ao banco de dados, ordenada pela data de modificação:

$sql = "SELECT * FROM posts ORDER BY data_mod DESC";
$result = mysql_query($sql);
$row = mysql_fetch_object($result);

Agora vamos converter essa data para GMT:

$gmt_date = gmdate("D, d M Y H:i:s", $row->data_mod) . ' GMT';

E pegar seu hash md5:

$hash = md5($gmdate);

Precisamos dos headers enviados pelo user agent:

$headers = getallheaders();

Esse hash será usado como valor para ETag. Como dito acima, não precisa ser md5, pode ser o que você quiser.

Vamos verificar então se o user agent enviou os headers que nos interessam e comparar com os valores obtidos acima:

if ((isset($headers["If-None-Match"])) || (isset($headers["If-Modified-Since"]))){
	if(((ereg($hash, $headers["If-None-Match"]))) || ($headers["If-Modified-Since"] == $gmtime)){
		header("HTTP/1.1 304 Not Modified");
		ob_end_clean();
		exit;
	}
}

Explicando: se o user agent enviou um dos dois referidos headers é porque já tem o conteúdo em cache. Comparamos os valores dos headers com os calculados acima e, caso haja coincidência, enviamos um 304 Not Modified, encerramos e esvaziamos o buffer de saída e finalizamos a execução do script. O cliente fica com a versão em cache.
Note que usei ereg() para comparar o valor da ETag com o If-None-Match. Eu poderia ter usado uma comparação simples incluindo as aspas. Na verdade tanto faz, mas como não se pode prever totalmente o comportamento dos user agents, eu preferi usar ereg().

Perceba que esse código deve ser colocado logo no início do arquivo, antes do conteúdo começar a ser enviado para a saída. Para completar, inclua ao final do arquivo:

header("Last-Modified: ".$gmtime);
header("ETag: "$hash"");
ob_end_flush();

Pronto. Está feito o caching. Se o seu site for muito visitado, com certeza você vai notar uma grande economia em tráfego. Se não, de qualquer maneira vai estar economizando a preciosa banda…

Alguns links sobre o assunto:

[Update] vou pensar sobre isso aqui e retorno com novidades.

Leia também:

6 Comentários sobre “Implementando caching em feeds RSS/ATOM”

Faça um comentário

Muito bom esse artigo, tenho passado por esse tipo de problema também e acho que isso vai ser muito útil para todos que divulgam conteúdo através de feeds RSS/Atom.

Parabéns.


#2 | Jorge Krug

E se o conteúdo dinâmico fosse gravado num arquivo .xml cada vez que houvesse alteração no conteúdo, atraves de um script cron, atraves de uma checagem periódica em um script que fosse dispardo conforme o acesso ou mesmo quando houvesse alteracao nos dados via inclusao ou alteracao da informação? Isso não resolveria o problema?


#3 | Hugo

Muito interessante a solução com o ETag, inclusive pode servir para outras coisas, mas ainda acho que a solução do JorgeKrug de gravar em um arquivo .xml toda vez que houver modificação é bem mais prático e rápido, pois o PHP sequer é executado =].


Bruno, aproveitando, os links para seus posts no seu feed está com problemas ;-)


Parabéns Bruno,

Curti pra caramba seu post, pode ter certeza que esta sua idéia vai ajudar muita gente a economizar banda, com certeza vou modificar a forma que o rss do meu site é gerado e utilizar estas suas idéias.

E quanto as explicações sobre HTTP, não foram meia boca, foram simples e bastante úteis.

Novamente parabéns pelo post.


[…] Outra coisa interessante é que usando o feedburner você automaticamente passa a economizar na quantidade de banda consumida pelos seus feeds. Isso acontece porque o feedburner acessa o feed uma vez a cada 30 minutos. Ou seja, independente do número de usuários e da frequência de atualização usada por cada um, o seu feed vai ser acessado apenas 48 vezes por dia. E se você usa algum mecanismo de caching no seu feed, ele será efetivamente baixado apenas uma vez a cada novo post ou atualização. É uma bela economia. […]


« Martin Michlmayr - Líder do projeto Debian GNU/Linux

Novo layout e style switcher »

Deixe seu comentário

Buscas populares: Ganhar dinheiro, AdSense, Velox, Acessibilidade, IE7, CSS Position, Quero ganhar dinheiro


Veja as estatísticas

uk vpn Mp3sparks