【问题标题】:I just discovered why all ASP.Net websites are slow, and I am trying to work out what to do about it我刚刚发现了为什么所有 ASP.Net 网站都很慢,我正在尝试解决这个问题
【发布时间】:2011-04-07 11:32:04
【问题描述】:

我刚刚发现 ASP.Net Web 应用程序中的每个请求在请求开始时都会获得一个 Session 锁,然后在请求结束时释放它!

如果你忘记了这一点的含义,就像一开始对我一样,这基本上意味着以下几点:

  • 任何时候 ASP.Net 网页需要很长时间才能加载(可能是由于数据库调用缓慢或其他原因),并且用户因为厌倦了等待而决定导航到不同的页面,他们不能! ASP.Net 会话锁强制新页面请求等待,直到原始请求完成其令人痛苦的缓慢加载。啊。

  • 任何时候 UpdatePanel 加载缓慢,并且用户决定在 UpdatePanel 完成更新之前导航到不同的页面......他们不能! ASP.net 会话锁强制新页面请求等待原始请求完成其令人痛苦的缓慢加载。双Arrrgh!

那么有哪些选择呢?到目前为止,我想出了:

  • 实现 ASP.Net 支持的自定义 SessionStateDataStore。我没有找到太多可以复制的东西,而且看起来风险很高,而且很容易搞砸。
  • 跟踪所有正在进行的请求,如果请求来自同一用户,则取消原始请求。似乎有点极端,但它会起作用(我认为)。
  • 不要使用会话!当我需要用户的某种状态时,我可以只使用缓存,并在经过身份验证的用户名上使用关键项,或者类似的东西。再次显得有些极端。

我真的不敢相信 ASP.Net 微软团队会在 4.0 版本的框架中留下如此巨大的性能瓶颈!我错过了一些明显的东西吗?为 Session 使用 ThreadSafe 集合有多难?

【问题讨论】:

  • 您确实意识到这个站点是建立在 .NET 之上的。也就是说,我认为它可以很好地扩展。
  • 好的,所以我的标题有点滑稽。尽管如此,恕我直言,开箱即用的会话实现所带来的性能令人吃惊。另外,我敢打赌,Stack Overflow 的人必须做一些高度定制的开发,才能获得他们已经实现的性能和可扩展性——并且对他们表示敬意。最后,Stack Overflow 是一个 MVC 应用程序,而不是 WebForms,我敢打赌它会有所帮助(尽管这仍然使用相同的会话基础结构)。
  • 如果 Joel Mueller 向您提供了解决问题的信息,您为什么不将他的答案标记为正确答案?只是一个想法。
  • @ars265 - Joel Muller 提供了很多很好的信息,我要感谢他。然而,我最终选择了一条不同于他帖子中建议的路线。因此,将另一个帖子标记为答案。

标签: asp.net performance session iis architecture


【解决方案1】:

如果您的页面没有修改任何会话变量,您可以选择退出大部分锁定。

<% @Page EnableSessionState="ReadOnly" %>

如果您的页面没有读取任何会话变量,您可以完全退出该页面的锁定。

<% @Page EnableSessionState="False" %>

如果您的页面都没有使用会话变量,只需在 web.config 中关闭会话状态即可。

<sessionState mode="Off" />

我很好奇,如果“ThreadSafe 集合”不使用锁,你认为它会如何变得线程安全?

编辑:我可能应该用“退出大部分锁定”的意思来解释。可以为给定会话同时处理任意数量的只读会话或非会话页面,而不会相互阻塞。但是,读写会话页面在所有只读请求完成之前无法开始处理,并且在运行时它必须具有对该用户会话的独占访问权限以保持一致性。锁定单个值是行不通的,因为如果一个页面将一组相关值作为一个组更改怎么办?您如何确保同时运行的其他页面能够获得用户会话变量的一致视图?

如果可能,我建议您在设置会话变量后尽量减少对它们的修改。这将允许您将大部分页面设置为只读会话页面,从而增加来自同一用户的多个同时请求不会相互阻止的机会。

