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

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

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

  • Каждый раз, когда загрузка веб-страницы ASP.Net занимает много времени (возможно, из-за медленного вызова базы данных или чего-то еще), и пользователь решает, что он хочет перейти на другую страницу, потому что ему надоело ждать, ОНИ НЕ МОГУТ! Блокировка сеанса ASP.Net заставляет запрос новой страницы ждать, пока исходный запрос завершит свою мучительно медленную загрузку. Arrrgh.

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

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

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

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

12 ответов

Решение

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

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

Это было:

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

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

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

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

<% @Page EnableSessionState="ReadOnly" %>

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

<% @Page EnableSessionState="False" %>

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

<sessionState mode="Off" />

Мне интересно, что, по вашему мнению, "коллекция ThreadSafe" могла бы сделать поточно-ориентированной, если она не использует блокировки?

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

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

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

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

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

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

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

Код здесь:

https://github.com/dermeister0/LockFreeSessionState

Модуль HashTable:

Install-Package Heavysoft.LockFreeSessionState.HashTable

Модуль ScaleOut StateServer:

Install-Package Heavysoft.LockFreeSessionState.Soss

Пользовательский модуль:

Install-Package Heavysoft.LockFreeSessionState.Common

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

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

Некоторые поставщики сеансов без блокировки, использующие Redis:

Если вы используете обновленный Microsoft.Web.RedisSessionStateProvider(начиная с 3.0.2) вы можете добавить это к вашему web.config разрешить одновременные сеансы.

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

Источник

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

  1. Не используйте сессию вообще
  2. Используйте сессию как есть и выполните точную настройку, как упоминалось выше.

Сеанс не только поточно-ориентированный, но и защищенный от состояния, так что вы знаете, что до тех пор, пока текущий запрос не будет завершен, каждая переменная сеанса не будет отличаться от другого активного запроса. Чтобы это произошло, вы должны убедиться, что сессия будет заблокирована до тех пор, пока текущий запрос не будет завершен.

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

Для конкретных проблем, которые вы упомянули, я думаю, что вы должны проверить HttpContext.Current.Response.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 все еще может быть записано даже в режиме только для чтения, и оно не будет генерировать никаких исключений (оно просто не блокируется для обеспечения согласованности), поэтому мы должны быть осторожны, чтобы пометить AcquireSessionLock в действиях контроллера, которые требуют записи состояния сеанса.

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

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

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

перечисление System.Web.SessionState.SessionStateBehavior имеет следующие значения:

  • По умолчанию
  • инвалид
  • ReadOnly
  • необходимые

Этот ответ о разрешении одновременного запроса на сеанс великолепен, но в нем отсутствуют некоторые важные детали:

  1. Параметр, разрешающий одновременные запросы на сеанс, реализован в новом модуле состояния сеанса ASP .NET, который имеет тип Microsoft.AspNet.SessionState.SessionStateModuleAsync. Этот параметр поддерживается любым провайдером, который может работать с этим модулем.
  2. Старый модуль sessionstate 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>

После борьбы со всеми доступными параметрами я закончил тем, что написал провайдер SessionStore на основе токена JWT (сеанс перемещается внутри файла cookie, и никакого внутреннего хранилища не требуется).

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

Преимущества:

  • Прямая замена, никаких изменений в вашем коде не требуется
  • Масштабирование лучше, чем в любом другом централизованном хранилище, поскольку серверная часть хранилища сеансов не требуется.
  • Быстрее, чем любое другое хранилище сеансов, поскольку не требуется извлекать данные из любого хранилища сеансов
  • Не требует ресурсов сервера для хранения сеанса.
  • Реализация по умолчанию без блокировки: одновременные запросы не будут блокировать друг друга и удерживать блокировку сеанса
  • Горизонтальное масштабирование вашего приложения: поскольку данные сеанса перемещаются вместе с самим запросом, вы можете иметь несколько веб-головок, не беспокоясь о совместном использовании сеанса.

Для пользователей Mono , которые столкнулись с этой проблемой и не нашли ни одно из решений полезным, вы не делаете ничего плохого.
В Mono есть ошибка (выпуск № 19618), которая делает бесполезным on , поэтому не имеет значения, если вы установите SessionStateBehaviorна Web.config/pages, Application_BeginRequestили установить атрибут на Controllerили Action. Ни один не будет работать. Я старался.

Однако логика, предотвращающая блокировку (вызов GetItemвместо GetItemExclusiveon ) существует с одним ограничением: HttpHandlerдолжен реализовать интерфейс маркера.

Поэтому вместо реализации собственного SessionStateModule, я выбрал другой (немного хакерский) подход.


Вашему вниманию:

      // Custom handler that derives from MvcHandler which implements IReadOnlySessionState
public class MvcReadOnlyHandler : MvcHandler, IReadOnlySessionState
{
    public MvcReadOnlyHandler(RequestContext requestContext) : base(requestContext)
    {
    }
}
      // Custom RouteHandler that derives from `MvcRouteHandler` which
// returns our very own `MvcReadOnlyHandler`
public class MvcConcurrentRouteHandler : MvcRouteHandler
{
    protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        return new MvcReadOnlyHandler(requestContext);
    }
}
      // On Global.asax.cs Application_Start, after all the Routes and Areas are registered
// change only the route handler to the new concurrent route handler
foreach (var routeBase in RouteTable.Routes)
{
    // Check if the route handler is of type MvcRouteHandler
    if (routeBase is Route { RouteHandler: MvcRouteHandler _ } route)
    {
         // Replace the route handler
         route.RouteHandler = new MvcConcurrentRouteHandler();
    }
}

Поскольку теперь маршрутизатор реализует IReadOnlySessionStateнет блокировки по идентификатору сеанса

Надеюсь, когда ошибка будет исправлена, мое решение будет лишним, но до тех пор я надеюсь, что оно кому-то поможет.


Важное примечание: это решение в основном делает хранение элементов в сеансе небезопасным, я не использую эту функцию, поэтому для меня она работает. Вы по-прежнему можете добавлять элементы, так как ReadOnly не предотвращает запись, она просто не блокируется.
Если вы хотите гарантировать безопасное письмо, вы можете добавить еще один метод расширения MapRouteк RouteCollectionиспользовать новый маршрутизатор, чтобы зарегистрировать маршруты, которые не блокируются. Таким образом, вы можете зарегистрировать свои маршруты для новых MvcConcurrentRouteHandlerроутер или к существующему для записи.

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

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

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

Другие вопросы по тегам