使用 Azure) 生成 Real-World Cloud Apps (Web 开发最佳做法

作者 :Rick AndersonTom Dykstra

下载修复项目下载电子书

使用 Azure 构建真实世界云应用 电子书基于 Scott Guthrie 开发的演示文稿。 其中介绍了 13 种模式和做法,可帮助你成功开发适用于云的 Web 应用。 有关电子书的信息,请参阅 第一章

前三种模式是关于设置敏捷开发过程:其余部分与体系结构和代码有关。 这是 Web 开发最佳做法的集合:

这些做法适用于所有 Web 开发,不仅适用于云应用,而且对于云应用尤其重要。 它们协同工作,帮助你充分利用云环境提供的高度灵活的缩放。 如果不遵循这些做法,在尝试缩放应用程序时会遇到限制。

智能负载均衡器后面的无状态 Web 层

无状态 Web 层 意味着不将任何应用程序数据存储在 Web 服务器内存或文件系统中。 使 Web 层保持无状态,可以提供更好的客户体验并节省资金:

  • 如果 Web 层是无状态的,并且它位于负载均衡器后面,则可以通过动态添加或删除服务器来快速响应应用程序流量的变化。 在云环境中,只要实际使用服务器资源,只需为服务器资源付费,这种响应需求变化的能力可以转化为巨大的节省。
  • 无状态 Web 层在体系结构上更易于横向扩展应用程序。 这也使你能够更快地响应缩放需求,并在此过程中在开发和测试上花费更少的资金。
  • 云服务器(如本地服务器)偶尔需要修补和重新启动;如果 Web 层是无状态的,则服务器暂时关闭时重新路由流量不会导致错误或意外行为。

大多数实际应用程序确实需要存储 Web 会话的状态;此处main点不是将其存储在 Web 服务器上。 可以使用缓存提供程序以其他方式存储状态,例如,在客户端上存储 cookie 或进程外服务器端 ASP.NET 会话状态。 可以将文件存储在 Windows Azure Blob 存储 中,而不是存储在本地文件系统中。

例如,如果 Web 层是无状态的,那么在 Windows Azure 网站中缩放应用程序是多么容易,请参阅管理门户中 Windows Azure 网站的“ 缩放 ”选项卡:

“缩放”选项卡

如果要添加 Web 服务器,只需将实例计数滑块向右拖动即可。 将其设置为 5 并单击“ 保存”,几秒钟内,Windows Azure 中有 5 个 Web 服务器处理网站的流量。

五个实例

你可以同样轻松地将实例计数设置为 3 或回退到 1。 缩减后,会立即开始节省资金,因为 Windows Azure 按分钟而不是按小时收费。

还可以告知 Windows Azure 根据 CPU 使用率自动增加或减少 Web 服务器的数量。 在以下示例中,当 CPU 使用率低于 60% 时,Web 服务器的数量将减少到至少 2 个,如果 CPU 使用率超过 80%,Web 服务器的数量将增加到最多 4 个。

按 CPU 使用率缩放

或者,如果你知道你的站点只会在工作时间忙碌呢? 你可以告诉 Windows Azure 在白天运行多个服务器,并减少到一个服务器晚上、晚上和周末。 以下一系列屏幕截图显示如何将网站设置为在非工作时间运行一台服务器,并在上午 8 点到下午 5 点的工作时间运行 4 台服务器。

按计划缩放

设置计划时间

日间计划

周夜计划

周末日程安排

当然,所有这些操作都可以在脚本和门户中完成。

只要通过保持 Web 层无状态来避免动态添加或删除服务器 VM 的障碍,应用程序在 Windows Azure 中横向扩展的能力几乎不受限制。

避免会话状态

在实际云应用中避免存储某种形式的用户会话状态通常是不现实的,但某些方法相比其他方法而言,对性能和可伸缩性的影响更大。 如果需要存储状态,最佳解决方案是使状态量保持较小并将其存储在 Cookie 中。 如果这不可行,下一个最佳解决方案是将 ASP.NET 会话状态与提供程序一起使用 ,用于分布式内存中缓存。 从性能和可伸缩性的角度来看,最差的解决方案是使用数据库支持的会话状态提供程序。

使用 CDN 缓存静态文件资产

CDN 是内容分发网络的首字母缩略词。 向 CDN 提供商提供静态文件资产(如图像和脚本文件),提供程序将这些文件缓存在世界各地的数据中心,以便无论人们在何处访问应用程序,他们都能获得相对快速的响应和较低的缓存资产延迟。 这可以加快网站的总体加载时间,并降低 Web 服务器上的负载。 如果要访问地理上分布广泛的受众,CDN 尤其重要。

Windows Azure 具有 CDN,你可以在 Windows Azure 或任何 Web 托管环境中运行的应用程序中使用其他 CDN。

使用 .NET 4.5 的异步支持来避免阻塞调用

.NET 4.5 增强了 C# 和 VB 编程语言,以便更轻松地异步处理任务。 异步编程的好处不仅适用于并行处理情况,例如,当你想要同时启动多个 Web 服务调用时。 它还使 Web 服务器能够在高负载条件下更高效、更可靠地执行。 Web 服务器的可用线程数有限,在高负载条件下,当所有线程都在使用时,传入请求必须等待,直到释放线程。 如果应用程序代码不异步处理数据库查询和 Web 服务调用等任务,则在服务器等待 I/O 响应时,许多线程将不必要地绑定。 这会限制服务器在高负载条件下可以处理的流量量。 使用异步编程,等待 Web 服务或数据库返回数据的线程将释放,以便为新请求提供服务,直到收到 数据。 在繁忙的 Web 服务器中,可以立即处理数百或数千个请求,否则这些请求将等待线程释放。

如前所述,减少处理网站的 Web 服务器数量与增加数量一样简单。 因此,如果服务器可以实现更大的吞吐量,则不需要太多吞吐量,并且可以降低成本,因为给定流量所需的服务器数比其他服务器要少。

ASP.NET 4.5 for Web Forms、MVC 和 Web API、Entity Framework 6 和 Windows Azure 存储 API 中包括对 .NET 4.5 异步编程模型的支持。

ASP.NET 4.5 中的异步支持

在 ASP.NET 4.5 中,不仅向 语言添加了对异步编程的支持,还添加到了 MVC、Web Forms 和 Web API 框架。 例如,ASP.NET MVC 控制器操作方法从 Web 请求接收数据,并将数据传递到视图,然后视图创建要发送到浏览器的 HTML。 通常,操作方法需要从数据库或 Web 服务获取数据,以便将其显示在网页中或保存在网页中输入的数据。 在这些情况下,很容易使操作方法异步:返回 Task<ActionResult 并使用异步关键字 (keyword) 标记方法,而不是返回 ActionResult> 对象。 在 方法中,当代码行启动涉及等待时间的操作时,请使用 await 关键字 (keyword) 标记它。

下面是一个简单的操作方法,用于调用数据库查询的存储库方法:

public ActionResult Index()
{
    string currentUser = User.Identity.Name;
    var result = fixItRepository.FindOpenTasksByOwner(currentUser);

    return View(result);
}

下面是异步处理数据库调用的相同方法:

public async Task<ActionResult> Index()
{
    string currentUser = User.Identity.Name;
    var result = await fixItRepository.FindOpenTasksByOwnerAsync(currentUser);

    return View(result);
}

编译器会生成相应的异步代码。 当应用程序调用 FindTaskByIdAsync时,ASP.NET 发出 FindTask 请求,然后展开工作线程,使其可用于处理另一个请求。 FindTask请求完成后,将重启线程以继续处理该调用后的代码。 在启动请求和返回数据之间的 FindTask 这段时间里,你有一个线程可用于执行有用的工作,否则这些工作将被绑起来等待响应。

异步代码存在一些开销,但在低负载条件下,该开销可以忽略不计,而在高负载条件下,可以处理请求,否则会等待可用线程。

自 ASP.NET 1.1 以来,可以进行这种异步编程,但很难编写、容易出错且难以调试。 现在,我们已在 ASP.NET 4.5 中简化了它的编码,因此没有理由不再这样做。

实体框架 6 中的异步支持

作为 4.5 中异步支持的一部分,我们提供了对 Web 服务调用、套接字和文件系统 I/O 的异步支持,但 Web 应用程序的最常见模式是命中数据库,而我们的数据库不支持异步。 现在,Entity Framework 6 添加了对数据库访问的异步支持。

在 Entity Framework 6 中,导致查询或命令发送到数据库的所有方法都具有异步版本。 此处的示例显示了 Find 方法的异步版本。

public async Task<FixItTask> FindTaskByIdAsync(int id)
{
    FixItTask fixItTask = null;
    Stopwatch timespan = Stopwatch.StartNew();

    try
    {
        fixItTask = await db.FixItTasks.FindAsync(id);
        
        timespan.Stop();
        log.TraceApi("SQL Database", "FixItTaskRepository.FindTaskByIdAsync", timespan.Elapsed, "id={0}", id);
    }
    catch(Exception e)
    {
        log.Error(e, "Error in FixItTaskRepository.FindTaskByIdAsynx(id={0})", id);
    }

    return fixItTask;
}

此异步支持不仅适用于插入、删除、更新和简单查找,还适用于 LINQ 查询:

public async Task<List<FixItTask>> FindOpenTasksByOwnerAsync(string userName)
{
    Stopwatch timespan = Stopwatch.StartNew();

    try
    {
        var result = await db.FixItTasks
            .Where(t => t.Owner == userName)
            .Where(t=>t.IsDone == false)
            .OrderByDescending(t => t.FixItTaskId).ToListAsync();

        timespan.Stop();
        log.TraceApi("SQL Database", "FixItTaskRepository.FindTasksByOwnerAsync", timespan.Elapsed, "username={0}", userName);

        return result;
    }
    catch (Exception e)
    {
        log.Error(e, "Error in FixItTaskRepository.FindTasksByOwnerAsync(userName={0})", userName);
        return null;
    }
}

方法有一个 Async 版本, ToList 因为在此代码中, 是导致将查询发送到数据库的方法。 WhereOrderByDescending 方法仅配置查询,而 ToListAsync 方法执行查询并将响应存储在 变量中result

摘要

可以在任何 Web 编程框架和任何云环境中实现此处概述的 Web 开发最佳做法,但我们在 ASP.NET 和 Windows Azure 中提供了一些工具来简化此操作。 如果遵循这些模式,可以轻松横向扩展 Web 层,并将费用降到最低,因为每台服务器将能够处理更多流量。

下一章介绍云如何实现单一登录方案。

资源

有关详细信息,请参阅以下资源。

无状态 Web 服务器:

Cdn:

异步编程:

有关其他 Web 开发最佳做法,请参阅以下资源: