SOA 技巧

通过分布式缓存来解决可伸缩性瓶颈

Iqbal Khan

在适应高流量用途的 Web 应用程序激增之后,下一波大浪潮是面向服务的体系结构 (SOA)。SOA 注定将成为开发具有极大可伸缩性的应用程序的标准方法,而 Windows Azure 这类云计算平台则代表在推动 SOA 实现此目标过程中的巨大飞跃。

SOA 使用户可以在 Internet 上将应用程序分发到多个位置、组织中的多个部门以及多个企业。此外,SOA 还允许在组织内重用现有代码,更为重要的是,允许在不同业务单位之间进行协作。

SOA 应用程序通常部署在负载平衡环境的服务器场中。这是为了使应用程序能够处理其承受的负载量。因而问题变为:若要同时提高 SOA 应用程序的性能和可伸缩性,应考虑哪些注意事项?

虽然 SOA 在设计上旨在提供可伸缩性,但是必须先解决许多问题,才能真正实现可伸缩性。其中一些问题涉及到如何编写 SOA 应用程序代码,但是最重要的瓶颈通常与如何存储和访问数据有关。在本文中,我将探讨这些问题并提供一些解决方案。

确定可伸缩性瓶颈

就应用程序体系结构而言,真正的 SOA 应用程序应该可轻松地进行伸缩。SOA 应用程序有两种组件:服务组件和客户端应用程序。客户端应用程序可以是 Web 应用程序、另一个服务或依赖于 SOA 服务组件来完成其工作的任何其他应用程序。

SOA 的一个关键理念是将应用程序分为多个小块,以便这些组件可以在多台服务器上作为单独的服务运行。

在理想情况下,这些服务应尽可能无状态。“无状态”意味着它们不会在多个调用之间保留任何数据,从而使您可以在多台计算机上运行这些服务。数据最后一次所处的位置无关紧要,因此在多个服务调用之间不会在任何特定服务器上保留数据。

因此,SOA 应用程序的体系结构在本质上是可伸缩的。它可以轻松地扩展到多台服务器上并跨数据中心。但是,与所有其他应用程序一样,SOA 应用程序也必须处理数据,这便可能是个问题。这种数据访问成为了可伸缩性瓶颈。瓶颈通常涉及到应用程序数据,这些数据存储在某个数据库(通常为关系数据库)中。如果 SOA 应用程序使用的是会话数据,则该数据的存储也是另一个潜在的可伸缩性瓶颈。

一个 SOA 应用程序依赖于其他 SOA 应用程序可能是造成性能和可伸缩性低下的另一个方面。假设您的应用程序调用一个服务来执行其工作,但是该服务会调用其他服务。这些服务可能位于同一个 Intranet 上,也可能分布在 WAN 上的其他位置。这类数据传输可能成本很高。如果您反复进行这些调用,则无法有效地伸缩应用程序,这些调用会导致可伸缩性瓶颈,如图 1 所示。


图 1 具有潜在可伸缩性瓶颈的 SOA 体系结构

用于提高性能的代码

可使用一些编程技术来帮助提高 SOA 应用程序性能。

将应用程序设计为使用“大块”Web 方法调用便是一种方法。不要在 SOA 客户端应用程序与 SOA 服务层之间进行频繁调用。通常这两者之间距离很远,因为它们不在同一台计算机上运行,甚至不在同一个数据中心中运行。从客户端应用程序对服务层进行的调用越少,性能便越高。大块调用在一个调用中完成的工作要多于用于完成相同工作的多个调用。

另一项很有用的技术是采用 Microsoft .NET Framework 支持的异步 Web 方法调用。这使 SOA 客户端应用程序在服务层的 Web 方法被调用且正在执行期间,可以继续执行其他操作。

序列化成本是应考虑的另一个方面,因此请不要序列化任何不必要的数据。只应来回发送必需的数据,从而使您可以在要执行的序列化类型方面具有很大的选择余地。

选择正确的通信协议

对于在 Windows Communication Foundation (WCF) 中开发的 SOA 应用程序,SOA 客户端可通过三种不同的协议与 SOA 服务对话。这三种协议是 HTTP、TCP 和命名管道。

如果客户端和服务都是在 WCF 中开发的,且在同一台计算机上运行,则命名管道可提供最佳性能。命名管道在客户端与服务器进程之间使用共享内存。

如果 SOA 客户端和服务器都是在 WCF 中开发的,但是在同一个 Intranet 中的不同计算机上运行,则适合使用 TCP。TCP 比 HTTP 更快,但是 TCP 连接在多个调用之间会保持为打开状态,因此您无法自动将每个 WCF 调用路由到不同服务器。通过使用连接池的 NetTcpBinding 选项,您可以频繁地使 TCP 连接到期以重新启动这些连接,因此它们会路由到不同服务器,从而提供某种形式的负载平衡。

请注意,TCP 无法在 WAN 上可靠地工作,因为套接字连接容易频繁断开。如果 SOA 客户端和服务不是基于 WCF,或者位于不同位置,则 HTTP 是最佳选择。虽然 HTTP 不如 TCP 快,但是可进行负载平衡,从而可提供优秀的可伸缩性。

使用缓存提高客户端性能

考虑周全的缓存使用可以在实际中提高 SOA 客户端性能。当 SOA 客户端对服务层进行 Web 方法调用时,您可以将结果缓存在客户端应用程序一端。因而,此 SOA 客户端下一次需要进行相同 Web 方法调用时,可改为从缓存获取该数据。

通过在客户端缓存数据,SOA 客户端应用程序可减少对服务层进行的调用数。此步骤可大大提高性能,因为不必进行代价高昂的 SOA 服务调用。还可减轻服务层上的整体压力,并提高可伸缩性。图 2 显示一个使用缓存的 WCF 客户端。

图 2 WCF 客户端缓存

using System;
using Client.EmployeeServiceReference;

using Alachisoft.NCache.Web.Caching;

namespace Client {
  class Program {
    static string _sCacheName = "mySOAClientCache";
    static Cache _sCache = NCache.InitializeCache(_sCacheName);

    static void Main(string[] args) {
      EmployeeServiceClient client = 
        new EmployeeServiceClient("WSHttpBinding_IEmployeeService");

      string employeeId = "1000";
      string key = "Employee:EmployeeId:" + employeeId;
            
      // first check the cache for this employee
      Employee emp = _sCache.Get(key);

      // if cache doesn't have it then make WCF call
      if (emp == null) {
        emp = client.Load("1000");

        // Now add it to the cache for next time
       _sCache.Insert(key, emp);
      }
    }
  }
}

在很多情况下,客户端会从服务层物理删除,并且跨 WAN 运行。在这种情况下,您无法了解缓存的数据是否已更新。因此,您必须确定要缓存的数据元素,这仅限于您认为至少在几分钟内到可能几小时内(具体取决于您的应用程序)不会更改的那些数据元素。随后您可以指定这些数据元素在缓存中的到期时间,以便缓存到时自动将其删除。这有助于确保缓存的数据始终保持最新且正确。

用于实现服务可伸缩性的分布式缓存

可在 SOA 服务层中通过缓存实际提高可伸缩性。尽管已提到了许多编程技术,但依靠这些技术并不总是能消除可伸缩性瓶颈,因为主要的可伸缩性瓶颈与数据存储和访问有关。服务通常位于负载平衡的服务器场中,以便服务本身可以进行良好的伸缩,但数据存储无法通过相同方式进行伸缩。数据存储因而成为 SOA 瓶颈。

您可以通过向服务器场中添加更多服务器,并通过这些额外的应用程序服务器增加计算能力,从而扩展服务层。但是所有这些 SOA 事务仍然要处理一些数据。这些数据必须存储在某处,这种数据存储可能很容易成为瓶颈。

