65.9K
CodeProject 正在变化。 阅读更多。
Home

MyCache:ASP.NET Web Farm 的分布式缓存引擎 - 第二部分:内部细节

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (20投票s)

2010年12月14日

CPOL

17分钟阅读

viewsIcon

84461

downloadIcon

1237

MyCache 的内部实现:一个用于 ASP.NET 应用程序的分布式缓存引擎,部署在负载均衡的 Web Farm 环境中,该引擎基于 WCF 和 Microsoft Caching Application Block 构建。

引言

如果您正在阅读本文,您可能已经阅读了我上一篇文章中关于 MyCache 的演示:MyCache:ASP.NET Web Farm 的分布式缓存引擎 - 第一部分:演示

如果您还没有阅读,建议您先阅读上述文章,了解该分布式缓存引擎的用法、功能和高层架构。

这是本文的第二部分,将详细介绍缓存引擎的构建块、性能和其他相关问题。开始吧

MyCache 架构

正如您可能已经知道的,MyCache 完全基于我们熟悉的、经过验证的 .NET 相关技术构建。以下是 MyCache 的两个主要构建块:

  • Microsoft Caching Application Block:它被用作核心缓存引擎,用于在内存中存储/检索对象。围绕 Caching Application Block 构建了一个类库,以实现基本的缓存功能。
  • WCF with net.tcp binding:WCF 被用作缓存服务和客户端应用程序之间的通信介质。WCF 服务使用 net.tcp 绑定,以实现尽可能快的通信速度。
  • IIS 7:使用 net.tcp 绑定的 WCF 服务托管在 IIS 中的 ASP.NET 应用程序下,以便将服务暴露给外部。

回顾一下,下图描绘了 MyCache 的高层架构:

Clusters.jpg

MyCache 架构

以下是基于 MyCache 的缓存管理的が基本工作原理:

  • 有一个缓存服务器,Web Farm 中的所有服务器都使用它来存储和检索缓存数据。
  • 当负载均衡 Web Farm 中的某个服务器收到请求时,该服务器会向缓存服务器请求数据。
  • 如果缓存可用,缓存服务器将从缓存中提供数据;否则,它会告知调用 ASP.NET 应用程序数据不存在。在这种情况下,调用服务器上的应用程序将从数据存储/服务加载数据,将其存储在缓存服务器中,然后将输出处理给客户端。
  • 稍后,另一台服务器收到需要相同数据的请求。因此,它会向缓存服务器请求数据。
  • 此时,缓存服务器在其缓存中拥有该特定数据。因此,它将提供数据。
  • 对于 Web Farm 中任何服务器后续需要相同数据的请求,数据都会从缓存服务器快速提供,最终的性能让每个人都满意。

一些技术问题

尽管存在 NCache、MemCache、Velocity 等一些成熟的分布式缓存引擎,但我还是决定开发 MyCache。

但是,为什么要再开发一个呢?

这是个好问题。对我来说,答案如下:

MyCache 简单且开源。它使用的技术非常基础、稳定,并且大多数 .NET 开发人员都熟悉。整体实现非常熟悉,并且对于需要根据自身需求进行自定义的人来说非常容易。因此,如果您使用 MyCache,您不会感觉到在使用一个第三方服务、组件或产品,而您对其控制不多,或者不确定。MyCache 并非真正的“产品”,而是一种简单思想的实现,让您可以构建自己的本地分布式缓存引擎。

为什么选择 Caching Application Block 作为核心缓存引擎?

我本可以尝试从头开始开发一个缓存引擎来在内存中存储和检索对象,但这需要花费大量时间来开发一个已经开发过、经过良好测试且已被社区广泛接受的东西。所以,我理所当然地选择了使用“Caching Application Block”作为“内存中”缓存引擎。

为什么选择 WCF with net.tcp binding in IIS?

缓存服务围绕 Caching Application Block 构建,并且必须将该服务暴露给 ASP.NET 客户端应用程序,以便它们能够使用。以下是可用选项:

  • Socket 编程
  • .NET 远程处理
  • Web服务
  • WCF

Socket 编程或 .NET Remoting 是让客户端应用程序使用缓存服务的最快方式。但选择它们的原因是因为它们需要大量工作来实现一个健壮的通信机制,而这个机制已经在 WCF 的不同绑定选项中提供了。

Web Service(相当于 WCF 中的 basicHttpBindingwsHttpBinding)是暴露和使用缓存服务的最简单方式,但选择它是因为基于 HTTP 协议的 SOAP 通信是所有 WCF 绑定选项中最慢的。

