预测: 多云

Windows Azure 缓存策略

Joseph Fultz

Joseph Fultz
我用于缓存的两步过程最初是在网络快速发展时期创建的。的确,我当时必须在客户端或内存中的各个位置缓存一些数据,以便能够对我在此之前构建的应用程序更轻松或更快捷地执行操作。但直到互联网(尤其是互联网商务)呈现爆炸式增长后,我的一些关于我在 Web 和桌面等应用程序中采用的缓存策略的想法才真正开始发生变化。

在本专栏中,我会将各种 Windows Azure 缓存功能映射到输出、内存中数据和文件资源的缓存策略,并且我还将尝试在新数据需求与最佳性能需求之间取得平衡。最后,我将简要介绍一下进行智能缓存的间接方法。

资源缓存

我所说的资源缓存是指已序列化为在端点使用的文件格式的任何内容,其中包括从序列化对象(例如 XML 和 JSON)到图像和视频的所有内容。您可以尝试使用标头和 META 标记来影响浏览器的缓存行为,但这些建议往往不能得到很好的遵循,并且服务接口将忽略标头,这几乎是必然的结局。因此,不要指望我们可以在 Web 客户端成功缓存缓慢变化的资源内容 - 至少为了保证负载下的相应性能和行为 - 我们必须后退一步。但是,对于大多数资源,我们可以使用内容交付网络,而不是将其返回到 Web 服务器。

考虑从客户端返回的路径,我们可以利用前端 Web 服务器和客户端之间(特别是在广泛的地理位置中)的分类路标将内容放到离使用者更近的位置。不仅在这些位置缓存该内容,而且更重要的是,它离最终使用者更近。用于分发的服务器统称为内容交付/分发网络。在互联网迅速发展的早期,针对 Web 进行分布式资源缓存的想法和实施方式相当新颖,并且诸如 Akami Technologies 之类的公司已发现销售可帮助扩大网站规模的服务的大好时机。一晃十年过去了,Web 将分散在世界各地的人们联系在一起,该策略比以往任何时候都更加重要。Microsoft 针对 Windows Azure 提供了 Windows Azure 内容交付网络 (CDN)。尽管 CDN 是一种缓存内容和将内容移动到离使用者更近位置的有效策略,但现实是,通常情况下它更多地由具备大规模资源和/或大量或大型资源的网站使用。可在 Steve Marx(他在 Windows Azure 团队工作)的博客 (bit.ly/fvapd7) 上找到有关使用 Windows Azure CDN 的精彩文章。

在部署网站的大多数情况下,需要将文件放在网站服务器上,这一点似乎很明显。在 Windows Azure Web 角色中,网站内容部署在程序包中 - 看,我已完成了。等等,市场营销部门的最新映像未随程序包一起推送;需要重新部署。更新该内容目前实际上意味着重新部署程序包。当然,可以按阶段部署程序包和转换程序包,但用户端会出现延迟,或者可能出现停顿。

提供可更新的前端 Web 内容缓存的直接方法是,将大部分内容存储到 Windows Azure 存储中,并将所有 URI 指向 Windows Azure 存储容器。但是,出于各种原因,通过 Web 角色保存内容或许是最好方式。确保可刷新 Web 角色内容或可添加新内容的一种方法是,将文件保存在 Windows Azure 存储中,然后在需要时将它们移到 Web 角色上的本地资源存储容器中。该主题有几种不同的形式,我在 2010 年 3 月发布的一篇博客文章 (bit.ly/u08DkV) 中讨论了其中一种形式。

内存中缓存

上面的缓存讨论实际上重点关注移动基于文件的资源,下面我将重点介绍网站的所有数据和动态呈现内容。我已针对网站的性能和背后的数据库进行了大量性能测试和优化。毫无例外,具有涵盖输出缓存(不必再次呈现、可直接发送到客户端的已呈现 HTML)和数据(通常为缓存端样式)的可靠缓存计划和实现方式能够使您显著扩大规模和改进性能,前提是数据库实现本身未中断。

在网站中实现缓存策略的难点在于,确定在每次请求时哪些内容需要缓存、哪些内容仍动态呈现以及缓存内容的刷新频率。除 Microsoft .NET Framework 为输出缓存和 System.Web.Caching 提供的标准功能外,Windows Azure 还提供了名为 Windows Azure App­Fabric 缓存(简称 AppFabric 缓存)的分布式缓存。

