65.9K
CodeProject 正在变化。 阅读更多。
Home

10 ASP.NET 性能和可伸缩性秘诀

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (234投票s)

2008 年 1 月 30 日

CPOL

38分钟阅读

viewsIcon

1549968

downloadIcon

2

10 种简单的方法,可以让 ASP.NET 和 AJAX 网站更快、更具可伸缩性,并以更低的成本支持更多流量

引言

ASP.NET 2.0 包含许多秘密,揭示它们可以极大地提升性能和可伸缩性。例如,Membership 和 Profile 提供程序存在一些隐蔽的瓶颈,通过轻松解决它们可以加快身份验证和授权的速度。此外,可以调整 ASP.NET HTTP 管道,以避免执行每个请求都会触发的非必要代码。不仅如此,还可以将 ASP.NET 工作进程推到极限,榨取其所有性能。页面片段输出缓存(在浏览器而非服务器上)可以在重复访问时显著节省下载时间。按需加载 UI 可以让您的网站感觉快速而流畅。最后,内容分发网络 (CDN) 和正确使用 HTTP 缓存标头,如果实施得当,可以让您的网站飞速运行。在本文中,您将学习这些技术,它们可以为您的 ASP.NET 应用程序带来巨大的性能和可伸缩性提升,并使其能够应对 10 倍到 100 倍的流量。

在本文中,我们将讨论以下技术

  • ASP.NET 管道优化
  • ASP.NET 进程配置优化
  • ASP.NET 上线前必须执行的操作
  • 内容分发网络
  • 缓存浏览器中的 AJAX 调用
  • 充分利用浏览器缓存
  • 按需渐进式 UI 加载,带来快速流畅的体验
  • 优化 ASP.NET 2.0 Profile 提供程序
  • 如何查询 ASP.NET 2.0 Membership 表而不导致网站崩溃
  • 防止拒绝服务 (DOS) 攻击

上述技术可以应用于任何 ASP.NET 网站,特别是那些使用 ASP.NET 2.0 的 Membership 和 Profile 提供程序的网站。

您可以通过我的书了解更多关于 ASP.NET 和 ASP.NET AJAX 网站的性能和可伸缩性改进 - 使用 ASP.NET 3.5 构建 Web 2.0 门户

ASP.NET 管道优化

ASP.NET 默认有几个 HttpModules,它们位于请求管道中,并拦截每个请求。例如,SessionStateModule 会拦截每个请求,解析会话 cookie,然后将正确的会话加载到 HttpContext 中。并非所有这些模块总是必需的。例如,如果您不使用 Membership 和 Profile 提供程序,就不需要 FormsAuthentication 模块。如果您不为用户使用 Windows 身份验证,就不需要 WindowsAuthentication。这些模块只是静静地驻留在管道中,为每个请求执行一些不必要的代码。

默认模块在 machine.config 文件中定义(位于 $WINDOWS$\Microsoft.NET\Framework\$VERSION$\CONFIG 目录)。

<httpModules>
  <add name="OutputCache" type="System.Web.Caching.OutputCacheModule" />
  <add name="Session" type="System.Web.SessionState.SessionStateModule" />
  <add name="WindowsAuthentication" 
        type="System.Web.Security.WindowsAuthenticationModule" />
  <add name="FormsAuthentication" 
        type="System.Web.Security.FormsAuthenticationModule" />
  <add name="PassportAuthentication" 
        type="System.Web.Security.PassportAuthenticationModule" />
  <add name="UrlAuthorization" type="System.Web.Security.UrlAuthorizationModule" />
  <add name="FileAuthorization" type="System.Web.Security.FileAuthorizationModule" />
  <add name="ErrorHandlerModule" type="System.Web.Mobile.ErrorHandlerModule, 
                             System.Web.Mobile, Version=1.0.5000.0, 
                             Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
</httpModules>

您可以通过在站点的 web.config 文件中添加 <remove> 节点来删除这些默认模块。

<httpModules>
         <!-- Remove unnecessary Http Modules for faster pipeline -->
         <remove name="Session" />
         <remove name="WindowsAuthentication" />
         <remove name="PassportAuthentication" />
         <remove name="AnonymousIdentification" />
         <remove name="UrlAuthorization" />
         <remove name="FileAuthorization" />
</httpModules>

上述配置适用于使用基于数据库的 Forms 身份验证且不需要任何会话支持的网站。因此,可以安全地删除所有这些模块。

ASP.NET 进程配置优化

ASP.NET 进程模型配置定义了一些进程级属性,例如 ASP.NET 使用的线程数、线程在超时前阻塞多长时间、在 I/O 工作完成前等待多少个请求等。在许多情况下,默认值都过于有限。如今,硬件变得非常便宜,双核带千兆内存的服务器已成为非常普遍的选择。因此,可以调整进程模型配置,使 ASP.NET 进程消耗更多系统资源,并从每个服务器提供更好的可伸缩性。

常规的 ASP.NET 安装将创建带有以下配置的 machine.config

<system.web>
    <processModel autoConfig="true" />  

您需要调整此自动配置,并为不同属性使用特定值,以自定义 ASP.NET 工作进程的工作方式。例如

<processModel 
   enable="true" 
   timeout="Infinite" 
   idleTimeout="Infinite" 
   shutdownTimeout="00:00:05" 
   requestLimit="Infinite" 
   requestQueueLimit="5000" 
   restartQueueLimit="10" 
   memoryLimit="60" 
   webGarden="false" 
   cpuMask="0xffffffff" 
   userName="machine" 
   password="AutoGenerate" 
   logLevel="Errors" 
   clientConnectedCheck="00:00:05" 
   comAuthenticationLevel="Connect" 
   comImpersonationLevel="Impersonate" 
   responseDeadlockInterval="00:03:00" 
   responseRestartDeadlockInterval="00:03:00" 
   autoConfig="false" 
   maxWorkerThreads="100" 
   maxIoThreads="100" 
   minWorkerThreads="40" 
   minIoThreads="30" 
   serverErrorMessageFile="" 
   pingFrequency="Infinite" 
   pingTimeout="Infinite" 
   asyncOption="20" 
   maxAppDomains="2000" 
