Azure 网站

使用 Azure 网站扩展 Web 应用程序

Yochay Kiriaty

下载代码示例

令人惊奇的是,扩展是 Web 应用程序开发中经常被忽视的一个方面。通常情况下,只有在事情处理失败或 UX 由于表示层进程缓慢或超时而遭到损害的情况下,才会关注 Web 应用程序的扩展。当一个 Web 应用程序开始出现此种性能不足时,它已达到了它的可扩展性极点—在这一点上,资源的不足(如 CPU、内存或带宽)束缚它的运行能力,而非代码中的逻辑错误。

这时就该扩展您的 Web 应用程序,并为其提供额外的资源,不管是更多的计算、额外的存储空间或是更强大的数据库后端。云中最常用的扩展形式为水平式,即添加额外的计算实例使该 Web 应用程序能在多个 Web 服务器(实例)上同步运行。云平台(如 Microsoft Azure)可轻松扩展支持您的 Web 应用程序的底层基础结构,方法是:只需手指一点,即可以虚拟机 (VM) 形式提供任意数量的 Web 服务器。但是,如果您的 Web 应用程序不支持在多个实例之间扩展和运行,那么将无法利用额外的资源,也无法产生预期的结果。

本文介绍扩展 Web 应用程序的关键设计理念和模式。实现细节和示例集中于在 Microsoft Azure 网站上运行的 Web 应用程序。

在开始之前,必须注意的一点是,扩展 Web 应用程序很大程度上依赖于上下文和应用程序的构建方式。本文中使用的 Web 应用程序很简单,但是它涉及扩展 Web 应用程序的基本内容,专门应对在 Azure 网站上运行时的扩展。

为迎合不同的业务需求有不同级别的扩展。在本文中,我将介绍四种不同级别的扩展能力,从无法在多个实例上运行的 Web 应用程序,到可在多个实例甚至是多个地理区域和数据中心之间进行扩展的 Web 应用程序。

第一步: 了解应用程序

我将从查看示例 Web 应用程序的限制开始。这一步骤将设置一个基线,基于此基线可进行增强应用程序可扩展性所需的修改。我选择修改一个现有的应用程序,而不是创建一个全新的应用程序并从头开始设计,因为现实生活中经常要求您做的也是这些。

我将在本文中使用的应用程序是 ASP.NET 网页的 WebMatrix 照片库模板 (bit.ly/1llAJdQ)。此模板是学习如何使用 ASP.NET 网页创建一个现实 Web 应用程序的好方法。它是一个功能完备的 Web 应用程序,使用户能够创建相册和上传图像。所有人都可以查看图像,登录用户可以发表评论。照片库 Web 应用程序可从 WebMatrix 部署到 Azure 网站上,或通过 Azure 网站图库直接从 Azure 门户部署到 Azure 网站上。

经过仔细查看 Web 应用程序代码,至少三个限制应用程序可扩展性的重要结构问题显示了出来: 将本地 SQL Server Express 用作数据库;使用一个进程内(本地 Web 服务器内存)会话状态;使用本地文件系统存储照片。

我将深入探讨这些限制中的每一个。

App_Data 文件夹中的 PhotoGallery.sdf 文件是与应用程序一起分布的默认 SQL Server Express 数据库。SQL Server Express 使得开始开发应用程序变得很容易,并能提供很好的学习体验,但是也严重限制了应用程序的扩展能力。一个 SQL Server Express 数据库本质上是文件系统中的一个文件。当前状态的照片库应用程序不能安全地在多个实例间扩展。试图在多个实例间扩展会导致 SQL Server Express 数据库文件产生多个实例,每一个都是本地文件并且很可能无法与其他文件同步。即使所有的 Web 服务器实例共享同一个文件系统,SQL Server Express 文件也会在不同的时间里被任意一个实例锁定,导致其他实例失败。

照片库应用程序还受其管理用户会话状态的方式所限。会话被定义为同一个用户在一个特定时间段内发起的一系列请求,通过将会话 ID 与每一个唯一用户关联来管理会话。ID 用于每个随后的 HTTP 请求,并由客户端提供,包含在 cookie 中或作为请求 URL 的一个特殊片段。会话数据存储于服务器端上一个受支持的会话状态存储器中,该存储器包含进程内内存、一个 SQL Server 数据库或 ASP.NET 状态服务器。