【讨论】:

  • 嗨乔尔感谢您花时间回答这个问题。这些是一些很好的建议和一些思考的食物。我不明白您说会话的所有值必须在整个请求中专门锁定的理由。任何线程都可以随时更改 ASP.Net 缓存值。为什么这对于会话应该有所不同?顺便说一句 - 我对只读选项的一个问题是,如果开发人员在只读模式下确实向会话添加了一个值,它会静默失败(无例外)。事实上,它会保留请求其余部分的值 - 但不会超出。
  • @James - 我只是在这里猜测设计师的动机,但我想在单个用户的会话中多个值相互依赖比在缓存中更常见因使用不足或内存不足而随时清除。如果一个页面设置了 4 个相关的会话变量,而另一个页面只修改了两个就读取它们,这很容易导致一些非常难以诊断的错误。我想设计者出于这个原因选择将“用户会话的当前状态”作为一个单独的单元来进行锁定。
  • 那么开发一个系统来满足那些无法解决锁定问题的最低公分母程序员的需求?目的是启用在 IIS 实例之间共享会话存储的 Web 场吗?您能否举一个将存储在会话变量中的示例?我什么都想不出来。
  • 是的,这是目的之一。重新考虑在基础架构中实施负载平衡和冗余时的各种场景。当用户在网页上工作时,即他在表单中输入数据,比方说,5 分钟,并且 webfarm 中的某些东西崩溃了 - 一个节点的电源消失了 - 用户不应该注意到这一点。他不能仅仅因为他的会话丢失而被踢出会话,仅仅因为他的工作进程不再存在。这意味着要处理完美的平衡/冗余,会话必须从工作节点外部化..
  • 另一个有用的选择退出级别是 web.config 中的 &lt;pages enableSessionState="ReadOnly" /&gt; 并使用 @Page 启用仅在特定页面上写入。
【解决方案2】:

好的,非常感谢 Joel Muller 的所有意见。我的最终解决方案是使用这篇 MSDN 文章末尾详细介绍的自定义 SessionStateModule:

http://msdn.microsoft.com/en-us/library/system.web.sessionstate.sessionstateutility.aspx

这是:

  • 实施起来非常快(实际上似乎比使用提供商路线更容易)
  • 使用了很多开箱即用的标准 ASP.Net 会话处理(通过 SessionStateUtility 类)

这对我们的应用程序“敏捷”的感觉产生了巨大的影响。我仍然无法相信 ASP.Net Session 的自定义实现会锁定整个请求的会话。这给网站增加了如此多的迟缓。从我必须做的大量在线研究(以及与几位真正有经验的 ASP.Net 开发人员的对话)来看,很多人都遇到过这个问题,但很少有人能找到原因。也许我会写一封信给 Scott Gu……

我希望这可以帮助一些人!

【讨论】:

  • 该引用是一个有趣的发现,但我必须提醒您一些事情 - 示例代码存在一些问题:首先,ReaderWriterLock 已被弃用,而支持 ReaderWriterLockSlim - 您应该使用相反。其次,lock (typeof(...)) 也已被弃用 - 您应该锁定私有静态对象实例。第三,短语“此应用程序不会阻止同时 Web 请求使用相同的会话标识符”是一个警告,而不是一个功能。
  • 我认为你可以做到这一点,但如果你想避免困难,你必须用线程安全类(可能基于ConcurrentDictionary)替换示例代码中SessionStateItemCollection的用法- 重现负载下的错误。
  • 我只是稍微研究了一下,不幸的是ISessionStateItemCollection 要求Keys 属性为System.Collections.Specialized.NameObjectCollectionBase.KeysCollection 类型——它没有公共构造函数。啧啧,谢谢各位。非常方便。
  • 好的,我相信我终于有了一个完整的线程安全、非读锁定的会话工作实现。最后一步涉及实现自定义线程安全 SessionStateItem 集合,该集合基于上述评论中链接的 MDSN 文章。最后一个难题是根据这篇精彩的文章创建一个线程安全的枚举器:codeproject.com/KB/cs/safe_enumerable.aspx
  • James - 显然这是一个相当古老的话题,但我想知道您是否能够分享您的最终解决方案?我尝试使用上面的 cmets 线程进行操作,但到目前为止还没有找到可行的解决方案。我相当肯定,在我们有限地使用需要锁定的会话中没有什么基本的东西。