/>

此处所有值都是默认值,但以下值除外

  • maxWorkerThreads - 默认值为每个进程 20 个。在双核计算机上,将为 ASP.NET 分配 40 个线程。这意味着 ASP.NET 可以在双核服务器上同时处理 40 个请求。我已将其增加到 100,以便为 ASP.NET 提供每个进程更多的线程。如果您的应用程序不是 CPU 密集型的,并且可以轻松处理每秒更多的请求,那么您可以增加此值。特别是当您的 Web 应用程序使用大量 Web 服务调用或下载/上传大量数据时,这些操作不会给 CPU 带来压力。当 ASP.NET 工作线程耗尽时,它将停止处理更多传入的请求。请求将进入队列并一直等待,直到工作线程被释放。这通常发生在网站开始接收比您最初计划的流量高得多的命中时。在这种情况下,如果您有多余的 CPU 资源,请增加每个进程的工作线程数。
  • maxIOThreads - 默认值为每个进程 20 个。在双核计算机上,将为 ASP.NET 分配 40 个用于 I/O 操作的线程。这意味着 ASP.NET 可以在双核服务器上同时处理 40 个 I/O 请求。I/O 请求可以包括文件读/写、数据库操作、Web 服务调用、Web 应用程序内部生成的 HTTP 请求等。因此,如果您的服务器有足够的系统资源来执行更多此类 I/O 请求,您可以将其设置为 100。特别是当您的 Web 应用程序并行下载/上传数据、调用许多外部 Web 服务时。
  • minWorkerThreads - 当空闲的 ASP.NET 工作线程数量低于此值时,ASP.NET 开始将传入的请求放入队列。因此,您可以将此值设置为一个较低的数字,以增加并发请求的数量。但是,不要将其设置得太低,因为 Web 应用程序代码可能需要进行一些后台处理和并行处理,为此它将需要一些空闲的工作线程。
  • minIOThreads - 与 minWorkerThreads 相同,但这是针对 I/O 线程的。不过,您可以将其设置为比 minWorkerThreads 更低的值,因为 I/O 线程不存在并行处理问题。
  • memoryLimit - 指定工作进程在 ASP.NET 启动新进程并重新分配现有请求之前可以消耗的系统总内存的百分比。如果您只在专用服务器上运行 Web 应用程序,并且没有其他进程需要 RAM,您可以将其设置为一个较高的值,例如 80。但是,如果您的应用程序存在内存泄漏,则最好将其设置为一个较低的值,以便在泄漏进程成为内存占用者之前尽快将其回收,从而保持您的网站健康。特别是当您使用 COM 组件并且存在内存泄漏时。然而,这是一个临时解决方案,您当然需要修复泄漏。

除了 processModel,还有一个非常重要的部分是 system.net ,您可以在其中指定可以对单个 IP 地址发出的最大出站请求数。

<system.net>
  <connectionManagement>
    <add address="*" maxconnection="100" />
  </connectionManagement>
</system.net>

默认值为 2,这太低了。这意味着您的 Web 应用程序无法同时与一个 IP 地址建立超过 2 个连接。大量获取外部内容的网站会因为默认设置而遭受拥塞。我已将其设置为 100。如果您的 Web 应用程序需要对特定服务器进行大量调用,您可以考虑设置一个更高的值。

您可以从“提高 ASP.NET 性能”中了解更多关于这些配置设置的信息。

ASP.NET 上线前必须执行的操作

如果您使用的是 ASP.NET 2.0 Membership Provider,在将应用程序部署到生产服务器之前,您应该在 web.config 文件中进行一些调整。

  • 在 Profile Provider 中添加 applicationname 属性。如果您不在此处添加特定名称,Profile provider 将使用一个 GUID。因此,在您的本地计算机上将有一个 GUID,在生产服务器上将有另一个 GUID。如果您将本地数据库复制到生产服务器,您将无法重用本地数据库中存在的记录,并且 ASP.NET 将在生产环境中创建一个新应用程序。您需要在此处添加它

    <profile enabled="true"> 
    <providers> 
    <clear /> 
    <add name="..." type="System.Web.Profile.SqlProfileProvider" 
    connectionStringName="..." applicationName="YourApplicationName" 
    description="..." /> 
    </providers> 
  • Profile provider 将在页面请求完成时自动保存配置文件。因此,这可能会导致数据库进行不必要的 UPDATE 操作,从而导致严重的性能损失。因此,关闭自动保存,并通过代码显式地使用 Profile.Save(); 进行保存。

    <profile enabled="true" automaticSaveEnabled="false" >
  • Role Manager 始终查询数据库以获取用户角色。这会造成显著的性能损失。您可以通过让 Role Manager 将角色信息缓存到 cookie 中来避免这种情况。但这仅适用于分配角色不多的用户,因为 cookie 有 2 KB 的限制。但这并不常见。因此,您可以安全地将角色信息存储在 cookie 中,从而在每次访问 *.aspx*.asmx 时节省一次数据库往返。

    <roleManager enabled="true" cacheRolesInCookie="true" >

上述三个设置对于高流量网站来说是必须的。

内容分发网络

