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

275

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

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

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

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

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

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

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

Джеймс
источник
40
Вы понимаете, что этот сайт построен на .NET. Тем не менее, я думаю, что это довольно хорошо масштабируется.
2010 г.
7
Хорошо, так что я был немного шутлив со своим названием. Тем не менее, ИМХО, потрясение производительности, которое навязывает стандартная реализация сессии, поражает. Кроме того, могу поспорить, что ребятам из Stack Overflow пришлось сделать немало нестандартных разработок, чтобы добиться производительности и масштабируемости, которых они достигли, - и слава им. И, наконец, Stack Overflow - это приложение MVC, а не WebForms, что, я уверен, помогает (хотя по-прежнему используется та же инфраструктура сеансов).
Джеймс
4
Если Джоэл Мюллер дал вам информацию, чтобы исправить вашу проблему, почему вы не пометили его ответ как правильный ответ? Просто мысль.
ars265
1
@ ars265 - Джоэл Мюллер предоставил много хорошей информации, и я хотел поблагодарить его за это. Однако в конечном итоге я пошел по другому пути, чем тот, который был предложен в его посте. Следовательно, отмечая другой пост как ответ.
Джеймс

Ответы:

201

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

<% @Page EnableSessionState="ReadOnly" %>

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

<% @Page EnableSessionState="False" %>

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

<sessionState mode="Off" />

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

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

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

Джоэл Мюллер
источник
2
Привет, Джоэл. Спасибо, что уделили время на этот ответ. Это хорошие предложения и пища для размышлений. Я не понимаю ваших аргументов в пользу того, что все значения для сеанса должны быть заблокированы исключительно по всему запросу. Значения ASP.Net Cache могут быть изменены в любое время любым потоком. Почему это должно быть иначе для сессии? Кроме того, у меня есть одна проблема с опцией readonly: если разработчик добавляет значение в сеанс, когда он находится в режиме только для чтения, он молча завершается неудачей (без исключения). Фактически он сохраняет значение для остальной части запроса - но не за его пределами.
Джеймс
5
@James - Я просто догадываюсь о мотивации дизайнеров, но я думаю, что более распространено, когда несколько значений зависят друг от друга в сеансе одного пользователя, а не в кеше, который можно очистить из-за отсутствия использования или низкого причины памяти в любое время. Если одна страница устанавливает 4 связанных переменных сеанса, а другая читает их после того, как были изменены только две, это может легко привести к ошибкам, которые трудно диагностировать. Я полагаю, что дизайнеры решили рассматривать «текущее состояние сеанса пользователя» как единое целое для целей блокировки по этой причине.
Джоэл Мюллер
2
Итак, разработайте систему, которая обслуживает программистов с наименьшим общим знаменателем, которые не могут понять блокировку? Является ли целью включение веб-ферм, совместно использующих хранилище сеансов, между экземплярами IIS? Можете ли вы привести пример чего-то, что вы бы сохранили в переменной сеанса? Я не могу думать ни о чем.
Джейсон Гомаат
2
Да, это одна из целей. Переосмыслите различные сценарии, когда в инфраструктуре реализовано распределение нагрузки и избыточность. Когда пользователь работает на веб-странице, то есть он вводит данные в форме, скажем, в течение 5 минут, и что-то в веб-ферме дает сбой - источник питания одного узла пыхтит - пользователь НЕ должен это замечать. Его нельзя выгнать из сеанса только потому, что его сеанс был потерян, просто потому, что его рабочий процесс больше не существует. Это означает, что для обеспечения идеальной балансировки / избыточности сеансы должны быть извлечены из рабочих узлов ..
quetzalcoatl
7
Другой полезный уровень отказа находится <pages enableSessionState="ReadOnly" />в web.config и использует @Page, чтобы разрешить запись только на определенных страницах.
MattW
84

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

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

Это было:

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

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

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

