Я тільки що з’ясував, чому всі веб-сайти ASP.Net повільні, і я намагаюся розібратися, що з цим робити


275

Щойно я виявив, що кожен запит у веб-додатку ASP.Net отримує блокування сеансу на початку запиту, а потім випускає його в кінці запиту!

Якщо наслідки цього втрачаються на вас, як це було для мене спочатку, це в основному означає наступне:

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

  • Будь-який час UpdatePanel завантажується повільно, і користувач вирішує перейти на іншу сторінку, перш ніж UpdatePanel закінчить оновлення ... НЕ МОЖЕ! Блокування сеансу ASP.net змушує новий запит на сторінку чекати, поки оригінальний запит не закінчиться його болісно повільним завантаженням. Подвійний Арррх!

То які варіанти? Поки що я придумав:

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

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


40
Ви розумієте, що цей сайт побудований на версії .NET. Це сказав, я думаю, це масштабує досить добре.
пшениці

7
Гаразд, так що я трохи пильнував свою назву. І все-таки IMHO, що виконує ефективність роботи, що нав'язує нестандартна реалізація сеансу, вражає. Крім того, я ставлю на облік, що хлопцям Stack Overflow довелося зробити хороший девайс із високою спеціальністю, щоб отримати продуктивність та масштабованість, яких вони досягли, - і кудо їм. Нарешті, переповнення стека - це додаток MVC, а не WebForms, що, мабуть, допомагає (хоча, правда, це все ще використовувало ту саму інфраструктуру сеансу).
Джеймс


4
Якщо Джоел Мюллер дав вам інформацію, щоб виправити свою проблему, чому ви не позначили його відповідь правильною відповіддю? Просто думка.
ars265

1
@ ars265 - Джоел Мюллер надав багато хорошої інформації, і я хотів подякувати йому за це. Однак я врешті-решт пішов іншим маршрутом, ніж той, який пропонував у своїй посаді. Отже, позначення іншого поста як відповіді.
Джеймс

Відповіді:


201

Якщо ваша сторінка не змінює жодних змінних сеансів, ви можете вимкнути більшість цього блокування.

<% @Page EnableSessionState="ReadOnly" %>

Якщо ваша сторінка не читає жодних змінних сеансів, ви можете повністю відмовитись від цього блокування для цієї сторінки.

<% @Page EnableSessionState="False" %>

Якщо жодна з ваших сторінок не використовує змінні сеансу, просто вимкніть стан сеансу в web.config.

<sessionState mode="Off" />

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

Редагувати: я, мабуть, повинен пояснити, що я маю на увазі під «відмовою від більшості цього блокування». Будь-яка кількість сторінок сеансу лише для читання або без сеансу може бути оброблена за певний сеанс одночасно, не блокуючи одна одну. Однак сторінка сеансу читання-запису не може почати обробку до тих пір, поки всі запити лише для читання не будуть виконані, і під час її запуску вона повинна мати ексклюзивний доступ до сеансу цього користувача, щоб зберегти послідовність. Блокування окремих значень не буде працювати, бо що робити, якщо одна сторінка змінює набір пов'язаних значень як група? Як би ви гарантували, що інші сторінки, що працюють одночасно, отримають послідовний вигляд змінних сеансів користувача?

Я б запропонував вам спробувати мінімізувати модифікацію змінних сеансу після їх встановлення, якщо це можливо. Це дозволить зробити більшість ваших сторінок для читання лише сеансом, збільшивши ймовірність того, що кілька одночасних запитів від одного користувача не заблокують один одного.


2
Вітаю, Джоел, Дякую за Ваш час на цю відповідь Ось кілька хороших пропозицій і трохи їжі для роздумів. Я не розумію, що ви міркуєте про те, що всі значення для сеансу повинні бути виключно заблоковані протягом усього запиту. Значення кешу ASP.Net можна будь-коли змінити будь-яким потоком. Чому це має бути різним для сеансу? Як осторонь - одна з проблем, що стосуються readonly, у мене є те, що якщо розробник додає значення сеансу, коли він перебуває лише в режимі читання, він мовчки не працює (не виняток). Насправді він зберігає значення для решти запиту - але не далі.
Джеймс

