Previsão: Nublado

Estratégias de Cache do Windows Azure

Joseph Fultz

Joseph Fultz
As minhas duas etapas com cache iniciaram há muito tempo, durante o boom do ponto-com. Certamente, foi preciso armazenar partes de dados no cliente ou na memória para facilitar ou agilizar os procedimentos para os aplicativos criados até aquele momento. No entanto, somente após o advento da Internet e, sobretudo, do comércio através da Internet, é que meu pensamento realmente evoluiu no tocante às estratégias de cache empregadas nos meus aplicativos, tanto na Web quanto na área de trabalho.

Nessa coluna, irei mapear diversos recursos de cache do Windows Azure das estratégias de cache para saída, dados na memória e recursos de arquivos e tentarei equilibrar o desejo de obter dados atualizados com o desejo de obter melhor desempenho. Finalmente, eu abordarei um pouco da indireção como meio de cache inteligente.

Armazenamento em cache do recurso

Ao abordar o armazenamento em cache do recurso, eu me refiro a tudo o que é serializado num formato de arquivo, consumido no ponto de extremidade. Isso inclui tudo, desde objetos serializados (como XML e JSON, por exemplo) a imagens e vídeos. Você pode tentar usar cabeçalhos e marcas meta para influenciar o comportamento do cache do navegador, porém, muitas vezes, as sugestões não serão acatadas adequadamente e é quase seguro afirmar que as interfaces do serviço ignorarão os cabeçalhos. Portanto, ao perder a esperança de que é possível armazenar em cache o conteúdo de recursos modificados lentamente no cliente da Web e obter sucesso, o que seria no mínimo uma garantia de desempenho e comportamento de acordo com a carga, nós precisamos retroceder uma etapa. No entanto, em vez de retornar ao servidor Web, na maioria dos recursos, é possível utilizar uma rede de distribuição de conteúdo.

Ponderando sobre o caminho a partir do cliente, observa-se a oportunidade existente entre os servidores Web front-end e o cliente, onde é possível aproveitar um ponto de passagem de espécies, principalmente no que se refere a amplitudes geográficas, para aproximar o conteúdo dos consumidores. O conteúdo não é armazenado apenas nesses pontos, mas principalmente, próximo aos consumidores finais. Os servidores usados para distribuição são conhecidos coletivamente como rede de entrega/distribuição de conteúdo. Nos primórdios da explosão da Internet, a ideia e as implementações do armazenamento em cache de recurso distribuído da Web eram relativamente recentes e empresas como a Akami Technologies detectaram uma grande oportunidade na venda de serviços para ajudar a expansão de sites da Web. Com o avanço de uma década, a estratégia é mais importante do que nunca num mundo onde a Web nos une, enquanto permanecemos fisicamente separados. Para o Windows Azure, a Microsoft fornece a Rede de Distribuição de Conteúdo (CDN) do Windows Azure. Embora seja uma estratégia válida de armazenamento em cache de conteúdo e de aproximação do consumidor, a realidade é que a CDN é mais comumente usada por sites da Web que precisem atender a uma ou a todas as condições de grande escala e grandes quantidades ou de tamanhos de recursos. Uma boa postagem sobre o uso da Windows Azure CDN pode ser encontrada no blog de Steve Marx (bit.ly/fvapd7), ele trabalha na equipe do Windows Azure.

Na maioria dos casos, ao implantar um site da Web, parece bastante óbvio que os arquivos precisem ser localizados nos servidores do site. Numa Função da Web do Windows Azure, o conteúdo do site é implantado no pacote, portanto, já concluí. Espere, as imagens de marketing mais recentes não foram instaladas com o pacote; é necessário implantar novamente o conteúdo. A atualização desse conteúdo significa, na realidade, a nova implantação do pacote. Certamente, o pacote pode ser implantado no estágio atual e alterado, todavia haverá um atraso ou uma possível interrupção para o usuário.

Uma maneira simples de fornecer uma armazenamento em cache atualizável do conteúdo front-end da Web é armazenar a maior parte do conteúdo no Armazenamento do Windows Azure e apontar todos os URIs para os contêineres do Armazenamento do Windows Azure. Contudo, por vários motivos, pode ser preferível manter o conteúdo com as Funções da Web. Um modo de garantir que o conteúdo da Função da Web possa ser atualizado ou que um novo conteúdo possa ser adicionado é manter os arquivos no Armazenamento do Windows Azure e movê-los para um contêiner de armazenamento de recursos local nas funções da Web, se necessário. Há algumas variações desse tema disponíveis e eu abordei uma delas na postagem de um blog de março de 2010 (bit.ly/u08DkV).