Джеймс
источник
19
Эта ссылка является интересной находкой, но я должен предупредить вас о нескольких вещах - у примера кода есть некоторые проблемы: во-первых, ReaderWriterLockон устарел в пользу ReaderWriterLockSlim- вы должны использовать его вместо этого. Во-вторых, lock (typeof(...))также устарел - вместо этого следует блокировать частный экземпляр статического объекта. В-третьих, фраза «Это приложение не запрещает одновременным веб-запросам использовать один и тот же идентификатор сеанса» является предупреждением, а не функцией.
Джоэл Мюллер
3
Я думаю, что вы можете сделать это, но вы должны заменить использование SessionStateItemCollectionв примере кода классом, ориентированным на многопотоковое исполнение (возможно, на основе ConcurrentDictionary), если вы хотите избежать труднопроизводимых ошибок при загрузке.
Джоэл Мюллер
3
Я просто еще немного разбираюсь в этом и, к сожалению, ISessionStateItemCollectionтребует, чтобы Keysсвойство System.Collections.Specialized.NameObjectCollectionBase.KeysCollectionимело тип - у которого нет открытых конструкторов. Ну и дела, спасибо, ребята. Это очень удобно.
Джоэл Мюллер
2
Хорошо, я считаю, что наконец-то у меня есть полная поточная, не блокирующая чтение реализация Session. Последние шаги включали реализацию настраиваемой коллекции SessionStateItem, обеспечивающей безопасность потоков, которая была основана на статье MDSN, на которую есть ссылка в приведенном выше комментарии. Последней частью головоломки с этим было создание многопоточного перечислителя на основе этой замечательной статьи: codeproject.com/KB/cs/safe_enumerable.aspx .
Джеймс
26
Джеймс - очевидно, это довольно старая тема, но мне было интересно, смогли ли вы поделиться своим окончательным решением? Я пытался проследить, используя ветку комментариев выше, но до сих пор не смог найти рабочее решение. Я совершенно уверен, что в нашем ограниченном использовании сеанса нет ничего принципиального, что потребовало бы блокировки.
bsiegel
31

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

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

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

gregmac
источник
Ваша ссылка на GitHub кажется 404-мертвой. library.io/github/angieslist/AL-Redis, кажется, новый URL?
Уве Кейм
Похоже, автор хотел удалить библиотеку даже со второй ссылки. Я не хотел бы использовать заброшенную библиотеку, но здесь есть ответвление: github.com/PrintFleet/AL-Redis и альтернативная библиотека, связанная здесь: stackoverflow.com/a/10979369/12534
Кристиан Давен
21

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

Der_Meister
источник
Требуются библиотеки из решения ScaleOut, что не бесплатно?
Hoàng Long
1
Да, я создал реализацию только для SOSS. Вы можете использовать упомянутые поставщики сеансов Redis, это бесплатно.
Der_Meister
Возможно, Hoàng Long упустил момент, когда у вас есть выбор между реализацией HashTable в памяти и ScaleOut StateServer.
Дэвид Де Слововере
Спасибо за ваш вклад :) Я попытаюсь увидеть, как он действует в тех немногих случаях использования, которые мы имеем с SESSION.
Агустин
Так как многие люди упоминали о получении блокировки определенного элемента в сеансе, было бы хорошо отметить, что реализация поддержки должна возвращать общую ссылку на значение сеанса через вызовы get, чтобы включить блокировку (и даже это не будет работать с серверами с балансировкой нагрузки). В зависимости от того, как вы используете состояние сеанса, здесь возможны условия гонки. Кроме того, мне кажется, что в вашей реализации есть блокировки, которые на самом деле ничего не делают, потому что они заключают в себе только один вызов чтения или записи (поправьте меня, если я здесь не прав).
теперь
11

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

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

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

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

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

Джордж Маврицакис
источник
10

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

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

Источник

rabz100
источник
Не уверен, почему это было в 0. +1. Очень полезно.
Пангамма
Работает ли это в пуле приложений в классическом режиме? github.com/Azure/aspnet-redis-providers/issues/123
Расти
это работает с поставщиком услуг по умолчанию inProc или Session State?
Ник Чан Абдулла
Обратите внимание, что этот плакат ссылается на то, что вы используете RedisSessionStateprovider, но он также может работать с этими более новыми асинхронными поставщиками AspNetSessionState (для SQL и Cosmos), поскольку он также содержится в их документации: github.com/aspnet/AspNetSessionState Я предполагаю, что он будет работать в classicmode, если SessionStateProvider уже работает в классическом режиме, скорее всего, состояние состояния сеанса происходит внутри ASP.Net (не IIS). С InProc это может не сработать, но станет меньшей проблемой, потому что решает проблему конкуренции за ресурсы, которая более сложна для сценариев вне процесса.
madamission
4

Для 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");
    }

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

Misterhex
источник
3

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

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

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

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

  • По умолчанию
  • Отключено
  • ReadOnly
  • необходимые
Майкл Кинг
источник
0

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

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

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

graduenz
источник
IDK, на каком типе проекта это было, но у меня нет Session_Startметода, и он все еще блокируется
Дени Дж. Лабрек,
0

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

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

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

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