从浏览器发出的每个请求都通过跨越世界的互联网骨干网传输到您的服务器。请求需要经过多少国家、大陆、海洋才能到达您的服务器,速度就会有多慢。例如,如果您的服务器在美国,而澳大利亚的某人正在浏览您的网站,每次请求实际上都要跨越整个地球才能到达您的服务器,然后再返回到浏览器。如果您的网站有大量静态文件,如图片、CSS、JavaScript,请求每个文件并跨越世界下载它们需要大量时间。如果您能在澳大利亚设置一个服务器并将用户重定向到您的澳大利亚服务器,那么每个请求所需的时间将只是到达美国所需时间的一小部分。不仅网络延迟会降低,数据传输速率也会更快,从而静态内容下载速度会快得多。如果您的网站富含静态内容,这将显著提高用户端的性能。此外,ISP 提供国家内部网络远高于互联网的速度,因为每个国家通常只有少数几个连接到所有 ISP 共享的互联网骨干网。因此,拥有 4 MBPS 宽带连接的用户可以从同一国家/地区的服务器获得完整的 4 MBPS 速度。但是,他们从国家/地区以外的服务器获得的速率可能低至 512 KBPS。因此,拥有同一国家/地区的服务器可以显著提高网站下载速度和响应能力。

除了提高网站加载速度,CDN 还可以分担 Web 服务器的流量。因为它处理的是可缓存的静态内容,所以 Web 服务器很少会收到这些内容的请求。因此,发送到 Web 服务器的请求数量会显著减少,Web 服务器可以腾出更多资源来处理动态请求。您的 Web 服务器还可以节省大量的 IIS 日志空间,因为 IIS 不需要记录静态内容的请求。如果您的网站上有大量图形、CSS 和 JavaScript,您每天可以节省数十 GB 的 IIS 日志。

clip_image002[4]

上图显示了 www.pageflakes.com 从华盛顿特区(服务器位于德克萨斯州达拉斯)的平均响应时间。平均响应时间约为 0.4 秒。此响应还包括服务器端执行时间。通常,在服务器上执行页面需要大约 0.3 到 0.35 秒。因此,花费在网络上的时间约为 0.05 秒或 50 毫秒。这是一个非常快的连接,因为从华盛顿特区到该地区只有 4 到 6 个跳跃。

clip_image004[4]

此图显示了从澳大利亚悉尼的平均响应时间。平均响应时间为 1.5 秒,远高于华盛顿特区。与美国相比,几乎是 4 倍。仅网络就存在大约 1.2 秒的开销。此外,从悉尼到达拉斯大约有 17 到 23 个跳跃。因此,该网站在澳大利亚的下载速度比在美国慢至少 4 倍。

内容分发网络 (CDN) 是一个跨互联网联网的计算机系统。这些计算机透明地协同工作,将内容(尤其是大型媒体内容)交付给最终用户。CDN 节点(特定位置的服务器集群)部署在多个位置,通常跨越多个骨干网。这些节点相互协作,为最终用户提供内容请求。它们还在后台透明地移动内容以优化分发过程。CDN 通过智能地选择最近的服务器来处理请求。它会寻找您计算机到您正在寻找的内容的最近节点之间的最快连接。不同国家/地区的节点数量以及 CDN 拥有的冗余骨干网连接数量衡量了其强度。一些最受欢迎的 CDN 包括 Akamai、Limelight、EdgeCast。Akamai 被微软、雅虎、AOL 等大公司使用。这是一个相对昂贵的解决方案。然而,Akamai 在全球拥有最佳性能,因为它们几乎在世界每个主要城市都有服务器。然而,Akamai 非常昂贵,并且他们只接受每月至少花费 5000 美元用于 CDN 的客户。对于小型公司,Edgecast 是一个更实惠的解决方案。

clip_image006[4]

上图显示了最接近浏览器的 CDN 节点拦截流量并提供响应。如果它没有缓存响应,它将通过更快的路线和比浏览器 ISP 提供的更优化的连接从源服务器获取。如果内容已缓存,则直接从节点提供,无需向源服务器发出请求。

通常有两种 CDN。一种是您通过 FTP 将内容上传到 CDN 的服务器,您将在其域下获得一个子域,例如 dropthings.somecdn.net。您将网站中所有静态内容的 URL 更改为从 CDN 域下载内容,而不是相对 URL 到您自己的域。因此,像 /logo.gif 这样的 URL 将被重命名为 http://dropthings.somecdn.net/logo.gif。这很容易配置,但存在维护问题。您需要始终使 CDN 存储与文件同步。部署会变得复杂,因为您需要同时更新您的网站和 CDN 存储。这种 CDN(非常便宜)的一个例子是 Cachefly

一种更方便的方法是将静态内容存储在您自己的网站上,但使用域别名。您可以将内容存储在指向您自己域的子域中,例如 static.dropthings.com。然后,您可以使用 CNAME 将该子域映射到 CDN 的名称服务器,例如 cache.somecdn.net。当浏览器尝试解析 static.dropthigns.com 时,DNS 查询请求将发送到 CDN 名称服务器。然后,名称服务器将返回最接近您且能为您提供最佳下载性能的 CDN 节点的 IP 地址。然后,浏览器将向该 CDN 节点发送文件请求。当 CDN 节点收到请求时,它会检查是否已缓存内容。如果已缓存,它将直接从本地存储提供内容。如果未缓存,它将向您的服务器发出请求,然后查看响应中生成的缓存标头。根据缓存标头,它决定在自己的缓存中缓存响应多长时间。在此期间,浏览器不会等待 CDN 节点获取内容并返回。CDN 在互联网骨干网上执行一个有趣的操作,将请求路由到源服务器,以便在 CDN 更新其缓存时,浏览器直接从源服务器获取响应。有时 CDN 会充当代理,拦截每个请求,然后通过更快的路线和优化的连接到源服务器来获取未缓存的内容。这种 CDN 的一个例子是 Edgecast