因此,使用 net.tcp 绑定的 WCF 是以最快、最可靠的方式暴露和使用服务的必然选择。尽管有一个替代方案是通过 Windows 服务托管 WCF 服务,但选择在 IIS 中托管使用 net.tcp 绑定的 WCF 服务是因为这样可以使所有内容都由 IIS 管理(尽管只有 IIS 7.0 及更高版本支持 net.tcp 绑定)。

好的,我想使用 MyCache,我该怎么做?

谢谢,如果您想这样做。在决定使用 MyCache 之前,您可能需要了解一些事实:

MyCache 基于 .NET Framework 4.0 构建,需要在 Windows Vista、Windows 7 或 Windows 2008 计算机上安装 IIS 7 或更高版本。如果您认为您的部署环境符合上述标准,您需要执行以下步骤来使用 MyCache:

  1. 在 IIS 上部署 MyCache WCF 服务,并启用 net.tcp 绑定(上一篇文章详细介绍了这一点)。
  2. 在您的 ASP.NET 客户端应用程序中添加对 MyCacheAPI.dll 的引用,并配置您的应用程序身份(在 AppSettings 中配置一个“WebFarmId”变量)。
  3. 添加对 WCF 服务(缓存服务)的引用,并在 web.config 中进行配置(可选)。
  4. 开始使用 MyCache(您已经知道如何做;请参阅文章的第一部分)。

MyCacheAPI:您唯一需要了解的 API

是的。一旦缓存服务在 IIS 中配置正确,您只需要了解 MyCacheAPI。只需添加 MyCacheAPI.dll 即可立即开始使用缓存服务。

以下是在 ASP.NET 应用程序中使用 MyCache 的最简单代码:

//Declare an instance of MyCache
MyCache cache = new MyCache();

if(data == null)
{
        //Data is not available in Cache. So, retrieve it from Data source
        data = GetDataFromSystem();
        //Store data inside MyCache
        cache.Add("Key", data);
}

//Remove data from Cache
cache.Remove("Key");

//Add data to MyCache with specifying a FileDependency
cache.Add("Key", Value, dependencyFilePath, 
          Cache.NoAbsoluteExpiration, new TimeSpan(0, 5, 10), 
          CacheItemPriority.Normal, 
          new CacheItemRemovedCallback(onRemoveCallback));

//Reload the data from dependency file and put into MyCache in the callback
protected void onRemoveCallback(string Key, object Value, 
               CacheItemRemovedReason reason)
{

    if (cache == null)
    {
        cache = new MyCache();
    }

    if (reason == CacheItemRemovedReason.DependencyChanged)
    {
        //Aquire lock on MyCache service for the Key and proceed only if
        //no lock is currently set for this Key. This has been done to prevent
        //multiple load-balanced web application update the same data on MyCache service
        //sumultaneously when the underlying file content is modified
        if (cache.SetLock(Key))
        {
            string dependencyFilePath = GetDependencyFilePath();
            object modifiedValue = GetObjectFromFile(dependencyFilePath);
            cache.Add(Key, modifiedValue, dependencyFilePath, 
              Cache.NoAbsoluteExpiration, new TimeSpan(0, 5, 60), 
              CacheItemPriority.Normal, 
              new CacheItemRemovedCallback(onRemoveCallback));

            //Release lock when done
            cache.ReleaseLock(Key);
        }
    }
}

服务多个负载均衡的 Web Farm

MyCache 能够服务多个负载均衡的 Web Farm,并且在一个 Web Farm 的 Web 应用程序中存储的对象无法被部署在另一个 Web Farm 中的 Web 应用程序访问。MyCache 是如何管理这一切的?

有趣的是,这非常简单。我只需要通过 WebFarmId 来区分每个 Web Farm,这需要在每个应用程序的 web.config 中配置,为每个 web.config 配置不同的值。

假设我们有两种不同的 ASP.NET 代码库(两个不同的 ASP.NET 应用程序),它们各自部署在自己的负载均衡 Web Farm 中。这两个应用程序都配置为使用 MyCache(通过添加对 MyCache WCF 服务的服务引用并添加对 MyCacheAPI.dll 的引用)。

由于我们不希望一个应用程序能够访问另一个应用程序在 MyCache 中的数据,因此我们需要将 WebFarmId 参数配置如下:

在 **Application1** 的 web.config 中: <add Key="WebFarmId" Value="1"/>

在 **Application2** 的 web.config 中: <add Key="WebFarmId" Value="2"/>

现在,每当应用程序提供一个键来存储/检索 MyCache 中的数据时,MyCacheAPI 都会在调用 MyCache 服务方法之前将 WebFarmId 与该键一起附加。

