我刚刚发现为什么所有的ASP.Net网站都很慢,而我正试图找出解决方法

Jam*_*mes 272 architecture asp.net iis performance session

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

如果这对你造成影响,就像我一开始对你而言,这基本上意味着以下几点:

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

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

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

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

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

Joe*_*ler 200

如果您的页面未修改任何会话变量,则可以选择退出此锁定的大部分内容.

<% @Page EnableSessionState="ReadOnly" %>
Run Code Online (Sandbox Code Playgroud)

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

<% @Page EnableSessionState="False" %>
Run Code Online (Sandbox Code Playgroud)

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

<sessionState mode="Off" />
Run Code Online (Sandbox Code Playgroud)

我很好奇,如果它不使用锁,你认为"一个ThreadSafe集合"会如何成为线程安全的?

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

如果可能的话,我建议你尽量减少会话变量的修改.这将允许您使大多数页面成为只读会话页面,从而增加了来自同一用户的多个同时请求不会相互阻塞的可能性.

  • 另一个有用的退出级别是web.config中的`<pages enableSessionState ="ReadOnly"/>`并使用@Page仅在特定页面上启用写入. (6认同)
  • @James - 我只是在猜测这里的设计师的动机,但我想在一个用户的会话中让多个值相互依赖比在缓存中因为缺乏使用而被清除的情况更为常见记忆原因随时都有.如果一个页面设置了4个相关的会话变量,而另一个页面仅在修改了两个会话变量之后读取它们,则很容易导致一些非常难以诊断的错误.我想设计师选择将"用户会话的当前状态"视为用于锁定目的的单个单元. (5认同)
  • 嗨Joel感谢您抽出宝贵时间回答这个问题.这些是一些很好的建议和一些思考的东西.我不明白你的理由是说会话的所有值必须在整个请求中被独占锁定.任何线程都可以随时更改ASP.Net Cache值.为什么会话会有所不同?作为一个问题,我对readonly选项的一个问题是,如果开发人员在只读模式时为会话添加了一个值,它就会默默地失败(没有例外).事实上,它保留了其余请求的价值 - 但不会超出. (2认同)
  • 那么开发一个能够满足无法找到锁定的最低标准程序员的系统呢?目的是启用在IIS实例之间共享会话存储的Web场吗?你能举例说明你会在会话变量中存储的东西吗?我什么都想不到. (2认同)
  • 是的,这是其中一个目的.在基础架构中实现负载平衡和冗余时,重新考虑各种方案.当用户在网页上工作时,即他在表单中输入数据时,比方说,5分钟,以及webfarm中的某些内容崩溃 - 一个节点的powersource变得粉扑 - 用户不应该注意到这一点.他不能因为他的会话丢失而被踢出会议,因为他的工作流程不再存在了.这意味着要处理完美的平衡/冗余,会话必须从工作节点外部化. (2认同)

Jam*_*mes 83

好的,这么大的道具给Joel Muller所有的投入.我的最终解决方案是使用本MSDN文章末尾详细介绍的Custom SessionStateModule:

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

这是:

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

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

我希望这能帮助那里的一些人!

  • 詹姆斯 - 显然这是一个相当古老的话题,但我想知道你是否能够分享你的终极解决方案?我试图使用上面的评论主题,但到目前为止还没有能够得到一个有效的解决方案.我非常肯定在我们对会话的有限使用中没有任何基础要求锁定. (26认同)
  • 这个引用是一个有趣的发现,但我必须提醒你一些事情 - 示例代码有一些问题:首先,`ReaderWriterLock`已经被弃用而不是`ReaderWriterLockSlim` - 你应该使用它.其次,`lock(typeof(...))`也已被弃用 - 你应该锁定私有静态对象实例.第三,短语"此应用程序不会阻止同时We​​b请求使用相同的会话标识符"是警告,而不是功能. (18认同)
  • 我觉得你可以做这项工作,但你必须有,如果你想避免负载下难以重现错误线程安全类(也许是基于`ConcurrentDictionary`)取代SessionStateItemCollection`的`使用的示例代码. (3认同)
  • 我只是稍微研究了一下,不幸的是`ISessionStateItemCollection`要求`Keys`属性是`System.Collections.Specialized.NameObjectCollectionBase.KeysCollection`类型 - 它没有公共构造函数.哎呀,谢谢你们.那很方便. (3认同)
  • 好吧,我相信我终于有一个完整的线程安全,非读取锁定的Session工作实现.最后一步涉及实现自定义线程安全的SessionStateItem集合,该集合基于上述注释中链接的MDSN文章.这个难题的最后一部分是基于这篇伟大的文章创建一个线程安全的枚举器:http://www.codeproject.com/KB/cs/safe_enumerable.aspx. (2认同)
  • 詹姆斯我同意@bsiegel,如果你能分享成功的产品,那将有助于人们. (2认同)