照片库应用程序使用 WebMatrix WebSecurity 类管理用户的登录和状态,而 WebSecurity 使用默认 ASP.NET 成员资格提供程序的会话状态。默认情况下,ASP.NET 成员资格提供程序的会话状态模式为进程内 (InProc)。在这种模式下,会话状态的值和变量存储于本地 Web 服务器实例 (VM) 上的内存中。根据 Web 服务器本地存储用户会话状态限制了应用程序在多个实例间运行的能力,因为来自单一用户的后续 HTTP 请求会在 Web 服务器的其他实例上结束。由于每个 Web 服务器实例都将自己的状态副本保存在自己的本地内存中,因此您在结尾可以使不同 InProc 会话状态对象位于同一用户的不同实例上。这将导致意外的不一致 UX。在此,可以看到用于管理用户状态的 WebSecurity 类:

_AppStart.cshtml

@{
  WebSecurity.InitializeDatabaseConnection
    ("PhotoGallery", "UserProfiles", "UserId", "Email", true);
}

Upload.cshtml

@{
  WebSecurity.RequireAuthenticatedUser();
    ...
...
}

WebSecurity 类是一个帮助程序,是一个用于简化 ASP.NET 网页编程工作的组件。 在后台,Web­Security 类与 ASP.NET 成员资格提供程序交互,这反过来会执行实施安全任务所要求的低级别工作。 ASP.NET 网页中的默认成员身份提供程序是 SimpleMembershipProvider 类,并且其默认会话状态模式是 InProc。

最终,当前版本的照片库 Web 应用程序将照片存储在数据库中,每张照片作为一个字节数组。 从本质上看,由于应用程序使用 SQL Server Express,因此照片存储在本地磁盘上。 对于照片库应用程序,最主要的情景之一是查看照片,因此应用程序可能需要处理和显示大量的照片请求。 从数据库读取照片是不理想的。 即使使用更复杂的数据库(如 SQL Server 或 Azure SQL 数据库),也不理想,这主要是因为检索照片是一项开销较大的操作。

简而言之,此版本的照片库是一个有状态的应用程序,而有状态的应用程序无法在多个实例之间很好的扩展。

第二步: 修改照片库使其成为无状态的 Web 应用程序

现在我已经解释了照片库应用程序一些有关扩展的限制,我会将其逐一解决以提升应用程序的扩展能力。 在第二步中,我将作一些必要的改动以将照片库从有状态转变为无状态。 在第二步结尾处,更新后的照片库应用程序可以安全地扩展,并在多个 Web 服务器实例 (VM) 之间运行。

首先,我将用一个更强大的数据库服务器(Azure SQL 数据库)替换 SQL Server Express。Azure SQL 数据库是 Microsoft 一项基于云的服务,作为 Azure 服务平台的一部分提供数据存储能力。 Azure SQL Database Standard 和 Premium SKU 提供先进的业务连续性功能,我将在第四步中使用该功能。 目前,我只将数据库从 SQL Server Express 迁移至 Azure SQL 数据库。 您可以使用 WebMatrix 数据库迁移工具或任何其他您想使用的工具来将 SDF 文件转化为 Azure SQL 数据格式,从而轻松地实现这一目的。

只要我已准备好迁移数据库,这就是个好机会,可以进行一些虽小但对应用程序扩展能力有重大影响的架构修改。

首先,我将一些表格(Galleries、Photos、UserProfiles 等)的 ID 列类型从 INT 转化为 GUID。 这一更改在第四步中非常有用,在该步骤中我会更新应用程序使其可在多区域之间运行并需要将数据库和照片内容保持同步。 必须注意的是,这样的修改不会强制修改应用程序的任何代码;应用程序中所有的 SQL 查询保持相同。

接下来,我将停止以字节数组的形式将照片存储在数据库中。 此修改包括架构修改和代码修改。 我将从 Photos 表格中删除 FileContents 和 FileSize 列,直接将照片存储至磁盘中,使用照片 ID(现在是 GUID)作为区分照片的方法。

以下代码段显示了更改之前的 INSERT 语句(请注意,fileBytes and fileBytes.Length 都直接存储在数据库中):

