企业网站开发性能策略






4.87/5 (86投票s)
2004 年 2 月 2 日
59分钟阅读

217055
本文介绍的企业网站性能策略,旨在应对高用户量。内容涵盖性能调优的规划、测量性能的工具和指标,以及提高页面速度的各种技术。
引言
与纯 ASP 相比,ASP.NET 和 .NET Framework 为 Web 开发人员提供了更丰富的工具箱。然而,如同任何新的语言或技术,选择正确的工具有时比实际使用工具更困难。例如,你不会用电锯去锯铜管,反之,也不会用钢锯去砍树。如果你不知道该用哪个工具,或者如何判断哪个工具最适合当前任务,你可能会犯错误,导致网站性能不佳。
在 .NET 中,几乎所有事情都有大约 30 种不同的方法可以实现。诀窍在于找出针对不同目标应使用哪种工具。对于大多数网站来说,代码不够高效并不会造成大问题。对于信息类网站或小型电子商务网站,页面加载需要 5 到 6 秒是可以接受的。但对于必须处理大量用户的大型电子商务网站而言,页面性能可能是网站能否存活的关键。对于这类以性能为生存之本的网站,在开发周期的每个阶段都考虑性能至关重要。
一个网站可以拥有数百个类别、数千种产品,可以使用最酷的图形和最新的技术,但如果页面加载速度不快,这一切都将毫无价值。很可能,有许多其他网站提供与你的网站完全相同的功能,如果用户浏览体验不够流畅,他们就会转向其他网站。
如今,Web 开发人员触手可及的工具琳琅满目,因此了解每种工具的优缺点以及如何量化哪个工具最适合每种情况至关重要。ASP.NET 发布后,出现了 1001 本书,展示了 .NET 时代 Web 开发的快速简便。“缩短上市时间,降低开发成本,提高可扩展性和可维护性”,这是来自 Redmond 的营销口号。但是,每当你听到这类营销术语时,都要明白它只是营销。在将任何关键功能实现到你的项目中之前,请自行调查、学习、分析并量化其价值。
本文是 ASP.NET 性能最佳实践的总结,这些实践是我在开发企业级 ASP.NET 网站时,与我合作过的开发人员共同总结出来的。文章标题也包含“性能策略”一词。我将介绍在性能调优过程中可以使用的策略,使这项任务更有条理和意义。我还要指出,本文几乎不包含代码,因为我认为(并希望)讨论足够清晰,你可以自行实现与性能相关的策略。
何时进行性能调优?
那么,项目生命周期的哪个阶段是开发团队应该关注代码性能的呢?答案是:始终,在项目生命周期的每个阶段。一些开发团队会将性能调优留到每个发布版本结束时,或者整个项目结束时(如果进行的话)。他们在开发生命周期中加入某种“性能调优阶段”。但我认为这是一个很大的错误。如果你的团队在设计和开发阶段没有考虑代码性能,你很可能会发现许多页面和算法都太慢,无法发布到生产环境。最终结果是,你不得不重新设计和重写它们。这会浪费大量时间,并可能推迟你的生产发布日期。为了避免这种陷阱,性能应在整个项目过程中得到认真考虑。
不过,我认为在每个发布版本结束时有一个性能调优阶段并非坏事。但是,在编写即将发布的功能时,仅仅因为有一个正式的性能阶段,而不考虑代码的性能,可能会导致严重问题。
实施性能阶段的一个好时机是将其与测试阶段并行运行。从开发团队中抽调几名开发者,让他们从忙于测试团队发现的 bug 中抽身,并让他们全身心地投入到运行性能分析工具和相应地调整代码。更佳的安排是让测试团队熟悉性能分析工具和技术,能够自己找出瓶颈和低效的内存使用,并将这些问题记录为 bug,然后让开发团队修复。但在大多数公司,这种安排并不现实。
一种可以帮助减少重写慢代码所浪费时间的技术是原型开发。在项目生命周期的早期,在分析、设计和早期开发阶段,你应该创建关键功能的原型。而且,不只是一个原型,同一种功能应该编写多个不同版本。这样,你就可以分析每个版本,看看哪个更有效率。开发人员最常犯的错误,尤其是在使用新技术时,是只学会一种编写功能的方法就认为“足够好”。你应该深入研究代码,使用计时器和分析器,找出哪些技术最有效率。这种策略初期会花费更多时间,延长你最初几个项目的进度,但最终你会建立起自己的性能最佳实践工具箱,用于未来的项目。
此时,我有一点需要提醒你注意。性能不应该压倒你项目中的任何关键“能力”。可扩展性、可维护性、可用性和复杂性都是在设计和开发网站时需要牢记的重要因素。很容易(相信我)沉迷于性能分析、调优和代码调整。你总能让你的代码运行得更快一点,但总要有一个点,你必须认为“足够好”。这就是性能调优中的“分析瘫痪”。
你应该努力在所有“能力”之间保持良好的平衡。有时创建一个速度极快的页面会以牺牲可维护性和复杂性为代价。在这种情况下,你必须权衡页面速度的好处与将来维护和扩展页面的工作量和时间。我总是喜欢使用“被公交车撞”的比喻。如果你的团队中只有一个人拥有编写和维护某个功能的技术能力,那么这个功能真的值得吗?如果那个人被公交车撞了怎么办?你该怎么办?
性能不仅仅是开发者的事:为性能而设计
当分析师和架构师勾勒出网站的需求并创建设计文档(希望是 UML 图)时,他们还应在设计网站时考虑性能。不要仅仅试图设计所需的功能,而是要努力设计尽可能高效的功能。例如,缓存是一个很好的性能考虑因素,可以在开发人员开始编码之前就进行设计和实现。例如,将网站设计成每次用户请求页面都调用数据库的过程效率非常低。可以在网站设计中引入数据缓存策略,以减少数据库命中次数。ASP.NET 内置了强大的缓存机制,你可以充分利用它。Microsoft 还提供了一个缓存应用程序块,如果需要更高级的缓存功能,它提供了更多功能(MSDN)。
我希望在本文后半部分将展示的示例能为你设计下一个网站提供一些好想法。
规划性能调优
在你使用代码性能工具并开始调整代码之前,你需要先规划好要做什么。没有一个好的性能调优计划和良好的程序遵循,你实际上可能会让你的网站运行得更慢。首先,将网站划分为战略性块。你应该根据每个块对网站成功的重要性来排序。然后按此顺序进行性能调优。一个运行飞快的“联系我们”页面,如果用户要等待 5-10 秒才能加载一个产品页面,那对网站就没有好处。
我通常遵循的顺序是:主页(如果主页加载不快,用户根本不会使用你的网站)、搜索页面和算法、类别和产品页面、结账流程、个人资料页面,然后是客户服务和辅助页面。将网站分解成这样几个阶段的原因是,你可能没有时间对整个网站进行性能调优。截止日期通常会迫使你选择要处理哪些部分,在这种情况下,你应该先处理最关键的部分。
如果你有一个正在重新设计或升级的现有网站,要弄清楚哪些页面对你网站的成功最关键,一个好的方法是解析你的 Web 服务器日志。找一天用户量特别大的时候,统计当天每个页面被访问的次数。很可能,只有大约 10 个页面构成了你网站 90% 的用户流量。这是一个很好的起点。
现在,在我们真正深入性能调优的细节之前,还有几个步骤需要执行。首先,你应该确定将使用哪些工具来分析代码和测量性能指标。我稍后会详细介绍一些工具。
接下来,你需要确定将使用哪些指标来衡量网站的性能。每个工具都附带了大量的指标。你可以研究所有可用的指标,但这将耗费永恒的时间。有几个关键指标对网站性能尤其重要,你应该最关注它们。在讨论不同工具时,我将一一介绍。
一旦你知道将使用哪些工具(并且知道如何使用它们),以及计划使用哪些指标来衡量网站性能,那么你就需要对你的网站进行基线测量。运行此基线是为了在应用任何代码优化之前,了解网站的当前状况。你将使用第一次基线测量来帮助你确定哪些数字对于生产环境是可接受的,以及需要付出多少努力才能达到目标。基线还将帮助你弄清楚网站的哪些部分已经足够快,哪些区域仍然需要改进。
在你启动分析器之前,你还需要弄清楚的是,在调优过程中进行代码更改时,将要遵循的性能调优方法。我通常遵循的方法有四个基本步骤。第一,在进行任何更改之前,使用分析器或你选择的负载测试工具记录一个度量基线。第二,进行 ONE 次更改。这并不意味着只更改一行代码,而是只对你的代码中一个小区域进行 ONE 次根本性更改(例如一个 aspx 页面)。
例如,如果你的代码使用 `StringBuilder` 来执行所有字符串连接,但你开发了一个自定义的、优化的字符串连接类,并且你想看看它是否比 `StringBuilder` 更快。取一个 aspx 页面,将所有的 `StringBuilder` 类替换为你的新自定义类,然后运行测试。一次更改的目的是在一个小区域内进行一次根本性更改。
你之所以应该只进行一次关键更改,是因为你有可能污染你的测试场景。这意味着;假设你在一个页面中进行了三种不同类型的更改。但当你运行度量时,你发现度量没有区别,或者更改实际上使页面变慢了。问题是,你所做的两项更改可能使页面运行得更快,但第三项更改却使页面实际变慢。慢到足以抵消那两项有益的更改。
第三步是使用新更改再次测量页面的度量,并评估基线与新更改之间的差异。
第四步是记录结果。这包括对所做更改的描述、基线数据、更改后的数据以及差异。在项目的性能调优阶段,记录和跟踪你的进度非常重要。这个日志将让你和项目经理对你离生产就绪目标还有多远有一个很好的了解。性能日志对于发布项目结束时的“经验教训”文档也很有价值,可以向其他开发团队传达性能最佳实践。
回顾一下你调优代码的步骤:进行基线测量,进行 ONE 次根本性更改,进行一次新测量,并记录结果。
最后一点说明,一旦确定某项更改对页面性能有益,该更改就应该推广到项目中的其余代码。但不要盲目地进行更改。你应该为此也使用 4 步法,只是为了确保你在一个页面上看到的优势对网站的其余部分同样有益。
代码分析器、网站负载测试器和网站度量……
有几十种工具可以帮助进行性能调优。但大多数工具可以分为两类;分析器和负载测试器。
我先谈谈负载测试器。你可以花费数千美元购买其中一种工具,市面上有许多非常好的选择。我最喜欢的一种(因为我比较穷而且它是免费的)是 Microsoft 的 Application Center Test (ACT),它随 Visual Studio Enterprise 版一起提供。使用 ACT,你可以将一个页面或一系列页面录制成测试脚本。当你使用 ACT 运行这个脚本时,它会在给定的时间内,为特定数量的用户重复调用该脚本。脚本运行完成后,ACT 会生成一个报告,显示该脚本的性能度量。在运行测试之前,你应该确定你的网站要处理多少用户(这可以在测试脚本的属性中设置)。如果你的网站是高流量网站,那么可以考虑从 15-25 个并发用户开始,然后随着页面调优和速度提升,测试多达 50-75 个用户。
有许多原因需要测试大量用户。首先,你需要查看当页面承受重压时,你的处理器在做什么。如果你的测试脚本只有 25 个用户就将处理器占用率推到 90% 以上,那么你可能在代码效率方面存在一些问题。此外,在大量用户运行测试时,你应该监控 ASP.NET 进程在垃圾回收中花费的时间百分比(下面在 PerfMon 部分将解释如何获取这些计数器结果)。垃圾回收时间百分比的一个良好指导原则是将其保持在 25% 以下(在我看来这已经很高了)。任何更高的值都意味着 ASP.NET 进程花费了太多时间来清理旧对象,而没有足够的时间来运行网站。如果垃圾回收时间百分比很高,你需要查看你创建了多少对象,并尝试找出你的代码如何才能更有效率(即,不要生成那么多对象!)。
一个很好的例子(我稍后会详细讨论)是 ASP.NET 控件的数据绑定。我们有一个页面显示大量信息,该页面完全使用数据绑定控件设计。页面在最多 10 个用户时运行良好。但当我们增加并发用户数到 50 时,处理器占用率达到 100%,页面变得极其无响应。我们发现 ASP.NET 进程在垃圾回收中花费了超过 30% 的时间!!!
在 ACT 中,你可以设置测试的运行时间,甚至可以长达几天,或者你可以将其设置为运行特定次数的迭代。我倾向于按时长运行,看看在那个时间段内我能获得多少次测试脚本迭代。测试运行完成后,结果将被存储并生成报告。ACT 最好的功能之一是能够相互比较两个或多个测试结果报告。你应该利用这一点来比较你的基线以及几组新的更改。这将使你能够轻松地看到代码更改对页面性能的影响。以下是 ACT 在其摘要报告中提供的度量列表,我喜欢监控它们。
- 测试迭代 – 在测试时间内脚本能够完全运行的次数。
- 总请求数 – 服务器在测试时间内能够处理的请求数量。服务请求越多越好。
- 每秒平均请求数 – 服务器每秒能够提供的请求数量。服务请求越多越好。ACT 的摘要报告还显示了每秒请求数的随时间变化图。这是一个很好的方法,可以查看随时间推移的每秒请求数是否存在任何明显模式(例如;垃圾回收启动)。
- 首次字节平均时间 – 从发送请求到收到 Web 服务器响应流的第一个部分之间的时间。
- 最后字节平均时间 – 从发送请求到收到 Web 服务器响应流的结束之间的时间。
- 接收字节数 – 在测试脚本期间 Web 服务器传输的字节数。这个度量很重要,但也可能具有误导性。当你在自己的服务器上进行测试时,页面可能非常快,但也可以向客户端传输大量数据。这不会对你的测试产生不利影响,因为它都是本地的,但如果用户使用的是 56K 调制解调器线路,那么巨大的页面性能将被下载半兆字节 HTML 所花费的时间所掩盖。为了正确使用此度量,你应该仅在测试脚本设置为运行特定迭代次数时才考虑它,而不是在运行特定持续时间时。如果你运行两次测试(针对两次不同的代码更改)20 分钟,其中一个比另一个快得多,那么你将看到更多字节数。这是否是因为一个测试运行了更多的迭代,还是因为它真的发送了更多数据?唯一真正有效地使用此度量的方法是设置固定的迭代次数来运行测试。
还有一个重要的注意事项。有时你可能希望使用 ACT 在两个不同的服务器上运行脚本。例如,你可能希望将最新的代码更改与你的测试服务器上的最新测试构建进行比较。这是完全可以接受的,并且很常见。但这种测试只有在测试服务器和你的服务器具有相同的硬件规格时才有效。此外,如果你的开发服务器上有网站代码、数据库和 Web 服务,但你的测试环境设置为分布式架构,那么这将使测试结果无效。虽然这可能看起来是常识,但我认为提一下是值得的,以免有人花一周时间试图让他们的 P3 1.5 GHz 开发盒表现得像他们的 P4 3.2 GHz 测试服务器一样。
我真正喜欢的另一个工具(因为,再说一次,我比较穷而且它是免费的)是 Windows NT 系列操作系统(NT、2000、XP Pro 和 2003 Server)附带的性能监视器工具。PerfMon 是一个在代码运行时记录并显示所需性能计数器图的工具。当 .NET 安装在你的开发盒上时,PerfMon 会添加数百个特定于 .NET 的 PerfMon 计数器。不仅如此,.NET Framework 还提供了类,你可以用它们来编写自己的性能计数器,以便在 PerfMon 中进行监视!
要查看 .NET 安装的一些计数器,请通过单击“开始”|“程序”|“管理工具”|“性能”打开 PerfMon。应用程序打开后,右键单击图表窗口,然后单击“添加计数器…”。在“添加计数器”对话框中,单击“性能对象”组合框,你将看到大约 60 种不同的性能计数器类别。选择其中一个类别,例如“.NET CLR Memory”类别,然后你会得到该类别的单个计数器列表。单击“添加”按钮将这些计数器添加到 PerfMon,以便它开始监视它们。我将留给你阅读提供的帮助文件和 MSDN 主题,了解不同计数器以及如何一般性地使用 PerfMon。学习每个计数器含义的一个技巧是;在“添加计数器”对话框中,单击名为“说明”的按钮。这将扩展对话框,并提供你点击的每个性能计数器的说明。这应该是你了解 PerfMon 功能的第一步。另一个很酷的功能是 ACT 也可以在脚本运行期间记录 PerfMon 计数器,并在其自己的摘要报告中显示计数器结果。我认为这是在 ACT 中记录垃圾回收时间百分比计数器以及在测试运行时处理器计数器的一个好方法。
PerfMon 提供了如此多的计数器,一开始可能会有点不知所措。下表是我在测试代码片段时通常会关注的计数器列表。
类别 | 计数器 | 描述 |
---|---|---|
.NET CLR 异常 | 抛出的异常数 | 自应用程序启动以来抛出的异常总数。 |
.NET CLR 内存 | 第 0 代集合数 | 自应用程序启动以来第 0 代对象被收集的次数 |
.NET CLR 内存 | 第 1 代集合数 | 自应用程序启动以来第 1 代对象被收集的次数 |
.NET CLR 内存 | 第 2 代集合数 | 自应用程序启动以来第 2 代对象被收集的次数。第 2 代集合对代码性能有害。 |
.NET CLR 内存 | GC 时间百分比 | 这是 ASP.NET 进程在垃圾回收中所花费的进程时间百分比。 |
.NET CLR 内存 | 从第 1 代晋升的内存 | 从第 1 代晋升到第 2 代的垃圾回收(GC)后幸存的内存字节数。如果你有很多对象幸存到第 2 代,你需要查看是什么阻止了对你对象的引用。第 2 代集合非常昂贵。 |
内存 | 可用 MB | 服务器上可用的兆字节数。使用此来查看你的应用程序是否使用了大量内存,或者它是否有内存泄漏(是的,这仍然是可能的)。 |
进程 | 处理器时间百分比 | 此进程所有线程在执行指令时使用的处理器的时间百分比 |
Processor | 处理器时间百分比 | 处理器执行非空闲线程的时间百分比 |
这不是我用过的所有计数器的列表,但这是我使用 PerfMon 时使用的基本模板。如果你正在使用 Remoting 和/或 Web 服务,那么 PerfMon 中有针对这两者的类别。此外,PerfMon 暴露了非常重要的 IIS 计数器,但我更喜欢使用 ACT 来收集 IIS 统计信息。
PerfMon 计数器细节
我想对刚才提到的其中一些计数器说几句。“处理器时间百分比”是你需要监视的最重要的计数器之一。如果你只用 5-10 个用户运行负载测试工具,并且“处理器时间百分比”高达 80-90%,那么你需要重新评估你的代码,因为它工作得太辛苦了。但如果你有 75 个以上用户运行测试,并且处理器占用率高达 90%,那么这是可以预期的。
“可用 MB”是我用来判断我的网站内存效率的指标。我还没有找到一个真正的好方法来精确找出 ASP.NET 进程占用了多少内存,但“可用 MB”可以给你一个不错的 ধারণা。运行 PerfMon 并在开始运行负载测试器之前添加此计数器。记录你可用的内存有多少兆字节,然后开始测试。在测试运行时,观察“可用 MB”如何下降(如果它上升,那么你就找到了世纪算法!)。测试结束后,找出你开始时有多少内存以及测试结束前有多少可用内存之间的差值。这是一个衡量你是否在网站中实现了某种缓存策略的重要计数器,因为你的网站可能会使用过多内存,导致 ASP.NET 进程回收。
“可用 MB”对于查看你的代码是否有内存泄漏也很有用,特别是如果你的代码调用了遗留的 COM 组件。创建一个运行 20-30 分钟的负载测试,看看“可用 MB”计数器是否找到某种恒定值,或者它是否持续下降。如果它在 30 分钟左右持续下降,那么你很可能在某个地方存在内存泄漏。是的,即使在 .NET 中,仍然可能创建内存泄漏。只是难度大了很多。
“第 2 代集合数”是另一个应该关注的指标。第 2 代集合相当昂贵,如果你的网站运行着大量的第 2 代集合,那么你需要查看你的代码,看看它是否因为某些原因而持有子对象引用时间过长。但这一点很难量化,因为没有关于第 2 代与第 1 代和第 0 代比例的建议。
“抛出的异常数”也非常重要。它可以告诉你你的代码是否在抛出大量异常,这也很昂贵。如果你的代码使用 `try`/`catch` 块来指导流程而不是仅仅捕获错误,就可能发生这种情况。这是一个我经常看到的糟糕实践,我稍后会稍微讨论一下。
代码分析器
市面上有许多工具可以帮助你分析代码和代码性能,找出你的问题所在。代码分析器通过使用 .NET Profiler API 挂接到你的程序集中,该 API 允许分析器作为被监视进程的一部分运行,并在特定事件发生时接收通知。市面上有几种不错的分析器。我喜欢使用 RedGate 的 ANTs 代码分析器。ANTs 是一款价格实惠且相对简单的分析器,仅限于记录函数和单行执行时间。当 aspx 页面运行时,它可以记录整个调用堆栈的度量,这样你就可以深入挖掘你的调用,看看 .NET Framework 类有多高效。ANTs 为函数和行分析提供了几种不同的度量,包括;最大调用次数,最小调用次数,平均调用次数,以及调用次数。最后一个很重要,如果你将网站分成了几个层。你可以对一个页面进行分析,通过查看是否有函数被调用了很多次,来判断 UI 层是否有效地利用了你的业务层和数据层。这是一种轻松削减页面执行时间的方法。
我用过的另一个分析器是 AutomatedQA 的 AQTime。这个分析器有更多的度量,包括性能分析器、内存和资源分析器以及异常分析器。我发现它与 .NET exe 程序集配合得非常好,但我无法让它与最简单的网站一起工作。
市面上还有其他几个不错的分析器,每个都应该提供免费试用下载供你试用。无论你决定使用哪个工具,你都应该非常熟练地使用它。每位开发人员应该精通的两项技能是调试和使用代码分析器。
如果你正在使用 SQL Server 作为你的数据库,还有一个你应该熟悉的分析器是 SQL Server Profiler。这是一个监视所有被调用到 SQL Server 的存储过程和内联 SQL 调用的出色工具。你应该使用这个分析器来查看存储过程的调用次数是否超过了需要,以及它们执行需要多长时间。这也是寻找可以缓存到你的 IIS 服务器上的数据的好方法。如果你看到相同的 SQL 或存储过程在不同的页面上执行,可以考虑实现稍后讨论的缓存策略之一。
我特别喜欢使用的一个工具是 Nick Wienholt 编写的 .NET Test Harness Framework。如果你想比较特定功能片段的两个不同原型,这是一个很棒的工具。例如,检查空字符串。你编写一个测试场景,将字符串与 `String.Empty` 常量进行比较,然后编写另一个场景,检查 `String.Length` 是否等于零。这个框架的便利之处在于,一旦你编写了不同的测试场景,你就可以使用 `QueryPerformanceFrequency` Win32 API 调用来准确测量每个测试函数花了多长时间来执行,从而执行每个测试场景指定的次数。当测试平台完成时,它会计算每个测试函数的最小、最大和平均执行时间。我使用这个工具来运行本文后半部分讨论的许多场景。
使用测试平台时,请确保在 Release 版本下运行你的官方测试。C# 编译器在编译 Release 版本时会使用一些 Debug 版本不使用的 IL 代码优化。此外,如果你在测试使用硬编码字符串的内容,请注意 C# 编译器会在编译时将字符串常量和字符串连接内联到你的代码中,这可能会导致测试结果不准确。避免这种情况的最佳方法是将这些字符串作为命令行参数传递给你的测试平台。这样编译器就无法执行任何字符串优化。
你可以在这里找到讨论如何使用测试平台框架的文档。
你可以在这里找到测试平台框架的代码。
我想要提到的最后一个工具,在我进行性能调优时发现它是必不可少的,是Anakrino。这个工具是一个 IL 反汇编器,它可以将任何 .NET 程序集反汇编成托管 C++ 或 C# 代码。当第三方或 .NET Framework 类似乎导致问题(bug 或性能瓶颈)时,这可能非常有价值。你只需要在 Anakrina 中打开有问题的类,自己查看代码,就能看到问题所在。
让你的网站运行更快的技巧
本节是本文的精髓。不再需要准备和规划,这里我将分享我在开发 ASP.NET 网站时学到的性能细节。
虽然让网站运行更快的最简单方法是升级你的服务器集群硬件,但这可能成本很高。但一个简单、经济有效的方法是通过编写高质量、高效的代码来挤出更多的每秒请求数。
在本文的其余部分,我将虚构一个销售苹果的网站进行讨论。它销售各种各样的苹果,并且由于世界各地的人们都使用这个网站购买他们的苹果,所以它必须运行得快,否则用户会开车去当地的 Safeway 商店买苹果。这个网站分为三个程序集;一个用户界面 (UI) 程序集,包含所有 aspx 页面;一个业务层程序集,负责业务对象创建和业务逻辑执行;以及一个数据访问层。
对象缓存
最容易实现并且能带来不错性能提升的方法之一是使用 `HttpRuntime.Cache` 类实现缓存策略。这个类本质上是一个由 ASP.NET 进程托管的哈希表包装器。这个哈希表是线程安全的,可以被多个 HTTP 请求同时安全访问。我将不详细介绍 `Cache` 类的具体 API,MSDN 已经做得相当好,但基本上你可以将对象存储在哈希表中,以后再从中取出并使用它们。只要它们属于同一个应用程序,对象就可以被任何请求访问。每个 AppDomain 都有自己的内存哈希表,所以如果你的 IIS 服务器托管了多个网站,每个网站只能访问它们自己特定放入缓存的对象。关于 `HttpRuntime.Cache` 类的一个重要注意事项是,你放入 `Cache` 类中的任何对象都可以被任何用户在任何请求中访问。因此,用户特定对象或在创建和填充后经常更新的对象不适合存储在缓存中。这类对象往往只用于保存输出数据,例如产品或类别对象。
那么我们如何使用 `Cache` 类呢?假设每次用户想查看 Granny Smith 苹果的产品页面时,我们都会调用数据库,创建一个包含 Granny Smith 数据的 `Product` 类实例,然后将此对象返回给 UI 层。每次有人想查看 Granny Smith 苹果时,网站都会经历这些步骤。但这比必要的数据库调用要多得多。我们需要在过程中增加两个步骤。当 UI 层调用业务层来获取 Granny Smith 产品时,代码首先检查缓存是否包含它。如果缓存中包含,代码就直接将其返回给 UI。但如果缓存中不包含,代码将继续调用数据库并创建一个新的 `Product` 实例。但在将苹果实例返回给 UI 之前,代码将其插入到 `Cache` 类中。这样,下次有人请求 Granny Smith 页面时,代码就不必调用数据库了,因为对象被保存在缓存中。
但这个实现有一个缺陷。如果 Granny Smith 苹果突然短缺,你需要将其价格提高三倍怎么办?(还记得你的微观经济学吗?)你可以对数据库进行更改,但我们仍然需要一种方法来将 `Product` 表的任何更改传播到你的网站。`Cache` 类的最酷的功能之一是你可以为放入缓存的任何对象设置过期超时,以及一个过期回调委托。当你的代码将 `Product` 实例插入缓存时,你可以指定它在缓存中停留多长时间。你还可以指定当对象在缓存中过期时应该调用的函数。
所以,假设你为放入缓存的每个 `Product` 实例设置了 10 分钟的过期时间,并且还指定了过期回调委托。在 `Product` 实例插入缓存 10 分钟后,ASP.NET 进程会将其踢出,并调用回调委托。回调委托的参数之一是实际被缓存的对象。如果你将苹果的数据库标识符存储在 `Apple` 类中,你可以使用它来查询数据库以获取更新的 `Apple` 数据。然后,你可以创建一个新的 `Product` 实例并填充返回的数据,或者更新现有对象的属性。但无论你做什么,请确保将更新后的实例放回缓存(当然还要带上过期时间和回调委托)。这种策略将为你提供持续更新的产品类在内存中,这将大大减少数据库服务器的负载以及进行所有额外数据库调用的时间。
如前所述,我不会在这篇文章中展示太多代码,我认为上面的解释已经足够清晰,无需额外编码。我将为你提供几个我认为在实现这一点时会有帮助的设计模式。首先,你应该创建一个自定义缓存类,该类封装了所有对 `HttpRuntime.Cache` 的调用。这样,所有缓存逻辑都集中在一个地方,如果你以后实现不同的缓存架构,只需要重写一个类。第二个模式是使用对象工厂。这些是辅助类,可以看作是“一站式商店”,你调用它们来获取对象实例。例如,一个 `ProductFactory` 类可能有一个 `CreateProduct` 方法,它接收产品 ID 并返回一个产品实例。`CreateProduct` 函数本身处理所有对数据库层和你自定义缓存类的调用。这样,你的对象创建逻辑就不会散布在你的网站各处。此外,如果你的网站在 aspx 页面中使用的是纯 `DataSet`、`DataTable` 或 `DataRow`,而不是创建 Product 类,那也没关系。`Cache` 类对这些对象同样有效。
关于我刚才描述的缓存框架的一个警告。如果你将大量对象放入缓存,并且正在使用回调委托重新加载它们,你可能会损害性能而不是提高它。我正在处理的一个网站有数千个类别和数万种产品。当所有这些对象被加载到缓存中时,每 10 分钟缓存重新加载自身时,处理器就会达到 100% 的占用率。因此,我们最终使用了一种缓存策略,即所有对象每 10 分钟过期,然后就会超出范围。所以,如果在 10 分钟的时间段内用户请求同一个产品,他们将节省一次数据库调用,否则网站就必须创建一个新实例,将其放入缓存,然后返回给 UI 层。
自助重载缓存策略的另一个问题是,如果用户请求一个相当冷门的产品,并且一周内没有人再次请求该产品。你的 Web 服务器每 10 分钟就会重新加载该产品,即使没有人请求过它。自助式缓存策略最适合高流量网站,但产品量不宜过大。有多少对象算太多,取决于你服务器的内存资源和处理器。但 PerfMon 是衡量服务器是否能处理这种策略的一个好方法。
基于数据依赖的缓存架构
在将对象实例插入缓存时,你还可以选择另一种方法来创建更有效的自助式缓存策略。有一个类叫做 `CacheDependency`。当你将对象插入 `HttpRuntime.Cache` 类时,你还可以传递一个 `CacheDependency` 类的实例。这个类充当一个触发器,将你的对象从缓存中踢出。当 `CacheDependency` 的触发器被触发时,它会告诉 `Cache` 类将与之关联的对象从缓存中踢出。有两种类型的缓存依赖项;文件依赖项和缓存项依赖项。文件依赖项版本通过使用一个名为 `FileChangesMonitor` 的内部类来工作。这个类监视你指定的任何文件,当文件发生更改时,它会调用其 `FileChangeEventHandler`,`HttpRuntime.Cache` 类已经注册了一个回调函数。这个回调将触发 `HttpRuntime.Cache` 类,将与 `CacheDependency` 实例关联的任何对象踢出。
那么我们如何使用它来创建一个更有效的自助式缓存策略呢?我们通过在存储对象数据的表的 SQL Server 触发器上来实现这一点。我们再次以 `Product` 类为例。我们还假设我们的数据库中有 250 种不同类型的苹果,每种都有自己的 `ProductID`。我们在 `Product` 表上创建一个触发器,这样每次 `Product` 表中的一行被更改时,它就会在网络上的某个位置创建一个文件,并将文件的文本设置为“0”。如果文件已存在,触发器将更新文件。这个策略的关键在于文件名和文件中的文本。文本只是一个字符,每次触发器更新文件时,你只需将字符从“0”更改为“1”或从“1”更改为“0”。文件名是刚刚更改的产品的 `ProductID`。
所以,在插入苹果 `Product` 对象到缓存时,创建一个 `CacheDependency` 实例,该实例引用文件名与要放入缓存的对象 `ProductID` 相同的那个文件。在这种情况下,你不会像以前那样在 `Cache.Insert()` 函数中传递过期时间,但你仍然可以指定回调委托。一旦苹果 `Product` 被插入缓存,它将一直保留在那里,直到数据库中的数据发生更改。当这种情况发生时,数据库触发器将触发并更新文件。这将导致 `CacheDependency` 被触发,你的 `Product` 实例将被从缓存中踢出。然后你的回调函数将调用数据库,用新数据重新创建 `Product` 实例,然后将其重新插入缓存。这将大大减少数据库调用次数,并且你的对象只会在必须时刷新,从而大大降低你 Web 服务器处理器的负载。
请求级缓存
另一个你可以用于缓存策略的哈希表是 `HttpContext.Current.Items` 类。这个类最初是为在 HTTP 请求期间在 `IHttpModules` 和 `IHttpHandlers` 之间共享数据而设计的,但没有什么可以阻止你在 aspx 页面或 aspx 页面启动的调用堆栈中的任何程序集中使用它。这个哈希表的范围是单个 HTTP 请求的持续时间,在此之后,它以及它引用的任何对象都将超出范围。
这个哈希表是存储具有短生命周期但在一个 HTTP 请求范围内被多次访问的对象的最理想位置。一个很好的例子可能是你的网站存储在文件或注册表中的连接字符串。假设你的数据访问程序集在每次调用数据库时都必须从注册表中读取其连接字符串并对其进行解密。如果数据访问程序集在一个请求期间被调用了多次,你可能会发现比必要时更频繁地读取注册表和解密连接字符串。绕过这种重复的一种方法是将解密后的连接字符串放入 `HttpContext.Current.Items` 哈希表中,作为每个请求期间第一次需要它的时候,然后在此请求期间每次调用数据访问程序集时,都可以从 `HttpContext.Current.Items` 类中拉取连接字符串。现在我知道你可能会想。为什么不将连接字符串存储在一个静态字段中,并在应用程序的生命周期内保持它?我建议避免这样做的原因是,如果你需要更改注册表中的连接字符串,你将不得不重新启动你的 Web 应用程序才能将它们重新加载到你的静态字段中。但如果它们只在每个请求的生命周期内存储,那么你就可以更改注册表,而不用担心将人们从你的网站赶走。
现在连接字符串可能不是一个很好的例子,但有许多类型的对象可以从这种缓存策略中获益。例如,如果你正在使用 Commerce Server,那么这是一个存储 `CatalogContexts` 或 `ProductCatalog` 等对象的好地方,这些对象可能在一个 HTTP 请求期间被创建多次。这将为你节省大量时间,并为你的网站带来巨大的性能提升。
页面和控件缓存
页面和控件缓存是一个显而易见的工具,并且在 MSDN 和许多 ASP.NET 书籍中都有很好的文档记录,所以我不会过多介绍,只是说如果可能就使用它们。这些技术可以极大地提高你的每秒请求数指标。
视图状态管理
当 ASP.NET 首次发布时,我听说过视图状态以及它的作用,我非常高兴!然后我实际上看到了视图状态中有多少文本,我的喜悦就消失了。视图状态的问题在于,它不仅在页面上存储数据,还存储文本的颜色、文本的字体以及文本的高度、宽度……嗯,你明白了。我们开发的一个页面非常大,以至于视图状态有 2MB!想象一下在 56K 调制解调器上加载那个页面。
现在,我没有微软聪明到能提出更好的视图状态方法,但我确实知道在某些页面上你就是不能使用它。它太大了,下载时间太长了。而且,诚然,打开视图状态后,你在回发时不必重新加载整个页面,但如果你有一个高效的 HTML 渲染策略(我将在最后讨论),我认为重新渲染页面的时间成本将抵消下载视图状态到客户端所需的时间(更不用说每次请求解码和编码视图状态所需的时间了)。
如果你确实决定使用视图状态,请查看你的 `machine.config` 文件,在 `
一个快速说明,这可能会为你节省几天的故障排除时间:如果你将 `enableViewStateMAC` 设置为 `true`,并且你正在一个负载均衡的服务器集群中运行,你还需要确保每个服务器都有相同的加密/解密密钥。ASP.NET 用于加密视图状态的加密算法基于机器密钥。这意味着在服务器 A 上加密的视图状态,如果服务器 B 处理返回请求,将无法解密,并且你会收到视图状态异常。要解决这个问题,请将 `machine.config` 文件中 `
我对配置文件还有两个观察,它们并不真正属于其他地方,既然我刚刚讨论了 `machine.config`,这里就是个好地方。`machine.config` 文件中有一个属性允许你分配 Web 服务器内存的百分比供 ASP.NET 进程使用。这是 `
第二项是在 `web.config` 文件中的 `
字符串操作
Microsoft 随 .Net Framework 发布的一个类是 `System.Text` 命名空间中的 `StringBuilder` 类。这个类是一种高性能的方法,可以将文本连接起来构建大型文本块。在旧的 ASP 时代,字符串连接被广泛用于构建 HTML 输出,但这严重影响了页面性能。所以现在有了 `StringBuilder`,你可能会认为它是字符串连接的最佳选择,对吗?嗯,是的,也不是。这取决于具体情况和你如何使用它。
字符串连接的一般规则是,如果你要连接 5 个或更多字符串,就使用 `StringBuilder`。对于 2-4 个字符串,你应该使用静态 `String.Concat` 函数。`String.Concat` 函数接收两个或多个字符串,并返回一个由所有传入字符串连接而成的新字符串。你也可以将任何其他数据类型传递给 `Concat` 函数,但它只会对这些类型调用 `ToString()` 并进行直接的“+”风格字符串连接,所以我建议只用于字符串。
如果您查看 Anakrino(在 mscorlib 文件中)的 String.Concat
方法,您会发现,如果您将 2 个或更多字符串传递给该函数,该函数会首先计算传入的所有字符串的总字符数。然后,它会调用一个 extern
函数 FastAllocateString
,并传入将要构建的新字符串的大小,我猜这会分配足够容纳整个返回字符串的内存块。然后,对于传入的每个字符串,Concat
会调用另一个名为 FillStringChecked
的 extern
函数。此函数接收指向 FastAllocateString
中分配的内存块的指针、要添加的字符串在内存中的起始位置、结束位置以及要添加的字符串。它对每个传入的字符串执行此操作,以构建新连接的字符串,然后返回该字符串。这是连接 2-4 个字符串的一种非常快的方法,根据我在 .NET Test Harness 上的测试,String.Concat
函数的性能比 StringBuilder
高出 130%。
这听起来足够简单,对吧?对于 2-4 个字符串,使用 String.Concat
,对于 5 个或更多字符串,使用 StringBuilder
。嗯,差不多。我脑子里突然有个想法,我想在 .NET Test Harness 和 PerfMon 中对其进行剖析,看看会发生什么。由于 String.Concat
最多可以接受 4 个字符串,为什么不使用一个 String.Concat
来连接 4 个内部 String.Concat
调用的输出呢?所以我设置了测试,发现对于最多 16 个字符串,将 String.Concat
函数嵌套在外部 String.Concat
函数中,其性能比 StringBuilder
类高出 180%(快 1.8 倍)。我用这种方法还发现,PerfMon 计数器“垃圾回收器中的百分比”在使用嵌套的 String.Concat
函数时要低得多(但要看到这一点,您需要连续进行数千次测试)。如果您的网站进行大量字符串连接,这对您来说是个好消息。嵌套 String.Concat
函数唯一的问题是它会让代码相当难以阅读。但如果您确实需要提高页面性能,那么您可能需要考虑它。
如果您发现需要使用 StringBuilder
来构建大块文本,有几种方法可以使其工作得更快一些。StringBuilder
的工作方式是,如果您使用默认构造函数,它的初始容量为 16 个字符。如果任何时候您添加的文本超过了它的容量,它就会将字符容量翻倍。因此,默认情况下,它将从 16 个字符增长到 32 个、64 个、……您懂的。但如果您对要构建的字符串的大小有一个好的估计,StringBuilder
的构造函数之一会接受一个 Int32 值,这将初始化它的字符容量。这可以提供更好的性能,因为如果您将 StringBuilder
初始化为 1000,那么直到您传入 1001 个字符之前,它都不需要分配更多内存。预初始化 StringBuilder
的性能提升并不是很大,大约 10%-15%,具体取决于最终字符串的大小,但如果您知道要添加多少个字符,那么值得尝试一下。
在谈论字符串时,我还有最后一件事想说明,那就是检查空字符串。有两种基本方法可以检查字符串是否为空。您可以将字符串与 String.Empty
静态属性进行比较,它只是一个常量 ""
if (firstName == String.Empty)
或者您可以检查字符串的长度,如下所示
if (firstName.Length > 0)
我在 .NET Test Harness 中设置了一个测试,发现 String.Empty
比较检查比长度检查慢 370%。这可能显得相当微不足道,但积少成多,对吧?
类字段初始化
如果您有许多私有字段的类,可以使用两种不同的技术来初始化类的私有字段,选择哪种技术会影响性能。这是一个常见场景。假设您有一个 Product
类,它有 45 个私有字段,45 个封装它们的公共属性,以及两个构造函数;一个默认构造函数,用于创建一个空产品;另一个构造函数接受一个 DataRow
,用于填充 45 个字段。您应该在某个地方初始化 45 个字段,因为如果您创建一个空产品,您可能想给您的属性一些默认值。但应该在哪里初始化字段?是在声明它们的地方还是在默认构造函数中?
如果您的代码使用在字段声明处初始化字段的技术,如
private int maxNumberAllowed = 999;
然后您使用默认构造函数创建一个新的 Product
实例,那么您就拥有了一个完美的 Product
实例,随时可以使用。但是,如果您使用 DataRow
构造函数创建新的 Product
实例,会发生什么?每个字段将被赋值两次!一次在声明时,第二次在 DataRow
构造函数中。
最佳实践是在构造函数中进行所有类级别的字段初始化。您可能会在每个构造函数中重复初始化代码,但可以保证每个私有字段在每次创建类时只赋值一次。
因此,为了说明这会如何影响性能,我用 .NET Test Harness 创建了一个测试。我创建了两个类,每个类都有 50 个私有字段。第一个类在声明处初始化其私有字段,第二个类在构造函数中执行所有初始化。两个类都有两个构造函数,一个默认构造函数和一个 DataRow
构造函数。当我使用 DataRow
构造函数创建 Product
实例时,在声明处初始化私有字段的类比在构造函数中拥有所有初始化代码的类慢了 50%。当我通过调用默认构造函数创建新的 Product
实例时,两个版本都差不多。
使用异常来控制进程流
我不会花太多时间详细介绍异常处理,除了说抛出异常对性能的开销很大。您只应该在需要捕获错误时使用 try
/ catch
块。您永远不应该用它来控制和指导程序的进程流。
例如,我见过如下代码(.NET Framework 实际上在其 VB 函数 IsNumeric
中也使用了这种方式)
public bool IsNumeric(string val)
{
try
{
int number = int.Parse(val);
return true;
}
catch
{
return false;
}
如果您有一个包含 50 个值的 XML 块,并且您想查看所有 50 个值是否都是数字,会怎么样?如果 XML 块中的任何值都不是数字,那么您就白白抛出了 50 个异常!
多线程
多线程是提高网站每秒请求数的强大方法。我绝不是多线程方面的专家,所以我不会假装知道足够多的信息来为您提供有关其使用方法的见解。我可以推荐 Alan Dennis 的《.NET Multithreading》一书,因为他很好地描述了多线程的方方面面。但我确实要提醒您注意一件事。虽然多线程功能强大,可以加快页面速度,但如果您不极其小心,它可能会在您的代码中引入非常微妙且难以调试的问题。此外,人们普遍认为您的代码可以根据需要生成尽可能多的线程来完成工作。但生成过多线程实际上会损害性能。请记住,您的服务器上的处理器数量有限,并且它们会在所有线程之间共享。
.NET 数据访问技术
拥有像 DataSet
s 这样的独立数据源很棒,并且能够修改独立的 DataSet
,然后重新连接到数据源并同步更改,这确实令人惊叹。但在我看来,这应该严格用于性能不是问题的场合。对于只读数据库访问,DataReader
将比 DataSet
提供惊人的性能提升。
我再次使用了 .NET Test Harness 并创建了两个测试。第一个测试调用了我的数据库,用 50 行 10 列填充了一个 DataSet
,遍历了 DataTable
中的每个 DataRow
,并从 DataRow
的每一列中提取了数据。第二个测试做了同样的事情,但使用了 DataReader
。由于 DataAdapter
在内部使用 DataReader
来填充 DataSet
(我使用 Anakrino 工具查看了 DataAdapter
的内部机制),我假设它会比 DataSet
更快。但我对 DataReader
实际有多快感到惊讶。在 Debug 和 Release 构建下,DataReader
都比 DataAdapter
快 75%!
但是 DataSet
内置的将更改更新到数据库的功能怎么样?确实,DataReader
无法处理此功能。但在我看来,对于高速数据库访问(即读取、更新、插入和删除)的最佳策略是通过使用存储过程。我将不详细介绍这一领域,因为它超出了本文的范围。我的经验法则是,在 Web 开发中尽量避免使用 DataSet
。我通常只在尝试只满足 AppDomain 中的一个用户时,在我的 WinForm 应用程序中使用它们。
当我告诉人们不要在 Web 开发中使用 DataSet
s 时,我听到的最大的争论是,它们对于数据绑定到 ASP.NET Web 控件非常方便,您无法将 Repeater
控件绑定到 DataReader
。与 DataSet
s 一样,我关于数据绑定的经验法则是“不要这样做!”,我将在下一节告诉您原因。
我想介绍的最后一个主题是如何从 DataSet
(如果您坚持使用它们)或 DataReader
中提取数据。DataRow
和 DataReader
都有索引器来访问列中的值。获取这些对象中数据的两个最常用的方法是像这样在索引器之后调用 ToString()
方法
string temp = dataReader["FirstColumn"].ToString();
这还可以。但是如果您要从 DataReader
中获取的是 Int32
而不是 string
呢?这是我见过的一种方法
int temp = int.Parse(dataReader["FirstColumn"].ToString();
我看到的另一种方法是这样的
int temp = (int)dataReader["FirstColumn"];
那么您能猜到哪一个更快吗?如果您猜的是第二个,那么您就对了。DataReader
和 DataSet
将它们的基础数据存储为 object 类型。因此,您可以直接从 DataReader
中强制转换为 Int32
。如果您先调用 ToString()
,您会将对象解装(unboxing)为一个 string
,然后对 string
调用 Int32.Parse
,后者会将其转换为 Int32
。这可能看起来很常识,但我见过使用 ToString()
方法的代码,所以我对直接转换快多少感到好奇。当我在 .NET Test Harness 中进行测试时,我发现直接转换快了 3 倍!这是一个相当大的性能提升,因为一个简单的转换错误而错过。
从 DataReader
中获取数据的另一个方法是使用它的 DataReader.Getxxx
方法。我尽量避免使用它们的主要原因是它们只接受基于数字的列序数。我试图在索引器中使用列名,因为如果您更改了存储过程中的列顺序,就不会在代码中引入细微的错误。
数据绑定:福音还是隐藏的魔鬼?
当 .NET 发布时,许多 Web 开发人员看到服务器端 ASP.NET 控件是多么容易使用,都非常兴奋。将控件放到页面上,将 DataSet
或 ArrayList
设置为 DataBind
属性,然后就可以出发了,即时网页。不再需要在 ASP 中循环 RecordSet
s 来构建 HTML。这太容易了!
这确实很容易,但付出了代价。我最近在一个广泛使用数据绑定 ASP.NET DataList
和 Repeater
控件来构建其网页的项目上工作。但性能结果非常令人失望。使用 ACT 对这些页面运行一些负载测试,在用户数量很少(5 个)的情况下,页面性能相当好。但一旦我们将用户数量增加到更现实的数量,比如 25 个,页面性能就一落千丈。因此,我们在运行测试时开始使用 PerfMon,并发现了一些非常有趣的事情。页面在垃圾回收中的时间百分比平均为 30%,最高峰值为 45%!此外,在整个测试运行中,处理器使用率百分比一直锁定在 95%。最后两个统计数据是巨大的红色警告灯,因为它们告诉我,我们的页面不仅运行缓慢,而且根本无法扩展。如果网站用户量很大,将会陷入严重困境。
这不好,对于发布到生产环境来说是不可接受的,因此我们开始深入研究数据绑定控件的工作原理。我们发现数据绑定过程做了两件事,这两件事都损害了性能。首先,数据绑定控件使用反射来查找正确的属性并从中提取数据。反射的成本相当高,如果您有一个 Repeater 控件从 40 个对象的数组中提取 6 个属性,那么性能损失会累积起来。
我们注意到的第二件事是,在数据绑定过程中创建的对象数量相当多(在 Anakrino 中查看 DataGrid
、DataList
和 Repeater
类 的 CreateControlHierarchy
,看看它是如何进行绑定的)。大量对象的创建导致了垃圾回收时间百分比如此之高。
因此,我们必须找到一种不使用数据绑定的方法来创建网页。我们尝试使用 ASP.NET 服务器控件并手动推送数据,但这并没有真正改变我们的统计数据。然后我们变得绝望,开始真正地集思广益。我们尝试在每个页面上放置一个 Literal
控件,并在后台代码的 PageLoad
事件中使用 StringBuilder
来构建页面的 HTML 结构,然后将 HTML 放入 Literal
控件的 text
属性中。这种技术表现得非常好,垃圾回收时间百分比几乎降至零。但 HTML 的可维护性将是一场噩梦。
然后我们决定尝试混合 ASP.NET 后台代码和 ASP 风格的 HTML 构建。我们在 aspx 的后台代码 PageLoad
事件中创建并填充了所有数据对象,以及页面所需的任何业务逻辑。然后,在 aspx 文件中,我们回到了 ASP 风格的 HTML 构建,使用老式的 <%=(C# code)%>
将数据从我们的数据对象插入到 HTML 中。这种技术表现与 StringBuilder 技术一样好,但代码的可维护性要好得多。
这种 ASP 风格的 HTML 渲染的唯一问题是,您又回到了像 ASP 一样的向仅前流写入 HTML。在使用 ASP.NET 控件时,您可以在代码的任何阶段更新任何控件的值。但是 Web 开发人员自 ASP 诞生以来一直在这样做,所以在 ASP.NET 控件无法满足性能要求的极端情况下,这是一种可行的选择。
一旦我们以这种方式编写了测试页面,我便运行了 ACT 对数据绑定版本和新的 ASP 风格版本进行了比较。在 15 分钟的运行中,有 10 个用户,ASP 风格页面的迭代次数与数据绑定版本一样多。平均每秒请求数从 72.55 个跃升至 152.44 个。平均最后一个字节时间从 21.79 毫秒下降到惊人的 2.57 毫秒!但最好的统计数据来自垃圾回收中的百分比和处理器百分比。垃圾回收中的平均百分比时间从 30% 下降到 0.79%,平均处理器百分比从 95% 下降到 10%!这意味着我们的 ASP 风格页面将能够轻松地扩展到更多的用户。
结论
我在这篇文章中谈到的一些性能测试结果,当我第一次看到它们时,真的让我大吃一惊。但这是否意味着您应该实施我在这篇文章中谈到的所有内容?不。以数据绑定 ASP.NET 服务器控件为例。您应该从现在开始将它们禁止在您的网站上吗?我希望不是。我认为它们很棒,并且有其存在的意义。您需要做的是决定一个页面对您网站的成功有多重要。对于许多网站来说,90% 的页面请求只占网站实际包含页面的 10%。解析您的 IIS 日志是查看每个页面被调用频率的好方法,从而决定该页面对您网站的成功有多重要。对代码进行性能调优通常会以增加复杂性和失去可维护性为代价。您应该权衡对每个页面进行的任何潜在性能更改的收益。在我刚刚谈到的项目中,我们决定放弃 ASP.NET 控件,并重写了 8 个请求最多的页面,但保留了其他 80 多个页面,使用数据绑定控件。
开发一个速度极快的网站的秘诀是,拥有一个好的性能分析工具集,并真正学会如何使用它们。剖析和分析您的代码正在做什么,并进行相应的调整。使用 Anakrino 深入研究您正在使用的类的代码,并了解后台真正发生的事情。尝试原型化几种不同的方法来实现任何给定的功能,并找出哪种方法最快。您将编写更有效的代码,并在过程中对 .NET 有更深入的理解。