缓存浏览器中的 AJAX 调用

浏览器可以将图片、JavaScript、CSS 文件缓存到用户的硬盘上,并且如果调用是 HTTP GET,它还可以缓存 XML HTTP 调用。缓存基于 URL。如果 URL 相同,并且它已在计算机上缓存,那么当再次请求时,响应将从缓存加载,而不是从服务器加载。基本上,浏览器可以缓存任何 HTTP GET 调用,并根据 URL 返回缓存数据。如果您将 XML HTTP 调用作为 HTTP GET 发出,并且服务器返回一些特殊的标头告知浏览器缓存响应,那么在未来的调用中,响应将立即从缓存加载,从而节省了网络往返时间和下载时间。

在 Pageflakes,我们缓存用户状态,这样当用户第二天再次访问时,用户将获得一个即时从浏览器缓存加载的页面,而不是从服务器加载。因此,第二次加载速度非常快。我们还缓存页面的一些小部分,这些部分会在用户操作时出现。当用户再次执行相同的操作时,缓存的结果会立即从本地缓存加载,从而节省了网络往返时间。用户将获得一个加载快速且响应迅速的网站。感知的速度急剧增加。

思路是在发出 Atlas Web 服务调用时使用 HTTP GET 调用,并返回一些特殊的 HTTP 响应标头,告诉浏览器将响应缓存一定时间。如果您在响应中返回 Expires 标头,浏览器将缓存 XML HTTP 响应。您需要在响应中返回两个标头,指示浏览器缓存响应

HTTP/1.1 200 OK 
Expires: Fri, 1 Jan 2030 
Cache-Control: public

这指示浏览器将响应缓存到 2030 年 1 月。只要您以相同的参数发出相同的 XML HTTP 调用,您将获得来自计算机的缓存响应,并且不会发出任何调用到源服务器。有更高级的方法可以进一步控制响应缓存。例如,这里有一个标头指示浏览器缓存 60 秒,但将在 60 秒后联系服务器并获取新响应。它还将阻止代理在浏览器本地缓存过期 60 秒后返回缓存的响应。

HTTP/1.1 200 OK 
Cache-Control: private, must-revalidate, proxy-revalidate, max-age=60

让我们尝试从 ASP.NET Web 服务调用生成此类响应标头

[WebMethod][ScriptMethod(UseHttpGet=true)]
public string CachedGet()
{
    TimeSpan cacheDuration = TimeSpan.FromMinutes(1);
    Context.Response.Cache.SetCacheability(HttpCacheability.Public);
    Context.Response.Cache.SetExpires(DateTime.Now.Add(cacheDuration));
    Context.Response.Cache.SetMaxAge(cacheDuration);
    Context.Response.Cache.AppendCacheExtension(
           "must-revalidate, proxy-revalidate");

    return DateTime.Now.ToString();
}

这将产生以下响应标头

Expires 标头设置正确。但问题在于 Cache-control。它显示 max-age 设置为 0,这将阻止浏览器进行任何形式的缓存。如果您确实要阻止缓存,则应发出这样的 cache-control 标头。看起来恰恰相反。

输出一如既往地不正确,且未缓存

ASP.NET 2.0 中存在一个 bug,您无法更改 max-age 标头。由于 max-age 设置为 0,ASP.NET 2.0 将 Cache-control 设置为 private,因为 max-age = 0 意味着不需要缓存。因此,您无法让 ASP.NET 2.0 返回正确的标头来缓存响应。这是由于 ASP.NET AJAX Framework 拦截了对 WebService 的调用,并在执行请求之前错误地将 max-age 默认设置为 0

是时候进行黑客攻击了。反编译 HttpCachePolicy 类(Context.Response.Cache 对象所属的类)的代码后,我发现了以下代码

不知何故,this._maxAge 被设置为 0,并且检查:“if (!this._isMaxAgeSet || (delta < this._maxAge))”阻止了它被设置为更大的值。由于这个问题,我们需要绕过 SetMaxAge 函数,并直接使用反射设置 _maxAge 字段的值。