db.Execute(@"INSERT INTO Photos
  (Id, GalleryId, UserName, Description, FileTitle, FileExtension,
  ContentType, FileSize, UploadDate, FileContents, Likes)
  VALUES (@0, @1, @2, @3, @4, @5, @6, @7, @8, @9, @10)",
  guid.ToString(), galleryId, Request.GetCurrentUser(Response), "",
  fileTitle, fileExtension, fileUpload.ImageFormat, fileBytes.Length,
  DateTime.Now, fileBytes, 0);

以下是数据库更改后的代码:

using (var db = Database.Open("PhotoGallery"))
{
  db.Execute(@"INSERT INTO Photos
  (Id, GalleryId, UserName, Description, FileTitle, FileExtension,
  UploadDate, Likes)
  VALUES (@0, @1, @2, @3, @4, @5, @6, @7)", imageId, galleryId,
  userName, "", imageId, extension,
  DateTime.UtcNow, 0);
}

在第三步中,我将更详尽地探讨我是如何修改应用程序的。 现在简单地说,就是照片存储至一个中心位置,如所有 Web 服务器实例均可访问的共享磁盘。

我在第二步中将进行的最后一个更改是停止使用 InProc 会话状态。 如前所述,WebSecurity 是一个与 ASP.NET 成员资格提供程序交互的帮助程序类。 默认情况下,ASP.NET SimpleMembership 会话状态模式是 InProc。 有一些您可以与 SimpleMembership 一起使用的进程外选项,包括 SQL Server 和 ASP.NET 状态服务器服务。 这两个选项使会话状态能够在多个 Web 服务器实例间共享而避免服务器关联;也就是说,它们不再要求会话与一个特定的 Web 服务器相关联。

我的方法同时也管理进程外状态,特别是使用数据库和 cookie。 但是,我依赖于我自己的实施方法而不是 ASP.NET,本质上是因为我想让事情保持简单。 这个实现方法使用一个 cookie 并将会话 ID 及其状态存储在数据库中。 一旦用户登录,我会分配一个新 GUID 作为一个存储于数据库中的会话 ID。 该 GUID 也以 cookie 的形式返回给用户。 以下代码显示了 CreateNewUser 方法,每当用户登录时就会调用此方法:

private static string CreateNewUser()
{
  var newUser = Guid.NewGuid();
  var db = Database.Open("PhotoGallery");
db.Execute(@"INSERT INTO GuidUsers (UserName, TotalLikes) VALUES (@0, @1)",
  newUser.ToString(), 0);
  return newUser.ToString();
}

响应一个 HTTP 请求时,GUID 作为 cookie 嵌入 HTTP 响应中。 传递至 AddUser 方法的用户名是 CreateNewUser 函数的结果,如下所示:

public static class ResponseExtensions
{
  public static void AddUser(this HttpResponseBase response, 
    string userName)
  {
    var userCookie = new HttpCookie("GuidUser")
    {
      Value = userName,
      Expires = DateTime.UtcNow.AddYears(1)
    };
    response.Cookies.Add(userCookie);
  }
}

处理一个传入的 HTTP 请求时,首先我尝试从 GuidUser cookie 提取以 GUID 表示的用户 ID。 接下来,在数据库中查找 userID (GUID) 并提取特定用户的所有信息。 图 1 显示了 Get­CurrentUser 实现方法的一部分。

图 1 GetCurrentUser

