Я только что выяснил, почему все сайты 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:
- https://github.com/angieslist/AL-Redis (предложено gregmac в этой теме.)
- https://github.com/welegan/RedisSessionProvider (NuGet: RedisSessionProvider)
- https://github.com/efaruk/playground/tree/master/UnlockedStateProvider (NuGet: UnlockedStateProvider.Redis)
Если вы используете обновленный Microsoft.Web.RedisSessionStateProvider
(начиная с 3.0.2
) вы можете добавить это к вашему web.config
разрешить одновременные сеансы.
<appSettings>
<add key="aspnet:AllowConcurrentRequestsPerSession" value="true"/>
</appSettings>
Если у вашего приложения нет особых потребностей, я думаю, у вас есть 2 подхода:
- Не используйте сессию вообще
- Используйте сессию как есть и выполните точную настройку, как упоминалось выше.
Сеанс не только поточно-ориентированный, но и защищенный от состояния, так что вы знаете, что до тех пор, пока текущий запрос не будет завершен, каждая переменная сеанса не будет отличаться от другого активного запроса. Чтобы это произошло, вы должны убедиться, что сессия будет заблокирована до тех пор, пока текущий запрос не будет завершен.
Вы можете создать поведение, похожее на сеанс, многими способами, но если оно не блокирует текущий сеанс, оно не будет "сеансом".
Для конкретных проблем, которые вы упомянули, я думаю, что вы должны проверить HttpContext.Current.Response.IsClientConnected. Это может быть полезно для предотвращения ненужных выполнений и ожидания на клиенте, хотя это не может полностью решить эту проблему, так как это может использоваться только путем объединения, а не асинхронно.
Для 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
{ }
Подключите созданный контроллер фабрики в 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
- необходимые
Этот ответ о разрешении одновременного запроса на сеанс великолепен, но в нем отсутствуют некоторые важные детали:
- Параметр, разрешающий одновременные запросы на сеанс, реализован в новом модуле состояния сеанса ASP .NET, который имеет тип
Microsoft.AspNet.SessionState.SessionStateModuleAsync
. Этот параметр поддерживается любым провайдером, который может работать с этим модулем. - Старый модуль sessionstate
System.Web.SessionState.SessionStateModule
не поддерживает это. - Убедитесь, что использование состояния сеанса является потокобезопасным, иначе в сеансе может возникнуть проблема с параллелизмом.
Резюме для включения этой функции:
Разрешить одновременный запрос:
<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
вместо
GetItemExclusive
on ) существует с одним ограничением:
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.
Это работает во всех проектах, которые я тестировал.