Cache na memória

Enquanto a discussão sobre cache anterior focou realmente no movimento dos recursos baseados em arquivos, a seguir, eu focarei em todos os dados e no conteúdo do site renderizado dinamicamente. Eu já realizei inúmeros testes de desempenho e de otimização focada no desempenho do site e do banco de dados por trás dele. Possuir um plano de cache e de implementação sólido, que cubra o cache de saída (HTML renderizado que não precise ser renderizado novamente e que possa ser enviado ao cliente) e de dados (geralmente no estilo cache-aside) o ajudará muito a melhorar a escala e o desempenho, supondo que a implementação do banco de dados não seja interrompida inerentemente.

A maior dificuldade ao implementar uma estratégia de cache num site é determinar o conteúdo a ser armazenado e a frequência da atualização desse conteúdo mediante o que permanece renderizado dinamicamente em cada solicitação. Além dos recursos fornecidos pelo Microsoft .NET Framework para o cache de saída e o System.Web.Caching, o Windows Azure fornece um armazenamento em cache distribuído denominado Windows Azure App­Fabric Cache (AppFabric Cache).

Cache distribuído

Um cache distribuído ajuda a resolver vários problemas. Por exemplo, embora o cache seja sempre recomendado para o desempenho do site, usar o estado de sessão é normalmente contra-indicado, mesmo que isso forneça um cache contextual. O motivo para tal é que obter o estado de sessão requer que o cliente esteja ligado a um servidor, o que afeta negativamente a escalabilidade ou que este esteja sincronizado com os servidores de um farm, o que geralmente tem a reputação de apresentar problemas e limitações. O problema do estado de sessão é solucionado pelo uso de um cache distribuído capaz e estável para oferecer suporte. Isso permite que os servidores obtenham os dados sem recorrer continuamente à caixa para obtê-los e fornece, ao mesmo tempo, um mecanismo de gravação dos dados e de propagação perfeita desses dados nos clientes do cache. Dessa forma, o desenvolvedor terá a seu dispor um cache contextual rico, além de manter a qualidade da escala de um farm da Web.

A melhor novidade sobre o AppFabric Cache é que você pode usá-lo sem fazer muito além de alterar algumas configurações relativas ao estado de sessão e também o fato deste possuir uma API de fácil utilização para uso programático. Consulte o artigo de Karandeep Anand e Wade Wegner na edição de abril de 2011 para obter alguns detalhes interessantes sobre o uso do cache(msdn.microsoft.com/magazine/gg983488).

Infelizmente, se você estiver trabalhando com um site existente que chame diretamente o System.Web.Caching no código, combinar o AppFabric Cache com este lhe dará um pouco mais de trabalho. Há dois motivos para isso:

  1. A diferença nas APIs (consulte Figura 1)
  2. A estratégia do que armazenar em cache e onde armazenar

Figura 1 Adicionar Conteúdo pela API do Cache

Adicionar o AppFabric Cache ao Cache Adicionar o System.Web.Caching ao Cache

DataCacheFactory cacheFactory=

  nova DataCacheFactory(configuração);

DataCache appFabCache =

  cacheFactory.GetDefaultCache();

valor da cadeia de caracteres =

  "Essa cadeia de caracteres deve ser armazenada em cache no local";

appFabCache.Put("SharedCacheString", valor);

System.Web.Caching.Cache LocalCache =

  novo System.Web.Caching.Cache();

valor da cadeia de caracteres =

  "Essa cadeia de caracteres deve ser armazenada em cache no local";

LocalCache.Insert("localCacheString", valor);

A Figura 1 ilustra claramente que quando você observa até os elementos básicos das APIs, há definitivamente uma diferença. Criar uma camada de indireção para mediar uma chamada ajudará a agilizar o código em seu aplicativo. Obviamente, isso demandará algum trabalho para oferecer a capacidade de usar os recursos avançados dos três tipos de cache, mas os benefícios compensam o esforço de implementar a funcionalidade necessária.