public static string GetCurrentUser(this HttpRequestBase request,
   HttpResponseBase response = null)
  {
    string userName;
    try
    {
      if (request.Cookies["GuidUser"] != null)
        {
          userName = request.Cookies["GuidUser"].Value;
          var db = Database.Open("PhotoGallery");
          var guidUser = db.QuerySingle(
            "SELECT * FROM GuidUsers WHERE UserName = @0", userName);
          if (guidUser == null || guidUser.TotalLikes > 5)
            {
              userName = CreateNewUser();
            }
        }
      ...
      ...
}

CreateNewUser 和 GetCurrentUser 均为 RequestExtensions 类的一部分。 类似地,AddUser 是 ResponseExtensions 类的一部分。 两个类都可以插入到 ASP.NET 请求处理管道中,分别处理请求和响应。

我管理会话状态的方法比较天真,因为此方法并不安全也不进行任何身份验证。 但是,它展示了管理进程外会话的优点,而且可扩展。 当您实施自己的会话状态管理时,无论是否基于 ASP.NET,都请确保使用安全的解决方案,其中要包含身份验证和对返回的 cookie 进行加密的安全方法。

此时,我可以自信地宣称更新后的照片库应用程序是一个无状态的 Web 应用程序了。 通过用 Azure SQL 数据库替换本地 SQL Server Express 数据库实现方法,及使用一个 cookie 和一个数据库将会话状态实现方法从 InProc 变更为进程外,我成功地将该应用程序从有状态转化为无状态,如图 2 所示。

Logical Representation of the Modified Photo Gallery Application
图 2 修改后的照片库应用程序的逻辑表示

采取必要步骤确保您的 Web 应用程序是无状态的,这或许是 Web 应用程序开发期间意义最深远的任务。 安全地在多个 Web 服务器实例之间运行而不必担心用户状态、数据损坏或功能正确性,是扩展 Web 应用程序最重要的因素之一。

第三步: 大有帮助的其他改进

第二步中对照片库 Web 应用程序所做的更改确保应用程序是无状态的且可以在多个 Web 服务器实例之间扩展。 现在,我将进行一些额外的改进,以进一步提升应用程序的可扩展性,并使 Web 应用程序能够以更少的资源处理更大的负载。 在此步骤中,我将检查存储策略,使用提升 UX 性能的异步设计模式。

第二步中讨论的更改之一是将照片存储到一个中心位置,如一个所有 Web 服务器实例均可访问的共享磁盘,而不是存储到数据库。 Azure 网站的体系结构确保一个 Web 应用程序在多个 Web 服务器之间运行的所有实例共享同一个磁盘,如图 3 所示。

With Microsoft Azure Web Sites, All Instances of a Web Application See the Same Shared Disk
图 3 在 Microsoft Azure 网站中,一个 Web 应用程序的所有实例共享同一个磁盘

从照片库 Web 应用程序的角度看,“共享磁盘”表示照片被用户上传并保存至 …/uploaded folder,该文件夹与本地文件夹类似。 但是,当该图像写入磁盘时,并不是“本地”保存在处理 HTTP 请求的特定 Web 服务器中,而是保存在所有 Web 服务器均可访问的一个中心位置。 因此,任何服务器都可以将任何照片写入共享磁盘,而其他所有 Web 服务器都可以读取该图像。 照片元数据存储在数据库中,并由应用程序用于读取照片 ID(一个 GUID)并将图像 URL 作为 HTML 响应的一部分返回。 以下代码段是 view.cshtml 的一部分(view.cshtml 是我用于启用查看图像的页面):

<img class="large-photo" src="@ImagePathHelper.GetFullImageUrl(photoId,
  photo.FileExtension.ToString())" alt="@Html.AttributeEncode(photo.FileTitle)" />

图像 HTML 元素的源代码用 GetFullImageUrl 帮助程序函数的返回值填充,该函数可以获取照片 ID 和文件扩展名(.jpg、.png 等)并返回一个代表图像 URL 的字符串。

将照片保存到中心位置确保 Web 应用程序是无状态的。 但是,基于当前的实现方法,给定的图像由运行该 Web 应用程序的 Web 服务器直接提供。 具体而言,每张图像源的 URL 指向 Web 应用程序的 URL。 因此,图像本身由运行该 Web 应用程序的一个 Web 服务器直接提供,这意味着图像实际字节作为来自 Web 服务器的 HTTP 响应进行发送。 这意味着您的 Web 服务器,除了要处理动态网页之外,还要提供静态内容,如图像。 Web 服务器大规模地提供静态内容,但是这样占用了很多资源,包括 CPU、IO 和内存。 如果您能确保静态内容(如图像)不是由运行您的 Web 应用程序的 Web 服务器直接提供,而是由其他地方提供,则可以减少对 Web 服务器的 HTTP 请求数量。 通过此方式,您可以释放 Web 服务器上的资源以处理更多的动态 HTTP 请求。

要进行的第一项更改就是使用 Azure Blob 存储 (bit.ly/TOK3yb) 来存储和提供用户照片。 当用户请求查看图像时,更新后的 GetFullImageUrl 返回的 URL 指向一个 Azure Blob。 最终结果与以下 HTML 类似,其中的图像 URL 指向 Blob 存储:

<img class="large-photo" alt="764beb6b-1988-42d7-9900-03ee8a60749b"
  src="http://photogalcontentwestus.blob.core.windows.net/
  full/764beb6b-1988-42d7-9900-03ee8a60749b.jpg">