[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 秒内再次进行相同的调用,它将返回相同的响应。这是一个测试输出,显示从服务器返回的日期和时间

1 分钟后,缓存过期,浏览器再次向服务器发出调用。客户端代码如下

function testCache()
{
    TestService.CachedGet(function(result)
    {
        debug.trace(result);
    });
}

还有另一个问题需要解决。在 web.config 文件中,您会看到 ASP.NET Ajax 会添加

<system.web>
<trust level="Medium"/>

这阻止了我们设置 Response 对象的 _maxAge 字段,因为它需要反射。因此,您必须删除此信任级别或将其设置为 Full

<system.web> 
<trust level="Full"/>

充分利用浏览器缓存

一致地使用 URL

浏览器根据 URL 缓存内容。当 URL 更改时,浏览器会从源服务器获取新版本。URL 可以通过更改查询字符串参数来更改。例如,/default.aspx 已在浏览器中缓存。如果您请求 /default.aspx?123,它将从服务器加载新内容。来自新 URL 的响应也可以在浏览器中缓存,前提是您返回了正确的缓存标头。在这种情况下,将查询参数更改为其他内容,如 /default.aspx?456,将从服务器返回新内容。因此,当您希望获得缓存响应时,需要确保在所有地方都使用一致的 URL。从主页开始,如果您请求了一个 URL 为 /welcome.gif 的文件,请确保从另一个页面使用相同的 URL 请求该文件。一个常见的错误是有时省略 URL 中的“www”子域。 www.pageflakes.com/default.aspx 不等于 pageflakes.com/default.aspx。两者都将被单独缓存。

长时间缓存静态内容

静态文件可以缓存更长时间,例如一个月。如果您认为应该缓存几天,以便在更改文件时用户能更快地获取到,那就错了。如果您更新了一个被 Expires 标头缓存的文件,新用户将立即获得新文件,而旧用户将继续看到旧内容,直到其在浏览器中过期。因此,只要您使用 Expires 标头缓存静态文件,就应该尽可能使用较高的值。

例如,如果您设置了 Expires 标头以缓存文件三天,一个用户今天获取文件并在接下来的三天内将其存储在缓存中。另一个用户明天获取文件,并在后天起缓存三天。如果您在后天更改文件,第一个用户将在第四天看到它,第二个用户将在第五天看到它。因此,不同的用户将看到不同版本的文件。结果是,设置一个较低的值假设所有用户都会很快获得最新文件,并不能奏效。您需要更改文件的 URL,以确保每个人都能立即获得完全相同的文件。

您可以在 IIS 管理器中从静态文件设置 Expires 标头。稍后将在一个单独的部分中介绍如何进行。

使用缓存友好的文件夹结构

将缓存内容存储在公共文件夹下。例如,将您网站的所有图片存储在 /static 文件夹下,而不是将图片单独存储在不同的子文件夹下。这将帮助您在整个网站中使用一致的 URL,因为您可以从任何地方使用 /static/images/somefile.gif。稍后,我们将了解到,当您将静态可缓存文件放在公共根文件夹下时,更容易迁移到内容分发网络。

重用公共图形文件

有时我们将公共图形文件放在多个虚拟目录下,以便编写更短的路径。例如,假设您在文件夹、某些子文件夹以及CSS文件夹下都有 indicator.gif。您这样做是为了无需担心来自不同位置的路径,并且可以直接使用文件名作为相对 URL。这无助于缓存。文件的每个副本都会在浏览器中单独缓存。因此,您应该收集整个解决方案中的所有图形文件,在消除重复项后将它们放在同一个根static文件夹下,并从所有页面和 CSS 文件中使用相同的 URL。

更改文件名以使缓存过期

当您希望更改静态文件时,不要只是更新文件,因为它已经被缓存到用户的浏览器中了。您需要更改文件名并在所有引用处更新,以便浏览器下载新文件。您还可以将文件名存储在数据库或配置文件中,并使用数据绑定动态生成 URL。这样,您就可以从一个地方更改 URL,并使整个网站立即接收到更改。

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

如果您不想在静态文件夹中堆积相同文件的多个副本,可以使用查询字符串来区分同一文件的不同版本。例如,可以通过一个虚拟的查询字符串来访问 GIF,例如 /static/images/indicator.gif?v=1。当您更改 indicator.gif 时,您可以覆盖同一个文件,然后将所有指向该文件的引用更新为 /static/images/indicator.gif?v=2。这样,您可以反复更改同一个文件,只需更新引用即可使用新版本号访问图形。

将可缓存文件存储在不同的域中

将静态内容放在不同的域中总是一个好主意。首先,浏览器可以打开另外两个并发连接来下载静态文件。另一个好处是您无需将 cookie 发送到静态文件。当您将静态文件放在与您的 Web 应用程序相同的域中时,浏览器会发送所有 ASP.NET cookie 以及您的 Web 应用程序生成的所有其他 cookie。这使得请求头不必要地增大并浪费带宽。您无需发送这些 cookie 即可访问静态文件。因此,如果您将静态文件放在不同的域中,这些 cookie 将不会被发送。例如,将您的静态文件放在 www.staticcontent.com 域中,而您的网站运行在 www.dropthings.com 上。另一个域不必是完全不同的 Web 站点。它可以只是一个别名并共享相同的 Web 应用程序路径。

SSL 不会被缓存,因此请尽量减少 SSL 的使用

通过 SSL 提供的任何内容都不会被缓存。因此,您需要将静态内容放在 SSL 之外。此外,您应该尝试将 SSL 限制在安全页面,如登录页面或付款页面。网站的其余部分应在 SSL 之外,通过常规 HTTP 访问。SSL 加密请求和响应,因此会给服务器带来额外的负担。加密内容也比原始内容大,因此需要更多带宽。

HTTP POST 请求永远不会被缓存

缓存仅发生在 HTTP GET 请求上。HTTP POST 请求永远不会被缓存。因此,任何您希望可以缓存的 AJAX 调用都需要启用 HTTP GET

生成 Content-Length 响应标头

当您通过 Web 服务调用或 HTTP 处理程序动态提供内容时,请务必发出 Content-Length 标头。当浏览器知道要从响应中下载多少字节(通过查看 Content-Length 标头)时,它会有一些优化来更快地下载内容。当此标头存在时,浏览器可以更有效地使用持久化连接。这可以避免浏览器为每个请求打开新连接。当不存在 Content-Length 标头时,浏览器不知道它将从服务器接收多少字节,因此连接会一直保持打开状态,直到从服务器接收到字节为止,直到连接关闭。因此,您会错过持久化连接的好处,而持久化连接可以大大减少 CS、JavaScript 和图像等多个小文件的下载时间。

如何在 IIS 中配置静态内容缓存

在 IIS 管理器中,Web 站点属性对话框有一个“HTTP 标头”选项卡,您可以在其中为 IIS 处理的所有请求定义 Expires 标头。在那里,您可以定义内容是立即过期还是在特定天数后或在特定日期过期。第二个选项(在…后过期)使用滑动过期,而不是绝对过期。这非常有用,因为它按请求工作。每当有人请求静态文件时,IIS 都会根据“在…后过期”的天数/月数来计算过期日期。

clip_image001

对于由 ASP.NET 提供的动态页面,处理程序可以修改 Expires 标头并覆盖 IIS 的默认设置。

按需渐进式 UI 加载,带来快速流畅的体验

AJAX 网站旨在将尽可能多的功能加载到浏览器中,而无需进行任何回发。如果您查看 Pageflakes 等起始页,它只有一个页面,就能提供整个应用程序的所有功能,而且零回发。一种粗略的方法是在页面加载期间将所有可能的 HTML 片段交付到隐藏的 div 中,然后在需要时使这些 div 可见。但这会使首次加载速度非常慢,并且浏览器性能 sluggish,因为 DOM 上有太多内容需要处理。因此,更好的方法是按需加载 HTML 片段和必要的 JavaScript。在我的dropthings项目中,我展示了一个如何实现这一点的例子。

clip_image002

当您单击“帮助”链接时,它会动态加载帮助的内容。此 HTML 不作为呈现第一个页面的default.aspx的一部分交付。因此,与帮助部分相关的巨大 HTML 和图形对网站加载性能没有影响。它仅在用户单击“帮助”链接时加载。此外,它会在浏览器中缓存,因此只加载一次。当用户再次单击“帮助”链接时,它将直接从浏览器缓存加载,而不是再次从源服务器获取。

原则是发出一个 XMLHTTP 请求到 *.aspx 页面,获取响应 HTML,将该响应 HTML 放入容器 DIV 中,然后使该 DIV 可见。

AJAX Framework 有一个 Sys.Net.WebRequest 类,您可以使用它来发出常规的 HTTP 调用。您可以定义 HTTP 方法、URI、标头和调用的正文。这是一种通过 XMLHTTP 进行直接调用的低级函数。一旦构造了一个 Web 请求,您就可以使用 Sys.Net.XMLHttpExecutor 来执行它。

function showHelp()
{
   var request = new Sys.Net.WebRequest();
   request.set_httpVerb("GET");
   request.set_url('help.aspx');
   request.add_completed( function( executor )
   {
      if (executor.get_responseAvailable()) 
      {

         var helpDiv = $get('HelpDiv');
         var helpLink = $get('HelpLink');
         var helpLinkBounds = Sys.UI.DomElement.getBounds(helpLink);

         helpDiv.style.top = (helpLinkBounds.y + helpLinkBounds.height) + "px";
         var content = executor.get_responseData();
         helpDiv.innerHTML = content;
         helpDiv.style.display = "block";                       

      }
   });

   var executor = new Sys.Net.XMLHttpExecutor();
   request.set_executor(executor); 
   executor.executeRequest();
}

该示例显示了如何通过命中 help.aspx 并将其响应注入 HelpDiv 来加载帮助部分。响应可以通过设置在 help.aspx 上的输出缓存指令进行缓存。因此,下次用户单击链接时,UI 将立即弹出。help.aspx 文件没有 <html> 块,只有 DIV 中的内容。

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Help.aspx.cs" 
    Inherits="Help" %>
<%@ OutputCache Location="ServerAndClient" Duration="604800" VaryByParam="none" %>
<div class="helpContent">
<div id="lipsum">
<p>
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.

使用此方法,您可以将 UI 分解成更小的 *.aspx 文件。虽然这些 *.aspx 文件不能包含 JavaScript 或样式表块,但它们可以包含您需要在 UI 上按需显示的大量 HTML。因此,您可以将初始下载量降至最低,仅用于加载基本内容。当用户探索网站的新功能时,逐步加载那些区域。

优化 ASP.NET 2.0 Profile 提供程序

您知道 ASP.NET 2.0 Profile Provider 中有两个重要的存储过程可以显著优化吗?如果在使用它们之前没有进行必要的优化,在重负载下,您的服务器将会崩溃,并可能导致您的业务随之倒闭。以下是一个故事

三月份,Pageflakes 在 MIX 2006 上展出。那时我们过得很愉快。我们是Atlas 网站展示上的第一家公司。每天的访问量都在急剧上升。有一天,我们注意到数据库服务器不再响应了。我们重启了服务器,把它带回来,它在一小时内又死了。在对服务器的残骸进行了大量事后分析后,我们发现它有 100% 的 CPU 和极高的 IO 使用率。硬盘过热,自行关闭以进行保护。这让我们感到非常惊讶,因为我们认为我们当时非常聪明,我们曾经对每一个 Web 服务函数进行过性能分析。所以,我们查看了数亿兆字节的日志,希望找到耗时最长的 Web 服务函数。我们怀疑一个。它是加载用户页面设置的第一个函数。我们将其分解成更小的部分,以便找出哪个部分占用了最多时间。

private GetPageflake(string source, string pageID, string userUniqueName)
{
  if( Profile.IsAnonymous ) 
  {
  using (new TimedLog(Profile.UserName,"GetPageflake"))
  {

您可以看到,整个函数体都被计时了。如果您想了解这种计时工作原理,我将在新文章中解释。我们也对我们怀疑占用最多资源的小部分进行了计时。但我们在代码中找不到任何一个地方耗时显著。我们的代码库始终经过高度优化(毕竟,您知道谁在审查它,是我)。

与此同时,用户在抱怨,管理层在尖叫,支持人员在电话里抱怨。没什么特别的,只是我们每月都会遇到几次的情况。

现在您一定在喊,“你可以使用 SQL Profiler,你这个白痴!”我们使用的是 SQL Server Workgroup Edition。它没有 SQL Profiler。所以,我们不得不采取一些非常规手段,设法让它在某个服务器上运行。别问我怎么做。运行 SQL Profiler 后,哇,我们惊呆了!那个给我们带来如此痛苦的尊贵 SP 的名字是伟大的存储过程 dbo.aspnet_Profile_GetProfiles

我们大量使用了(并且仍然使用)Profile provider。

这是 SP

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

   DECLARE @ApplicationId  uniqueidentifier

    SELECT @ApplicationId = NULL

   SELECT @ApplicationId = ApplicationId FROM aspnet_Applications
   WHERE LOWER(@ApplicationName) = LoweredApplicationName

   IF (@ApplicationId IS NULL) 
      RETURN

然后,它创建一个临时表(应该使用表类型)来存储用户的配置文件。

    -- 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。SP 的编写方式是,如果一个用户有多个配置文件,它将返回该用户的所有配置文件。但通常我们每用户存储一个配置文件。因此,没有必要创建临时表。此外,没有必要进行 LIKE LOWER(@UserNameToMatch)。我们总是使用一个完整的用户名进行调用,我们可以直接使用 equal 运算符进行匹配。

所以,我们打开了存储过程,并进行了这样的开放式心脏搭桥手术

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

它在本地运行正常。现在是时候在服务器上运行了。这是一个重要的 SP,由 ASP.NET 2.0 Profile Provider 使用,是 ASP.NET Framework 的核心。如果我们在这里做错了什么,我们可能不会立即看到问题,但也许一个月后我们会意识到用户的配置文件混乱了,并且无法恢复。因此,直接在实时生产服务器上运行此 SP,而没有进行充分的测试,这是一个相当艰难的决定。我们也没有时间进行充分的测试。我们已经瘫痪了。所以,我们聚在一起,祈祷,然后按下了 SQL Server Management Studio 上的“Execute”按钮。

SP 运行正常。在服务器上,我们注意到 CPU 使用率从 100% 下降到 30%。IO 使用率也下降到 40%。

我们再次上线了!

这是另一个 SP,由于我们大量使用 Profile provider,它在我们网站的每次页面加载和 Web 服务调用时都会被调用。

CREATE 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

运行 SP 时,查看统计信息

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 进行 SELECT 操作,以从应用程序名称中查找应用程序 ID。您可以轻松地将此替换为 SP 中硬编码的应用程序 ID,从而节省一些工作。通常,我们在生产服务器上只运行一个应用程序。因此,没有必要在每次调用时都查找应用程序 ID。这是一个可以进行的快速优化。然而,从客户端统计信息中,您可以看到真正的性能瓶颈在哪里

Client_20statistics.png

然后看最后一个块,其中 aspnet_users 表用 LastActivityDate 更新。这是最昂贵的。

Update_20cost.png

这样做是为了确保 Profile provider 记住用户配置文件上次访问的时间。我们不必在每次访问 Profile 对象的所有页面加载和 Web 服务调用时都这样做。也许可以在用户首次登录和注销时这样做。在我们的例子中,在用户浏览页面时会调用很多 Web 服务。反正只有一个页面。因此,我们可以轻松地删除它,以节省每次 Web 服务调用时在巨大的 aspnet_users 表上进行的昂贵的更新。

如何查询 ASP.NET 2.0 Membership 表而不导致网站崩溃

此类查询将在您的开发环境中愉快地运行

Select * from aspnet_users where UserName = 'blabla'

或者,您可以使用以下方式轻松获取某个用户的配置文件

Select * from aspnet_profile where userID = '…...'

您甚至可以像这样很好地更新 aspnet_membership 表中某个用户的电子邮件

Update aspnet_membership 
SET Email = 'newemailaddress@somewhere.com' 
Where Email = '…'

但是,当您的生产服务器上有一个巨大的数据库时,运行这些查询中的任何一个都会导致您的服务器崩溃。原因是,虽然这些查询看起来非常明显,并且您会频繁使用它们,但它们都不是任何索引的一部分。因此,所有上述查询都会导致在数百万行的数据上进行“表扫描”(对任何查询来说都是最糟糕的情况)。

在我们这里发生的情况是这样的。我们在 Pageflakes 的许多营销报告中使用了 UserNameEmailUserIDIsAnonymous 等字段。这些报告只有营销团队使用,其他人不使用。现在,网站运行正常,但每天有好几次,营销团队和用户会打电话给我们并大喊“网站很慢!”、“用户报告性能极差!”、“有些页面超时了!”等等。通常,当他们打电话给我们时,我们会告诉他们“等等,正在检查”,然后我们会彻底检查网站。我们使用 SQL Profiler 来查看出了什么问题。但我们到处都找不到任何问题。Profiler 显示查询运行正常。CPU 负载在参数范围内。网站运行平稳。我们会在电话里告诉他们,“我们看不到任何问题,出了什么事?”

那么,为什么我们在调查问题时看不到任何缓慢,但网站在一天中不被调查的时候有好几次变得非常慢呢?

营销团队有时会运行上述之类的分析报告,每天运行几次。每当他们运行这些查询中的任何一个时,由于字段不是任何索引的一部分,它会导致服务器 IO 飙升,CPU 也随之飙升——类似于这样

我们拥有 SCSI 驱动器,每分钟 15000 转,非常昂贵,非常快速。CPU 是双核双 Xeon 64 位。两者都是各自领域非常强大的硬件。尽管如此,这些查询由于巨大的数据库大小仍然使我们瘫痪。

但这在营销团队打电话给我们并让我们在电话里找出问题时从未发生过。因为当他们打电话给我们并与我们交谈时,他们没有运行任何会使服务器瘫痪的报告。他们在网站的其他地方工作,大多尝试做与抱怨的用户相同的事情。

让我们看看索引

表:aspnet_users

  • 聚集索引 = ApplicationID, LoweredUserName
  • 非聚集索引 = ApplicationID, LastActivityDate
  • 主键 = UserID

表:aspnet_membership

  • 聚集索引 = ApplicationID, LoweredEmail
  • 非聚集索引 = UserID

表:aspnet_Profile

  • 聚集索引 = UserID

大多数索引都包含 ApplicationID。除非您在 WHERE 子句中放入 ApplicationID='',否则它不会使用任何索引。结果是,所有查询都会受到表扫描的影响。只需在 where 子句中放入 ApplicationID(从 aspnet_Application 表中查找您的 ApplicationID),所有查询都会变得飞快。

请勿在 WHERE 子句中使用 Email UserName 字段。它们不是索引的一部分,而是 LoweredUserName LoweredEmail 字段与 ApplicationID 字段结合使用。所有查询都必须在 WHERE 子句中包含 ApplicationID

我们的 Admin 站点包含多个此类报告,每个报告都包含大量此类查询,这些查询针对 aspnet_usersaspnet_membershipaspnet_Profile 表。结果是,每当营销团队尝试生成报告时,他们都会占用 CPU 和 HDD 的全部能力,而网站的其他部分变得非常缓慢,有时甚至没有响应。

务必始终将所有查询的 WHERE JOIN 子句与索引配置进行交叉检查。否则,当您上线时,您将注定失败。

防止拒绝服务 (DOS) 攻击

Web 服务是黑客最具吸引力的目标,因为即使是学前黑客也可以通过反复调用执行昂贵工作的 Web 服务来使服务器崩溃。Ajax 起始页(如 Pageflakes)是此类 DOS 攻击的最佳目标,因为如果您反复访问主页而不保留 cookie,每一次命中都会产生一个全新的用户、新的页面设置、新的小部件等等。首次访问体验是最昂贵的。尽管如此,它是最容易被利用并导致网站崩溃的。您可以自己尝试一下。只需编写一个简单的代码

for( int i = 0; i < 100000; i ++ )
{
   WebClient client = new WebClient();
   client.DownloadString("http://www.pageflakes.com/default.aspx");
}

让您大吃一惊的是,您会发现,在几次调用后,您将无法获得有效响应。并不是您成功地使服务器崩溃了。而是您的请求被拒绝了。您很高兴您不再获得任何服务,从而实现了拒绝服务(对您自己)。我们很高兴拒绝您服务(DYOS)。

我手中的技巧是一种廉价的方法来记住来自特定 IP 地址的请求数量。当请求数量超过阈值时,拒绝在该期间内的进一步请求。思路是在 ASP.NET 缓存中记住调用者的 IP 并维护每个 IP 的请求计数。当计数超过预定限制时,拒绝该特定持续时间(例如 10 分钟)内的进一步请求。10 分钟后,再次允许来自该 IP 的请求。

我有一个名为 ActionValidator 的类,它维护特定操作(如首次访问、重新访问、异步回发、添加新小部件、添加新页面等)的计数。它检查特定 IP 的此类特定操作的计数是否超过阈值。

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 分钟)内的阈值。

一个名为 IsValidstatic 方法执行检查。如果请求限制未通过,则返回 true ;如果需要拒绝请求,则返回 false。一旦获得 false,您可以调用 Request.End() 并阻止 ASP.NET 继续执行。您也可以切换到一个显示“恭喜!您已成功进行拒绝服务攻击。”的页面。

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;
}

缓存键是通过操作类型和客户端 IP 地址的组合构建的。首先,它检查 cache 中是否存在该操作和客户端 IP 的条目。如果不存在,则开始计数,并在缓存中记住该 IP 的计数,持续特定时间。cache 项上的绝对过期确保在持续时间结束后,cache 项将被清除,计数将重新开始。当 cache 中已存在条目时,获取上次命中计数,并检查限制是否已超过。如果未超过,则增加计数器。无需再次将更新后的值存储在 cache 中,方法是:Cache[url]=hit;因为 hit 对象是通过引用传递的,更改它意味着它在缓存中也得到了更改。事实上,如果您再次将其放入 cache 中,cache 过期计数器将重新启动,并导致在特定持续时间后重新启动计数的逻辑失败。

用法非常简单,在 default.aspx

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();
   }
}

