分享

asp.net的10个提升性能或扩展性的秘密(一)

 kittywei 2012-03-15

简介

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。


未完,待续。。。


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 提供程序的网站。


最大限度地正确使用浏览器缓存


使用统一的URL

浏览器基于内容缓存URL。当URL发生改变时,浏览器会从服务器获取该URL的一个新版本。URL可以通过查询参数来改变。例如,/default.aspx被缓存在客户端浏览器,如果你请求/default.aspx?123,它将从服务器重新获取内容。如果你正确地设置了用于缓存的响应头,该请求的响应也能够被缓存到浏览器。在那种情况下,改变请求参数,都将从服务器返回新的内容。所以,当你想获得缓存的响应时你需要确信你在各处都使用了统一的URL。一个通常的错误是,有时www子域会被URL忽略。比如,www./default.aspx是不同于/default.aspx的。它们将会被分开缓存。

更长时间地缓存静态内容

静态文件可以被缓存更长时间,比如一个月。如果你觉得你应该缓存两天,这样当你改变文件时,用户将在不久能获取到,你的想法是错误的!如果你更新了一个通过Expires头缓存的文件,新用户将可以直接获取最新的文件,而老用户在你本地浏览器失效日期之前,将一直只能看到老文件。所以,一旦你正在使用Expires头来缓存静态文件,你应该使用一个尽可能大的值。

例如,如果你已经设置了一个Expires头来缓存一个文件三天,一个用户今天获得了文件,并且在接下来的三天内它都将存储在缓存中。另一个用户在明天获得了该文件,并缓存它在明天过后的三天。如果你在后天修改了文件,第一个用户将在第四天看到,第二个用户将在第五天看到。所以,不同的用户将看到不同版本的文件。结果是,你不需要通过设置一个很小的失效日期来使得所有的用户都在不久之后获得新版本。你将不得不修改文件的URL来确保每一个用户立刻获得相同的最新文件。

你可以通过IIS 管理器来设置静态文件的Expires头。在后面你将学到如何来设置。

使用缓存友好的文件目录结构

将缓存内容存储在一个公共目录下。例如,存储你站点所有的图片到static文件来代替将图片分开放置到不同的子目录下。这将有助于你使用统一的URL。例如,在任何地方你都可以这样写:/static/images/somefile.gif。

重用公共图片文件

有时,我们将公共的图片文件放在几个不同的虚拟目录中,这样我们能够写很短的路径来引用文件。例如,你在根目录、一些子目录和CSS文件夹下都有一个indicator.gif。你这么做因为你不需要当心不同的路径,并且你可以仅仅使用文件名来作为虚拟URL。但,这很不利于缓存。每一个这种文件的拷贝在浏览器上都是被独立缓存的。所以,你应该收集解决方案内所有的图片文件,然后将它们放置到一个相同的文件中,并使用相同的路径来引用它们。

当你希望缓存失效时,改变文件名称

当你希望改变一个静态文件时,不要仅仅更新文件,因为它已经被缓存到用户的浏览器中去了。你需要改变文件的名称,并改变对其的所有引用,来确保浏览器下载新文件。你也可以在数据库中存储文件名,或者存储在配置文件中然后使用数据动态绑定到URL中(比如在请求后面追加版本号)。

在访问静态文件时使用一个版本号

如果你不想你的文件因为存在同一文件的多个拷贝而杂乱,你可以使用查询参数来对相同的文件标识不同的版本。例如,一个GIF文件可以被这样访问:/static/images/indicator.gif?v=1。当你想改变这个indicator.gif文件时,你可以覆盖相同的文件,然后更新所有对该文件的引用为:/static/images/indicator.gif?v=2。这种方式你可以再次更新相同的文件,并且仅需要更新版本号。

存储可缓存的文件到一个不同的域名下

将静态文件放到一个不同的域名下总是一个不错的注意。首先,浏览器能两个并发的连接来下载静态文件。另一个好处是你不需要给静态文件发送cookies。当你将静态文件作为你Web应用程序的一部分并和其他文件放置在同一个域名下,浏览器需要给它发送所有的asp.net Cookies以及你Web 应用程序产生的所有其他的cookies。这使得请求头部变得很大(其实没有必要)并且浪费了带宽。你不需要发送那些cookies来访问这些静态文件。所以,如果你将这些静态文件放置到另一个域名下,那些cookies将不会发送。例如,将静态文件放置在www.域名下,而你的Web站点运行在www.。这里所谓的其他的域名,并不一定需要是另外一个完整的web站点。它可以仅仅是和当前Web 应用程序共享路径的一个别名。

SSL不能被缓存,所以最小程度地使用SSL

任何涉及到SSL的内容都不能被缓存。所以,你需要将那些静态文本放置到SSL外部。另外,你应该尝试限制只在那些安全的页面(例如登陆页面,付款页面)上使用SSL。站点其他的页面采用通常的HTTP请求。SSL会加密请求与相应内容,并因此给服务器带来额外负载。加密内容也比通常文件的内容更大并因此在传输过程中会占用更多的带宽。

HTTP POST请求将永远无法缓存

缓存仅仅发生在HTTP GET 请求上。HTTP POST 请求永远不会被缓存。所以,你想缓存的任何形式的AJAX调用都需要是HTTP GET形式的请求。

生成Content-Length响应头部