这就表示,图像由 Blob 存储直接提供,而不是由运行该 Web 应用程序的 Web 服务器提供。

相反地,以下部分显示了保存到 Azure 网站共享磁盘的照片:

<img class="large-photo" alt="764beb6b-1988-42d7-9900-03ee8a60749b"
  src="http:// builddemophotogal2014.websites.net/
  full/764beb6b-1988-42d7-9900-03ee8a60749b.jpg">

照片库 Web 应用程序使用两个容器:全尺寸和缩略图。 如您所预想,全尺寸存储原始尺寸的图片,而缩略图存储显示在图库视图中的较小图像。

public static string GetFullImageUrl(string imageId, 
  string imageExtension)
{
  return String.Format("{0}/full/{1}{2}",
    Environment.ExpandEnvironmentVariables("%AZURE_STORAGE_BASE_URL%"),
    imageId, imageExtension);
}

AZURE_STORAGE_BASE_URL 是包含 Azure Blob 基本 URL 的环境变量,在本案例中为 http://­photogalcontentwestus.blob.core.windows. 此环境变量可在 Azure 门户中的“网站配置”选项卡上设置,或者也可以作为应用程序 web.config 的一部分。 但在 Azure 门户中设置环境变量更为灵活,因为更改更加容易,不需要再部署。

Azure 存储空间使用方法几乎与内容传送网络 (CDN) 相同,主要是因为图像的 HTTP 请求不是由应用程序的 Web 服务器提供,而是由 Azure 存储容器直接提供。 这在实质上减少了到达您的 Web 服务器的静态 HTTP 请求的流量,使 Web 服务器可以处理更多的动态请求。 同时也要注意,Azure 存储空间可以处理的流量比一般的 Web 服务器多得多,单个容器可扩展到每秒提供数万个请求。

除了将 Blob 存储用于静态内容,还可以添加 Microsoft Azure CDN。 在您的 Web 应用程序中添加一个 CDN 可以提升性能,因为 CDN 将提供所有的静态内容。 对已缓存在 CDN 上的照片的请求不会到达 Blob 存储。 另外,CND 还可提升可感知性能,因为 CDN 通常有一个更靠近最终用户的边缘服务器。 向一个简单应用程序中添加一个 CDN 的详细信息不在本文探讨的范围之内,因为更改主要是关于 DNS 注册和配置。 但是当您要处理大规模内容,并且想确保您的客户能享受快捷响应的 UI 时,您应该考虑使用 CDN。

我还没有查看处理用户已上传图像的代码,但是这是一个很好的机会来介绍可提升 Web 应用程序性能和 UX 的基本异步模式。 这也将帮助数据在两个不同区域之间进行同步,如您将在第四步中看到的那样。

我将对照片库 Web 应用程序所做的下一个更改是添加一个 Azure 存储队列,作为将应用程序的前端(网站)与后端业务逻辑(WebJob + 数据库)分离的方法。 如果没有队列,照片库代码同时处理前端和后端,即上传代码将全尺寸图像保存至存储中,创建一个缩略图并将其保存至存储中,再更新 SQL Server 数据库。 在此期间,用户等待响应。 然而,引入一个 Azure 存储队列后,前端仅在队列中写入一个消息就立即向用户返回一个响应。 后台进程 WebJob (bit.ly/1mw0A3w) 从队列中获得该消息并执行所需的后台业务逻辑。 对于照片库来说,这包括处理图像,将其保存至正确的位置并更新数据库。 图 4 阐释了第三步中所做的更改,包括使用 Azure 存储空间和添加队列。

Logical Representation of Photo Gallery Post Step Three
图 4 照片库第三步后的逻辑表达

既然我有了一个队列,我需要更改 upload.cshtml 代码。 在以下代码中您可以看到,我将使用 StorageHelper 来将消息(包括照片 ID、照片文件扩展名和图库 ID)加入队列,而不是执行复杂的业务逻辑和图像处理:

var file = Request.Files[i];
var fileExtension = Path.GetExtension(file.FileName).Trim();
guid = Guid.NewGuid();
using
var fileStream = new FileStream(
 Path.Combine( HostingEnvironment.MapPath("~/App_Data/Upload/"),
 guid + fileExtension), FileMode.Create))
{
  file.InputStream.CopyTo(fileStream);
  StorageHelper.EnqueueUploadAsync(
    Request.GetCurrentUser(Response),
     galleryId, guid.ToString(), fileExtension);
}

