I just discovered why all ASP.Net websites are slow, and I am trying to work out what to do about it

前端 未结 10 659
情话喂你
情话喂你 2020-11-22 07:53

I just discovered that every request in an ASP.Net web application gets a Session lock at the beginning of a request, and then releases it at the end of the request!

相关标签:
10条回答
  • 2020-11-22 08:14

    If you are using the updated Microsoft.Web.RedisSessionStateProvider(starting from 3.0.2) you can add this to your web.config to allow concurrent sessions.

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

    Source

    0 讨论(0)
  • 2020-11-22 08:17

    OK, so big Props to Joel Muller for all his input. My ultimate solution was to use the Custom SessionStateModule detailed at the end of this MSDN article:

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

    This was:

    • Very quick to implement (actually seemed easier than going the provider route)
    • Used a lot of the standard ASP.Net session handling out of the box (via the SessionStateUtility class)

    This has made a HUGE difference to the feeling of "snapiness" to our application. I still can't believe the custom implementation of ASP.Net Session locks the session for the whole request. This adds such a huge amount of sluggishness to websites. Judging from the amount of online research I had to do (and conversations with several really experienced ASP.Net developers), a lot of people have experienced this issue, but very few people have ever got to the bottom of the cause. Maybe I will write a letter to Scott Gu...

    I hope this helps a few people out there!

    0 讨论(0)
  • 2020-11-22 08:20

    After struggling with all available options, I ended up writing a JWT token based SessionStore provider (the session travels inside a cookie, and no backend storage is needed).

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

    Advantages:

    • Drop-in replacement, no changes to your code are needed
    • Scale better than any other centralized store, as no session storage backend is needed.
    • Faster than any other session storage, as no data needs to be retrieved from any session storage
    • Consumes no server resources for session storage.
    • Default non-blocking implementation: concurrent request won't block each other and hold a lock on the session
    • Horizontally scale your application: because the session data travels with the request itself you can have multiple web heads without worrying about session sharing.
    0 讨论(0)
  • 2020-11-22 08:21

    If your page does not modify any session variables, you can opt out of most of this lock.

    <% @Page EnableSessionState="ReadOnly" %>
    

    If your page does not read any session variables, you can opt out of this lock entirely, for that page.

    <% @Page EnableSessionState="False" %>
    

    If none of your pages use session variables, just turn off session state in the web.config.

    <sessionState mode="Off" />
    

    I'm curious, what do you think "a ThreadSafe collection" would do to become thread-safe, if it doesn't use locks?

    Edit: I should probably explain by what I mean by "opt out of most of this lock". Any number of read-only-session or no-session pages can be processed for a given session at the same time without blocking each other. However, a read-write-session page can't start processing until all read-only requests have completed, and while it is running it must have exclusive access to that user's session in order to maintain consistency. Locking on individual values wouldn't work, because what if one page changes a set of related values as a group? How would you ensure that other pages running at the same time would get a consistent view of the user's session variables?

    I would suggest that you try to minimize the modifying of session variables once they have been set, if possible. This would allow you to make the majority of your pages read-only-session pages, increasing the chance that multiple simultaneous requests from the same user would not block each other.

    0 讨论(0)
  • 2020-11-22 08:26

    I prepared a library based on links posted in this thread. It uses the examples from MSDN and CodeProject. Thanks to James.

    I also made modifications advised by Joel Mueller.

    Code is here:

    https://github.com/dermeister0/LockFreeSessionState

    HashTable module:

    Install-Package Heavysoft.LockFreeSessionState.HashTable
    

    ScaleOut StateServer module:

    Install-Package Heavysoft.LockFreeSessionState.Soss
    

    Custom module:

    Install-Package Heavysoft.LockFreeSessionState.Common
    

    If you want to implement support of Memcached or Redis, install this package. Then inherit the LockFreeSessionStateModule class and implement abstract methods.

    The code is not tested on production yet. Also need to improve error handling. Exceptions are not caught in current implementation.

    Some lock-free session providers using Redis:

    • https://github.com/angieslist/AL-Redis (Suggested by gregmac in this thread.)
    • https://github.com/welegan/RedisSessionProvider (NuGet: RedisSessionProvider)
    • https://github.com/efaruk/playground/tree/master/UnlockedStateProvider (NuGet: UnlockedStateProvider.Redis)
    0 讨论(0)
  • 2020-11-22 08:27

    For ASPNET MVC, we did the following:

    1. By default, set SessionStateBehavior.ReadOnly on all controller's action by overriding DefaultControllerFactory
    2. On controller actions that need writing to session state, mark with attribute to set it to SessionStateBehavior.Required

    Create custom ControllerFactory and override 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");
        }
    

    AcquireSessionLockAttribute

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

    Hook up the created controller factory in global.asax.cs

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

    Now, we can have both read-only and read-write session state in a single 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);
        }
    }
    

    Note: ASPNET session state can still be written to even in readonly mode and will not throw any form of exception (It just doesn't lock to guarantee consistency) so we have to be careful to mark AcquireSessionLock in controller's actions that require writing session state.

    0 讨论(0)
提交回复
热议问题