5
@James - Я просто здогадуюсь про мотивації дизайнерів тут, але я думаю, що частіше, коли в одному сеансі одного користувача багато значень залежать один від одного, ніж у кеші, який може бути очищений через недостатнє використання або низький- причини пам'яті в будь-який час. Якщо одна сторінка встановлює 4 пов'язані змінні сеансу, а інша читає їх після зміни двох лише двох, це може призвести до появи дуже важких помилок. Я думаю, що дизайнери вирішили розглядати "поточний стан сеансу користувача" як єдиний блок для цілей блокування.
Джоель Мюллер

2
Тож розробити систему, яка обслуговує найменших програмістів із загальним знаменником, які не можуть з'ясувати блокування? Чи є метою ввімкнути веб-фермерські господарства, які діляться магазином сеансів між інстанціями IIS? Чи можете ви навести приклад того, що ви зберегли б у змінній сесії? Я нічого не можу придумати.
Jason Goemaat

2
Так, це одна з цілей. Перегляньте різні сценарії, коли балансування навантаження та надмірність реалізується в інфраструктурі. Коли користувач працює над веб-сторінкою, тобто він вводить дані у форму, скажімо, за 5 хвилин, і щось в збої webfarm виходить з ладу - джерело повноважень одного вузла стає плавким - користувач НЕ повинен цього помічати. Його не можна вигнати з сесії лише тому, що його сесія була втрачена, просто тому, що його робочий процес більше не існує. Це означає, що для управління ідеальним балансуванням / надмірністю сеансів слід екстерналізуватися від робочих вузлів ..
quetzalcoatl

6
Ще один корисний рівень відмови - <pages enableSessionState="ReadOnly" />у web.config та використовуйте @Page, щоб дозволити писати лише на певних сторінках.
MattW

84

Добре, такі великі реквізити Джоелу Мюллеру за весь його внесок. Моє остаточне рішення - використовувати користувальницький SessionStateModule, детально описаний в кінці цієї статті MSDN:

http://msdn.microsoft.com/en-us/library/system.web.sesionstate.sesionstateutility.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
Гаразд, я вважаю, що нарешті я маю повну безпечну нитку, реалізацію роботи сеансу, що не читається. Останні кроки включали реалізацію власної безпечної колекції SessionStateItem, яка базувалася на статті MDSN, зв'язаній у вищенаведеному коментарі. Останнім фрагментом головоломки було створення переліку безпечних потоків на основі цієї чудової статті: codeproject.com/KB/cs/safe_enumerable.aspx .
Джеймс

26
Джеймс - очевидно, це досить стара тема, але мені було цікаво, чи зможете ви поділитися своїм кінцевим рішенням? Я намагався прослідкувати, використовуючи нитку коментарів вище, але поки не вдалося отримати робоче рішення. Я впевнений, що в нашому обмеженому використанні сеансу немає нічого принципового, що вимагало б блокування.
bsiegel

31

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

На мою думку, якщо ваша програма структурована розумним чином, це не проблема. Якщо вам потрібні заблоковані, послідовні дані в рамках сеансу, вам слід спеціально здійснити перевірку блокування / сумісності самостійно.

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


Здається, ваша посилання на GitHub 404. Бібліотеки.io/ github/ angieslist/ AL- Redis, схоже, є новою URL-адресою?
Уве Кеїм

Схоже, автор хотів видалити бібліотеку навіть із другого посилання. Я б НЕ вирішуються використовувати занедбану бібліотеку, але є розвилка тут: github.com/PrintFleet/AL-Redis і альтернативна бібліотека пов'язана тут: stackoverflow.com/a/10979369/12534
Christian Davén

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:


Для цього потрібні бібліотеки з рішення ScaleOut, яке не є безкоштовним?
Hoàng Long

1
Так, я створив реалізацію лише для SOSS. Ви можете використовувати згаданих постачальників сесій Redis, це безкоштовно.
Der_Meister

Можливо, Хоанг Лонг пропустив те, що у вас є вибір між реалізацією HashTable у пам’яті та ScaleOut StateServer.
Девід Де Слоувере