StorageHelper.EnqueueUploadAsync 简单地创建一个 CloudQueueMessage 并将其异步上传至 Azure 存储队列:

public static Task EnqueueUploadAsync
  (string userName, string galleryId, string imageId, 
    string imageExtension)
{
  return UploadQueue.AddMessageAsync(
    new CloudQueueMessage(String.Format("{0}, {1}, {2}, {3}",
    userName, galleryId, imageId,
    imageExtension)));
}

WebJob 现在负责后台业务逻辑。 Azure 网站的 WebJobs 新功能提供了在网站上运行诸如服务或后台任务之类程序的简便方法。 WebJob 侦听队列上的更改并获取所有新消息。 每当队列中有至少一个消息时就会调用 ProcessUploadQueueMessages 方法(如图 5 所示)。 QueueInput 属性是 Microsoft Azure WebJobs SDK (bit.ly/1cN9eCx) 的一部分。Microsoft Azure WebJobs SDK 是一个可简化向 Azure 网站中添加后台进程任务的框架。 WebJobs SDK 不在本文探讨的范围之内,但是您真正需要了解的是,在我的 uploadqueue 案例中,WebJob SDK 让您轻松地绑定到一个队列,并侦听传入的消息。

图 5 从队列中读取消息并更新数据库

public static void ProcessUploadQueueMessages
  ([QueueInput(“uploadqueue”)] string queueMessage, IBinder binder)
{
  var splited = queueMessage
    .Split(‘,’).Select(m => m.Trim()).ToArray();
  var userName = splited[0];
  var galleryId = splited[1];
  var imageId = splited[2];
  var extension = splited[3];
  var filePath = Path.Combine(ImageFolderPath, 
    imageId + extension);
  UploadFullImage(filePath, imageId + extension, binder);
  UploadThumbnail(filePath, imageId + extension, binder);
  SafeGuard(() => File.Delete(filePath));
  using (var db = Database.Open(“PhotoGallery”))
  {
    db.Execute(@”INSERT INTO Photos Id, GalleryId, UserName,
      Description, FileTitle, FileExtension, UploadDate, Likes)
      VALUES @0, @1, @2, @3, @4, @5, @6, @7)”, imageId,
      galleryId, userName, “”, imageId, extension, DateTime.UtcNow, 0);
  }
}

每个消息都被解码,将输入字符串分解成各个独立的部分。 接下来,此方法调用两个帮助程序函数来处理图像并将其上传至 Blob 容器。 最后,数据库更新完成。

此时,更新后的照片库 Web 应用程序每天可处理数百万个 HTTP 请求。

第四步: 全球范围内

我已极大地改进了照片库 Web 应用程序的扩展能力。 如我所指出的,该应用程序现在只需要使用 Azure 网站上的少数几个大型服务器便可处理数百万个 HTTP 请求。 目前,所有这些服务器均位于单个 Azure 数据中心。 虽然从单个数据中心运行准确地说不是扩展限制(至少从扩展的标准定义上说不是),但如果您来自全球的客户需要低的延迟,您就需要从多个数据中心运行您的 Web 应用程序。 这也将提升您的 Web 应用程序的持久性和业务连续性能力。 在极少数情况下,如果其中一个数据中心停止运行,您的 Web 应用程序会继续从第二个位置提供通信服务。

在这一步骤中,我对该应用程序所做的更改使其能够在多个数据中心之间运行。 本文中,我将集中于在主动-主动模式下从两个位置运行,在该模式下,两个数据中心的应用程序都允许用户查看照片、上传照片以及发表评论。

请牢记,因为我了解照片库 Web 应用程序的背景,所以我知道用户的大部分操作是读取操作,即显示照片。 只有一小部分的用户请求涉及上传新照片或更新评论。 对于照片库 Web 应用程序来说,我可以自信地说读取/写入的比例中读取至少占 95 个百分点。 这让我做出一些假设,例如,在系统中最终保持一致性是可以接受的,因为写入操作的响应较慢。

必须了解的是,这些假设是基于了解上下文的基础上的,并且取决于给定应用程序的特定特性,不同应用程序之间很可能不同。

