10 ASP.NET Performance and Scalability Secrets(转载推荐)
Introduction介绍
ASPNET 2.0 有很多的秘密,当探究了之后,能够给你带来很大的性能级可测两性的提升。例如,Membership和Profile provider 有一些秘密的瓶颈,能够很容易的解决并且使得身份验证和授权更加快速。此外,通过设置ASPNET HTTP 管道,能够避免执行在每一个不必要的代码。不仅是这样,ASPNET工作进程可以把每一个降低性能的点排除,将性能推向极限。浏览器中的页面局部缓存(不是服务器端),能够在重复的请求中明显的减少下载量。一经请求,UI的加载能够给你的站点一个快速流畅的感觉。最终,内容分发网络 (CDN)和恰当的使用HTTP Cache 头能够使得你的网站非常快。在这篇文章中,你可以学习到这些技术,使得你的ASPNET应用程序有一个性能上面的提升,并且可以增加10到100倍的负载。
这篇文章中,我们将会讨论下面的技术:
- ASPNET 管道优化
- ASPNET进程配置优化
- 上线之前,你必须要做的
- Content Delivery Network
- 浏览器中缓存AJAX调用
- 最佳的利用浏览器的缓存
- 为了更加流畅的体验,按照要求进行的UI加载
- 优化ASPNET2.0 Profile provider
- How to query ASP.NET 2.0 Membership tables without bringing down the site
- 在不降低整个网站的性能的情况下,应该如何查询ASPNET2.0
- 防止DOS攻击
上面的技术在任何一个ASPNET网站都可以被实现,尤其是那些使用了ASPNETMembership和Profile Provider的。
你可以了解到很多的关于ASPNET和ASPNET AJAX网站性能优化的东西,从我的书- Building a Web 2.0 portal using ASP.NET 3.5.
ASPNET管道优化
有一些ASPNET默认的HTTPModule,在请求管道中,而且在每一个请求都会拦截。举个例子,SessionStateModule
拦截每一个请求,转换Session Cookie并且加载相应的Session到HttpContext。并不是所有的这些Module总是必要的。举个例子,如果你没有使用Membership和Profile Provider你就不需要FormsAuthentication
module。如果你没有为你的用户使用Windows 身份验证你就不需要WindowsAuthentication。这些 module都在管道中,为每一个请求都执行一些不需要的代码。
默认的Module在machine.config中定义(位于$WINDOWS$\Microsoft.NET\Framework\$VERSION$\CONFIG 目录).
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
2
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
3
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
4
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
5
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
6
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
7
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
8
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
9
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
10
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
11
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
12
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
13
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
14
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
15
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
16
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
17
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
你可以出去这些默认的module,通过在你的应用程序的web.config中添加 <remove>
节点。例如:
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
2
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
3
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
4
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
5
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
6
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
7
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
8
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
9
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
上面的配置适合使用数据库的基于Form验证并且不需要任何Session支持的站点。所以这些module都可以安全的被删除掉。
ASPNET进程配置优化
ASPNET 进程模型配置定义了一些线程级别的属性,像ASPNET使用多少个线程,在超时之前等待线程多长时间,多少个请求等待IO工作的完成,等等。大多数情况下,默认的值都太小了。现在,硬件变得非常的便宜,双核上G内存的服务器已经变成了一个非常普遍的选择。所以,进程模型配置允许使得ASPNET进程消耗更多的系统资源并且提供更好的吞吐量、
一个常规ASPNET安装将会创建一个如下配置的machine.config 。
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
2
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
你需要设置这个自动配置,并且为一些属性用特定的值,来定制ASPNET工作进程的工作。例如:
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
2
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
3
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
4
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
5
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
6
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
7
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
8
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
9
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
10
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
11
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
12
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
13
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
14
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
15
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
16
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
17
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
18
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
19
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
20
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
21
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
22
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
23
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
24
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
25
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
26
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
27
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
28
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
29
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
30
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
除去以下的,这里的值都是默认的:
maxWorkerThreads
- 这个默认值是20。在一个双核的计算机中,将会有40个线程供ASPNET分配。这意味着,ASPNET可以同时处理40个请求。我将这个升到了100,以便给每一个ASPNET进程更多线程。如果你有一个应用程序,并不耗费CPU,并且能够容易的在每秒处理更多请求,这样,你可以增加这个值。尤其是当你的web应用程序用了很多的Web Service调用或者,没有什么CPU压力的上传/下载很多数据的时候。当ASPNET运行超出了现成的数量,它将会停止处理其他请求。请求进入一个队列,等待线程被释放。这大概会发生在站点接收到比你预期的多得多的请求数量,如果你的CPU有空闲,那么增加这个工作线程的数量。maxIOThreads
- 每一个进程默认。在一个双核的计算机上面,将会有40个线程来为ASPNET进行I/O操作。这意味着在同一时间,ASPNET可以在一台双核计算机上处理40个I/O请求。I/O请求可以使文件的读写,数据库操作,web service调用,以及从内部web应用程序生成的HTTP请求,等等。所以,你可以将这个值设成100,如果你的服务器有足够的系统资源。尤其是当你的web应用程序下载/上传数据,调用外部的web service。minWorkerThreads
- 当若干个空闲的ASPNET工作线程小于这个数字,ASPNET开始将进入的请求排入队列。所以,你能够设置这个值为一个比较小的数字,来增加并发的请求。然而,不要将这个数字设置的太小,因为Web应用程序的的代码需要做一些背后的处理,而且,并发请求需要一些空闲的工作线程。minIOThreads
- 和minWorkerThreads
相同,这是对I/O线程的。然而,你可以设置一个比minWorkerThreads
更小的数字,因为没有并行处理I/O线程的问题、memoryLimit
- 指定允许使用的最大内存容量,是整个系统内存的百分比,指示可以在运行一个新进程以及重新分配已经存在的请求之前,可以使用的内存。如果你只有一个应用程序,而且没有其他的进程需要内存,你可以设置一个更高的值,像80。然而,如果你有一个有漏洞的程序,而且它不断的泄露内存,那么你最好设置一个更小的值,可以使得这个有漏洞的进程尽快的回收,这样,保证你的站点健康。尤其是,当你使用COM组件,消耗内存的时候。然而,这是一个临时的解决方案,你当然会修复这个漏洞。
此外还有processModel
, 这里由一个非常重要的节点,在system.net,你可以指定,同一个IP最大数量上限的请求。
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
2
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
3
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
4
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
5
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
6
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
7
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
默认为2,这太小了。这意味着同一个IP,你不能有两个以上并发请求。获取外部内容的站点,都因为这个默认设置而遭受堵塞。这里,我设置成了100。如果你的web应用程序有很多的调用到一个指定的服务器,你可以考虑将这个值设置的更高。
你可以从 "Improving ASP.NET Performance" 了解更多的配置。
上线之前,你必须要做的
如果你使用 ASP.NET 2.0 Membership Provider,你需要在上线之前,调整你的web.config:
- 添加
applicationname
属性在Profile Provider。如果你不他家一个指定的名字,Profile provider将会使用一个GUID。所以,在你的本地你将会由一个GUID,在应用程序服务器上面,将会是另外一个GUID,如果你将本地的数据库复制到服务器上面,你将不能复用这个本地的数据库,而且,ASPNET将会创建一个新的应用程序。这里,你需要添加:
1<profile enabled="true">
2<providers>
3<clear />
4<add name="..." type="System.Web.Profile.SqlProfileProvider"
5connectionStringName="..." applicationName="YourApplicationName"
6description="..." />
7</providers>
- Profile provider将会自动保存profile,当一个请求完成的时候。所以,这将导致在数据库上面一个不必要的更新,这需要一个很明显的性能损耗。所以,关闭自动保存,并且在你的代码中显示的调用
Profile.Save();
Collapse
<profile enabled="true" automaticSaveEnabled="false" >
- 角色管理器总是查询数据库,来获得当前用户的角色。这是非常大的性能损耗。你可以通过将其缓存到Cookie上,来避免这点。但是这点,需要你的用户没有被分配很多的角色,还没有超出Cookie的2KB的限制。然而这并不是一个常见的情况。所以,你可安全的保存的角色信息到 Cookie 并且,在每一个*.aspx和 *.asmx的请求,节省一个数据库请求。
Collapse
<roleManager enabled="true" cacheRolesInCookie="true" >
上面的设置,对于高容量的网站,一定要配置的。
Content Delivery Network 内容分发网络
每一个请求,从浏览器到你的服务器,需要经过跨越全球的 Internet 主干道。经过的国家,洲,海洋越多,访问的速度就越慢。举个例子,如果你在美国有一个服务器,有人在澳大利亚访问你的站点,每一个请求都要经过节点到达你的服务器,然后用样的从服务器返回到浏览器。如果你的站点有一些比较大的静态文件,像图片,css,JavaScript;为他们发送的每一个请求和下载他们,都花费大量的时间。如果你能在澳大利亚建立一个服务器,并且将用户重定向到澳大利亚的服务器,然后每一个请求将会分数形式到达美国。不仅网络延迟将会变小,而且,数据传输率将会变快,这样,静态的内容将会变的更快。如果你的站点有很多的静态内容,这将会在用户端带来一个明显的性能的改进。此外,ISP提供的国家之内的网络连接比Internet更加快速,因为每一个国家只有少量的介入Internet主干道的接口,这个接口被所有的ISP共享。结果,拥有4M带宽的用户,将会在本国服务器之间传递获得全部的4M带宽。但是在他们和国外的服务器传递数据时,只能获得一个512KB的速度。这样,在同一个国家内使用服务器,能够明显的加快站点的下载和相应速度。
除了改善站点的加载速度,CDN也能减少你的服务器的负载。因为它处理静态的可以缓存的内容,对于这些内容,你的服务器将很少的获到请求。这样,到达你服务器的请求明显下降,可以节省你的服务器资源来处理动态的请求。你的服务器也可以省下很多的IIS日志,因为IIS不需要记录静态内容的请求了。如果你站点有很多的图片,CSS和JavaScript你每天可以节省上GB的服务器空间。
上面的图片,显示了http://www.pageflakes.com/ 在华盛顿特区的平均的响应时间,服务器在达拉斯,德克萨斯。平均的响应时间在0.4秒。这个响应时间也包括了服务器断的执行时间。通常,他话费 0.3-0.35秒来执行在服务器的每一个页面。所以,花费在网络传输的时间大概在0.05秒或者50毫秒。这是非常快的,因为达拉斯和华盛顿之间只有4 到6个节点。
这幅图显示了从澳大利亚悉尼的平均响应时间。平均的响应时间是1.5秒,比华盛顿的时间多了很多。几乎是美国的4倍。这里有几乎1.2秒的时间消耗在网络传输上面。此外,有17到23个节点在悉尼和达拉斯之间。所以,站点在美国任何地点的下载速度,至少是澳大利亚的倍被的速度。
内容传递网络(content delivery network - CDN) 是一个通过Internet 互联的计算机系统。计算机互相透明合作,来向用户传递内容(尤其是大的媒体内容)。CDN节点(指定地区的服务器群集)被部署在多个地点,经常是跨越多个主干道。这些节点互相协作,服务于将请求发往用户。他们也透明的移动内容,在一个优化过的传递进程背后。CDN服务器只能的选择最近的服务器。它在你的计算机和最近的节点中查找最快的连接。不同国家的节点数量,连接到主干道的数量,一个CDN测量它的强壮度。一些最流行的CDN有Akamai, Limelight, EdgeCast。Akamai被一些大公司使用,像Microsoft,Yahoo,AOL。这是相当贵的解决方案。但是Akamai拥有最好的性能,因为,他们几乎在每一个比较好的城市都有他们的服务器。然而,Akamai非常昂贵,而且他们只接受能够每月最少5K的客户。对于一些小的公司,Edgecast是一个更加合算的解决方案。
这幅图显示了距离浏览器端最近的CDN节点拦截了传输和服务器响应。如果它在缓存中没有响应,他用更加快速的路由而且比ISP提供的更加优化连接,从原始的服务器获得响应。如果这个响应已经被缓存,那么,就可以接在CDN节点进行服务,而不用直接的访问原始服务器。
大体上,有两种CDN。一种是通过FTP上传内容,你将会在他们的域名中获得一个子域名,就像dropthings.somecdn.net.你将所有的静态内容的URL从你自己的站点域名改成CDN的域名。所以,像/logo.gif 将会变成http://dropthings.somecdn.net/logo.gif.这非常好设置,但是仍旧有一些问题。你需要始终同步CDN上面的存储的内容。部署就变得复杂,因为你需要同时上传你的网站和CDN存储。这样的一个CDN(非常的便宜)是Cachefly.
一个更加方便的方法是保存你自己站点的内容,但是使用域名domain aliasing. 你可以将你的内容保存在一个子域名,这个子域名是指向你自己的域名的,就像static.dropthings.com. 然后,你用CNAME将这个子域名映射到CDN的 命名服务器nameserver 像cache.somecdn.net.当一个浏览器试着解析static.dropthigns.com, DNS检查请求到CDN nameserver。然后nameserver 返回一个距离你最近的CDN节点的IP,这样,就给你一个最好的下载性能。浏览器发送请求到CDN节点。当CDN节点发现了这个请求,它会检测这个内容是否已经被缓存了。如果已经被缓存了,就直接发送缓存的内容。如果没有,一个请求将会到达你的服务器,并且检测在response中的cache。 它根据cache header,决定自己的缓存时间。同时,浏览器不等待CDN节点获取返回内容。当CDN在更新缓存的时候,他就会路由请求到原始服务器上面。有些时候,CDN扮演一个代理,拦截每一个请求,并且使用更快的路由和优化的连接,从原始服务器获得没有缓存到的内容。这样的一个CDN是Edgecast.
浏览器上缓存AJAX调用
浏览器可以缓存图片,JavaScript,CSS文件到用户的硬盘上,并且,他也能够缓存XML HTTP 调用,如果这个调用是一个HTTP GET.缓存是基于URL的,如果URL相同,并且,他已经被缓存在硬盘上了,这样,应答(Response)就会从缓存中读取,而不是在请求的时候,再一次访问服务器。基本上,浏览器可以缓存任何的HTTP GET 调用,并且返回基于URL的缓存数据。如果你以HTTP GET的方式来获得数据,并且服务器返回特殊的头部(header),这通知浏览器来缓存Response,在以后的调用中,Response将会立刻从缓存中返回,这样就节省了网络延迟的时间以及下载时间。
在 PageFlakes,我们缓存了用户的状态,这样,当用户后面几天再访问的时候,这个用户立即得到浏览器中缓存的页面,并不是从服务器上。这样,第二次访问变得非常的快速。我们也将一些基于用户动作而出现的页面上小的部分缓存起来。当用户做了相同的动作,一个被本地缓存的结果立刻被调用,这样,节省了网络交互时间。用户获得一个加载迅速,反映迅速的站点,速度增加的很明显。
这个想法使得 通过Atlas WebService 的HTTP GET
调用返回指定的HTTPResponse 头信息,来告知浏览器在在指定日期的长度缓存响应。如果你返回Expires 头信息在相应过程中,浏览器将会缓存这个XML HTTP Response。这里有两个头信息你需要返回,来告知浏览器缓存这个相应:
Collapse
HTTP/1.1 200 OK
Expires: Fri, 1 Jan 2030
Cache-Control: public
这个指示浏览器将这个相应缓存到2030,只要你有相同的带有相同参数的XML HTTP调用,你将会从你的电脑上获得缓存内容,而不是在原始的服务器。还有一些高级的方法来进一步的控制相应缓存。几个例子,这里有一个header 指示了浏览器缓存60秒,但是在60秒之后,联系服务器来获得缓存的更新。这样当缓存过期了,也保护了缓存的代理。
Collapse
HTTP/1.1 200 OK
Cache-Control: private, must-revalidate, proxy-revalidate, max-age=60
让我们试一试制作这样的相应,在一个ASP.NET Web Service调用中:
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
2
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
3
![](http://blog.ncuhome.cn/Images/OutliningIndicators/ExpandedBlockStart.gif)
4
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
5
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
6
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
7
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
8
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
9
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
10
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
11
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
12
![](http://blog.ncuhome.cn/Images/OutliningIndicators/ExpandedBlockEnd.gif)
13
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
这将会返回一下的Response头:
这个Expires头设置恰当了,但是问题是Cache-control。这里显示的max-age被设置为0.这样就阻止了浏览器做任何的缓存。如果你想禁止缓存,那么可以使用这样的一个cache-control头。相反的事情发生了。
输出是不正确的,没有缓存:
这里有一个ASP.NET2.0的bug,你不能改变max-age头。由于max-age被设置成了0,ASP.NET 2.0 设置Cache-Control私有,因为,max-age = 0,意味着不需要缓存。这样,就没有方法使得ASP.NET 2.0 返回恰当的头,来缓存应答。这是由于,ASP.NET AJAX framework 拦截了WebService 的调用,并且错误的设置了max-age为默认的0,在执行一个Request之前。
Hack 的时间了,在反编译HttpCachePolicy类的代码之后(Context.Response.Cache 对象),我发现下面的代码:
不知为什么,this._maxAge 将被设置为0,并却检测"if (!this._isMaxAgeSet || (delta < this._maxAge))
" 来防止被设置为更大的值。由于这个问题,我们需要跳过SetMaxAge方法,直接的设置_maxAge,使用反射。
Collapse
[WebMethod][ScriptMethod(UseHttpGet=true)]
public string CachedGet2()
{
TimeSpan cacheDuration = TimeSpan.FromMinutes(1);
FieldInfo maxAge = Context.Response.Cache.GetType().GetField("_maxAge",
BindingFlags.Instance|BindingFlags.NonPublic);
maxAge.SetValue(Context.Response.Cache, cacheDuration);
Context.Response.Cache.SetCacheability(HttpCacheability.Public);
Context.Response.Cache.SetExpires(DateTime.Now.Add(cacheDuration));
Context.Response.Cache.AppendCacheExtension(
"must-revalidate, proxy-revalidate");
return DateTime.Now.ToString();
}
这将会返回下面的头信息:
现在,max-age被设置成了60,这样,浏览器将会缓存这个相应60秒。如果你在60秒之内有相同的调用,那么将会返回相同的应答。这里一个测试的输出,显示了从服务器返回的时间:
在一分钟之后,缓存过期,并且浏览器重新访问服务器。客户端的代码如下:
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
2
![](http://blog.ncuhome.cn/Images/OutliningIndicators/ExpandedBlockStart.gif)
3
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
4
![](http://blog.ncuhome.cn/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
5
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
6
![](http://blog.ncuhome.cn/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
7
![](http://blog.ncuhome.cn/Images/OutliningIndicators/ExpandedBlockEnd.gif)
有另外一个问题需要解决。在web.config中,你将会看到,ASP.NET ajax 将会添加:
Collapse
<system.web>
<trust level="Medium"/>
这将会阻碍我们来设置_maxAge字段,因为需要使用反射,所以,你需要删除这个信任级别,或者设置为Full。
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
2
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
最佳地利用浏览器缓存
使用不变的URL
浏览器缓存内容是根据URL的。当URL改变了,浏览器从原始的服务器中获得新的版本。URL可以通过QueryString而改变。举个例子,/default.aspx在浏览器中缓存。如果请求/default.aspx?123 这将会从服务器中获取新版本。从服务器新返回的内容也会被缓存,如果有一个正确的缓存头(header)的话。在这种情况下,改变queryString为 /default.aspx?456将会从服务器返回一个新的内容。所以,如果你想获得缓存的响应,那么你就要随处保证使用不变的URL。在首页,如果你请求了一个文件/welcome.gif,确保在其他的页面中,那么使用相同的URL访问相同的文件。一个常见的问题是,有些时候在URL中省略掉 “www”子域名。www.pageflakes.com/default.aspx 和PageFlakes.com/default.aspx 并不相同。这两个会分开缓存。
长时间的缓存静态内容
静态文件可以缓存更长的时间,像一个月。如果你认为,你应该仅仅缓存一两天,这样,当你改变文件的内容,用户将会更快的获得,你错了。如果你想更新一个文件,已经通过Expires头信息缓存过了,新用户将会立刻就会看到这个新文件,而老用户则会一直要到浏览器缓存过期。这样,只要你是用了Expires 头,那么你应该尽可能的为其设置一个较大值。
举个例子,如果你设置了Expires头信息来缓存一个文件3天,一个用户今天获得这个文件,并且后面的3天中,将其缓存。另外一个用户明天获得这个文件,将会从明天开始的3开后,缓存。如果你后天修改了这个文件,第一个用户将会在第四天看到这个文件,第二个用户将会在第五天看到这个文件。这样,不同的用户将会看到不用的版本。结果,设置一个较小的值并不能起到使得用户尽快看到更新的作用。你应该改变这个文件的URL来保证每一个用户立刻获得准确的数据。
你可以设置Expires 头信息在IIS的管理器中。你将会在后面的部分了解到怎么设置。
使用缓存友好的目录结构
存储被缓存的内容在一个公用的文件夹中。举个例子,保存你站点的所有的图片在/static目录,而不是将图片分散的放在不同的子目录。这样将会帮助你获得一个不变的URL,引文你在所有的地方都可以使用/static/images/somefile.gif.继而,我们将会了解到,当你将静态可以缓存的文件放在一个共有的根目录下,将其转移到内容分发网络(CDN)上面变的更加容易。
复用公用的图片文件
有些时候,我们将公用的图片文件放在一写虚拟目录下面,这样,我们能够书写更见短小的路径。举一个例子,说你由一个indicator.gif文件,分别放在根目录,一些子目录以及CSS的目录。你这样做是因为,你就不用但是相对路径的问题了。这对缓存并没有帮助。每一个文件的副本,都在浏览器中单独被缓存。这样你需要收集解决方案的所有的图片文件,在除去副本之后,将他们放在放在一个相同的根目录下面,在页面和CSS文件中使用相同的URL。
当你想使缓存过期的时候,修改文件的名字
当你想改变一个静态文件,不要仅仅更新这个文件,因为这个文件已经在缓存在客户的浏览器中了。你需要改变文件的名字,并且更新所有地方的引用,这样,浏览器将会下载新的文件。你可以将文件的名字保存在数据库中或者配置文件中,并且使用数据绑定动态的生成URL。这个方法,你能够修改一个地方,立刻获得整个站点的URL更新。
使用一个版本号,当访问一个静态文件
如果你不想 用一个文件的多个副本而使得讲台文件目录混杂,你能使用QueryString来区分相同文件的不同版本。举个例子,一个GIF能够通过一个虚假的 QueryString来访问到,像,/static/images/indicator.gif?v=1.当你改变了indicator.gif文件,你可以覆盖掉同名文件,并且将所有的引用修改为/static/images/indicator.gif?v=2.这个方法,你能够重复的相同的文件,而仅仅修改访问图片的引用。
将可缓存文件放置在另外的一个域名
将静态的内容放在一个不同的域名,始终是一个很好的主意。首先,浏览器能够打开两个并发联结来下载静态文件。另外一点好处,你不需要发送Cookie到静态文件。当你把将静态文件放在和你的应用程序相同的域名下,浏览器发送所有的ASP.NET cookie 和你的应用程序里面的其他的Cookie。这就会请求的头变的不必要的大,从而浪费带宽。你不需要发送这些Cookie来访问静态文件。所以将文件保存在http://www.staticcontent.com/ 域名中当你的网站运行在http://www.dropthings.com/ 这个域名下。另外的域名不需要一个完全不同的网站,他仅仅是一个化名,并且共享web 应用程序路径。
不会被缓存,所以,减少使用SSL
基于SSL的任何的内容,都不回被缓存、所以,你需要将一些静态的内同放出SSL。另外,你应该试着限制SSL仅仅在一些安全的页面,像登录页面以及支付页面。其余的内容都应该在SSL之外。SSL加密了请求和响应,并且这样就有另外的服务器的负载。加密的内容也比原始的内容更大,这样将会占用更大的带宽。
HTTP POST 的请求从来不能被缓存
Cache仅仅能够发生在HTTP GET 的请求。HTTP POST 请求不会被请求。所以,任何的你想要缓存的AJAX 调用,需要HTTP GET 开启。(译者:WebService 的 get 开启)
生成 Content-Length 响应头
当你通过 webservice 调用或者 HTTPHandler 来动态的输出内容,确保你添加了Content-length头。当通过Content-length获得需要下载的大小时,浏览器会对下载内容进行优化,获取更快的速度。浏览器能够更加有效地使用持续的连接,当这个头信息出现。这将会使得浏览器不会为每一个请求都开启另外一个连接。当没有 content-length头,浏览器不知道从服务器获得多少数据,这样只要从服务器获取内同,就保持这个连接打开直到连接关闭。所以,你失去了持续连接的益处,持续连接可以大大的减少下载一些小文件的速度,像CSS,JavaScript,和图片。
怎样在IIS中设置缓存静态内容
在 IIS管理器,网站属性对话框,有HTTP 头选项卡,这里你可以设置所有的请求的Expires 头信息。这里,你可以设置是否立刻就过期还是在一定天数之内或者指定的日期过期。第二个选项(Expires after),使用了滑动过期,不会绝对过期。这非常有效,因为这是针对的每一个请求。无论什么时候,某人请求一个静态文件,IIS将会基于这个设置来计算这个过期时间。
对于动态的页面,ASPNET控制的,一个handler可以修改expire 头而覆盖掉IIS 的默认设置。
按需加载UI,获得一个更流畅的体验
AJAX 网站都加载尽可能多的特性到浏览器,而不带有任何的PostBack。如果你看到像Pageflakes的起始页,它仅仅一个单独的页面,不通过任何PostBack,给你提供所有的特性。一个快速但是“脏”地方法就是在页面加载时,将每一个可能的HTML标签都放进一个隐藏的div,当需要的时候,将这些隐藏的div显示出来。但是这使得第一次加载的时候,速度太慢了,而且,浏览器对于大量的dom操作,性能也会下降。所以,一个更好的方式是当需要的时候加载HTML。在我的dropthings 项目中,我已经为这个做出了一个例子。
当你点击 “help”链接,它将会动态加载帮助的内容。这些HTML并不会default.aspx的第一次呈现的时候产生。这样,大量的HTML以及相关图片并没有影响到网站的负载性能。他仅仅在用户点击了”help“这个链接之后进行加载。另外,它将会被浏览器缓存,这样,仅仅需要加载一次。当用户再一次点击链接之后,仅仅从浏览器的缓存中获得数据,而不是从原始服务器中获得数据。
这里的原理是,生成一个调用*.aspx的XMLHTTP调用,获得相应的HTML,将这些相应的HTML放到一个DIV容器里面,并且使这个div可见。
AJAX 框架有一个 Sys.Net.WebRequest 类,它能够使你进行一个标准的HTTP 调用。你可以定义这个HTTP 方法,地址,头信息,以及调用的正文。这是一种底层的通过XMLHTTP的直接调用。一旦你构造了一个 web request,你可以使用Sys.Net.XMLHttpExecutor 来执行。
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
2
![](http://blog.ncuhome.cn/Images/OutliningIndicators/ExpandedBlockStart.gif)
3
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
4
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
5
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
6
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
7
![](http://blog.ncuhome.cn/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
8
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
9
![](http://blog.ncuhome.cn/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
10
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
11
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
12
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
13
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
14
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
15
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
16
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
17
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
18
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
19
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
20
![](http://blog.ncuhome.cn/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
21
![](http://blog.ncuhome.cn/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
22
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
23
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
24
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
25
![](http://blog.ncuhome.cn/Images/OutliningIndicators/InBlock.gif)
26
![](http://blog.ncuhome.cn/Images/OutliningIndicators/ExpandedBlockEnd.gif)
这个例子显示了帮助模块的信息,通过访问help.aspx来加载,并且将其被容注入到Helpdiv中。这个应答能通过在help.aspx里面设置 output cache directive 节点而被缓存。这样,下一次当用户再一次点击这个链接的时候,UI将会立刻的显示出来。help.aspx没有<html>仅仅是在div里面的内容。
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
2
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
3
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
4
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
5
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
6
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
7
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
8
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Duis lorem
eros, volutpat sit amet, venenatis vitae, condimentum at, dolor. Nunc
porttitor eleifend tellus. Praesent vitae neque ut mi rutrum cursus.
使用这个方法,你能够将你的用户界面分割到一个个小的aspx文件中,尽管这些aspx文件不能有JavaScript或者样式表模块,他们能够包含你需要显示的大量的HTML。这样,你就能保持最初的下载足够的小,仅仅加载最基本的东西。当用户使用新的功能的时候,动态加载这些区域。
优化 ASP.NET 2.0 Profile Provider
你知道有两个重要的存储过程在ASPNET2.0的Profile Provider中,你能显著的优化?如果你没有足够的优化,你的服务器在大负载的情况下将会停止你的业务。这里有一个故事:
在3月,Pageflakes 在mix2006上面展示。回来之后,我们有了一个迷人的时间。我们在Showcase of Atlas Web site 作为第一个公司展示。每天的访问量上升的非常的快。一天,我们通知,我们的数据库服务器不够用了。我们重启了服务器,恢复了,然而一个小时候,又死掉了。在做对还留在服务器中的,死机时候的情况过很多的分析之后,我们发现,100% cpu利用率,还有相当高的IO操作。硬盘过热,为了自我保护,硬盘自动关闭。这让我们非常的奇怪,因为,我们认为我们非常聪明,并且,我们已经测试过每一个WebService方法。所以,我们在上百兆的日志中寻找,以便寻找出哪个WebService 方法占用了时间。我们猜测是一个,它是第一个函数,用来读取用户的页面设置。我们将这个方法分割成小部分,以便找到哪个部分占用了大部分时间。
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
2
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
3
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
4
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
5
![](http://blog.ncuhome.cn/Images/OutliningIndicators/None.gif)
6
![](http://blog.ncuhome.cn/Images/OutliningIndicators/ExpandedBlockStart.gif)
你看,整个方法都被计时了。如果你想知道这个计时是怎样工作的,我将会在另外一篇文章中说明。我们也在我们怀疑的更小的部分添加了计时。但是我们没能够找到任何的在我们的代码中耗时的部分。我们的数据库始终都是被优化的(毕竟,你知道睡在检查他,是我)。
同时,用户在大叫,管理人员在尖叫,支持人员在电话上被抱怨。开发者证狂流汗,他们额头上的血管也清晰可见了。没有什么特别的,仅仅是一个我们每一个月都要面对的典型状况。
现在,你一定会喊,“你可以使用SQL Profiler,白痴!”。我们正在使用SQL Server workgroup 版,这并没有SQL profiler。这样,我们必须设法让其运行在运行在服务器上。不要问是如何做到的。在运行了SQL Profiler之后,我们震惊了。带给我们如此大痛苦的这个光荣的存储过程是dbo.aspnet_Profile_GetProfiles
!
我们广泛的使用(并且仍在使用) Profile provider.
这是这个存储过程:
CREATE PROCEDURE [dbo].[aspnet_Profile_GetProfiles]
@ApplicationName nvarchar(256),
@ProfileAuthOptions int,
@PageIndex int,
@PageSize int,
@UserNameToMatch nvarchar(256) = NULL,
@InactiveSinceDate datetime = NULL
AS
BEGIN
DECLARE @ApplicationId uniqueidentifier
SELECT @ApplicationId = NULL
SELECT @ApplicationId = ApplicationId
FROM aspnet_Applications
WHERE LOWER(@ApplicationName)
= LoweredApplicationName
IF (@ApplicationId IS NULL)
RETURN
-- Set the page bounds
DECLARE @PageLowerBound int
DECLARE @PageUpperBound int
DECLARE @TotalRecords int
SET @PageLowerBound = @PageSize * @PageIndex
SET @PageUpperBound = @PageSize - 1 + @PageLowerBound
-- Create a temp table TO store the select results
CREATE TABLE #PageIndexForUsers
(
IndexId int IDENTITY (0, 1) NOT NULL,
UserId uniqueidentifier
)
-- Insert into our temp table
INSERT INTO #PageIndexForUsers (UserId)
SELECT u.UserId
FROM dbo.aspnet_Users
u, dbo.aspnet_Profile p
WHERE ApplicationId = @ApplicationId
AND u.UserId = p.UserId
AND (@InactiveSinceDate
IS NULL OR LastActivityDate
<= @InactiveSinceDate)
AND (
(@ProfileAuthOptions = 2)
OR (@ProfileAuthOptions = 0
AND IsAnonymous = 1)
OR (@ProfileAuthOptions = 1
AND IsAnonymous = 0)
)
AND (@UserNameToMatch
IS NULL OR LoweredUserName
LIKE LOWER(@UserNameToMatch))
ORDER BY UserName
SELECT u.UserName, u.IsAnonymous, u.LastActivityDate,
p.LastUpdatedDate, DATALENGTH(p.PropertyNames)
+ DATALENGTH(p.PropertyValuesString)
+ DATALENGTH(p.PropertyValuesBinary)
FROM dbo.aspnet_Users
u, dbo.aspnet_Profile p, #PageIndexForUsers i
WHERE
u.UserId = p.UserId
AND p.UserId = i.UserId
AND i.IndexId >= @PageLowerBound
AND i.IndexId <= @PageUpperBound
DROP TABLE #PageIndexForUsers
END
END
首先,它查找 ApplicationID
Collapse
DECLARE @ApplicationId uniqueidentifier
SELECT @ApplicationId = NULL
SELECT @ApplicationId = ApplicationId FROM aspnet_Applications
WHERE LOWER(@ApplicationName) = LoweredApplicationName
IF (@ApplicationId IS NULL)
RETURN
然后创建临时表,以便存储用户信息。
Collapse
-- Create a temp table TO store the select results
CREATE TABLE #PageIndexForUsers
(
IndexId int IDENTITY (0, 1) NOT NULL,
UserId uniqueidentifier
)
-- Insert into our temp table
INSERT INTO #PageIndexForUsers (UserId)
如果它被调用的非常频繁,那么将会产生非常高的IO因为临时表的创建。他也穿过了两个大表aspnet_Users 和
aspnet_Profile
。存储过程的这种写法,如果一个用户有多个个人信息,将会返回所有的信息。但是通常,我们仅仅为每一个用户存储一个个人信息。所以,就没必要创建临时表了。另外,没有必要做 LIKE LOWER(@UserNameToMatch)
这样的操作。我们总是存储用户的全名,直接的满足相等的条件。
所以,我们打开存储过程,并且将其改为直接访问:
Collapse
IF @UserNameToMatch IS NOT NULL
BEGIN
SELECT u.UserName, u.IsAnonymous, u.LastActivityDate, p.LastUpdatedDate,
DATALENGTH(p.PropertyNames)
+ DATALENGTH(p.PropertyValuesString) + DATALENGTH(p.PropertyValuesBinary)
FROM dbo.aspnet_Users u
INNER JOIN dbo.aspnet_Profile p ON u.UserId = p.UserId
WHERE u.LoweredUserName = LOWER(@UserNameToMatch)
SELECT @@ROWCOUNT
END
ELSE
BEGIN -- Do the original bad things
在本地,它运行正常。现在,需要将其运行在服务器上面。这是一个非常重要的存储过程,被ASPNET 2.0的Profile Provider使用,ASPNET的核心。如果我们做错了些什么,我们也许不能立刻就发现这个错误,但是也许在一个月之后,我们发现了,用户的 Profile混淆了,这就没有办法补救了。所以,这是一个非常难的决定,将这个运行在一个上线中的项目,而且没有足够的测试。我们没有足够的时间来做测试了。我们已经挂掉了。所以,我们聚集在一起,一起祈祷,并且在SQL Server Management Studio中按下了“Excute”按钮。
这个存储过程,运行良好。在服务器上面,我们发现CPU从100%降到30%。IO使用降到了40%。
我们重新活了!
这是另外一个存储过程,在每一个页面,WebService都会被调用,因为我们使用Profile Provider 非常广泛。
REATE PROCEDURE [dbo].[aspnet_Profile_GetProperties]
@ApplicationName nvarchar(256),
@UserName nvarchar(256),
@CurrentTimeUtc datetime
AS
BEGIN
DECLARE @ApplicationId uniqueidentifier
SELECT @ApplicationId = NULL
SELECT @ApplicationId = ApplicationId
FROM dbo.aspnet_Applications
WHERE LOWER(@ApplicationName) = LoweredApplicationName
IF (@ApplicationId IS NULL)
RETURN
DECLARE @UserId uniqueidentifier
SELECT @UserId = NULL
SELECT @UserId = UserId
FROM dbo.aspnet_Users
WHERE ApplicationId = @ApplicationId
AND LoweredUserName =
LOWER(@UserName)
IF (@UserId IS NULL)
RETURN
SELECT TOP 1 PropertyNames, PropertyValuesString, PropertyValuesBinary
FROM dbo.aspnet_Profile
WHERE UserId = @UserId
IF (@@ROWCOUNT > 0)
BEGIN
UPDATE dbo.aspnet_Users
SET LastActivityDate=@CurrentTimeUtc
WHERE UserId = @UserId
END
END
当你运行这个存储过程,看这个统计:
Collapse
Table 'aspnet_Applications'. Scan count 1, logical reads 2, physical reads 0,
read-ahead reads 0, lob logical reads 0, lob physical
reads 0, lob read-ahead reads 0.
(1 row(s) affected)
Table 'aspnet_Users'. Scan count 1, logical reads 4, physical reads 0,
read-ahead reads 0, lob logical reads 0, lob physical
reads 0, lob read-ahead reads 0.
(1 row(s) affected)
(1 row(s) affected)
Table 'aspnet_Profile'. Scan count 0, logical reads 3, physical reads 0,
read-ahead reads 0, lob logical reads 0, lob physical
reads 0, lob read-ahead reads 0.
(1 row(s) affected)
Table 'aspnet_Users'. Scan count 0, logical reads 27, physical reads 0,
read-ahead reads 0, lob logical reads 0, lob physical
reads 0, lob read-ahead reads 0.
(1 row(s) affected)
(1 row(s) affected)
这个存储过程支持了Profile对象的所有定制属性,当Profile对象在一次请求中第一次被访问的时候。
首先,它做了一个查询,在aspnet_application
来通过Application name 找到当前的Application id。你可以轻松的替换掉这个,通过硬编码的形式在存储过程中指出这个Application id 来节省工作。通常我们仅仅是运行一个应用程序,所以,没有必要在每一个次调用中,都查询Application id。这是一个快速的优化,然而,从客户端统计,你能够看到,哪里是性能的瓶颈:
然后,看最后的一个模块,aspnet_users
表更新了LastActivityDate
字段。这是一个最费开销的操作。
这个用来确保Profile Provider 知道用户的Profile 什么时候最后被访问的。但是我们不需要做这个在包含访问Profile的每一个单独的页面加载或者web service的调用。也许,我们能够做这个在用户的第一个登录,或者用户退出。在我们的情况中,当用户在页面时,很多的web service被请求。无论如何,也仅仅有一个页面。所以,我们能够轻松的删除这个,为了节省在每一个请求中,更新巨大的aspnet_users
表。
怎样才在不使站点挂掉的情况下查询 ASP.NET 2.0 Membership 表
这样的查询在你的开发环境中运行的很好:
Collapse
Select * from aspnet_users where UserName = 'blabla'
或者,你可以毫无问题的获得一些用户的信息,使用:
Collapse
Select * from aspnet_profile where userID = '…...'
你甚至可以这样更新aspnet_membership
表:
Collapse
Update aspnet_membership
SET Email = 'newemailaddress@somewhere.com'
Where Email = '…'
但是,当你的产品服务器上面有一个巨大的数据库,运行上面的任何的都会是你的服务器挂掉。原因是尽管这些查询看起来是我们频繁的使用的,但是没有一个是带索引的。所以,以上的结果在“表查询”(最差的查询)在各有数百万条数据的表中。
我就是对于我们来说,发生了什么。我们使用这样的字段,像UserName
, Email
, UserID
, IsAnonymous
等等的,在Pageflakes很多的市场报表中。这些报表仅仅是市场团队使用,其他人并不使用。现在站点运行良好,但是一天会有几次市场团队和用户叫我们,尖叫:“网站变慢了!”,“用户报表非常慢!”,“有一些页面超时了!”等等。通常,当他们叫我们,我们会告诉他们,“等等,现在看看”并且,我们彻底的检测站点。我们使用SQL Profiler 来看到底什么有问题。但是我们什么都没有找到。Profiler 显示的查询运行良好。CPU负载在正常范围内。站点运行良好和流畅。我们在电话中告诉他们“我们没有看到任何问题,怎么了?”
那么,为什么当我们像试着调查出这个问题的时候,我们没有发现任何的缓慢?但是站点有时候确实变的非常慢,在我们没有进行调查的某些时候。
市场团队每天运行像上面的查询几次。当他们运行任何的这些查询,由于这些部分没有在索引中,会使得IO以及CPU变的非常高-像这样:
我们使用的是SCSI 硬盘,15000RPM。非常贵,也非常快。CPU是64位的Dual core Dual Xeon 。这两个硬件在各自的领域都是非常强劲的。但是像这样子的查询仍旧会使得我们挂掉,由于由一个巨大的数据库。
但是,当市场团队给我们打电话并且我们保持连线找到问题的时候,从来不会发生。因为他们再给我们打电话,他们没有运行任何的可以使服务器挂掉的报表查询。他们也在站点的其他的部分工作,和大部分投诉的用户做着一样的事情。
让我们来看看索引:
Table: aspnet_users
- Clustered Index =
ApplicationID
,LoweredUserName
- NonClustered Index =
ApplicationID
,LastActivityDate
- Primary Key =
UserID
Table: aspnet_membership
- Clustered Index =
ApplicationID
,LoweredEmail
- NonClustered =
UserID
Table: aspnet_Profile
- Clustered Index =
UserID
大部分索引都包含ApplicationID 在里面。除非你使用ApplicationID = “…”在where子句中,它将不会使用任何的索引。结果就是所有的查询都会遭受一个整表查询。将ApplicationID放到where子句中(在 aspnet_Applicaiont表中找到ApplicationID),这样,所有的查询都会变得非常非常快。
不要使用Emai
或者 UserName
字段在where子句中。他们不是索引的一部分,而应该使用LoweredUserName
和LoweredEmail
字段,联合使用ApplicationID
字段。所有的查询一定要有ApplicationID
在WHERE
子句中。
我们的管理站点,包含了一些这样的查询,结果,每当市场团队试着生成报表,他们就占用了所有的CPU和硬盘资源,其他的站点就变得相当慢,有些时候会出现没有响应的情况。
确保你的查询的where子句中始终包含了索引列。否则,当你上线了,你一定会受到惩罚。
防止拒绝服务(DOS)攻击
Web服务是最吸引黑客的目标,因为甚至一个刚上学前班的黑客都能都过重复的调用 消耗量大的web 服务是一个服务器挂掉。像 Pageflakes这样的Ajax的起始页,是运行这样的dos攻击的最佳目标,因为,如果你在不保存Cookie的情况下,重复的访问首页,每一个的访问都会创建一个新的用户,一个新的页面设置,新的widgets,或者诸如此类的. 第一次访问,是开销最大的一次。尽管如此,他是最容发现的一个使得站点挂掉。你能自己试一下,就是仅仅这样简单的代码:
Collapse
for( int i = 0; i < 100000; i ++ )
{
WebClient client = new WebClient();
client.DownloadString("http://www.pageflakes.com/default.aspx");
}
让你非常吃惊的,你将会注意到,在一系列的调用之后,你不能获得一个有效地响应。这并不是你成功的是这个站点挂掉了,而是你的请求被拒绝了。你高兴与你不能获得任何的服务了,这样,你活着了拒绝服务(对于你自己)。我们非常高兴的拒绝了你的服务。Deny You of Service (DYOS).
这里我是用的方法是使用一种开销并不大的方式来记住多少个请求来自同一个IP。当这个数字超过了入口,拒绝后面的服务一段时间。这个方式要记住调用这个IP 在ASPNET的Cache,并且,将一定每一个IP的请求数量存入其中。当这个数字超过预定的界限,拒绝后面的服务一段时间,像10分钟。在10分钟后,重新允许这个IP的请求。
我有一个类叫做 ActionValidator
,它包含了第一次访问,重复访问,匿名PostBack。添加新的模块,添加新的页面等等的数量。他会检查,是否这个动作的数量超过了没有。
Collapse
public static class ActionValidator
{
private const int DURATION = 10; // 10 min period
public enum ActionTypeEnum
{
FirstVisit = 100, // The most expensive one, choose the value wisely.
ReVisit = 1000, // Welcome to revisit as many times as user likes
Postback = 5000, // Not must of a problem for us
AddNewWidget = 100,
AddNewPage = 100,
}
这个枚举包含了各种动作的类型,来检测在一个指定时间段-10分钟的操作数量。
一个静态方法叫做IsValid
来进行检测。如果没有超过请求的限制,它将会返回true,否则就返回false。一旦,你获得一个false,那么,你就可以调用 Request.End()(译者:应该是Response.End()吧。。)使得ASPNET不用进行下一步的处理。或者,你也将其转到一个页面,并且显示“祝贺,你的拒绝服务攻击成功了!”
Collapse
public static bool IsValid( ActionTypeEnum actionType )
{
HttpContext context = HttpContext.Current;
if( context.Request.Browser.Crawler ) return false;
string key = actionType.ToString() + context.Request.UserHostAddress;
var hit = (HitInfo)(context.Cache[key] ?? new HitInfo());
if( hit.Hits > (int)actionType ) return false;
else hit.Hits ++;
if( hit.Hits == 1 )
context.Cache.Add(key, hit, null, DateTime.Now.AddMinutes(DURATION),
System.Web.Caching.Cache.NoSlidingExpiration,
System.Web.Caching.CacheItemPriority.Normal, null);
return true;
}
缓存的key由动作类型已经客户端ip共同构成。首先,它检查当前动作,当前ip,是否有任何的记录,如果没有记录,开始计数并且在这个时间段内的记录到缓存中。缓存对象的绝对日期保证了在这个时间段后,数据将会被清除,并别重新开始记录。如果当在缓存中已经有相应的数据的时候,检测最后的点击次数,判断是否超过限制。如果没有超过限制,增加这个计数器,不用再一次的将更改过的数据存储在缓存中通过:Cache[url]=hit
;因为hit对象是一个引用,修改它,就意味着也在缓存中修改了,事实上,如果你将其重新的放进缓存中,缓存过期时间将会重新计算,这样,你的计算逻辑就失败了。
这个方法的使用非常简单,在default.aspx:
Collapse
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
// Check if revisit is valid or not
if( !base.IsPostBack )
{
// Block cookie less visit attempts
if( Profile.IsFirstVisit )
{
if( !ActionValidator.IsValid(ActionValidator.ActionTypeEnum.FirstVisit))
Response.End();
}
else
{
if( !ActionValidator.IsValid(ActionValidator.ActionTypeEnum.ReVisit) )
Response.End();
}
}
else
{
// Limit number of postbacks
if( !ActionValidator.IsValid(ActionValidator.ActionTypeEnum.Postback) )
Response.End();
}
}
这里,我检测各种动作,像第一个访问,重复访问,PostBack等。
当然,你可以使用一些思科的防火墙,来防止DOS攻击。你将会从你的服务器提供商那里获得保证,他们的整个网络都是DOS或者DDOS(分布式DOS)攻击免疫的。他们保证的是网络级的攻击,像TCP SYN 攻击,或者malformed packet floods 等等。没有办法调查的包,找到不带Cookie的加载站点过多次,或者添加太多的widgets的特定的ip。这叫做应用程序级的攻击,没有硬件的保护。这就必须在你的代码中实现。
很少的站点使用了应用程序级的方法来预防DOS攻击。这样,通过写一个简单的循环,连续不断的点击开销大的页面或者是web service,很轻松的使服务器挂掉。我希望这个简单而有效的类能够帮助你来防止DOS攻击。
结论
你现在已经了解了一些方法将ASPNET发挥到极限,在相同的硬件配置情况下,来获得更好的性能。你也了解到了一些遍历的AJAX技术使得你的网站负载以及访问速度上升。最后,你了解到了怎么样抵御大量的访问,以及把静态内容通过内容分发网络来传递,以抵抗巨大的流量要求。所有的这些技术能使你的站点加载的更加快速,感觉更加流畅,在一个低花费的情况下获得更高的流量。你可以进一步的了解到 改进ASPNET和ASPNET AJAX的性能的信息在我的书里
from http://blog.ncuhome.cn/llj098/logs/2008/12/3/25270.html
原文 :http://www.codeproject.com/KB/aspnet/10ASPNetPerformance.aspx
本文来自博客园,作者:古道轻风,转载请注明原文链接:https://www.cnblogs.com/88223100/archive/2012/06/06/10ASPNetPerformance.html