可在多个级别上改进可伸缩性的这种数据存储障碍。SOA 服务处理两种类型的数据。一种是会话状态数据,另一种是驻留在数据库中的应用程序数据(请参见图 3)。这两种数据都会导致可伸缩性瓶颈。


图 3 分布式缓存如何减轻对数据库的压力

在分布式缓存中存储会话状态

默认会话状态存储的一个局限性是不支持 Web 场,因为它是在 WCF 服务进程中进行的内存中存储。另一种好得多的选择是在 WCF 服务中使用 ASP.NET 兼容性模式和 ASP.NET 会话状态。这样,您便可以指定 OutProc 存储(包括 StateServer、SqlServer 或分布式缓存)作为会话状态存储。

启用 ASP.NET 兼容性模式的过程包括两个步骤。首先,必须在类定义中指定 ASP.NET 兼容性,如图 4 所示。随后必须在 app.config 文件中指定这一点,如图 5 所示。请注意,图 4 还演示了如何在同一个 web.config 文件中将分布式缓存指定为 SessionState 存储。

图 4 在代码中为 WCF 服务指定 ASP.NET 兼容性

using System;
using System.ServiceModel;
using System.ServiceModel.Activation;

namespace MyWcfServiceLibrary {
  [ServiceContract]
  public interface IHelloWorldService {
    [OperationContract]
    string HelloWorld(string greeting);
  }

  [ServiceBehavior (InstanceContextMode = 
    InstanceContextMode.PerCall)]
  [AspNetCompatibilityRequirements (RequirementsMode = 
    AspNetCompatibilityRequirementsMode.Allowed)]

  public class HelloWorldService : IHelloWorldService {
    public string HelloWorld(string greeting) {
      return string.Format("HelloWorld: {0}", greeting);
    }
  }
}

图 5 在配置中为 WCF 服务指定 ASP.NET 兼容性

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.web>
    <sessionState cookieless="UseCookies"
      mode="Custom" 
      customProvider="DistCacheSessionProvider" 
      timeout="20">
      <providers>
        <add name="DistCacheSessionProvider" 
          type="Vendor.DistCache.Web.SessionState.SessionStoreProvider"/>
      </providers>
    </sessionState>
    <identity impersonate="true"/>
  </system.web>

  <system.serviceModel>
    <!-- ... -->
    <serviceHostingEnvironment 
      aspNetCompatibilityEnabled="true"/>
  </system.serviceModel>
</configuration>

StateServer 和 SqlServer 会话存储选项无法进行良好伸缩,而且对于 StateServer,这还是单点故障。分布式缓存是好得多的选择,因为它可以很好地进行伸缩,并且可将会话复制到多台服务器以保证可靠性。

缓存应用程序数据

应用程序数据是在 WCF 服务中使用最频繁的数据,其存储和访问是主要的可伸缩性瓶颈。为了解决此可伸缩性瓶颈问题,您可以在 SOA 服务层实现中使用分布式缓存。分布式缓存用于仅缓存数据库中的部分数据(基于 WCF 服务在几小时内需要哪些数据)。

此外,分布式缓存可显著提高 SOA 应用程序的可伸缩性,因为借助于所采用的体系结构,此缓存可以进行扩展。它使内容在多台服务器上分布,同时仍为 SOA 应用程序提供一个逻辑视图,以使您只将其视为一个缓存。但是缓存实际上位于多台服务器上,因此允许缓存真正地进行伸缩。如果您在服务层与数据库之间使用分布式缓存,则会显著提高服务层的性能和可伸缩性。

要实现的基本逻辑是在连接到数据库之前,检查缓存中是否已包含所需数据。如果包含,则从缓存中取出这些数据。否则,连接到数据库以获取数据,并将其放在缓存中以供下次使用。图 6 显示了一个示例。

图 6 使用缓存的 WCF 服务

using System.ServiceModel;
using Vendor.DistCache.Web.Caching;

