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

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

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

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

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

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

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

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

Ответ 1

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

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

Это было:

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

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

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

Ответ 2

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

<% @Page EnableSessionState="ReadOnly" %>

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

<% @Page EnableSessionState="False" %>

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

<sessionState mode="Off" />

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

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

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

Ответ 3

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

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

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

Ответ 4

Я подготовил библиотеку на основе ссылок, размещенных в этом потоке. Он использует примеры из 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:

Ответ 5

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

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

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

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

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

Ответ 6

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

  • По умолчанию установите SessionStateBehavior.ReadOnly на все действия контроллера, переопределив DefaultControllerFactory
  • В отношении действий контроллера, которые требуют записи в состояние сеанса, отметьте атрибутом, чтобы установить его на 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
{ }

Подключите созданный контроллер factory в 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 в действиях контроллера, которые требуют записи состояния сеанса.

Ответ 7

Отмечает состояние сеанса контроллера как readonly или disabled, решает проблему.

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

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

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

  • По умолчанию
  • Отключено
  • ReadOnly
  • Обязательно

Ответ 8

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

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

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

Ответ 9

Измените режим отладки на off в вашем файле web.config.

Разработчики обычно используют его при обработке и создании вашего приложения для ошибок.

Но нам это не нужно при хостинге.

На самом деле, что происходит здесь, при создании приложения разработчикам нужен код, который нужно скомпилировать в браузере.

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

Кроме того, весь сайт компилируется, когда приложение запускается на сервере.