Дякуємо за ваш внесок :) Я спробую переконатися, як він діє у кількох випадках використання, які ми маємо за участю СЕССІЇ.
Агустін Гарзон

Оскільки багато людей згадували про блокування певного елемента в сеансі, можливо, було б добре зазначити, що реалізація резервної копії повинна повертати загальну посилання на значення сеансу для отримання дзвінків, щоб увімкнути блокування (і навіть які не працюватимуть із збалансованими навантаженнями серверами). Залежно від того, як ви використовуєте стан сеансу, тут є потенціал для перегонових умов. Крім того, мені здається, що у вашій реалізації є замки, які насправді нічого не роблять, оскільки вони завершують лише один виклик читання або запису (виправте мене, якщо я тут не прав).
nw.

11

Якщо ваша програма не має особливих потреб, я думаю, у вас є два підходи:

  1. Не використовуйте сеанс взагалі
  2. Використовуйте сеанс таким, який є, і виконайте тонку настройку, як згадував Джоел.

Сесія не лише безпечна для потоків, але і безпечна для держави, таким чином, щоб ви знали, що до завершення поточного запиту кожна змінна сесія не змінюватиметься від іншого активного запиту. Для того, щоб це сталося, ви повинні переконатися, що сеанс буде ЗАКЛЮЧЕНО до завершення поточного запиту.

Ви можете створити сеанс як поведінку багатьма способами, але якщо він не блокує поточний сеанс, він не буде "сеансом".

Для конкретних проблем, які ви згадали, я думаю, ви повинні перевірити HttpContext.Current.Response.IsClientConnected . Це може бути корисно для запобігання непотрібних виконання та очікування клієнта, хоча він не може вирішити цю проблему повністю, оскільки це може використовуватися лише шляхом об'єднання, а не асинхронізацією.


10

Якщо ви використовуєте оновлений Microsoft.Web.RedisSessionStateProvider(починаючи з 3.0.2), ви можете додати це до свого, web.configщоб дозволити паралельні сеанси.

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

Джерело


Не впевнений, чому це було в 0. +1. Дуже корисний.
Пангама

Чи працює це в пулі додатків класичного режиму? github.com/Azure/aspnet-redis-providers/isissue/123
Іржавий

чи працює це з постачальником за умовчанням в постачальнику послуг inProc або сесії?
Нік Чан Абдулла

Зауважте, що посилання на плакати, якщо ви використовуєте RedisSessionStateprovider, але може також працювати з цими новішими постачальниками AspNetSessionState Async (для 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дій контролера, які вимагають написання стану сеансу.



3

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

Ви можете прикрасити контролер наступним атрибутом, щоб позначити його лише для читання:

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

System.Web.SessionState.SessionStateBehavior перерахування має наступні значення:

  • За замовчуванням
  • Інваліди
  • Лише для читання
  • вимагається

0

Просто для того, щоб допомогти будь-кому з цією проблемою (блокування запитів під час виконання іншого з того ж сеансу) ...

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

Це працює у всіх перевірених нами проектах.


IDK, над яким типом проекту було це, але у мене немає Session_Startметоду і все ще блокується
Денис Г. Лабрекк

0

Зупинившись на всіх доступних варіантах, я закінчив написання JWT-провайдера SessionStore на основі лексеми (сеанс перебуває всередині файлу cookie, і не потрібно зберігати резервне копіювання).

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

Переваги:

  • Заміна, що змінюється, жодних змін у вашому коді не потрібно
  • Масштабуйте краще, ніж будь-який інший централізований магазин, оскільки не потрібен сервер для зберігання сеансів.
  • Швидше, ніж будь-яке інше сховище сеансу, оскільки жодних даних не потрібно отримувати з будь-якого сеансового сховища
  • Не зберігає ресурсів сервера для зберігання сеансів.
  • Неблокуюча реалізація за замовчуванням: паралельний запит не блокує один одного та не заблокує блокування на сеансі
  • Горизонтально масштабуйте свою програму: оскільки дані сеансу подорожують із самим запитом, ви можете мати декілька веб-голів, не турбуючись про обмін сеансом.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.