namespace MyWcfServiceLibrary {
  [ServiceBehavior]
  public class EmployeeService : IEmployeeService {
    static string _sCacheName = "myServiceCache";
    static Cache _sCache = 
      DistCache.InitializeCache(_sCacheName);

    public Employee Load(string employeeId) {
      // Create a key to lookup in the cache.
      // The key for will be like "Employees:PK:1000".
      string key = "Employee:EmployeeId:" + employeeId;

      Employee employee = (Employee)_sCache[key];
      if (employee == null) {
        // item not found in the cache. 
        // Therefore, load from database.
        LoadEmployeeFromDb(employee);

        // Now, add to cache for future reference.
       _sCache.Insert(key, employee, null,
          Cache.NoAbsoluteExpiration,
          Cache.NoSlidingExpiration,
          CacheItemPriority.Default);
      }

      // Return a copy of the object since 
      // ASP.NET Cache is InProc.
      return employee;
    }
  }
}

通过缓存应用程序数据,WCF 服务省去了大量成本高昂的数据库往返,改为在邻近的内存内缓存中查找频繁使用的事务性数据。

使缓存的数据到期

通过到期设置,您可以指定在缓存自动删除数据之前,数据应在缓存中保留多长时间。您可以指定两种类型的到期:绝对时间到期以及滑动时间或空闲时间到期。

如果缓存中的数据也存在于数据库中,那么您知道,其他可能无法访问缓存的用户或应用程序可以在数据库中更改这些数据。发生这种情况时,缓存中的数据就会过时,这是您不希望发生的。如果您可以推断出这些数据在缓存中保留多长时间是安全的,则可以指定绝对时间到期。您可能指定诸如“从此刻起 10 分钟后使此项到期”或“在今天午夜使此项到期”这样的设置。到指定时间时,缓存会使此项到期:

using Vendor.DistCache.Web.Caching;
...
// Add an item to ASP.NET Cache with absolute expiration
_sCache.Insert(key, employee, null, 
  DateTime.Now.AddMinutes(2),
  Cache.NoSlidingExpiration, 
  CacheItemPriority.Default, null);

如果没有人会在给定时间段内使用某个项,则您也可以使用空闲时间或滑动时间到期来使该项到期。您可以指定诸如“如果 10 分钟内无人读取或更新此项,则使其到期”这类的设置。如果当您的应用程序临时需要数据时,以及当应用程序使用完数据时,您希望缓存自动使数据到期,则这会十分有用。ASP.NET 兼容性模式会话状态就是空闲时间到期的好例子。

请注意,使用绝对时间到期,可帮助您避免缓存中的数据副本比数据库中的主副本更旧或过时这种情况。另一方面,空闲时间到期用于完全不同的用途。这种到期实际上意味着,一旦应用程序不再需要数据,只需清理缓存即可。您可让缓存负责此清理,而不是让应用程序对此进行跟踪。

管理缓存中的数据关系

大部分数据来自关系数据库,而且即使不是来自关系数据库,数据在本质上也是相关的。例如,您尝试缓存一个客户对象和一个订单对象,这两者相关。一个客户可以具有多个订单。

当您具有这些关系时,您需要能够在缓存中对其进行处理。这表示缓存应了解客户与订单之间的关系。如果您在缓存中更新或删除客户,则可能希望缓存自动从缓存中删除相应的订单对象。这在很多情况下有助于保持数据完整性。

如果缓存无法跟踪这些关系,则您必须自己进行跟踪,从而使应用程序更加繁琐且复杂。如果您只需在添加数据时向缓存告知此关系,则事情便容易多了。这样缓存便了解,如果更新或删除该客户,则同时必须删除相应订单。

ASP.NET 具有一个名为 CacheDependency 的有用功能,使您可以跟踪不同缓存项之间的关系。一些商用缓存也具有此功能。下面的示例演示如何通过 ASP.NET 来跟踪缓存项之间的关系:

using Vendor.DistCache.Web.Caching;
...
public void CreateKeyDependency() {
  Cache["key1"] = "Value 1";

  // Make key2 dependent on key1.
  String[] dependencyKey = new String[1];
  dependencyKey[0] = "key1";
  CacheDependency dep1 = 
    new CacheDependency(null, dependencyKey);

  _sCache.Insert("key2", "Value 2", dep2);
}

这是一个多层依赖关系,表示 A 可依赖于 B,B 可依赖于 C。因此,如果您的应用程序更新 C,则必须同时从缓存中删除 A 和 B。

将缓存与数据库同步

因为数据库实际上在多个应用程序之间共享,且并非所有这些应用程序都可以访问缓存,所以便需要数据库同步。如果您的 WCF 服务应用程序是唯一一个更新数据库的应用程序,并且可以方便地更新缓存,则您可能无需数据库同步功能。

但是在现实环境中,情况并不总是这样。第三方应用程序会更新数据库中的数据,这样您的缓存便会与数据库不一致。将缓存与数据库同步可确保缓存始终了解这些数据库更改,并可相应地更新自己。

与数据库同步通常意味着使缓存中的相关缓存项失效,因此在下次应用程序需要该项时,必须从数据库中进行获取,因为缓存中已没有该项。

ASP.NET 具有一个 SqlCacheDependency 功能,您可通过该功能将缓存与 SQL Server 2005、SQL Server 2008 或 Oracle 10g R2 以及更高版本(基本上是支持 CLR 的所有数据库)同步。一些商用缓存也提供了此功能。图 7 显示了使用 SQL 依赖关系与关系数据库同步的示例。

图 7 通过 SQL 依赖关系同步数据

using Vendor.DistCache.Web.Caching;
using System.Data.SqlClient;
...

public void CreateSqlDependency(
  Customers cust, SqlConnection conn) {

  // Make cust dependent on a corresponding row in the
  // Customers table in Northwind database

  string sql = "SELECT CustomerID FROM Customers WHERE ";
  sql += "CustomerID = @ID";
  SqlCommand cmd = new SqlCommand(sql, conn);
  cmd.Parameters.Add("@ID", System.Data.SqlDbType.VarChar);
  cmd.Parameters["@ID"].Value = cust.CustomerID;

  SqlCacheDependency dep = new SqlCacheDependency(cmd);
  string key = "Customers:CustomerID:" + cust.CustomerID;
_  sCache.Insert(key, cust, dep);
}

ASP.NET 并未提供、但是某些商业解决方案提供了的一个功能是基于轮询的数据库同步。如果您的 DBMS 不支持 CLR,因而您无法使用 SqlCacheDependency,则可以使用这项功能。在这种情况下,如果您的缓存可以按可配置的时间间隔轮询数据库并检测到表中某些行发生了更改,则会十分美妙。如果这些行已更改,则缓存会使其对应的缓存项失效。

用于实现 SOA 可伸缩性的企业服务总线

企业服务总线 (ESB) 是一个行业概念,其中使用了许多技术进行构建。ESB 是 Web 服务的基础结构,可协调组件之间的通信。简而言之,ESB 是让多个应用程序异步共享数据的一个简单而强大的方法。不过它并非为了跨组织甚至跨 WAN 使用。通常,SOA 应用程序在设计上分为多个部分,因此当这些部分需要相互之间共享数据时,ESB 会十分有用。

可采用很多方式构建 ESB。图 8 显示了使用分布式缓存创建的 ESB 的示例。多个松散耦合的应用程序或服务组件可使用该 ESB 在网络上相互之间实时地共享数据。


图 8 使用分布式缓存创建的 ESB

分布式缓存本质上是跨多台计算机的。这使其具有高度的可伸缩性,从而满足了 ESB 的第一个条件。此外,良好的分布式缓存会以智能方式复制其所有数据,以确保在任何缓存服务器出现故障时都不会造成数据丢失。(我将在稍后对此进行讨论。)最后,良好的分布式缓存可提供智能的事件传播机制。