令人惊奇的是,从两个不同的位置运行照片库所需的工作量很小,因为最繁重的任务已在第二步和第三步中完成。 图 6 显示了从两个不同数据中心运行的应用程序拓扑的高级框图。 美国西部的应用程序 是“主”应用程序,并且基本拥有第三步的输出。 美国东部的应用程序 是“次要”站点,并且除这两个应用程序之外,还设置了 Azure 流量管理器。 Azure 流量管理器有一些配置选项。 我将使用“性能”选项,它能使流量管理器监控两个站点在各自区域的延迟性并在最低延迟的基础上路由通信。 在这种情况下,纽约(东海岸)的客户将转到美国东部的 的站点,旧金山(西海岸)的客户将转到美国西部的 站点。 两个站点同时处于活动状态,提供通信。 如果一个区域的应用程序出现了性能问题,不论出于何种原因,流量管理器都将通信路由至另一个应用程序。 因为数据已同步,所以不会丢失任何数据。

Logical Representation of Photo Gallery Post Step Four
图 6 照片库第四步后的逻辑表达

我将介绍对美国西部的 应用程序所做的更改。 唯一的代码更改是,针对 WebJob 侦听队列中的消息。 WebJob 将照片保存到本地和“远程”Blob 存储中,而不是保存凹一个 Blob。 在图 5 中,UploadFullImage 是一个将照片保存到 Blob 存储的辅助方法。 为了能够将照片复制到一个远程 Blob 和本地 Blob,我在 UploadFullImage 的末尾添加了 ReplicateBlob 帮助程序函数,如以下所示:

private static void UploadFullImage(
  string imagePath, string blobName, IBinder binder)
{
  using (var fileStream = new FileStream(imagePath, FileMode.Open))
  {
    using (var outputStream =
      binder.Bind<Stream>(new BlobOutputAttribute(
      String.Format("full/{0}", blobName))))
    {
      fileStream.CopyTo(outputStream);
    }
  }
  RemoteStorageManager.ReplicateBlob("full", blobName);
}

以下代码中的 ReplicateBlob 方法有很重要的一行:最后一行调用 StartCopyFromBlob 方法。该方法要求服务将所有的内容、属性和一个 Blob 的元数据复制到一个新的 Blob 中(我让 Azure SDK 和存储服务处理剩余事务):

public static void ReplicateBlob(string container, string blob)
{
  if (sourceBlobClient == null || targetBlobClient == null)
    return;
  var sourceContainer = 
    sourceBlobClient.GetContainerReference(container);
  var targetContainer = 
    targetBlobClient.GetContainerReference(container);
  if (targetContainer.CreateIfNotExists())
  {
    targetContainer.SetPermissions(sourceContainer.GetPermissions());
  }
  var targetBlob = targetContainer.GetBlockBlobReference(blob);
  targetBlob.StartCopyFromBlob(sourceContainer.GetBlockBlobReference(blob));
}

在美国东部,ProcessLikeQueueMessages 方法不会处理任何事情,只是将消息推送至美国西部的 队列。 该消息会在美国西部处理,图像会如上面所解释的那样被复制,数据库也将得到同步,如我现在解释的这样。

这是最后一步:同步数据库。 为实现此目的,我将使用 Azure SQL 数据库的活动异地复制(连续复制)预览功能。 通过此功能,您可以获得您的主数据库的次级只读副本。 写入主数据库的数据会自动复制到次级数据库中。 主数据库被配置为读写数据库,而所有次级数据库都是只读的,这就是我的案例中消息从美国东部队列推送至 美国西部的原因。 一旦您配置了活动异地复制(通过门户),数据库将会进行同步。 不需要有超出我已介绍内容的代码更改。

总结

Microsoft Azure 让您能轻松地建立可进行重大扩展的 Web 应用程序。 在本文中,我展示了如何通过少量几个步骤将一个由于无法在多个实例之间运行而完全无法扩展的 Web 应用程序修改为一个可以在多个实例甚至多个区域之间运行并能处理数百万(数千万)个 HTTP 请求的 Web 应用程序。 所举的示例适用于特定的应用程序,但是概念是可行的,并且可以在任何给定的 Web 应用程序上实现。

Yochay Kiriaty 是 Microsoft Azure 团队的主要项目经理主管,负责 Azure 网站工作。 您可以通过 yochay@microsoft.com 与他联系,也可以关注他的 Twitter:twitter.com/yochayk

衷心感谢以下 Microsoft 技术专家对本文的审阅: Mohamed Ameen Ibrahim