以下方法用于将 WebFarmId 值与键一起附加,MyCacheAPI.dll 使用它在调用任何服务方法之前构建适当的键以区分 Web Farm:

public string BuildKey(string Key)
{
    string WebFarmId = ConfigurationManager.AppSettings["WebFarmId"];
    Key = WebFarmId == null ? Key : 
             string.Format("{0}_{1}", WebFarmId, Key);
    return Key;
}

cache.Add(Key, Value);

这会将一个对象添加到缓存服务器,使用提供的键,并在将对象添加到 MyCache 服务之前,构建适当的键来区分 Web Farm。以下是函数定义:

public void Add(string Key, object Value)
{
        //Builds appripriate key for corresponding web farm
        Key = BuildKey(Key);

        cacheService.Add(Key, Value);
}

MyCache 中 FileDependency 的实现

ASP.NET 缓存有一个很棒的“FileDependency”功能,它允许您将对象添加到缓存中,并指定一个“文件依赖项”,这样当文件内容在文件系统中被修改时,回调方法会自动被调用,您可以在回调方法中重新加载对象到缓存中(可能重新从文件中读取内容)。

在 MyCache 中实现此功能有一个潜在的方法。该方法是使用 MyCache 服务中的 Caching Application Block 的 FileDependency。但是,由于以下原因,此方法不成功:

WCF 的“Duplex”通信的性质

是的,正是 WCF 的“duplex”通信性质,它没有让我们以理想且更干净的方式实现 FileDependency 功能。以下部分对此问题进行了详细解释。

但是,在此之前,我们有一个先决条件。为了能够在 MyCache 中使用 FileDependency 功能,缓存服务需要部署在 ASP.NET 客户端应用程序部署的同一个局域网中。以下部分详细解释了这个问题。

依赖文件访问要求

与 ASP.NET 缓存一样,Microsoft Caching Application Block 也具有 CacheDependency 功能。因此,当 ASP.NET 客户端应用程序需要将对象添加到 MyCache 中并指定 FileDependency 时,可以将必要的参数发送到 MyCache WCF 服务,并在 MyCache 中的 Caching Application Block 中添加对象时指定 FileDependency。但是,存在一个基本的文件访问问题。

MyCache 是一个分布式缓存服务,只要客户端 ASP.NET 应用程序可以在某个终端节点(URL)上使用缓存服务,缓存服务就可以托管在任何网络上的任何机器上。但是,为了实现 FileDependency,要求缓存服务部署在与 ASP.NET 客户端应用程序和缓存服务器应用程序都能访问网络文件位置的同一个局域网中。

为了更好地理解这一点,让我们假设我们有一个 Web Farm,其中单个 ASP.NET 应用程序已部署在多个负载均衡的 Web 服务器上。所有这些 Web 服务器都指向相同的代码库,并且它们都位于同一个局域网中。因此,它们可以使用 UNC 路径访问存储在局域网某处的文件夹(例如,\\Network1\Files\\Cache\Data.xml)。

现在,无论 MyCache WCF 服务部署在哪里,服务器应用程序都必须能够访问与负载均衡服务器相同的 LAN 中存储的同一个文件(\\Network1\Files\\Cache\Data.xml)。这将允许 MyCache 服务应用程序检测底层文件的更改。

WCF“Duplex”通信问题

缓存服务应用程序不应实现属于客户端 ASP.NET 应用程序的任何逻辑。因此,读取依赖文件并将其存储到缓存服务中的责任实际上属于相应的客户端应用程序,而缓存服务应用程序应该只关心如何将数据加载和存储到缓存引擎(Caching Application Block)中。

因此,假设依赖文件存储在缓存服务和负载均衡服务器上的 ASP.NET 客户端应用程序都可以访问的公共网络位置,客户端应用程序应该只将文件位置和必要的参数发送到服务器方法,指示在将对象添加到缓存时应指定 FileDependency。另一方面,缓存服务应该通过指定 CacheDependency(到指定的文件位置)将对象添加到 Caching Application Block 中,当文件内容被修改时,而不是从磁盘重新读取文件内容,Caching Application Block 应该向相应的客户端 ASP.NET 应用程序触发一个回调,以重新读取文件内容并将更新的数据存储到缓存中。

WCF 支持双向通信,客户端不仅可以调用服务器功能,服务器还可以调用客户端应用程序的功能,这看起来很有希望。但遗憾的是,只有当客户端与服务器应用程序保持“实时”通信状态时,这才是可能(或可行的)。