分布式缓存必须提供两种类型的事件才能适用于 ESB。首先,ESB 的任何客户端应用程序都应该能够在 ESB 上登记所需的数据元素,从而在任何人修改或删除该数据元素时,都会立即通知客户端应用程序。其次,缓存应该允许客户端应用程序将自定义事件引发到 ESB 中,从而可立即通知连接到 ESB 并且需要此自定义事件的其他所有应用程序,无论这些应用程序在网络上处于何种位置(当然,应在 Intranet 中)。

借助 ESB,原本需要从一个应用程序对另一个应用程序进行 SOA 调用的大量数据交换可以通过 ESB 非常轻松地完成。此外,简单 WCF 服务在设计上无法轻松地实现异步数据共享。但是通过 ESB 可以无缝地完成此工作。如果 ESB 的客户端事先表示需要数据,则您可以轻松地将数据推送到这些客户端。

缓存可伸缩性和高可用性

缓存拓扑是一个术语,用于指示数据在分布式缓存中存储的实际方式。可使用各种缓存拓扑来适应不同环境。我在这里将讨论其中三种缓存拓扑:分区缓存、分区复制缓存和复制缓存。

分区缓存和分区复制缓存是在可伸缩性方案中发挥主要作用的两种缓存拓扑。在这两种拓扑中,缓存划分为多个分区,每个分区存储在群集中的不同缓存服务器中。分区复制缓存将每个分区的副本存储在不同缓存服务器上。

分区缓存和分区复制缓存对于事务性数据缓存(在这种情况下,对缓存的写入操作与读取操作一样频繁)而言是可伸缩性最强的拓扑,因为随着您将更多缓存服务器添加到群集,您不仅会增加事务容量,同时也会增加缓存的存储容量,因为所有这些分区一起构成了整个缓存。

第三种缓存拓扑(即复制缓存)将整个缓存复制到缓存群集中的每个缓存服务器。这就意味着,复制的缓存可提供高可用性,适用于大量读取的用途。但是该拓扑不适用于频繁的更新,因为更新要对所有副本同步进行,无法像使用其他缓存拓扑时那样快速完成。

图 9 所示,分区复制缓存拓扑是同时提供可伸缩性和高可用性的理想方法。由于每个分区都有副本,因此不会丢失任何数据。


图 9 用于实现可伸缩性的分区复制缓存拓扑

高可用性可通过动态缓存群集进一步得到增强,动态缓存群集表示可在运行时对缓存群集添加或删除缓存服务器,而无需停止缓存或客户端应用程序。因为分布式缓存在生产环境中运行,所以高可用性是重要的功能要求。

后续步骤

如您所见,如果某个 SOA 应用程序使用的数据保存在无法针对频繁事务进行伸缩的存储中,则该应用程序便无法有效地进行伸缩。这便是分布式缓存的真正作用。

分布式缓存是一个新概念,但是 .NET 开发人员正在迅速接受此概念,将其作为任何高事务应用程序的最佳实践。传统的数据库服务器也在改进,但是如果不采用分布式缓存,则这些服务器无法满足当前应用程序中对可伸缩性需求的爆炸性增长。

我所介绍的技术应有助于提高您 SOA 应用程序的可伸缩性。现在就开始尝试这些技术吧。有关分布式缓存的更多讨论,请参见 J.D 撰写的 MSDN 库文章。msdn.microsoft.com/library/ms998562 上的 Meier、Srinath Vasireddy、Ashish Babbar 和 Alex Mackman。          

Iqbal Khan Alachisoft 的总裁和技术推广者。Alachisoft 提供 NCache,这是业内领先的 .NET 分布式缓存,可以大大提高企业应用程序的性能和可伸缩性。Khan 在布卢明顿的印第安那大学中获得了计算机科学专业的硕士学位。您可以通过 iqbal@alachisoft.com 与他联系。

衷心感谢以下技术专家对本文的审阅:Kirill Gavrylyuk 和 Stefan Schackow