我在这里检查特定场景,如首次访问、重新访问、回发等。

当然,您可以使用思科防火墙并防止 DOS 攻击。您的托管提供商会向您保证,他们的整个网络都不会受到 DOS 和 DDOS(分布式 DOS)攻击。他们保证的是网络级别的攻击,如 TCP SYN 攻击或格式错误的包洪水等。他们无法分析数据包并找出某个 IP 地址在不支持 cookie 的情况下尝试多次加载网站或尝试添加过多小部件。这些被称为应用程序级别的 DOS 攻击,硬件无法阻止。它必须在您自己的代码中实现。

很少有网站会为应用程序级别的 DOS 攻击采取这样的预防措施。因此,通过编写一个简单的循环并从您的家庭宽带连接不断地命中昂贵的页面或 Web 服务,很容易使服务器变得疯狂。我希望这个小型但有效的类能帮助您防止自己的 Web 应用程序中的 DOS 攻击。

结论

您已经学会了多种技巧,可以充分发挥 ASP.NET 的潜力,从而在相同的硬件配置下实现更快的性能。您还学会了一些实用的 AJAX 技术,使您的网站加载和感觉更快。最后,您学会了如何防御大量命中并将静态内容分发到内容分发网络,以应对大量的流量需求。所有这些技术都可以使您的网站加载更快、感觉更流畅,并以更低的成本提供更高的流量。您可以通过我的书了解更多关于 ASP.NET 和 ASP.NET AJAX 网站的性能和可伸缩性改进 - 使用 ASP.NET 3.5 构建 Web 2.0 门户

© . All rights reserved.