CACHEFS: Um sistema de arquivos volátil para cache de processamento
Andrew Clausen, Elvis Pfützenreuter (redator), Rik Van Riel
Mandar comentários para
lvis.pfutzenreuter@gmail.com
NOTA: as idéias centrais do artigo surgiram de uma conversa on-line entre as pessoas citadas acima, portanto considero-os todos como autores do artigo. Como achei a idéia interessante, me propus a redigir um texto a respeito.
0) Diminuindo a latência pelo armazenamento ("cache") dos resultados de processamentos custosos
A latëncia ("atraso") das operações é o principal fator de satisfação, ou instatisfação, de um usuário ao utilizar um computador. Muito embora a vazão (throughput) seja o índice mais popular de metrificação de desempenho, na verdade a latência é mais importante.
Tomando um exemplo aleatório, considere-se um link ADSL versus uma linha discada. A experiëncia do usuário em ADSL é considerada muitíssimo mais satisfatória, embora a vazão possa ser apenas 4 ou 5 vezes maior (considerando uma conexão discada 56kbps contra um link ADSL de 300kbps). Boa parte, ou a maior parte, da melhoria percebida com ADSL deve-se à menor latência de rede. (Ainda, em se tratando de rede TCP/IP, a latência da conexão é exacerbada pelos protocolos TCP e HTTP.)
Os computadores de mesa têm cada vez mais RAM e CPU à sua disposição. Ainda assim, a experiência do usuário não tem melhorado na mesma proporção, até porque os aplicativos também estão cada vez mais complexos. Antigamente, o usuário abria um arquivo em modo texto. Hoje, ele abre um PDF ou um página Web cheia de figuras, cuja renderização tem alto custo de processamento, e naturalmente o aplicativo "hesita" antes de exibir o conteúdo, o que é percebido pelo usuário como lentidão pura e simples ("meu documento em modo texto abria num piscar de olhos!")
Por mais que haja RAM e CPU abundantes, o custo e conseqüente latência de algumas operações comuns vai continuar relevante por um bom tempo. Os aplicativos têm de usar outros meios, menos convencionais, de aproveitar os recursos disponíveis para melhorar a experiência do usuário.
Um desses meios é o armazenamento temporário (cache) do processamento. O uso de caches já é lugar-comum na exploração da localidade de dados (caches de CPU e proxies Web estão em todo lugar), mas ainda não no caso de processamento.
Armazenar e.g. a renderização de uma página Web parece algo heterodoxo, pois é um grande volume de dados. Mas, como já foi dito, a "sobra" de recursos como RAM e CPU autoriza a adoção desse tipo de truque.
1) Como e onde armazenar tais processamentos de forma globalmente eficiente
Qualquer aplicativo poderia armazenar dados pré-processados, seja em RAM ou em arquivos temporários. Mas fazer isso indiscriminadamente pode ser danoso para o desempenho do computador como um todo. Talvez o sistema de I/O seja excessivamente lento, e refazer o processamento fosse mais barato. Ou talvez a RAM esteja escassa mas há CPU sobrando.
O problema é que um processo tem uma visão muito limitada do computador, e não tem como saber perfeitamente qual seria a melhor estratégia global.
A primeira idéia, levantada pelo Clausen, foi delegar essa decisão ao gerenciador de memória virtual (VMM) do sistema operacional. A VMM já toma decisões do gênero em sistemas operacionais típicos (e.g. quanta RAM usar como cache de disco numa situação de escassez).
O próximo problema é - como os processos informam à VMM que determinado objeto é temporário?
2) Sistema de arquivos temporário volátil ("cachefs")
Uma possível resposta é um sistema de arquivos temporário volátil, que batizamos de "cachefs".
Tal sistema de arquivos funcionaria de forma semelhante ao "tmpfs" do Linux e outros Unixes comerciais, ou seja, um sistema de arquivos em RAM para arquivos temporários, não para dados persistentes.
A primeira e principal diferença do cachefs em relação ao tmpfs é que no cachefs os arquivos são voláteis, ou seja, podem ser apagados pela VMM a qualquer tempo sem aviso prévio.
A segunda diferença é que o cachefs não deveria ter tamanho máximo fixo. Seu tamanho aumenta ou diminui conforme a escassez de RAM.
Um sistema de arquivos nesses moldes poderia ser utilizado para "cache" de processamentos custosos, sem a necessidade de criação de qualquer API nova.
3) Como os aplicativos devem usar o sistema de arquivos
Pelo fato de os arquivos do cachefs poderem ser preemptivamente apagados a qualquer momento, os aplicativos que forem armazenar dados nele devem adotar a seguinte estratégia:
a) criar o arquivo com o conteúdo do processamento, com um nome unívoco;
b) fechar o arquivo, retendo seu nome;
c) quando o cache do processamento for requerido, tentar abrir o arquivo. Se ele não existir, fazer "fallback" para reprocessamento.
A priori, há a necessidade de fechar o arquivo e reabrí-lo, pois em Unix um arquivo aberto, mesmo apagado, continua ocupando espaço no sistema de arquivos, e o dono do arquivo aberto tem a garantia de continuar usando o arquivo até fechá-lo.
Isto aponta para o próximo problema: como evitar que algum aplicativo abuse do cachefs, mantendo os arquivos abertos para evitar sejam apagados pela VMM?
Uma possível saída é retirar, do cachefs apenas, a garantia Unix de que "arquivo aberto não desaparece". A VMM poderia eliminar o arquivo e também invalidar os manipuladores de arquivo correspondentes. Quando o aplicativo tentasse ler ou gravar o arquivo, a chamada retornaria erro.
O aplicativo deve tratar graciosamente este erro, optando por refazer o processamento ao invés de recuperá-lo do cache.
Mesmo que o arquivo-cache ainda esteja em fase de gravação (para posterior recuperação), o arquivo poderia ser invalidado e a chamada write() retornar erro. Deste erro, o aplicativo deve inferir que o cache não é bem-vindo pela VMM.
Se o aplicativo quiser ser realmente esperto, evitará usar o cache por determinado tempo, depois de ter recebido um erro durante criação ou leitura do arquivo (visto que a VMM tentaria realmente apagar arquivos fechados em primeiro lugar).
Portanto, apesar do cachefs não adicionar nada à API, os aplicativos teriam de ser muito mais cuidadosos do que normalmente são, quando fossem lidar com arquivos de cache.
Neste cenário em que um arquivo pode "sumir" mesmo aberto, uma chamada de sistema problemática é a mmap(), que mapeia um arquivo na memória virtual.
Até onde lembro, o acesso inválido a um arquivo mapeado em memória (e.g. um arquivo de um volume de rede NFS ou SMB) gera um sinal SIGBUS. Poderíamos obrigar os aplicativos usuários do cachefs a tratar o sinal SIGBUS de forma graciosa, mas honestamente não sei se isto é uma boa idéia:
* Tratar sinais complica a vida do desenvolvedor do aplicativo. Infelizmente não há nada como a SEH (Structured Exception Handling) do Win32;
* Sinais em processos multithreaded são problemáticos, pois é a thread primária quem recebe os sinais. O sinal teria então de ser comunicado à thread que realmente o provocou, o que complica ainda mais a vida do desenvolvedor;
* O despacho de sinais é relativamente lento, não é algo para se usar rotineiramente, e sim para acontecimentos realmente excepcionais.
Uma outra saída seria proibir o uso de mmap() para o cachefs. Talvez seja uma boa saída, já que arquivos de cache tendem a ser gravados e lidos sempre inteiros, não são "trabalhados" por longos períodos de tempo; desta forma, proibir o mmap() não seria causa de perda de desempenho.
4) E como a VMM elegeria os arquivos a serem apagados?
Um belo dia, a RAM do sistema escasseia, e chega a hora de apagar alguns arquivos de cache. O coração de um bom cachefs é o algoritmo de escolha dos arquivos a serem apagados.
Tal algoritmo levaria em conta os seguintes fatores:
a) Custo do processsamento que cada arquivo representa (mais custo = menos chance de ser apagado)
b) Arquivo aberto ou fechado (aberto = não apaga, ou ao menos evita apagar)
c) Tamanho do arquivo (maior = mais chance de ser apagado)
d) Idade do arquivo (mais velho = mais chance de ser apagado)
e) Último uso do arquivo (mais antigo = mais chance de ser apagado)
f) Freqência de uso do arquivo (menos vezes = mais chance de ser apagado)
5) Como calcular o custo de processmento e rotular o arquivo?
Determinar o custo de processamento de um determinado objeto é por si só um problema complexo. Seria necessário determinar a quantidade de RAM, CPU e I/O aplicados naquele processamento.
Isto teria de ser feito dentro do próprio aplicativo, pois apenas ele tem condição de saber que recursos do sistema foram aplicados naquele processamento (e mesmo assim é um problema difícil).
Algumas opções simplistas seriam cronometrar o tempo demandado no processamento, ou atribuir um "peso" pré-fixado. Atribuir um peso tem o mérito da extrema simplicidade. Cronometrar o tempo dá uma boa idéia da latência enfrentada pelo usuário.Afinal, o objetivo final de tudo isso é reduzir o tempo que o usuário fica esperando...
Em qualquer das alternativas acima, restam dois problemas:
* o aplicativo pode mentir, superestimando os custos para diminuir a chance de que os "seus" arquivos de cache sejam apagados do cachefs.
* seria necessário ou criar uma API ou impor um formato de dados para que o aplicativo repasse o valor de custo ao sistema operacional. (o "resouce forks" do Macintosh fez falta aqui!)
Seria realmente muito mais interessante que o cachefs pudesse determinar preemptivamente o custo de processamento, e armazenasse essa informação internamente, o que elimina de plano todos os problemas supracitados.
FIXME
6) Outras questões em aberto
a) Balanço entre RAM usada para cache convencional de arquivos (na verdade, cache dos dispositivos de armazenamento) e a RAM consagrada ao cachefs;
b) Limites mínimo e máximo de tamanho do cachefs;
c) Balanço entre swapping e tamanho do cachefs. Neste ponto, eu penso que o cachefs deve ser reduzido a zero *antes* de fazer swapping. O Clausen objeta, alegando que pode haver processamento mais custoso que swapping - o que justificaria fazer swap de páginas do próprio cachefs.

