通过避免下列 10 个常见 asp.net 缺陷使网站平稳运行 2
图 5 消除不必要的会话状态数据库访问
那么您应该怎么办呢?很简单:禁用不使用会话状态的页中的会话状态。这样做总是一个好办法,但是当会话状态存储在数据库中时,该方法尤其重要。图 5 显示如何禁用会话状态。如果页面根本不使用会话状态,请在其 page 指令中包含 enablesessionstate="false",如下所示:
<%@ page enablesessionstate="false" ... %>
该指令阻止会话状态管理器在每个请求中读取和写入会话状态数据库。如果页面从会话状态中读取数据,但却不写入数据(即,不修改用户会话的内容),则将 enablesessionstate 设置为 readonly,如下所示:
<%@ page enablesessionstate="readonly" ... %>
最后,如果页面需要对会话状态进行读/写访问,则省略 enablesessionstate 属性或将其设置为 true:
<%@ page enablesessionstate="true" ... %>
通过以这种方式控制会话状态,可以确保 asp.net 只在真正需要时才访问会话状态数据库。消除不必要的数据库访问是构建高性能应用程序的第一步。
顺便说一下,enablesessionstate 属性是公开的。该属性自 asp.net 1.0 以来就已经进行了说明,但是我至今仍很少见到开发人员利用该属性。也许是因为它对于内存中的默认会话状态模型并不十分重要。但是它对于 sql server 模型却很重要。
返回页首
未缓存的角色
以下语句经常出现于 asp.net 2.0 应用程序的 web.config 文件以及介绍 asp.net 2.0 角色管理器的示例中:
<rolemanager enabled="true" />
但正如以上所示,该语句确实会对性能产生明显的负面影响。您知道为什么吗?
默认情况下,asp.net 2.0 角色管理器不会缓存角色数据。相反,它会在每次需要确定用户属于哪个角色(如果有)时参考角色数据存储。这意味着一旦用户经过了身份验证,任何利用角色数据的页(例如,使用启用了安全裁减设置的网站图的页,以及使用 web.config 中基于角色的 url 指令进行访问受到限制的页)将导致角色管理器查询角色数据存储。如果角色存储在数据库中,那么对于每个请求需要访问多个数据库的情况,您可以轻松地免除访问多个数据库。解决方案是配置角色管理器以在 cookie 中缓存角色数据:
<rolemanager enabled="true" cacherolesincookie="true" />
您可以使用其他<rolemanager> 属性控制角色 cookie 的特征 — 例如,cookie 应保持有效的期限(以及角色管理器因此返回角色数据库的频率)。角色 cookie 默认情况下是经过签名和加密的,因此安全风险虽然不为零,但也有所缓解。
返回页首
配置文件属性序列化
asp.net 2.0 配置文件服务为保持每个用户的状态(例如个性化首选项和语言首选项)的问题提供了一个现成的解决方案。要使用配置文件服务,您可以定义一个 xml 配置文件,其中包含要保留的代表单个用户的属性。然后,asp.net 编译一个包含相同属性的类,并通过添加到页的配置文件属性提供对类实例的强类型访问。
配置文件灵活性很强,它甚至允许将自定义数据类型用作配置文件属性。但是,其中却存在一个问题,我亲眼看到该问题导致开发人员出差错。图 6 包含一个名为 posts 的简单类,以及将 posts 用作配置文件属性的配置文件定义。但是,该类和该配置文件在运行时会产生意外的行为。您能找出其中的原因吗?
问题在于 posts 包含一个名为 _count 的私有字段,该字段必须进行序列化和反序列化,才能完全冻结和重新冻结类实例。但是 _count 却没有经过序列化和反序列化,因为它是私有的,而且默认情况下 asp.net 配置文件管理器使用 xml 序列化对自定义类型进行序列化和反序列化。xml 序列化程序将忽略非公共成员。因此,会对 posts 的实例进行序列化和反序列化,但是每次反序列化类实例时,_count 都会重设为 0。
一种解决方案是使 _count 成为公共字段而非私有字段。另一种解决方案是使用公共读/写属性封装 _count。最佳解决方案是将 posts 标记为可序列化(使用 serializableattribute),并将配置文件管理器配置为使用 .net framework 二进制序列化程序对类实例进行序列化和反序列化。该解决方案能够保持类本身的设计。与 xml 序列化程序不同的是,二进制序列化程序序列化字段,而不管是否可以访问。图 7 显示 posts 类的修复版本并突出显示了更改的附带配置文件定义。
您应该牢记的一点是,如果您使用自定义数据类型作为配置文件属性,并且该数据类型具有必须序列化才能完全序列化类型实例的非公共数据成员,则在属性声明中使用 serializeas="binary" 属性并确保类型本身是可序列化的。否则,将无法进行完整的序列化,并且您还将浪费时间来尝试确定配置文件无法工作的原因。
返回页首
线程池饱和
在执行数据库查询并等待 15 秒或更长时间来获得返回的查询结果时,我经常对看到的实际的 asp.net 页数感到非常惊讶。(我也等待了 15 分钟才看到查询结果!)有时,延迟是由于返回的数据量很大而导致的不可避免的无奈结果;而有时,延迟则是由于数据库的设计不佳导致的。但不管是什么原因,长时间的数据库查询或任何类型的长时间 i/o 操作在 asp.net 应用程序中都会导致吞吐量的下降。
关于这个问题我以前已经详细地描述过,所以在此就不再作过多的说明了。我只说一点就够了,asp.net 依赖于有限的线程池处理请求,如果所有线程都被占用来等待数据库查询、web 服务调用或其他 i/o 操作完成,则在某个操作完成并且释放出一个线程之前,其他请求都必须排队等待。当请求排队时,性能会急剧下降。如果队列已满,则 asp.net 会使随后的请求失败并出现 http 503 错误。这种情况不是我们希望在 web 生产服务器的生产应用程序上所乐见的。
解决方案非异步页面莫属,这是 asp.net 2.0 中最佳却鲜为人知的功能之一。对异步页面的请求从一个线程上开始,但是当它开始一个 i/o 操作时,它将返回该线程以及 asp.net 的 iasyncresult 接口。操作完成后,请求通过 iasyncresult 通知 asp.net,asp.net 从池中提取另一个线程并完成对请求的处理。值得注意的是,当 i/o 操作发生时,没有占用线程池线程。这样可以通过阻止其他页面(不执行较长的 i/o 操作的页面)的请求在队列中等待,从而显著地提高吞吐量。
您可以在 msdn?magazine 的 2005 年 10 月刊中阅读有关异步页面的所有信息。i/o 绑定而不是计算机绑定且需要很长时间执行的任何页面很有可能成为异步页面。
当我将关于异步页面的信息告知开发人员时,他们经常回答“那真是太棒了,但是我的应用程序中并不需要它们。”对此我回答说:“你们的任何页面需要查询数据库吗?它们调用 web 服务吗?您是否已经检查 asp.net 性能计数器中关于排队请求和平均等待时间的统计信息?即使您的应用程序至今运行正常,但是随着您的客户规模的增长,应用程序的负载可能会增加。”
实际上,绝大多数实际的 asp.net 应用程序都需要异步页面。请切记这一点!
返回页首
模拟和 acl 授权
以下是一个简单的配置指令,但是每当在 web.config 中看到它时都让我眼前一亮:
<identity impersonate="true" />
此指令在 asp.net 应用程序中启用客户端模拟。它将代表客户端的访问令牌附加到处理请求的线程,以便操作系统执行的安全性检查针对的是客户端身份而不是辅助进程身份。asp.net 应用程序很少需要模拟;我的经验告诉我,开发人员通常都是由于错误的原因而启用模拟的。以下是原因所在。
开发人员经常在 asp.net 应用程序中启用模拟,以便可以使用文件系统权限来限制对页面的访问。如果 bob 没有查看 salaries.aspx 的权限,则开发人员将会启用模拟,以便可以通过将访问控制列表 (acl) 设置为拒绝 bob 的读取权限,阻止 bob 查看 salaries.aspx。但是存在以下隐患:对于 acl 授权来说,模拟是不必要的。在 asp.net 应用程序中启用 windows 身份验证时,asp.net 会自动为请求的每个 .aspx 页面检查 acl 并拒绝没有读取文件权限的调用者的请求。即使禁用了模拟,它仍会这样操作。
有的时候需要证明模拟的合理性。但是您通常可以用良好的设计来避免它。例如,假定 salaries.aspx 在数据库中查询只有管理人员才能知道的工资信息。通过模拟,您可以使用数据库权限拒绝非管理人员查询工资数据的能力。或者您可以不考虑模拟,并且通过为 salaries.aspx 设置 acl 以使非管理人员不具有读取权限,从而限制对工资数据的访问。后一种方法提供的性能更佳,因为它完全避免了模拟。它也消除了不必要的数据库访问。为什么查询数据库仅由于安全原因被拒绝?
顺便说一下,我曾经帮助对一个传统的 asp 应用程序进行故障排除,该应用程序由于内存占用不受限制而定期重新启动。一个没有经验的开发人员将目标 select 语句转换成了 select *,而没有考虑要查询的表包含图像,这些图像很大而且数目很多。问题由于未检测到内存泄漏而恶化。(我的托管代码领域!)多年来运行正常的应用程序开始突然停止工作,因为以前返回一两千字节数据的 select 语句现在却返回了几兆字节。如果再加上不充分的版本控制,开发团队的生活将不得不“亢奋起来”— 这里所谓的“亢奋”,就如同当您在晚上要睡觉时,还不得不看着您的孩子玩令人厌烦的足球游戏一样。
理论上,传统的内存泄漏不会发生在完全由托管代码组成的 asp.net 应用程序中。但是内存使用量不足会通过强制垃圾收集更频繁地发生而影响性能。即使是在 asp.net 应用程序中,也要警惕 select *!
返回页首
不要完全信赖它 — 请设置数据库的配置文件!
作为一名顾问,我经常被询问为何应用程序没有按预期执行。最近,有人询问我的团队为何 asp.net 应用程序只完成请求文档所需吞吐量(每秒的请求数)的大约 1/100。我们以前所发现的问题是我们在不能正常运行的 web 应用程序中发现的问题特有的 — 和我们所有人应该认真对待的教训。
我们运行 sql server profiler 并监视此应用程序和后端的数据库之间的交互情况。在一个更极端的案例中,仅仅只是一个按钮单击,就导致数据库发生了 1,500 多个错误。您不能那样构建高性能的应用程序。良好的体系结构总是从良好的数据库设计开始。不管您的代码的效率有多高,如果它被编写不佳的数据库所拖累,就会不起作用。
糟糕的数据访问体系结构通常源于下面的一个或多个方面:
? 拙劣的数据库设计(通常由开发人员设计,而不是数据库管理员)。
? datasets 和 dataadapters 的使用 — 尤其是 dataadapter.update,它适用于 windows 窗体应用程序和其他胖客户端,但是对于 web 应用程序来说通常不理想。
? 具有拙劣编制计算程序、以及执行相对简单的操作需消耗很多 cpu 周期的设计糟糕的数据访问层 (dal)。
必须先确定问题才能对其进行处理。确定数据访问问题的方式是运行 sql server profiler 或等效的工具以查看后台正在执行的操作。检查应用程序和数据库之间的通信之后,性能调整才完成。尝试一下 — 您可能会对您的发现大吃一惊。
返回页首
结论
现在您已经了解在生成 asp.net 生产应用程序过程中可能遇到的一些问题及其解决方案了。下一步是仔细查看您自己的代码并尝试避免我在此概述的一些问题。asp.net 可能降低了 web 开发人员的门槛,但是您的应用程序完全有理由灵活、稳定和高效。请认真考虑,避免出现新手易犯的错误。
图 8 提供了一个简短检查列表,您可以使用它来避免本文中描述的缺陷。您可以创建一个类似的安全缺陷检查列表。例如:
? 您是否已经对包含敏感数据的配置节进行加密?
? 您是否正在检查并验证在数据库操作中使用的输入,是否使用了 html编码输入作为输出?
? 您的虚拟目录中是否包含具有不受保护的扩展名的文件?
如果您重视网站、承载网站的服务器以及它们所依赖的后端资源的完整性,则这些问题非常重要。
jeff prosise 是对 msdn magazine 贡献很大的编辑以及多本书籍的作者,这些书籍中包括 programming microsoft .net (microsoft press, 2002)。他也是软件咨询和教育公司 wintellect 的共同创始人。
摘自 msdn magazine 的 2006 年 7 月刊。