分布式缓存

分布式缓存可帮助解决多个问题。例如,虽然始终建议通过缓存来保证网站性能,但即使会话状态可提供上下文缓存,通常也禁止使用它。原因是,获得会话状态要求客户端绑定到服务器(这会对可伸缩性产生不利影响),或者会话状态会在服务器场中的服务器之间进行同步,而通常有充分的理由认为这会导致出现问题和局限性。使用功能强大且可靠的分布式缓存对会话状态进行备份可解决会话状态问题。这使得服务器能够在无需连续访问数据库以获取数据的情况下便可拥有这些数据,同时提供了一种写入数据并在缓存客户端中无缝传播该数据的机制。这样可向开发人员提供充足的上下文缓存,同时维护 Web 场的规模质量。

有关 AppFabric 缓存的最好消息是,当涉及会话状态时,您只需更改一些配置设置便可使用它,并且它有一个可用于编程的简单易用的 API。有关使用该缓存的一些有用的详细信息,请阅读 Karandeep Anand 和 Wade Wegner 在 2011 年 4 月的期刊中发表的文章 (msdn.microsoft.com/magazine/gg983488)。

遗憾的是,如果您要处理直接在代码中调用 System.Web.Caching 的现有网站,则引入 AppFabric 缓存会比较麻烦。这有两个原因:

  1. API 之间存在差异(参见图 1
  2. 缓存内容和缓存位置的策略

图 1 按缓存 API 添加内容

添加到 AppFabric 缓存 添加到 System.Web.Caching 缓存

DataCacheFactory cacheFactory=

  new DataCacheFactory(configuration);

DataCache appFabCache =

  cacheFactory.GetDefaultCache();

string value =

  "This string is to be cached locally.";

appFabCache.Put("SharedCacheString", value);

System.Web.Caching.Cache LocalCache =

  new System.Web.Caching.Cache();

string value =

  "This string is to be cached locally.";

LocalCache.Insert("localCacheString", value);

图 1 清楚地演示了即使当您查看 API 的基本元素时,也肯定会看到其中存在差别。创建间接层以调配调用将有助于提高应用程序中代码的灵活性。很明显,需要做一些工作才能轻松地使用三种缓存类型的高级功能,而获得的好处将超过为了实现所需功能需要付出的努力。

虽然分布式缓存确实可以解决一些通常难于解决的问题,但不应将其用作解决所有问题的万能钥匙,它也不可能具有与万能钥匙相同的功效。首先,根据各方面因素达到平衡的方式和进入缓存中的数据,可能需要进行更多独立于计算机的提取才能使数据进入本地缓存客户端,而这会对性能产生负面影响。更重要的是部署成本。到撰写本文时为止,4GB AppFabric 共享缓存的成本是每月 325 美元。尽管此金额本身并不大,而且 4GB 似乎是一个合适的缓存空间,但在高流量网站中,尤其是使用 AppFabric 缓存备份会话状态和包含大量丰富的针对性内容的网站中,将很容易填满多个该大小的缓存。此时可以考虑根据客户层调整价格差或自定义合约定价的产品目录。

缓存端间接层

和技术行业中的许多事情一样(并且我猜想在许多其他行业也一样),设计过程是指对根据实际财务状况进行修改的理想技术实现方式的某种融合。因此,即使在只使用 Windows Server 2008 R2 AppFabric 缓存时,也有理由继续使用 System.Web.Caching 提供的本地缓存。在创建间接层的第一轮操作中,我可能封装了对每个缓存库的调用,并为每个缓存库提供了一个函数,例如 AddtoLocalCache(key, object) 和 AddtoSharedCache(key, object)。但是,这意味着每次需要执行缓存操作时,开发人员都会对应进行缓存的位置做出不透明的个人决策。这种逻辑在进行维护时和在较大团队中会被迅速摧毁,并将不可避免地造成无法预料的错误,因为开发人员可能选择将对象添加到不适当的缓存中,或添加到某个缓存中然后意外地从另一缓存中提取对象。因此,将需要执行许多额外的数据提取操作,因为在提取数据时,该数据没有位于相应缓存中,或者位于错误的缓存中。这会导致在注意到性能异常差时,经检查才发现,添加操作是在一个缓存中完成的,但却莫名其妙地在另一缓存中执行了获取操作,而这只是因为开发人员忘记了缓存位置或出现了键入错误。而且,在正确规划系统时,将提前确定这些数据类型(实体),并且在后续定义中还应考虑每个实体的使用位置、一致性要求(尤其是跨负载平衡服务器时)以及必需的数据新鲜程度。由此可见,可提前做出有关缓存位置(共享与否)和过期时间的决策并使其成为声明的一部分。

正如我在上面提到的,应制定缓存计划。很多时候,人们都是在项目结束时随意增加缓存计划,但应像对待应用程序的任何其他方面一样,仔细考虑和设计缓存计划。在涉及到云时,这一点尤其重要,因为考虑不周的决策通常会导致额外的成本和应用程序行为缺陷。在考虑应缓存的数据类型时,一种方法是确定所涉及的实体(数据类型)及其在应用程序和用户会话中的生命周期。这种方法快速揭示出,如果可以只基于实体类型对实体本身进行智能缓存,会很不错。幸运的是,可借助一个自定义属性轻松完成此任务。

我将跳过任一缓存的设置过程,因为上面引用的材料中已详细介绍这方面的内容。对于我的缓存库,我仅使用静态方法创建了一个静态类作为示例。在其他实现中,有充分的理由使用实例对象执行此操作,但为了简化本示例,我将其设为静态。

我将声明一个指示位置和继承属性的类的枚举,以实现我的自定义属性,如图 2 所示。

图 2 声明枚举以实现自定义属性

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; } }
}

