您正在编写为用户提供动态信息的、基于 Web 的应用程序。您观察到许多用户访问某个特定页面,但动态信息不发生改变。
如果动态生成的 Web 页被频繁请求并且构建时需要耗用大量的系统资源,那么,如何才能改进这类网页的响应时间?
下列影响因素作用于此上下文内的系统,在考虑问题的解决方案时必须协调这些因素:
生 成动态 Web 页会耗用各种各样的系统资源。当 Web 服务器收到页面请求时,它通常必须从外部数据源(如数据库或 Web Service)检索所请求的信息。对这些资源的访问通常需要通过有限的资源池(如数据库连接、套接字或文件描述符)进行。因为 Web 服务器通常需要处理很多并发请求,所以对这些共享资源的争夺可能会延迟页面请求,直到资源变为可用。在将请求发送给外部数据源之后,仍然必须将结果转换为 HTML 代码以便进行显示。
使系统速度更快的一种显而易见的方法是购买更多的硬件。此方法可能很吸引人,因为硬件便宜(或者供应商这样说),而且不必更改程序。另一方面,更多的硬件只能在未达到其物理限制之前才会对性能有所帮助。网络限制(如数据传输速率)或等待时间使这些物理限制更明显。
使系统速度更快的第二种方法是减少系统处理的工作。此方法要求开发人员做更多的工作,但可以极大地提高性能。下面的内容将探讨此方法带来的挑战。
下 面的内容使用了一个有关天气的示例,以说明额外的开发工作如何能够减少处理负载。如果在 1 小时内有 10,000 个用户查看伦敦的天气预报,默认的 Web 服务器实现可能会连接到天气服务 10,000 次,并生成 10,000 个包含云雨图像的 HTML 页,即使一整天都在下雨。为了减少处理负载,您可以仅在第一个请求过程中获取实际的天气预报,并生成 HTML 页,然后保存该预先生成的页面以便随后使用。当系统收到下一个查看伦敦天气预报的请求时,系统可以将已保存的页面返回给客户端浏览器,而不必连接到天气服 务或生成另一个页面。
这样,就会节省生成冗余 HTML 页的 CPU 周期,从而缩短响应时间。但是,为了存储预先生成的页面,需要分配一定的内存。如果要提供对应于每个邮政编码的天气预报,可能要求您存储数千个天气预报页 面。(仅仅在美国就有数千个邮政编码。)为了判断用户正在请求哪个页面,您还需要按邮政编码或可能的其他参数(如用户正在运行哪种 Web 浏览器;对于不同的浏览器,HTML 可能稍有不同)来为预先生成的页面编制索引。此外,您收到的查看大城市天气预报的请求可能要比查看小城镇天气预报的请求多得多。同样,某些地区天气预报更 改的频率可能比其他地区高。因此,您需要巧妙地处理下列问题:预先生成哪些页面;在重新生成它们之前要将其保留多长时间。
要考虑的另一个因素是页面的构成。或许您希望显示天气和最新股票价格的组合页面。因为股票价格的变化频率比天气更高,所以您无法存储整个页面。您可以存储页面的各个部分,但此后必须分别管理每个页面组件,并在收到请求时将预先生成的组件重新组合成页面。
还 有一个要考虑的因素是页面的可变性。例如,天气数据可能没有发生改变,但是根据用户首选项的不同,如语言、颜色、浏览器和设备(计算机、个人数字助理 (PDA) 或电话等,不同用户所看到的该页面仍然可能有所不同。如果存储已生成的页面,则必须分别存储每个版本。但是,如果存储天气数据,则可省去对天气服务的访 问,但是仍然必须生成该页面。因此,这又会增加几个 CPU 周期,但是减少了需要缓存的信息。有关此方法的更详细说明,请参阅"Page Data Caching"。
对频繁访问但更改不太频繁的动态 Web 页使用页面缓存。
结构
页面缓存的基本结构是相对简单的。Web 服务器维护包含预先生成的页面的本地数据存储(见图 1)。
工作原理
下 面的序列图阐明了页面缓存可以改进性能的原因。第一个序列图(见图 2)描述尚未缓存所需页面的初始状态(即所谓的缓存未命中)。在这种情况下,Web 服务器必须访问数据库,并生成 HTML 页,再将其存储在缓存中,然后将它返回给客户端浏览器。请注意,此过程比不进行缓存的情况稍慢,因为它执行了下列额外步骤:
确定页面是否已缓存
将页面转换为 HTML 代码,然后存储在缓存中
与数据库访问和 HTML 生成相比,其中的任一步骤都不应该花费很长时间。但是,因为在此情况下需要进行额外处理,所以您必须确保在系统完成与缓存未命中关联的步骤之后连续多次命中缓存(如图 2 所示)。
在图 3 所示的缓存命中情况下,页面已经处于缓存中。通过跳过数据库访问、页面生成和页面存储,缓存命中节省了循环。
实现
缓存策略是一个范围很广的主题,在单个模式中无法详尽论述。但是,在实现包括 Page Cache 的解决方案时,讨论最相关的注意事项是很重要的。
Page Cache 解决方案包含以下关键机制:
页面(或页面片段)存储
页面索引
缓存刷新
下面几段内容分别讨论这几个机制。
页面存储
页 面缓存必须存储预先生成的页面,以便系统可以快速检索它们。您还希望能够存储尽可能多的页面,以便提高缓存命中的几率。在进行存储时,您通常需要对速度、 大小和成本进行权衡。假定您手头没有无限的资金,通常是要么选择小的缓存,要么选择快的速度。较小的缓存可以驻留在内存中,而且速度可以非常快。较大的磁 盘存储缓存提供的存储量较大,但是速度很慢。
要找出速度和大小的最佳平衡点,必须仔细确定缓存哪些页面。一些页面的访问频率比其他页 面 高得多,因此,在理想情况下,应该只缓存访问频率高的页面,而忽略很少使用的页面。作出此决定并不是始终很容易的,因为使用模式往往是不同的。许多缓存实 现的是诸如 LFU(最不常使用)的策略,以删除自存储以来很少使用的页面。其他缓存方案允许用户为每个单独页面指定缓存策略。
下 一个最重要的决定是缓存中的片段应该多大。存储完整页面可在页面命中之后快速显示页面,因为系统从缓存中检索页面,并立即将其发送到客户端,而不必执行任 何其他操作。但是,如果页面的某些部分更改频繁而其他部分不是这样(例如,包含天气预报和股票价格的页面),则存储完整页面可能导致增加许多存储。存储较 小的片段可提高页面命中的几率,但需要更多的存储开销(存在更多要编制索引的片段)和更多的 CPU 耗用(测试对多个片段的缓存并组合成最终页面)。有关如何将已缓存的片段组合成页面的说明,请参阅"Page Fragment Caching "。
页面索引
考 虑系统如何在缓存中找到页面也是很重要的。系统查找页面的最简单方法是使用 URL。如果页面不依赖于任何其他因素,则只需通过将请求的 URL 与存储在缓存中的页面的 URL 进行比较,即可从缓存中检索到它。但是,很少发生这种情况。几乎所有动态页面都是根据参数(如用户首选项、查询字符串、窗体域和内部应用程序状态)建立 的。例如,前面示例中的天气页面依赖于用户输入的邮政编码。因此,系统可能必须根据参数存储一个页面的多个实例。在邮政编码示例中,系统可能要存储数千个 页面。这样做的效率很低,因为天气服务实际上并不维护对应于每个邮政编码的天气预报,而是按城市或地区进行维护。如果您知道天气服务如何将邮政编码转换为 天气地区,则可以按大小顺序减少已缓存页面数和提高平均命中率。如往常一样,具有的信息越多,效率就越高。您可以使用 Vary-By-Parameter Caching 来实现此类型的缓存,其中页面内容依赖于参数。
缓存刷新
系统将项目在缓存中保留多长时间也是很重要的。按固定时间存储页面是最简单的方法(请参阅在 ASP.NET 中使用绝对过期实现 Page Cache )。 但是,此方法可能未必一定是足够的。在天气示例中,如果不寻常的天气模式(如冷锋或飓风)正逼近一个主要城市,则可能希望每 15 分钟更新一次。通过将缓存持续时间与外部事件关联,您可以解决这些问题。例如,在外部事件(例如,最新消息)到达时您可能会选择刷新缓存,以强制页面在下 一个请求到达时重新生成。
一些缓存策略尝试在低流量周期内预先生成页面。如果具有可预知的流量模式,并可以存储页面足够长的时间,以避免在高峰流量时间内进行刷新,则此方法可以是非常有效的。
Page Cache 具有以下优缺点:
优点
节省生成页面所需的 CPU 周期。 对于大量并发用户,这导致响应时间更短,并提高了 Web 服务器的可伸缩性。
消除到数据库或其他外部数据源的不必要往返行程。 此优点是特别重要的,因为这些外部源通常仅提供必须由资源池中所有并发页面请求共享的、有限数目的并发连接。对外部数据源的频繁访问会因资源争夺而很快导致 Web 服务器突然停止。
节省客户端连接。 从客户端浏览器到 Web 服务器的每个并发连接都会耗用有限的资源。处理页面请求所用的时间越长,耗用连接资源的时间就越长。
支持许多页面请求的并发访问。 因为页面缓存主要是一个只读资源,所以可以相当容易地对它进行多线程处理。因此,它防止了系统访问外部数据源时会发生的资源争夺。唯一必须同步的部分是缓存更新,因此围绕更新频率的注意事项对于获得良好性能是最关键的。
提高应用程序的可用性。 如果系统需要访问外部数据源以生成页面,则它依赖于可用的数据源。甚至是在外部源变得不可用时,页面缓存页也允许系统将已缓存页面传递给客户端;数据可能不是最新的,但很可能比完全没有数据要好。
注意: 如果此功能对于您的缓存策略来说是至关重要的,请考虑使用 Page Data Cache(它可以为外部数据源提供更多灵活性)。
缺点
显示的信息不是最新的。 如果缓存刷新机制配置得不正确,则 Web 站点可能显示无效数据,该数据可能使人误解或者甚至是有害的。例如,对于根据数据作出购买决定的用户来说,现货供应中延续过长的缓存间隔可能变得非常昂贵。
需要 CPU 和内存( RAM 或磁盘)资源。 将不频繁查看的页面缓存起来,或设置太短的刷新间隔,可以导致开销增加,而且事实上会降低服务器性能。与所有性能度量一样,请使用实际的度量值和性能指示器进行全面的分析,以确定正确的设置。匆忙决定(如缓存每个页面)的坏处会大于好处。
增加了系统的复杂性并使其难于测试和调试。 在大多数情况下,您应该在没有缓存的情况下开发和测试应用程序,然后在性能优化阶段启用缓存选项。
需要注意另外的安全事项。 缓 存所涉及的这一问题经常被忽略。当 Web 服务器处理多个用户发出的对机密信息的并发请求时,避免这些请求发生交叉是很重要的。因为页面缓存是全局实体,所以配置有误的页面缓存可能将原本为另一个 用户生成的页面传递给浏览器。对于天气预报来说,这可能不是一个问题,但是在某些情况下(例如,系统将一个用户的银行帐单显示给另一个用户)将会引起严重 的问题。
可以动态产生不一致的响应时间。 虽然在 99% 的情况下快速传递页面肯定比每次都慢速传递页面要好,但是,如果缓存策略对缓存命中率的优化过高,并对缓存未命中率的优化过低,则会导致不定时发生的超时。同简单的 HTML 页相比,此问题与 Web Service 特别相关。
下列模式描述了实现Page Cache 的各种策略:
在 ASP.NET 中使用绝对过期实现 Page Cache 。此模式将指令插入到要缓存的每个页面中。指令指定刷新间隔(以秒为单位)。刷新间隔不依赖于外部事件,而且缓存不能全部刷新。
Vary-By-Parameter Caching . 此模式使用 Absolute Expiration 的变型,该变型使开发人员能够指定影响页面内容的参数。因此,缓存将存储页面的多个版本,并按参数值为这些页面版本编制索引。
Sliding Expiration Caching . 此模式与 Absolute Expiration 的类似之处是,页面在指定的时间内是有效的。但是,在每次请求时都会重置刷新间隔。例如,您可能使用滑动过期缓存,将一个页面缓存最长 10 分钟。只要对页面的请求是在 10 分钟内发出的,就将过期时间再延长 10 分钟。