Я только что понял, почему все ASP.Net веб-сайты медленные, и я пытаюсь понять, что с этим делать

Я только что обнаружил, что каждый запрос в ASP.Net веб-приложение получает блокировку сеанса в начале запроса, а затем освобождает его в конце запроса!

в случае, если последствия этого потеряны для вас, как это было для меня сначала, это в основном означает следующее:

  • в любое время ASP.Net веб-страница занимает много времени для загрузки (возможно, из - за медленного вызова базы данных или что-то еще), и пользователь решает, что они хотят перейти к другая страница, потому что они устали ждать, они не могут! В ASP.Net блокировка сеанса заставляет новый запрос страницы ждать, пока исходный запрос не завершит свою мучительно медленную загрузку. Аррргх.

  • в любое время UpdatePanel загружается медленно, и пользователь решает перейти на другую страницу, прежде чем UpdatePanel завершит обновление... ОНИ НЕ МОГУТ! В ASP.net блокировка сеанса заставляет новый запрос страницы ждать, пока исходный запрос не завершит его болезненно медленная загрузка. Двойной Arrrgh!

какие есть варианты? До сих пор я придумал:

  • реализовать пользовательский SessionStateDataStore, который ASP.Net поддержка. Я не нашел слишком много, чтобы скопировать, и это кажется довольно рискованным и легко испортить.
  • следите за всеми выполняемыми запросами, и если запрос поступает от того же пользователя, отмените исходный запрос. Кажется экстремальным, но это сработает (I думать.)
  • Не используйте сеанс! Когда мне нужно какое-то состояние для пользователя, я могу просто использовать кэш и ключевые элементы аутентифицированного имени пользователя или что-то в этом роде. Опять какая-то крайность.

Я действительно не могу поверить, что ASP.Net команда Microsoft оставила бы такое огромное узкое место в производительности в рамках версии 4.0! Я упускаю что-то очевидное? Насколько сложно будет использовать коллекцию ThreadSafe для сеанса?

8 ответов


Если ваша страница не изменяет переменные сеанса, вы можете отказаться от большей части этой блокировки.

<% @Page EnableSessionState="ReadOnly" %>

Если ваша страница не читает никаких переменных сеанса, вы можете полностью отказаться от этой блокировки для этой страницы.

<% @Page EnableSessionState="False" %>

Если ни одна из ваших страниц не использует переменные сеанса, просто выключите состояние сеанса в интернете.конфиг.

<sessionState mode="Off" />

мне любопытно, как вы думаете, что "ThreadSafe collection" сделает, чтобы стать потокобезопасным, если он не использует замки?

Edit: я, вероятно, должен объяснить, что я имею в виду под "отказаться от большей части этого замка". Любое количество страниц только для чтения или без сеанса можно обрабатывать для данного сеанса одновременно, не блокируя друг друга. Однако страница сеанса чтения и записи не может начать обработку, пока не будут выполнены все запросы только для чтения, и во время работы она должна иметь исключительный доступ к сеансу этого пользователя для поддержания согласованности. Блокировка отдельных значений не будет работать, потому что, если одна страница изменяет набор связанных значений как группу? Как вы гарантируете, что другие страницы, работающие одновременно, получат согласованное представление переменных сеанса пользователя?

Я бы предложил вам попытаться минимизировать изменение переменных сеанса после их установки, если это возможно. Это позволит вам сделать большинство ваших страниц страниц только для чтения, увеличивая вероятность того, что несколько одновременных запросов от одного пользователя не будут блокировать друг с другом.


хорошо, так большой реквизит для Джоэла Мюллера за все его вклад. Мое окончательное решение состояло в том, чтобы использовать пользовательский SessionStateModule, подробный в конце этой статьи MSDN:

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

Это:

  • очень быстро реализовать (на самом деле казалось проще, чем идти по маршруту поставщика)
  • используется много стандартных ASP.Net обработка сеанса из поле (через класс SessionStateUtility)

Это имеет огромное значение для ощущения "snapiness" в нашем приложении. Я все еще не могу поверить в пользовательскую реализацию ASP.Net сеанс блокирует сеанс для всего запроса. Это добавляет такое огромное количество вялости веб-сайтам. Судя по количеству онлайн-исследований, которые мне пришлось сделать (и разговоров с несколькими действительно опытными ASP.Net разработчики), многие люди испытали эту проблему, но очень немногие люди когда-либо добирались до сути дела. Может быть, я напишу письмо Скотту ГУ...

Я надеюсь, что это поможет нескольким людям там!


Я начал использовать AngiesList.Рэдис.RedisSessionStateModule, что помимо использования (очень быстро) сервер Redis для хранения (я использую порт windows -- хотя есть и порт MSOpenTech), Он абсолютно не блокирует сеанс.

на мой взгляд, если ваше приложение структурировано разумным образом, это не проблема. Если вам действительно нужны заблокированные, согласованные данные как часть сеанса, вы должны специально реализовать проверку блокировки / параллелизма самостоятельно.

MS решает, что каждый ASP.NET сессия должна быть заблокирована по умолчанию только для обработки плохого дизайна приложения-это плохое решение, на мой взгляд. Тем более, что кажется, что большинство разработчиков не/даже не понимают, что сеансы были заблокированы, не говоря уже о том, что приложения, по-видимому, должны быть структурированы, чтобы вы могли делать состояние сеанса только для чтения как можно больше (отказаться, где это возможно).


Я подготовил библиотеку на основе ссылок, размещенных в этом потоке. Он использует примеры из MSDN и CodeProject. Благодаря Джеймсу.

Я также внес изменения, рекомендованные Джоэлом Мюллером.

код здесь:

https://github.com/dermeister0/LockFreeSessionState

модуль HashTable:

Install-Package Heavysoft.LockFreeSessionState.HashTable

модуль сервера состояний ScaleOut:

Install-Package Heavysoft.LockFreeSessionState.Soss

таможня модуль:

Install-Package Heavysoft.LockFreeSessionState.Common

Если вы хотите реализовать поддержку memcached или Redis, установите этот пакет. Тогда наследуйте LockFreeSessionStateModule class и реализовать абстрактные методы.

код еще не протестирован на производстве. Также необходимо улучшить обработку ошибок. Исключения не учитываются в текущей реализации.

некоторые поставщики сеансов без блокировки с помощью Редис:


Если ваше приложение не имеет особых потребностей, я думаю, у вас есть 2 подхода:

  1. не использовать сессии
  2. используйте сеанс как есть и выполните тонкую настройку, как упоминал Джоэл.

сеанс не только потокобезопасен, но и безопасен для состояний, таким образом, вы знаете, что пока текущий запрос не будет завершен, каждая переменная сеанса не изменится от другого активного запроса. Для того, чтобы это произошло, вы должны обеспечить этой сессии БУДЕТ ЗАБЛОКИРОВАН до завершения текущего запроса.

вы можете создать сеанс как поведение многими способами, но если он не блокирует текущий сеанс, это не будет "сеанс".

для конкретных проблем, которые вы упомянули, я думаю, вы должны проверить HttpContext.Текущий.Ответ.IsClientConnected. Это может быть полезно для предотвращения ненужных исполнений и ожидания клиента, хотя это не может полностью решить эту проблему, так как это может используйте только пул, а не асинхронно.


для ASPNET MVC мы сделали следующее:

  1. по умолчанию SessionStateBehavior.ReadOnly на все действия контроллера путем переопределения DefaultControllerFactory
  2. на действиях контроллера, которые нужно записать в состояние сеанса, отметьте атрибутом, чтобы установить его в SessionStateBehaviour.Required

создать пользовательский ControllerFactory и переопределить GetControllerSessionBehaviour.

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

подключите созданный контроллер фабрики в global.asax.cs

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

теперь мы можем иметь как read-only и read-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);
    }
}

Примечание: состояние сеанса ASPNET все еще может быть записано даже в readonly режим и не будет бросать какую-либо форму исключения (он просто не блокируется гарантия согласованности), поэтому мы должны быть осторожны, чтобы отметить AcquireSessionLock в действиях контроллера, требующих записи состояния сеанса.


маркировка состояния сеанса контроллера как только для чтения или отключен решит проблему.

вы можете украсить контроллер следующим атрибутом, чтобы отметить его только для чтения:

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

на


просто, чтобы помочь кому-либо с этой проблемой (блокировка запросов при выполнении другого из того же сеанса)...

сегодня я начал решать эту проблему и, после нескольких часов исследований, я решил ее путем удаления Session_Start метод (даже если пустые) от глобальные.асакс.

это работает во всех проектах, которые я тестировал.