当你正在通过Web Service或者HTTP Handlers动态产生内容时,确保你设置了Content-Length头部。当浏览器通过查看Content-Length头部知道需要下载多少字节时,它能够对更快速地下载数据有多种优化方式。当这个头是当前连接的头部时,浏览器能够更有效地使用持续连接。这可以避免浏览器为每一个请求都打开一个新的连接。当没有Content-Length头部时,浏览器不知道它将从服务器接受多少字节数据,因此连接一直被打开着,从服务端获取数据直到连接被关闭。因此,你错失了“持续”连接的优势,它能够大大地减少对一些像CSS、JS、图片等小文件的下载时间。(关于持续连接,请查看我之前的Comet系列的文章)


怎样在IIS上配置静态文件缓存


在IIS管理器上,Web站点属性窗口有个”HTTP 头”的选项卡,在那里你可以为所有的IIS处理的请求定义Expires头。你可以定义是否直接让内容失效或者在一定的天数或者特定的日期之后失效。第二个选项(Expires after)使用的是“滑动过期”,而不是“绝对过期”。这非常得有用,因为它作用在每一个请求上。无论什么时候,某个人请求一个静态文件,IIS将基于该项计算失效日期。


对于那些被asp.net处理的动态页面,通过handler可以修改Expires头部并且覆写IIS默认的设置。


基于需求的页面渐进加载以提升平滑的用户体验


一个好的解决方案是,动态加载HTML片段以及需要的Javascript。在dropthings这个项目中,我在下面的截图中显示了如何做它:


当你点击“Help”链接的时候,它动态加载帮助的内容。这段HTML不是作为default.aspx页面的一部分来解析的。因此和help相关的内容不影响站点本身的加载性能。而仅仅当用户点击“Help” 链接时才会动态加载。当用户再次点击”Help”链接,它可以直接从浏览器的缓存里读取。

它的原理是向一个*.aspx页面发出一个XMLHttp调用,获得响应的html标签,将这段html代码加入到一个DIV容器中,并让DIV可见。

用这种方法,你可以将UI内容分拆到许多更小的*.aspx文件中。尽管这些文件不能含有javascript或者stylesheet块,但是它们可以包含大量的你需要按需展示的html代码。因此你可以让只需要加载最基本的内容。


应对拒绝服务攻击


Web Service是黑客最容易攻击的目标,因为甚至一个初级黑客都可能通过直接调用那些很耗费资源的Web Service而挂掉一台服务器。Ajax起始页是DOS最好的攻击目标。因为如果你只是访问主页而不保护cookies,每一个攻击都建立一个新的用户,请求一个新页面。首次访问是最消耗资源的一次。但,它是挂掉一个网站最容易攻击的方式。你可以自己尝试,仅需要写如下的简单代码:


给你的最大的惊喜是,你将发现,在两次调用之后,你不能得到有效的响应。但这不是你表明你已经成功得挂掉了服务器。这只是你的请求被拒绝了。你很高兴你不再能获取任何服务,因此你实现了拒绝服务(你自己这么认为的)。而我们会高兴拒绝为你服务。

我的做法很简单,我会记录一个IP发送了多少次请求。当请求的数量超过一个特殊值时,拒绝一定时间内的进一步请求。方案是在asp.net的Cache中记录调用者的IP并且维护对每一个发起请求的统计。当一个IP的统计值超出限制,拒绝该IP更之后的请求,在10分钟之内。在十分钟之后,再次响应来自该IP的请求。

我有一个类叫做ActionValidator,它维护了一些特殊操作的统计,例如:首次访问,再次访问,异步回传,添加新部件,添加新页面等等。它检测是否为该特定IP的具体操作数超过了限制值。


枚举包含了操作的类型,以及检测它们的限制值,并设置了一个特定的再次接受服务的时间。

一个静态方法,IsValid做具体的检测处理。如果请求没有超过限制值,它返回true,否则如果请求需要被拒绝,返回false。你也可以显示一个页面,显示结果。


缓存的键是通过操作类型与客户端IP地址共同构建的。首先,它检测在缓存中是否有关于该操作与IP的实体。如果没有,开始计数并且为该操作记录其IP到缓存中。该缓存项的绝对失效时间可以确保在缓存项持续时间之后将被清除并且重新开始计数。当已经有了一个实体在缓存中时,获取最后一次访问统计,并且检查是否有超出限制值。如果没有超过,增加计数。通过做:Cache[url]=hit;,不需要再次存储更新过的值到缓存中。因为修改的是hit对象的引用,更新它就意味着也更新了缓存里的对象本身。事实上,如果你在缓存中再存储一次,cache失效计数将重新计数并且失去了在特殊持续时间后重新计数的功能。

使用非常简单,在default.aspx页面:


我在这里检查了特殊的场景,例如首次访问,再次访问,回发等等。

当然,你可以加入一些思科防火墙以防止拒绝服务攻击。你将得到你的托管服务提供商的一个保证整个网络中对DOS以及DDOS(分布式DOS)攻击是免疫的。但这些应用程序级别的DOS攻击是硬件无法阻止的,它必须在你自己的代码中实现。

只有非常少的部分网站采用了这种措施来防止应用程序级别的DOS攻击。因此,通过写一个简单的循环,访问很耗资源的网页或者以你家庭宽带不断调用Web Service,可以很轻易地挂掉一个服务器。




    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多