在构造函数中传递位置使您稍后可在代码中轻松使用位置,但我还将提供一个只读方法来提取值,因为我需要在 Case 语句中使用该值。 在我的 CacheManager 库中,我已创建一对用于添加到两个缓存中的私有方法:

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

在真正的实现中,我可能需要一些其他信息(例如缓存名称、依赖关系、过期时间等等),但目前这些信息就已足够。 用于向缓存中添加内容的主要公共函数是一个模板方法,这使我能够根据类型轻松确定缓存,如图 3 所示。

图 3 向缓存中添加内容

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;
}

我将只使用传入类型获取自定义属性,并通过 GetCustomAttribute(type, type) 方法请求自定义属性类型。 获取自定义属性后,只需调用该只读属性和 Case 语句,并且我已成功将该调用路由到相应的缓存提供程序。 为确保正确执行此操作,我需要适当地修饰类声明:

[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; }
]}

应用程序基础结构全部设置完毕后,我就可以在应用程序代码内使用该基础结构。 我打开 default.aspx.cs 文件以创建示例调用并添加代码以创建类型,分配一些值并将它们添加到缓存中:

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);

类型名称清楚地表明了将缓存数据的位置。 但是,我可以更改类型名称,这不会对由自定义属性检查控制的缓存产生明显影响。 使用此模式会向页面开发者隐藏数据缓存位置的详细信息以及与缓存项配置相关的其他详细信息。 因此,应由团队中负责创建数据词典和设定上述数据的整个生命周期的人员做出这些决策。 请注意传递到对 AddToCache<t>(string, t) 的调用的类型。 实现 CacheManager 类的其余方法(即 GetFromCache)时将采取此处实现 AddToCache 方法时使用的相同模式。

在成本与性能和规模之间取得平衡

Windows Azure 提供了必要的软件基础结构以便在实现方式的任何方面为您提供帮助,包括缓存以及缓存是针对资源(例如通过 CDN 分发的资源)还是针对可能保存在 AppFabric 缓存中的数据。 获得出色设计和后续良好实现的关键是在成本与性能和规模之间取得平衡。 最后一个注意事项: 如果您正在处理新应用程序,并且计划在其中构建缓存,请继续并立即创建该间接层。 这是一项额外的工作,但随着 AppFabric 缓存等新功能的上线,这种做法会让您能够更轻松地将这些新功能周全、有效地整合到您的应用程序中。

Joseph Fultz 是 Hewlett-Packard Co. 的软件架构师,参与 HP.com 全球 IT 小组的工作。 之前,他是 Microsoft 的软件架构师,协助 Microsoft 顶层企业和 ISV 客户定义体系结构和设计解决方案。

衷心感谢以下技术专家对本文的审阅:Wade Wegner