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:
- HTTP Conditional Get for RSS Hackers
- RSS Scaling Issues
- RSS Scaling Problems: How Can We Help?
- Sam Ruby: Vary: ETag
- Joel’s RSS problem
- Manual do PHP: header()
- Manual do PHP: ob_start()
[Update] vou pensar sobre isso aqui e retorno com novidades.


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.