【解决方案3】:

我开始使用AngiesList.Redis.RedisSessionStateModule,除了使用(非常快的)Redis server 进行存储(我使用的是windows port——虽然还有一个MSOpenTech port),它绝对没有锁定会话。

在我看来,如果您的应用程序结构合理,这不是问题。如果您确实需要锁定、一致的数据作为会话的一部分,您应该自己专门实施锁定/并发检查。

在我看来,MS 决定默认锁定每个 ASP.NET 会话只是为了处理糟糕的应用程序设计,这是一个糟糕的决定。特别是因为似乎大多数开发人员没有/甚至没有意识到会话被锁定,更不用说应用程序显然需要结构化,以便您可以尽可能多地执行只读会话状态(在可能的情况下选择退出) .

【讨论】:

【解决方案4】:

我根据此线程中发布的链接准备了一个库。它使用来自 MSDN 和 CodeProject 的示例。感谢詹姆斯。

我还根据 Joel Mueller 的建议进行了修改。

代码在这里:

https://github.com/dermeister0/LockFreeSessionState

哈希表模块:

Install-Package Heavysoft.LockFreeSessionState.HashTable

ScaleOut StateServer 模块:

Install-Package Heavysoft.LockFreeSessionState.Soss

自定义模块:

Install-Package Heavysoft.LockFreeSessionState.Common

如果你想实现对 Memcached 或 Redis 的支持,请安装这个包。然后继承LockFreeSessionStateModule类,实现抽象方法。

代码尚未在生产环境中测试。还需要改进错误处理。当前实现中没有捕获异常。

一些使用 Redis 的无锁会话提供者:

【讨论】:

  • 它需要来自ScaleOut解决方案的库,这不是免费的?
  • 是的,我只为 SOSS 创建了实现。您可以使用上面提到的 Redis 会话提供程序,它是免费的。
  • 也许 Hoàng Long 错过了您可以在内存中的 HashTable 实现和 ScaleOut StateServer 之间进行选择这一点。
  • 感谢您的贡献 :) 我将尝试看看它如何作用于我们涉及的 SESSION 的少数用例。
  • 提供程序不支持跨多个 Web 请求的锁定。它被称为“无锁”,这意味着您理解并接受不能保证会话一致性。锁仅用于实现线程安全的数据结构。如果您需要锁定单个会话/缓存项的能力,请使用 Redis。
【解决方案5】:

如果您使用更新后的Microsoft.Web.RedisSessionStateProvider(从3.0.2 开始),您可以将其添加到您的web.config 以允许并发会话。

<appSettings>
    <add key="aspnet:AllowConcurrentRequestsPerSession" value="true"/>
</appSettings>

Source

【讨论】:

  • 不知道为什么这是 0。+1。非常有用。
  • 这在经典模式应用程序池中有效吗? github.com/Azure/aspnet-redis-providers/issues/123
  • 这是否适用于默认的 inProc 或 Session 状态服务提供者?
  • 请注意,如果您使用的是 RedisSessionStateprovider,则海报引用,但它也可能适用于这些较新的 AspNetSessionState 异步提供程序(用于 SQL 和 Cosmos),因为它也在他们的文档中:github.com/aspnet/AspNetSessionState 我的猜测是如果 SessionStateProvider 已经在经典模式下工作,它将在经典模式下工作,很可能会话状态内容发生在 ASP.Net(而不是 IIS)内部。使用 InProc 可能无法正常工作,但问题会更小,因为它解决了资源争用问题,而这对于 out-proc 场景来说是一个更大的问题。