gre*_*mac 31

我开始使用AngiesList.Redis.RedisSessionStateModule,除了使用(非常快)Redis服务器进行存储(我使用的是Windows端口 - 虽然还有一个MSOpenTech端口),它绝对没有锁定会话.

在我看来,如果您的应用程序以合理的方式构建,这不是问题.如果您确实需要锁定的,一致的数据作为会话的一部分,您应该专门实现锁定/并发检查.

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


Der*_*ter 20

我根据这个帖子中发布的链接准备了一个库.它使用MSDN和CodeProject中的示例.感谢詹姆斯.

我也做了Joel Mueller建议的修改.

代码在这里:

https://github.com/dermeister0/LockFreeSessionState

HashTable模块:

Install-Package Heavysoft.LockFreeSessionState.HashTable
Run Code Online (Sandbox Code Playgroud)

ScaleOut StateServer模块:

Install-Package Heavysoft.LockFreeSessionState.Soss
Run Code Online (Sandbox Code Playgroud)

定制模块:

Install-Package Heavysoft.LockFreeSessionState.Common
Run Code Online (Sandbox Code Playgroud)

如果要实现Memcached或Redis的支持,请安装此软件包.然后继承LockFreeSessionStateModule类并实现抽象方法.

该代码尚未在生产中进行测试.还需要改进错误处理.当前实施中没有遇到例外情况.

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


Geo*_*kis 11

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

  1. 根本不要使用会话
  2. 按原样使用会话并按照joel提到的那样执行微调.

会话不仅是线程安全的,而且是状态安全的,您知道在当前请求完成之前,每个会话变量都不会从另一个活动请求更改.为了实现这一点,您必须确保会话将被锁定,直到当前请求完成.

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

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


rab*_*100 8

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

<appSettings>
    <add key="aspnet:AllowConcurrentRequestsPerSession" value="true"/>
</appSettings>
Run Code Online (Sandbox Code Playgroud)

资源

  • 请注意,海报引用了您是否使用 RedisSessionStateprovider,但它也可能适用于这些较新的 AspNetSessionState 异步提供程序(适用于 SQL 和 Cosmos),因为它也在他们的文档中:https://github.com/aspnet/AspNetSessionState 我的猜测是如果 SessionStateProvider 已经在经典模式下工作,那么它会在经典模式下工作,会话状态可能发生在 ASP.Net(而不是 IIS)内部。对于 InProc,它可能不起作用,但不会成为一个问题,因为它解决了资源争用问题,而这对于进程外场景来说是一个更大的问题。 (3认同)

Mis*_*hex 6

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

  1. 默认情况下,SessionStateBehavior.ReadOnly通过覆盖设置所有控制器的操作DefaultControllerFactory
  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");
    }
Run Code Online (Sandbox Code Playgroud)

获取会话锁属性

[AttributeUsage(AttributeTargets.Method)]
public sealed class AcquireSessionLock : Attribute
{ }
Run Code Online (Sandbox Code Playgroud)

将创建的控制器工厂连接到 global.asax.cs

ControllerBuilder.Current.SetControllerFactory(typeof(DefaultReadOnlySessionStateControllerFactory));
Run Code Online (Sandbox Code Playgroud)

现在,我们可以同时read-onlyread-write在单个会话状态Controller

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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


小智 6

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

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

[SessionState(System.Web.SessionState.SessionStateBehavior.ReadOnly)]
Run Code Online (Sandbox Code Playgroud)

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

  • 默认
  • 残疾人
  • 只读
  • 必需的