Embora o cache distribuído solucione alguns problemas complexos, ele não deverá ser usado como uma poção mágica que cura tudo ou correrá o risco de ter a mesma eficácia de um veneno de cobra. Em primeiro lugar, dependendo de como as coisas forem balanceadas e dos dados que serão inseridos no cache, é possível que mais buscas fora da máquina sejam necessárias para obter dados no cliente do cache local, o que afetaria negativamente o desempenho. O mais importante é o custo da implantação. Até a publicação deste, o custo de 4GB do cache compartilhado do AppFabric é de $325 por mês. Embora essa não seja uma quantia demasiadamente alta e 4GB pareça um bom espaço de cache, num site de tráfego intenso, principalmente um site de suporte ao estado de sessão com o AppFabric Cache e um conteúdo direcionado rico, seria fácil preencher vários caches desse tamanho. Considere os catálogos de produtos que tenham diferenças de preços com base nas camadas de clientes ou na determinação de preços de contrato personalizado.

Indireção de Cache-Aside

Como ocorre em muitas situações na indústria de tecnologia, e eu diria que em várias outras, o desenho é uma mistura de implementações técnicas ideais modificadas pela realidade fiscal. Assim, mesmo quando você estiver apenas usando o Windows Server 2008 R2 AppFabric Caching, haverá motivos para continuar usando o cache local fornecido pelo System.Web.Caching. Numa primeira etapa da indireção, eu posso ter inserido as chamadas em cada biblioteca de cache e fornecido uma função para cada uma, tal como AddtoLocalCache(chave, objeto) e AddtoSharedCache(chave, objeto). No entanto, isso significa que cada vez que uma operação de cache for necessária, o desenvolvedor tomará uma decisão opaca e pessoal sobre o local onde o cache deverá ser realizado. Essa lógica é rompida rapidamente na manutenção e em equipes maiores e levará, inevitavelmente, a erros imprevistos, já que o desenvolvedor poderia optar por adicionar um objeto a um cache inapropriado ou adicionar um cache e acessar outro acidentalmente. Portanto, várias buscas de dados adicionais seriam necessárias porque os dados não estarão no cache ou estarão no cache errado quando forem acessados. Isso acarreta cenários tais como a observação inesperada de um desempenho ruim para detectar que as operações de adição foram realizadas num cache e que as operações de aquisição foram, inexplicavelmente, realizadas em outro, pelo simples motivo de esquecimento ou de erro de digitação do desenvolvedor. Além disso, ao planejar um sistema adequadamente, esses tipos de dados (entidades) serão identificados antes do tempo e, com essa definição, também devem ser apresentadas ideias do local de utilização de cada entidade, dos requisitos de coerência (principalmente nos servidores com carga balanceada) e do grau de atualização destes. Portanto, as decisões sobre onde armazenar em cache (compartilhado ou não) e a expiração podem ser tomadas antes do tempo e ser parte da declaração.

Como mencionado anteriormente, deve haver um plano para o cache. Muitas vezes, este é adicionado aleatoriamente ao final de um projeto, mas ele requer a mesma ponderação e projeto de qualquer outro aspecto do aplicativo. Ele é muito importante ao lidar com a nuvem porque as decisões que não são bem ponderadas podem gerar custo extra, além de deficiências no comportamento do aplicativo. Ao considerar os tipos de dados que devem ser armazenados em cache, uma opção é identificar as entidades (tipos de dados) envolvidos e o seu ciclo de vida no aplicativo e na sessão do usuário. A partir dessa observação, nota-se que seria interessante se a própria entidade pudesse realizar o armazenamento em cache de maneira inteligente, com base no seu tipo. Felizmente, essa é uma tarefa fácil com a ajuda de um atributo personalizado.

Eu estou pulando a configuração de cache porque o material mencionado anteriormente abrange bem esse assunto. Para a minha biblioteca de cache, eu simplesmente criei uma classe estática com métodos estáticos para a minha amostra. Em outras implementações, há bons motivos para fazer isso com objetos de instância, mas para garantir a simplicidade desse exemplo, eu estou fazendo isso de forma estática.

Vou declarar um enum para indicar o local e a classe que herda o Atributo para implementar meu atributo personalizado, conforme mostrado na Figura 2.

Figura 2 Declarando um Enum para Implementar um Atributo Personalizado

public enum CacheLocationEnum
{
  None=0,
  Local=1,
  Shared=2
}
public class CacheLocation:Attribute
{
  private CacheLocationEnum _location = CacheLocationEnum.None;
  public CacheLocation(CacheLocationEnum location)
  {
    _location = location;
  }
  public CacheLocationEnum Location { get { return _location; } }
}

Transmitir o local no construtor facilita o uso posterior no código, porém eu também fornecerei um método somente leitura para buscar um valor porque precisarei deste para a instrução do caso. Criei alguns métodos privados para serem adicionados aos dois caches dentro da minha biblioteca CacheManager:

private static bool AddToLocalCache(string key, object newItem)
{...}
private static bool AddToSharedCache(string key, object newItem)
{...}

Para uma implementação real, eu provavelmente precisarei de outras informações (como o nome do cache, as dependências, a expiração, etc.), mas, por agora, isso é o suficiente. A principal função pública para adicionar conteúdo ao cache é um método de modelo, facilitando a determinação do cache a partir do tipo, como mostrado na Figura 3.

Figura 3 Adicionando Conteúdo ao Cache

public static bool AddToCache<T> (string key, T newItem)
{
  bool retval = false;
  Type curType = newItem.GetType();
  CacheLocation cacheLocationAttribute =
    (CacheLocation) System.Attribute.GetCustomAttribute(typeof(T), 
    typeof(CacheLocation));
  switch (cacheLocationAttribute.Location)
  {
    case CacheLocationEnum.None:
      break;
    case CacheLocationEnum.Local:
      retval = AddToLocalCache(key, newItem);
      break;
    case CacheLocationEnum.Shared:
      retval = AddToSharedCache(key, newItem);
      break;
  }
  return retval;
}

Eu simplesmente usarei o tipo transmitido para obter o atributo personalizado e solicitar o meu tipo de atributo personalizado por meio do método GetCustomAttribute(tipo, tipo). Feito isso, basta uma simples chamada da propriedade somente leitura e uma instrução de caso e terei encaminhado a chamada ao provedor de cache apropriado. Para garantir o funcionamento adequado, é necessário adornar as declarações de classe adequadamente:

[CacheLocation(CacheLocationEnum.Local)]
public class WebSiteData
{
  public int IntegerValue { get; set; }
  public string StringValue { get; set; }
}
[CacheLocation(CacheLocationEnum.Shared)]
public class WebSiteSharedData
{
  public int IntegerValue { get; set; }
  public string StringValue { get; set; }
]}

Com toda a infra-estrutura do aplicativo configurada, será possível consumir no código do aplicativo. Eu abro o arquivo default.aspx.cs para criar as amostras de chamadas e adicionar o código para criar os tipos, atribuir valores e adicioná-los ao cache:

WebSiteData data = new WebSiteData();
data.IntegerValue = 10;
data.StringValue = "ten";
WebSiteSharedData sharedData = new WebSiteSharedData();
sharedData.IntegerValue = 50;
sharedData.StringValue = "fifty";
CachingLibrary.CacheManager.AddToCache<WebSiteData>("localData", data);
CachingLibrary.CacheManager.AddToCache<WebSiteSharedData>(
  "sharedData", sharedData);

Os nomes dos tipos esclarecem onde os dados serão armazenados em cache. No entanto, eu poderia alterar os nomes do tipo e ele seria menos óbvio com o cache controlado pela inspeção do atributo personalizado. Usar esse padrão ocultará do desenvolvedor de página os detalhes de onde os dados serão armazenados em cache e outros detalhes relacionados à configuração de item do cache. Assim, essas decisões são deixadas para a parte da equipe responsável pela criação de dicionários de dados e pela prescrição do ciclo de vida global dos dados mencionados. Observe o tipo transmitido às chamadas para AddToCache<t>(cadeia de caracteres, t). Implementar o restante dos métodos para a classe CacheManager (ou seja, GetFromCache) permitiria assumir o mesmo padrão usado aqui para o método AddToCache.

Equilibrando o custo com o desempenho e a escala

O Windows Azure fornece a infra-estrutura de software necessária para ajudá-lo em qualquer aspecto de sua implementação, inclusive do cache e se o cache se destina a recursos como os distribuídos por meio da CDN ou de dados que podem ser mantidos no AppFabric Cache. A chave para um grande desenho e subsequente implementação é balancear o custo com o desempenho e a escala. Uma última observação: Se você estiver trabalhando num novo aplicativo agora e estiver planejando a criação de cache no mesmo, vá em frente e inclua essa camada de indireção agora. Isso significa um pouco de trabalho adicional, mas como novos recursos como o AppFabric Caching são disponibilizados online, essa prática facilitará a incorporação ponderada e eficaz de novos recursos no seu aplicativo.    

Joseph Fultz é arquiteto de software da Hewlett-Packard Co. e trabalha no grupo de TI global do HP.com. Anteriormente, era arquiteto de software na Microsoft, trabalhando com seus clientes empresariais e ISV de camada superior, definindo soluções de arquitetura e design.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Wade Wegner