【解决方案6】:

除非您的应用程序有特殊需要,否则我认为您有两种方法:

  1. 根本不使用会话
  2. 按原样使用会话并按照 joel 所述执行微调。

Session 不仅是线程安全的,而且是状态安全的,在某种程度上你知道,在当前请求完成之前,每个会话变量都不会从另一个活动请求中改变。为此,您必须确保该会话将被锁定,直到当前请求完成。

您可以通过多种方式创建类似会话的行为,但如果它不锁定当前会话,则不会是“会话”。

对于你提到的具体问题,我认为你应该检查HttpContext.Current.Response.IsClientConnected。这对于防止不必要的执行和客户端等待很有用,尽管它不能完全解决这个问题,因为这只能通过池方式使用而不是异步。

【讨论】:

    【解决方案7】:

    对于 ASPNET MVC,我们做了以下工作:

    1. 默认情况下,通过覆盖 DefaultControllerFactory 在所有控制器的操作上设置 SessionStateBehavior.ReadOnly
    2. 在需要写入会话状态的控制器操作上,用属性标记以将其设置为SessionStateBehavior.Required

    创建自定义 ControllerFactory 并覆盖 GetControllerSessionBehavior

        protected override SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, Type controllerType)
        {
            var DefaultSessionStateBehaviour = SessionStateBehaviour.ReadOnly;
    
            if (controllerType == null)
                return DefaultSessionStateBehaviour;
    
            var isRequireSessionWrite =
                controllerType.GetCustomAttributes<AcquireSessionLock>(inherit: true).FirstOrDefault() != null;
    
            if (isRequireSessionWrite)
                return SessionStateBehavior.Required;
    
            var actionName = requestContext.RouteData.Values["action"].ToString();
            MethodInfo actionMethodInfo;
    
            try
            {
                actionMethodInfo = controllerType.GetMethod(actionName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
            }
            catch (AmbiguousMatchException)
            {
                var httpRequestTypeAttr = GetHttpRequestTypeAttr(requestContext.HttpContext.Request.HttpMethod);
    
                actionMethodInfo =
                    controllerType.GetMethods().FirstOrDefault(
                        mi => mi.Name.Equals(actionName, StringComparison.CurrentCultureIgnoreCase) && mi.GetCustomAttributes(httpRequestTypeAttr, false).Length > 0);
            }
    
            if (actionMethodInfo == null)
                return DefaultSessionStateBehaviour;
    
            isRequireSessionWrite = actionMethodInfo.GetCustomAttributes<AcquireSessionLock>(inherit: false).FirstOrDefault() != null;
    
             return isRequireSessionWrite ? SessionStateBehavior.Required : DefaultSessionStateBehaviour;
        }
    
        private static Type GetHttpRequestTypeAttr(string httpMethod) 
        {
            switch (httpMethod)
            {
                case "GET":
                    return typeof(HttpGetAttribute);
                case "POST":
                    return typeof(HttpPostAttribute);
                case "PUT":
                    return typeof(HttpPutAttribute);
                case "DELETE":
                    return typeof(HttpDeleteAttribute);
                case "HEAD":
                    return typeof(HttpHeadAttribute);
                case "PATCH":
                    return typeof(HttpPatchAttribute);
                case "OPTIONS":
                    return typeof(HttpOptionsAttribute);
            }
    
            throw new NotSupportedException("unable to determine http method");
        }
    

    获取会话锁定属性

    [AttributeUsage(AttributeTargets.Method)]
    public sealed class AcquireSessionLock : Attribute
    { }
    

    global.asax.cs中挂接创建的控制器工厂

    ControllerBuilder.Current.SetControllerFactory(typeof(DefaultReadOnlySessionStateControllerFactory));
    

    现在,我们可以在一个 Controller 中同时拥有 read-onlyread-write 会话状态。

    public class TestController : Controller 
    {
        [AcquireSessionLock]
        public ActionResult WriteSession()
        {
            var timeNow = DateTimeOffset.UtcNow.ToString();
            Session["key"] = timeNow;
            return Json(timeNow, JsonRequestBehavior.AllowGet);
        }
    
        public ActionResult ReadSession()
        {
            var timeNow = Session["key"];
            return Json(timeNow ?? "empty", JsonRequestBehavior.AllowGet);
        }
    }
    

    注意:即使是只读的,ASPNET 会话状态仍然可以写入 模式并且不会抛出任何形式的异常(它只是不锁定到 保证一致性)所以我们必须小心在需要写入会话状态的控制器动作中标记AcquireSessionLock

    【解决方案8】:

    将控制器的会话状态标记为只读禁用将解决问题。

    您可以使用以下属性装饰控制器以将其标记为只读:

    [SessionState(System.Web.SessionState.SessionStateBehavior.ReadOnly)]
    

    System.Web.SessionState.SessionStateBehavior 枚举具有以下值:

    • 默认
    • 已禁用
    • 只读
    • 必填

    【讨论】:

      【解决方案9】:

      只是为了帮助任何人解决这个问题(在同一会话中执行另一个请求时锁定请求)...

      今天我开始解决这个问题,经过几个小时的研究,我通过从 Global.asax 文件中删除 Session_Start 方法(即使为空)解决了这个问题。

      这适用于我测试过的所有项目。

      【讨论】:

      • IDK 这是什么类型的项目,但我的没有Session_Start 方法并且仍然锁定
      • "我发现只有在附加调试器时才会出现这种行为(使用 F5 运行)。如果在没有附加调试器的情况下运行它(Ctrl-F5),那么它似乎没问题。所以,也许这不是一个严重的问题,但它仍然很奇怪。”源代码stackoverflow.com/questions/4451786/…
      【解决方案10】:

      在尝试了所有可用选项之后,我最终编写了一个基于 JWT 令牌的 SessionStore 提供程序(会话在 cookie 中传输,不需要后端存储)。

      http://www.drupalonwindows.com/en/content/token-sessionstate

      优点:

      • 直接替换,无需更改代码
      • 更好地扩展 比任何其他集中式存储,因为没有会话存储后端 需要。
      • 比任何其他会话存储都快,因为不需要存储任何数据 从任何会话存储中检索
      • 不消耗任何服务器资源 会话存储。
      • 默认非阻塞实现:并发 请求不会互相阻塞并锁定会话
      • 水平扩展您的应用程序:因为会话数据会传输 使用请求本身,您可以拥有多个网络头,而无需 担心会话共享。

      【讨论】:

        【解决方案11】:

        This 关于允许每个会话并发请求的答案很棒,但它缺少一些重要的细节:

        1. 允许每个会话并发请求的设置在 较新的 ASP .NET 会话状态 module 是类型 Microsoft.AspNet.SessionState.SessionStateModuleAsync。这个设置是 支持任何可以使用此模块的提供商。
        2. 较老的 会话状态模块System.Web.SessionState.SessionStateModule 不支持这个。
        3. 确保会话状态使用是线程安全的或 会话中可能出现并发问题

        启用此功能的摘要:

        允许并发请求:

        <appSettings>
            <add key="aspnet:AllowConcurrentRequestsPerSession" value="true"/>
        </appSettings>
        

        确保使用较新的会话状态模块:

        <system.webServer>
          <modules>
            <!-- remove the existing Session state module -->
            <remove name="Session" />
            <add name="Session" preCondition="integratedMode" type="Microsoft.AspNet.SessionState.SessionStateModuleAsync, Microsoft.AspNet.SessionState.SessionStateModule, Version=1.1.0.0, Culture=neutral" />
          </modules>
        </system.webServer>
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-05-30
          • 1970-01-01
          • 2023-03-26
          • 2021-10-30
          相关资源
          最近更新 更多