asp.net的10个提升性能或扩展性的秘密(一)
简介
Asp.net有很多值得你挖掘的“秘密”,当你发现了它们,将会给你网站的性能和可扩展性带来巨大提升!例如,对于Membership以及Profile提供程序有一些秘密的瓶颈,它们很容易被解决,从而使认证和授权更加快速。另外,asp.net的http管线可以为每一个请求作处理,防止执行了某些不必要的代码而遭受攻击。不只是这些,asp.net工作进程能够突破默认限制,从而完全发挥它的威力。在浏览器端(不是在服务器端)的页面分段输出缓存能显著减少由于请求访问所需要占用的大量下载时间。在需要的用户界面上的加载能够给你的网站带来快速而平稳的体验。最后,内容分发网络(CDN)以及对HTTP缓存头的正确使用能够使你的网站得到快速响应。
在这篇文章中,你将学习这些技术,给你的asp.net应用程序性能和可扩展性带来巨大的提升。下面是接下来将要讨论的技术:
- Asp.net管线优化
- Asp.net进程配置优化
- Asp.net网站上线之前你必须要做的事
- 内容分发网络(CDN)
- 浏览器端缓存Ajax调用
- 最大限度地正确使用浏览器缓存
- 在需要的UI上逐步加载以提供快速平滑的体验
- 优化asp.net 2.0Profile 提供程序
- 怎样在不“下线”站点的情况下查询asp.net 2.0的Membership表
- 防止拒绝服务攻击
上面的技术都能够在任何的asp.net网站中实现,特别是那些使用了asp.net 2.0的Membership以及Profile 提供程序的网站。
Asp.net管线优化
有一些asp.net默认的HttpModules被设置在请求管线中,它们会参与每一个请求。例如,SessionStateModule会处理每一个请求,转换会话cookie然后给HttpContext加载适当的session。不是所有的这些模块都总是被需要的。例如,如果你没有使用Membership 以及 Profile provider,你就不需要FormsAuthentication模块。如果你没有对你的用户使用Windows认证,你就不需要WindowsAuthentication模块。这些模块只是被安置在管线之内,对每一个请求执行某些不是必须要的代码。
这些默认模块被定义在machine.config文件中(位于$WINDOWS$\Microsoft.NET\Framework\$VERSION$\CONFIG directory)
你可以从你的web应用程序中通过在web.config文件中加入<remove>节点移除这些默认的modules。
上面的配置非常适合那些使用数据库并基于Forms认证,并且不需要任何Session支持的网站。所以,所有上面这些模块都能够被安全地移除。
Asp.net进程配置优化
Asp.net进程模型配置定义了一些进程级别的属性,例如asp.net使用了多少线程数,在超时之前它会阻塞线程多久,允许多少请求 等待I/O工作完成等。这些默认的配置在很多情况下有太多的限制。现在,硬件已经变得相当便宜,技嘉科技的双核RAM服务器已经变成了非常普遍的选择。所以,进程模型配置可以配置得让asp.net线程使用更多的系统资源,并为每一个服务器提供更好的扩展性。
一个通常的asp.net的安装将按如下的配置创建一个machine.config:
你需要修改这个自动配置,为不同的属性使用一些特殊值来自定义asp.net工作进程的工作。例如:
这里除了以下的一些值,其他值都是默认值:
- maxWorkerThreads-对每个进程来说20是默认值。在一个双核计算机上,将会给asp.net分配40个线程。这意味着asp.net一台双核机器上一次能够并行处理40个请求在。我将它增加到100,给予asp.net每个进程更多的线程。如果你有一个不是CPU密集型应用程序,可以很容易地处理更多地请求量,那么你可以增加该值。特别是如果你的Web应用程序使用许多Web Service调用或者下载/上传的许多数据没有给CPU带来压力的话。当asp.net运行超过它允许的工作线程数,它将会停止处理更多到来的请求。请求被放进一个队列中,并保持等待知道一个工作线程被释放。当你的站点开始遭受比你预期的更多的攻击时,通常会发生这样的情况。如果遇到这种情况,如果你的CPU有空闲,给每个进程(asp.net进程)增加工作线程数。
- maxIOThreads-对每个进程来说20是默认值。在一个双核计算机上,将会给asp.net分配40个线程来进行I/O操作。这意味着在一台双核服务器上asp.net一次可以并行处理40个I/O请求。I/O请求可能是文件的读写、数据库操作、Web Service的调用Web应用程序内部生成的Http请求等。所以,你可以将它设置为100,如果你的服务器有足够的系统资源来处理更多的I/O请求的话。特别是,当你的Web应用程序经常下载或上传数据,并行调用许多外部的webservice时,效率提升会很明显。
- minWorkerThreads-当可用的asp.net工作线程数下降到该值以下时,asp.net开始将即将到来的请求压入队列。所以你可以将该值设置为一个很低的数字来增加可被处理的当前请求的数目。然而,不要将它设置地过低,因为Web应用程序代码可能需要做某些后台处理以及某些并行处理需要一定数量的空闲工作线程。
- minIOThreads-与minWorkerThreads相同,但这个值涉及的是I/O线程数。但是,你可以给它设置一个比minWorkerThreads更低得数字,因为没有发生在I/O线程上并行处理的情况。
- momoryLimit-指定允许使用内存大小的最大值,是与系统总内存的百分比。它是在asp.net在启动一个新进程和重新分配正在处理的请求之前工作进程可以消耗的内存大小。如果只有你的Web应用程序允许在一个“专用的盒子里”并且没有其他的进程需要RAM,你可以设置它为一个很高的值,比如80。但是,如果你有一个内存泄露的应用程序总是不断泄露内存,最好将它设置为一个低一点的值,以便泄露内存的进程可以在它变得无法处理之前尽快被彻底回收。特别是,当你正在使用一个COM组件并且它产生内存泄露时,就会遇到这种情况。
除了processModel节点,还有一个非常重要的节点——system.net,你可以指定给一个IP它能外发请求数的最大值。
默认值为2,它太低了。这意味着你不能从你的Web应用程序到一个IP建立超过两个同时连接。那些需要获取外部内容的站点很大程度上都受到这个默认配置的制约。这里我将它设置为100,如果你的Web应用程序需要对特定服务器有很多调用需求的话,你可以考虑设这一个甚至更高的值。
Asp.net网站上线之前你必须要做的事
如果你正在使用Asp.net 2.0的Membership Provider,你应该对你的web.config做一些调整:
- 在ProfileProvider中加入applicationname属性。如果你没有加入一个特别的名称,ProfileProvider将使用一个GUID。所以,在你的本地机器上,你将有一个GUID而在发布的服务器上又将有另外一个GUID。如果你拷贝你本地数据库到发布服务器,你将不能重用在你本地数据库中以存在的记录,并且asp.net将在发布服务器上创建一个新的应用程序。你需要在这里加入它:
- 无论什么时候一个页面的请求完成之后,ProfileProvider将自动地保存profile。所以,这可能导致你数据库的一个不必要的更新,它有很显著的性能损失!关闭自动保存并且在你的代码中使用Profile.Save()明确地完成。
- Role Manager一直查询着数据库来获取用户的角色。这同样也有显著的性能损失。你可以让Role Manager缓存角色信息到cookie中来避免它。但是它也会为那些没有很多角色的用户分配差不多2KB的Cookie,但这也不是一种常见的场景。所以,你可以很安全地在cookie中存储角色信息。
上面的三个设置主要是为了高访问量的网站。
内容分发网络
来自浏览器的每一个请求都是通过了跨越世界的骨干网到达你的服务器的。请求需要经过一定数量的国家、大陆、海洋才传递给你的服务器,所以它会变得很慢。例如,如果你将你的服务器架在USA,并且一些人在澳大利亚浏览你的网站,每一个请求都从地球的一端到另一段才能到达你的服务器,然后再返回给浏览器。如果你的站点有很大数量的静态文件,像图片、CSS、Javascript。为它们发送请求,跨越世界去下载它们,将会花费大量的时间。如果你能够在澳大利亚架设一台服务器,并且重定向用户到你在澳大利亚的服务器上去,那每一个这样的请求将比到达美国花费更少的时间。不仅网络延迟会更小,数据传输的路由也将更为快速,因此静态内容将能够以更快的速度下载。
注:由于本节与asp.net技术无关,牵扯到网络规划,所以不作讨论,仅仅给出一张示意图:
在浏览器上缓存AJAX调用
浏览器能缓存图片,JS文件,CSS文件到硬盘上。它同时也能缓存XML HTTP调用,如果调用是Http GET的话。缓存是基于URL的。如果是相同的URL,再次请求时,它被缓存到计算机上,然后响应从缓存去加载,而不是从服务器。基本上,浏览器能缓存任何的HttpGet请求调用,并返回基于该URL的缓存数据。如果你像HttpGet请求一样发出XML HTTP请求,并且服务器返回某些特殊响应头,指示浏览器缓存响应数据。在未来再次调用的时候,响应将会直接从缓存返回数据。因此节省了网络往返延时和下载时间。
我们缓存了用户的状态,那样当用户在接下来的一些天再次访问网站,用户从浏览器的缓存中直接获取缓存过的页面,而不是从服务器获得。因此第二次加载会变得非常快。我们也缓存了页面的某些部分,这取决于用户的操作。当用户再次执行相同的操作,一个缓存的结果就被直接从本地缓存加载,因此省去了网络往返的时间。
如果你在请求的响应期间返回Expires头,浏览器将缓存XML HTTP响应。有两个响应头你需要随着响应返回来让浏览器缓存响应数据:
这指示浏览器缓存响应数据知道2030年1月。尽管你发起携带相同参数的相同的XML HTTP请求,你也将仅会从你本地计算机中获得缓存后的响应数据。当然还有一个更合理的方式来控制浏览器缓存。例如,有些请求头指示浏览器缓存60秒,但在60秒之后会重新连接服务器并获得一个刷新的数据。当浏览器本地缓存超过60秒时它也将防止代理返回缓存的响应数据。
让我们通过一个asp.net web service调用来输出这样的响应头。
这将导致请求头变成如下的形式:
Expires头被正确地设置了。但起决定作用的还是Cache-control。你可以看到max-age被设置为0,它将防止浏览器做任何形式的缓存。如果你确信想要防止缓存,你应该设置这样的一个cache-control头。看起来就好像事情都是实时发生的。
输出就像下面一样,没有缓存:
Asp.net 2.0有一个Bug——你不能改变max-age头。因为max-age被设置为0,asp.net 2.0设置Cache-control为私有的。因为max-age=0意味着不需要缓存。所以,没有办法让asp.net 2.0返回缓存了响应的适当的头。这是由于asp.net Ajax 框架它在执行发出一个请求前可以拦截对Web Services的调用以及默认地不正确地设置max-age为0。
“黑客”时刻到来!在反编译HttpCachePolicy类(Context.Response.Cache对象所属的类)的源码后,我发现如下的代码:
不管怎样,this._maxAge将会被设置为0,并且检查——if (!this._isMaxAgeSet || (delta < this._maxAge))也将组织设置一个更大的值。由于这个原因,我们需要绕开SetMaxAge方法,直接设置_maxAge的值。当然,这需要用到“反射”机制:
这将返回下面的头:
现在,max-age被设置为60,因此浏览器将缓存响应60秒。如果你在60秒内再次发出相同的请求。它将返回相同的响应。这里有个输出测试,显示了从服务器返回的日期/时间:
在一分钟之后,缓存失效,浏览器向服务器再次发出一个请求。客户端代码如下:
还有另一个问题有待解决:在web.config中,你将看到asp.net ajax将看到:
这将阻止我们设置Response对象的_maxAge对象,因为它要求反射。所以你将不得不删除该项设置,或者将值改为:Full。
未完,待续。。。