在我们的案例中,信息流应该如下进行:

  • 客户端应用程序调用缓存服务上的 WCF 方法,发送必要的参数以添加具有 CacheDependency 的对象到缓存中。
  • WCF 服务应该将对象添加到 Caching Application Block 中,指定 FileDependency 并返回(客户端-服务器对话在此结束,不再是实时的)。
  • 底层文件内容以某种方式被修改(手动或由外部应用程序)。
  • 服务器端的 Microsoft Caching Application Block 检测到该更改,并调用服务器端的 callback 方法,该方法反过来尝试调用客户端端的 callback。

不幸的是,此时,客户端 ASP.NET 应用程序不再与 WCF 服务具有“实时”通信通道,因此客户端 callback 方法调用失败。最终,客户端 ASP.NET 应用程序从服务器端收不到关于文件修改的信号,因此它无法重新读取文件内容并将其存储到 MyCache 中。

那么 CacheDependency 是如何实现的呢?

它通过一个非常简单的方法实现的。我只是借用了 ASP.NET Cache 的帮助。是的,您没听错!

尽管我们正在使用 MyCache 来满足分布式缓存管理需求,但我们不应忘记,我们老朋友 ASP.NET Cache 仍然在 ASP.NET 客户端应用程序中可用。因此,我们可以在 MyCacheAPI.dll 中轻松使用它,仅用于获取文件内容被修改时的事件通知(因为服务器应用程序无法发送此类信号)。一旦我们在客户端 ASP.NET 应用程序中收到事件通知,就可以轻松地重新读取文件内容并更新 MyCache 中的数据。

以下是 ASP.NET Cache 如何与 MyCache WCF 服务结合使用来实现 CacheDependency 功能的:

  • 对象使用常规的 cache.Add() 方法添加到 MyCache 缓存服务中,并指定 FileDependency
  • cache.Add("Key", Value, dependencyFilePath, 
          Cache.NoAbsoluteExpiration, new TimeSpan(0, 5, 10), 
          CacheItemPriority.Normal, 
          new CacheItemRemovedCallback(onRemoveCallback));
  • MyCache 服务器应用程序将对象添加到 Caching Application Block 中,并指定 CacheDependency 文件位置。同时,MyCacheAPI.dll 将键添加到 ASP.NET Cache 中(作为键和值),并指定 FileDependency 和一个 callback 方法。
    //Set the Key (Both as a Key and a Value) in Asp.net Cache
    // with specifying FileDependency and CallBack method
    // to get event notification when the underlying
    //file content is modified
    cache.Add(Key, Key, dependency, absoluteExpiration, 
              slidingExpiration, priority, onRemoveCallback);
    cacheService.Insert(Key, objValue, dependencyFilePath, 
                        strPriority, absoluteExpiration, slidingExpiration); 
  • 在 MyCache 服务端,当文件内容被修改时,Microsoft Caching Application Block 会将对象从其缓存中移除,但不会调用任何 callback 方法,因为没有指定 callback 方法。
  • 同时,在 ASP.NET 客户端端,ASP.NET Cache 会在所有负载均衡站点上调用 callback 方法。每个站点尝试为该键获取锁,一个站点获取锁并更新 MyCache 服务中的对象值,而其他站点则无法获取锁,因此不会继续进行不必要的相同对象更新操作(已经被其中一个负载均衡应用程序更新过)。
  • if (cache.SetLock(Key))
    {
        string dependencyFilePath = GetDependencyFilePath();
        object modifiedValue = GetObjectFromFile(dependencyFilePath);
        cache.Add(Key, modifiedValue, dependencyFilePath, 
          Cache.NoAbsoluteExpiration, new TimeSpan(0, 5, 60), 
          CacheItemPriority.Normal, 
          new CacheItemRemovedCallback(onRemoveCallback));
    
        //Release lock when done
        cache.ReleaseLock(Key);
    }

锁定和解锁

分布式缓存服务由负载均衡的 ASP.NET 客户端站点使用,多个站点可能同时尝试在 MyCache 服务上更新和读取相同的数据(在同一 Web Farm 内)。因此,在更新操作中保持数据一致性很重要,以便:

  • 对某个键的更新操作(cache.Add(Key,Value))不会覆盖同一键上另一个正在进行的(未完成的)更新操作。
  • 对某个键的读取操作(cache.Get(Key))不会读取“脏数据”(读取操作不会在当前更新操作完成前读取同一键的数据)。
  • 来自 CacheItemRemovedCallBack 的 CacheService 更新操作不会为每个负载均衡的应用程序多次调用。

幸运的是,Microsoft Caching Application Block 的每一个操作都是“线程安全的”。这意味着,只要一个特定线程没有完成其操作,其他线程就无法访问共享操作,因此不会发生“脏读”或“脏写”。

然而,需要实现某种形式的锁定,以防止 CacheItemRemovedCallBack 方法在缓存服务上为每个负载均衡的应用程序多次更新相同的数据(当底层文件内容被修改时)。

因此,LockManager 应运而生。

什么是 LockManager?

LockManager 是一个封装 MyCache 服务锁定/解锁逻辑的类。基本上,该类为特定键构成一个“锁键”,并将其放入缓存中(使用锁定键作为键和值),以指示 MyCache 目前已锁定该特定键。

例如,假设当前键是 1_Key1(WebFarmId_Key)。只要 Microsoft Caching Application Block 在其缓存中拥有键“1_Key1”,就假定 MyCache 对于具有键 1_Key1 的特定数据是锁定的。

当底层文件内容被修改时,每个负载均衡应用程序上都会触发 CacheItemRemovedCallBack 方法,每个应用程序都会尝试获取指定键的锁。第一个获取该特定键的锁的应用程序将有机会更新缓存服务上的数据,而其他应用程序则什么也不做。

这种“基于键”的单个锁定确保了锁定发生在每个单独对象的每个操作级别,因此对某个对象的锁定不会影响其他对象。最终,这减少了 MyCache 上读/写操作构建长等待队列的可能性,从而提高了整体性能。

请注意,MyCacheAPI 上提供了以下两个方法,它们仅用于在 ASP.NET 应用程序的 CacheItemRemovedCallBack 方法中使用(以确保只有一个负载均衡的应用程序通过设置键的锁来更新 MyCache 服务器上的数据)。

  • SetLock(string Key):在 MyCache 上获取键的更新锁
  • ReleaseLock(string Key):释放更新锁

因此,在 MyCache 的常规读写操作中(除了 CacheItemRemovedCallBack 方法),客户端代码不需要编写任何锁定功能,因为锁定逻辑是在 MyCache 服务端实现的。

LockManager 类定义如下:

/// <summary>
/// Manages locking functionality
/// </summary>
class LockManager
{
    ICacheManager cacheManager;

    public LockManager(ICacheManager cacheManager)
    {
        this.cacheManager = cacheManager;
    }
    
    /// <summary>
    /// Releases lock for the speficied Key
    /// </summary>
    /// <param name="Key"></param>
    public void ReleaseLock(string Key)
    {
        Key = BuildKeyForLock(Key);
        if (cacheManager.Contains(Key))
        {
            cacheManager.Remove(Key);
        }
    }

    /// <summary>
    /// Obtains lock for the specified Key
    /// </summary>
    /// <param name="Key"></param>
    /// <returns></returns>
    public bool SetLock(string Key)
    {
        Key = BuildKeyForLock(Key);
        if (cacheManager.Contains(Key)) return false;

        cacheManager.Add(Key, Key);
        return true;
    }

    /// <summary>
    /// Builds Key for locking an object in Cache
    /// </summary>
    /// <param name="Key"></param>
    /// <returns></returns>
    private string BuildKeyForLock(string Key)
    {
        Key = string.Format("Lock_{0}", Key);
        return Key;
    }
}

性能

MyCache 在不同的进程中管理缓存数据,可能在不同的机器上。因此,很明显,其性能远不如 ASP.NET Cache,后者将数据存储在“内存中”。

鉴于分布式缓存管理的需求,“进程外”存储缓存数据是一个自然的要求,因此进程间通信(或跨机器网络通信)和数据序列化/反序列化开销是无法避免的。因此,必须使通信和序列化/反序列化开销最小化。

net.tcp 绑定是 WCF 中跨两台不同机器的最快的通信机制,已在 MyCache 架构中使用。此外,WCF 服务和客户端应用程序可以始终配置为从系统中获得最大的性能。

我开发了一个简单的页面(ViewPerformance.aspx)来展示 MyCache 在我的 Core-2 Duo 3 GB Windows Vista Premium PC 上的性能。客户端和服务器组件都部署在同一台机器上,以下是示例性能输出:

MyCachePerformance.png

MyCache 的性能测量示例

尽管测试环境在任何方面都说服力不足(没有真实的服务器环境,系统没有真实的负载,一切都在同一台 PC 上),但上述数据表明整体性能足够有希望,可以被视为一个分布式缓存引擎。毕竟,如果在一个实际应用程序中,从 MyCache 中检索中等大小的数据的检索操作最多在 1 秒内完成,我将有信心将其用作我的下一个分布式缓存引擎。

请尝试一下,并让我知道任何问题或改进建议。我将不胜感激。

© . All rights reserved.