Właśnie odkryłem, dlaczego wszystkie strony ASP.Net są wolne i staram się dowiedzieć, co z tym zrobić

275

Właśnie odkryłem, że każde żądanie w aplikacji sieci web ASP.Net otrzymuje blokadę sesji na początku żądania, a następnie zwalnia je na końcu żądania!

W przypadku, gdy konsekwencje tego zostaną dla ciebie utracone, tak jak na początku dla mnie, oznacza to w zasadzie:

  • Za każdym razem, gdy strona ASP.Net ładuje się długo (być może z powodu powolnego wywołania bazy danych lub cokolwiek innego), a użytkownik decyduje, że chce przejść do innej strony, ponieważ ma dość czekania, NIE MOGĄ! Blokada sesji ASP.Net zmusza nowe żądanie strony do czekania, aż oryginalne żądanie zakończy się boleśnie powolnym ładowaniem. Arrrgh.

  • Za każdym razem, gdy UpdatePanel ładuje się powoli, a użytkownik decyduje się przejść do innej strony, zanim UpdatePanel zakończy aktualizację ... NIE MOGĄ! Blokada sesji ASP.net zmusza nowe żądanie strony do czekania, aż oryginalne żądanie zakończy się boleśnie powolnym ładowaniem. Double Arrrgh!

Jakie są więc opcje? Do tej pory wymyśliłem:

  • Zaimplementuj niestandardowy SessionStateDataStore, który obsługuje ASP.Net. Nie znalazłem zbyt wielu do skopiowania i wydaje się to dość ryzykowne i łatwe do zepsucia.
  • Śledź wszystkie żądania w toku, a jeśli żądanie pochodzi od tego samego użytkownika, anuluj pierwotne żądanie. Wydaje się to trochę ekstremalne, ale zadziałałoby (tak myślę).
  • Nie używaj sesji! Kiedy potrzebuję jakiegoś stanu dla użytkownika, mógłbym po prostu użyć pamięci podręcznej zamiast tego i wprowadzić kluczowe elementy w uwierzytelnionej nazwie użytkownika lub coś w tym rodzaju. Znowu wydaje się to ekstremalne.

Naprawdę nie mogę uwierzyć, że zespół ASP.Net Microsoft pozostawiłby tak ogromne wąskie gardło w zakresie wydajności w wersji 4.0! Czy brakuje mi czegoś oczywistego? Jak trudno byłoby użyć kolekcji ThreadSafe do sesji?

James
źródło
40
Zdajesz sobie sprawę, że ta witryna jest zbudowana na platformie .NET. To powiedziawszy, myślę, że całkiem dobrze się skaluje.
pszenicy
7
OK, więc byłem trochę żartobliwy z moim tytułem. Mimo to, IMHO zaskakuje wydajność dławienia wydajności, którą narzuca natychmiastowa implementacja sesji. Założę się też, że faceci Stack Overflow musieli zrobić sporo niestandardowych twórców, aby uzyskać wydajność i skalowalność, którą osiągnęli - i pochwały. Wreszcie, Stack Overflow to aplikacja MVC, a nie WebForms, co, założę się, pomaga (chociaż trzeba przyznać, że nadal korzystała z tej samej infrastruktury sesji).
James
4
Jeśli Joel Mueller dostarczył ci informacje, aby rozwiązać problem, dlaczego nie oznaczyłeś jego odpowiedzi jako prawidłowej? Tylko myśl.
ars265,
1
@ ars265 - Joel Muller dostarczył wiele dobrych informacji i chciałem mu za to podziękować. Ostatecznie jednak wybrałem inną drogę niż sugerowana w jego poście. Dlatego zaznaczenie innego postu jako odpowiedzi.
James

Odpowiedzi:

201

Jeśli twoja strona nie modyfikuje żadnych zmiennych sesji, możesz zrezygnować z większości tej blokady.

<% @Page EnableSessionState="ReadOnly" %>

Jeśli strona nie odczytuje żadnych zmiennych sesji, możesz całkowicie zrezygnować z tej blokady dla tej strony.

<% @Page EnableSessionState="False" %>

Jeśli żadna ze stron nie używa zmiennych sesji, po prostu wyłącz stan sesji w pliku web.config.

<sessionState mode="Off" />

Ciekawe, co według ciebie zrobiłaby „kolekcja ThreadSafe”, aby stać się bezpieczną dla wątków, jeśli nie używa zamków?

Edycja: Prawdopodobnie powinienem wyjaśnić, co mam na myśli, mówiąc „zrezygnuj z większości tego zamka”. Dowolna liczba stron sesji tylko do odczytu lub stron bez sesji może być przetwarzana dla danej sesji jednocześnie bez wzajemnego blokowania. Jednak strona sesji odczytu-zapisu nie może rozpocząć przetwarzania, dopóki wszystkie żądania tylko do odczytu nie zostaną zakończone, a podczas działania musi mieć wyłączny dostęp do sesji tego użytkownika, aby zachować spójność. Blokowanie poszczególnych wartości nie działałoby, ponieważ co, jeśli jedna strona zmienia zestaw powiązanych wartości jako grupę? W jaki sposób zapewniłbyś, że inne strony działające w tym samym czasie uzyskałyby spójny widok zmiennych sesji użytkownika?

Sugeruję, abyś próbował zminimalizować modyfikowanie zmiennych sesji po ich ustawieniu, jeśli to możliwe. Pozwoliłoby to na uczynienie większości stron stronami sesji tylko do odczytu, zwiększając prawdopodobieństwo, że wiele jednoczesnych żądań od tego samego użytkownika nie blokuje się nawzajem.

Joel Mueller
źródło
2
Cześć Joel Dzięki za poświęcony czas na tę odpowiedź. Oto kilka dobrych sugestii i trochę do myślenia. Nie rozumiem twojego uzasadnienia dla powiedzenia, że ​​wszystkie wartości dla sesji muszą być zablokowane wyłącznie w całym żądaniu. Wartości pamięci podręcznej ASP.Net można w dowolnym momencie zmienić za pomocą dowolnego wątku. Dlaczego to powinno wyglądać inaczej w przypadku sesji? Nawiasem mówiąc - jednym z problemów, który mam z opcją tylko do odczytu, jest to, że jeśli programista doda wartość do sesji w trybie tylko do odczytu, po cichu zawiedzie (bez wyjątku). W rzeczywistości zachowuje wartość dla pozostałej części żądania - ale nie dalej.
James
5
@James - zgaduję tylko z motywacji projektantów, ale wyobrażam sobie, że bardziej powszechne jest, że wiele sesji zależy od siebie w sesji jednego użytkownika niż w pamięci podręcznej, którą można wyczyścić z powodu braku użycia lub niskiego poziomu przyczyny pamięci w dowolnym momencie. Jeśli jedna strona ustawia 4 powiązane zmienne sesji, a inna odczytuje je po zmodyfikowaniu tylko dwóch, może to łatwo prowadzić do bardzo trudnych do zdiagnozowania błędów. Wyobrażam sobie, że projektanci postanowili postrzegać „bieżący stan sesji użytkownika” jako pojedynczą jednostkę do celów blokowania z tego powodu.
Joel Mueller
2
Więc opracować system, który obsługuje programistów o najniższym wspólnym mianowniku, którzy nie potrafią ustalić blokady? Czy celem jest umożliwienie farmom internetowym, które współużytkują magazyn sesji między instancjami IIS? Czy możesz podać przykład czegoś, co chciałbyś przechowywać w zmiennej sesji? Nic nie mogę wymyślić.
Jason Goemaat
2
Tak, to jeden z celów. Zastanów się nad różnymi scenariuszami, gdy w infrastrukturze wdrożono równoważenie obciążenia i redundancję. Gdy użytkownik pracuje na stronie internetowej, tj. Wprowadza dane w formularzu, na przykład przez 5 minut, a coś w przeglądarce internetowej ulega awarii - źródło zasilania jednego węzła ulega zaciągnięciu - użytkownik NIE powinien tego zauważyć. Nie można go wyrzucić z sesji tylko dlatego, że jego sesja została utracona, tylko dlatego, że jego proces robotniczy już nie istnieje. Oznacza to, że aby poradzić sobie z doskonałym równoważeniem / redundancją, sesje muszą być eksternalizowane z węzłów roboczych.
quetzalcoatl
6
Innym przydatnym poziomem rezygnacji jest <pages enableSessionState="ReadOnly" />web.config i użyj @Page, aby umożliwić zapis tylko na określonych stronach.
MattW
84

OK, wielkie rekwizyty dla Joela Mullera za cały jego wkład. Moim najlepszym rozwiązaniem było użycie niestandardowego modułu SessionStateModule opisanego na końcu tego artykułu MSDN:

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

To było:

  • Bardzo szybki do wdrożenia (w rzeczywistości wydawał się łatwiejszy niż wybranie trasy dostawcy)
  • Użyłem dużo standardowej obsługi sesji ASP.Net po wyjęciu z pudełka (za pośrednictwem klasy SessionStateUtility)

To ogromnie wpłynęło na poczucie „zgryźliwości” w naszej aplikacji. Nadal nie mogę uwierzyć, że niestandardowa implementacja sesji ASP.Net blokuje sesję dla całego żądania. To dodaje tak ogromnej ospałości stronom internetowym. Sądząc po ilości badań, które musiałem przeprowadzić (i rozmów z kilkoma naprawdę doświadczonymi programistami ASP.Net), wiele osób doświadczyło tego problemu, ale bardzo niewiele osób doszło do sedna sprawy. Może napiszę list do Scotta Gu ...

Mam nadzieję, że pomoże to kilku osobom!

James
źródło
19
To odniesienie jest ciekawym znaleziskiem, ale muszę cię ostrzec o kilku rzeczach - przykładowy kod ma pewne problemy: po pierwsze, ReaderWriterLockjest przestarzały na korzyść ReaderWriterLockSlim- powinieneś go użyć. Po drugie, lock (typeof(...))również jest przestarzałe - zamiast tego należy zablokować prywatną instancję obiektu statycznego. Po trzecie, wyrażenie „Ta aplikacja nie uniemożliwia jednoczesnym żądaniom sieci Web korzystania z tego samego identyfikatora sesji” jest ostrzeżeniem, a nie funkcją.
Joel Mueller
3
Myślę, że możesz to zrobić, ale musisz zastąpić użycie SessionStateItemCollectionw przykładowym kodzie klasą bezpieczną dla wątków (być może opartą na ConcurrentDictionary), jeśli chcesz uniknąć trudnych do odtworzenia błędów pod obciążeniem.
Joel Mueller
3
Właśnie przyjrzałem się temu trochę więcej i niestety ISessionStateItemCollectionwymaga, aby Keyswłaściwość była typu System.Collections.Specialized.NameObjectCollectionBase.KeysCollection- który nie ma publicznych konstruktorów. Rany, dzięki chłopaki. To bardzo wygodne.
Joel Mueller
2
OK, wierzę, że w końcu mam pełną, bezpieczną dla wątków, blokującą implementację sesji działającą bez odczytu. Ostatnie kroki obejmowały wdrożenie niestandardowej kolekcji SessionStateItem, która została stworzona na podstawie artykułu MDSN połączonego z powyższym komentarzem. Ostatnim elementem łamigłówki było stworzenie modułu wyliczającego wątki na podstawie tego wspaniałego artykułu: codeproject.com/KB/cs/safe_enumerable.aspx .
James
26
James - oczywiście jest to dość stary temat, ale zastanawiałem się, czy możesz podzielić się swoim ostatecznym rozwiązaniem? Starałem się śledzić za pomocą powyższego wątku komentarzy, ale jak dotąd nie udało mi się znaleźć działającego rozwiązania. Jestem całkiem pewien, że w naszym ograniczonym użyciu sesji nie ma nic fundamentalnego, co wymagałoby zablokowania.
bsiegel
31

Zacząłem używać AngiesList.Redis.RedisSessionStateModule , który oprócz używania (bardzo szybkiego) serwera Redis do przechowywania (korzystam z portu systemu Windows - chociaż jest też port MSOpenTech ), absolutnie nie blokuje sesji .

Moim zdaniem, jeśli twoja aplikacja ma rozsądną strukturę, nie stanowi to problemu. Jeśli faktycznie potrzebujesz zablokowanych, spójnych danych w ramach sesji, powinieneś osobiście wdrożyć kontrolę blokady / współbieżności.

MS decydujące, że każda sesja ASP.NET powinna być domyślnie zablokowana tylko w celu obsługi złego projektu aplikacji, jest moim zdaniem złą decyzją. Zwłaszcza, że ​​wydaje się, że większość programistów nie zdawała sobie sprawy, że sesje były zablokowane, nie mówiąc już o tym, że aplikacje najwyraźniej muszą mieć taką strukturę, aby można było wykonywać stan sesji tylko do odczytu (w miarę możliwości). .

gregmac
źródło
Twój link do GitHub wydaje się być 404 martwy. libraries.io/github/angieslist/AL-Redis wydaje się być nowym adresem URL?
Uwe Keim
Wygląda na to, że autor chciał usunąć bibliotekę, nawet z drugiego linku. Wahałbym się przed skorzystaniem z opuszczonej biblioteki, ale tutaj jest widelec: github.com/PrintFleet/AL-Redis i alternatywna biblioteka, do której link znajduje się tutaj: stackoverflow.com/a/10979369/12534
Christian Davén
21

Przygotowałem bibliotekę na podstawie linków zamieszczonych w tym wątku. Wykorzystuje przykłady z MSDN i CodeProject. Dzięki Jamesowi.

Dokonałem również modyfikacji zalecanych przez Joela Muellera.

Kod jest tutaj:

https://github.com/dermeister0/LockFreeSessionState

Moduł HashTable:

Install-Package Heavysoft.LockFreeSessionState.HashTable

Moduł ScaleOut StateServer:

Install-Package Heavysoft.LockFreeSessionState.Soss

Moduł niestandardowy:

Install-Package Heavysoft.LockFreeSessionState.Common

Jeśli chcesz wdrożyć obsługę Memcached lub Redis, zainstaluj ten pakiet. Następnie odziedzicz klasę LockFreeSessionStateModule i zaimplementuj metody abstrakcyjne.

Kod nie został jeszcze przetestowany pod kątem produkcji. Również należy poprawić obsługę błędów. Wyjątki nie są wychwytywane w bieżącej implementacji.

Niektórzy dostawcy sesji bez blokady korzystający z Redis:

Der_Meister
źródło
Wymaga bibliotek z rozwiązania ScaleOut, które nie jest darmowe?
Hoàng Long
1
Tak, stworzyłem implementację tylko dla SOSS. Możesz skorzystać ze wspomnianych dostawców sesji Redis, to nic nie kosztuje.
Der_Meister
Może Hoàng Long nie zauważył, że masz wybór między implementacją HashTable w pamięci a ScaleOut StateServer.
David De Sloovere
Dzięki za Twój wkład :) Spróbuję zobaczyć, jak to działa w przypadku kilku przypadków użycia, które mamy w związku z SESSION.
Agustin Garzon
Ponieważ wiele osób wspomniało o zablokowaniu określonego elementu w sesji, warto zauważyć, że implementacja tworzenia kopii zapasowych musi zwracać wspólne odniesienie do wartości sesji w wywołaniach get, aby umożliwić blokowanie (a nawet które nie będą działać z serwerami z równoważeniem obciążenia). W zależności od tego, w jaki sposób korzystasz ze stanu sesji, mogą się tu pojawić warunki wyścigu. Wydaje mi się również, że w twojej implementacji są blokady, które w rzeczywistości nic nie robią, ponieważ zawijają tylko jedno wywołanie odczytu lub zapisu (popraw mnie, jeśli się tu mylę).
nw.
11

Chyba że twoja aplikacja ma specjalne potrzeby, myślę, że masz 2 podejścia:

  1. W ogóle nie używaj sesji
  2. Używaj sesji w takiej postaci, w jakiej się znajdują i wykonuj dostrajanie, jak wspomniano joel.

Sesja jest nie tylko bezpieczna dla wątków, ale także bezpieczna dla stanu, w taki sposób, że wiesz, że dopóki bieżące żądanie nie zostanie zakończone, każda zmienna sesji nie zmieni się z innego aktywnego żądania. Aby tak się stało, musisz upewnić się, że sesja ZOSTANIE ZABLOKOWANA do czasu zakończenia bieżącego żądania.

Możesz utworzyć zachowanie podobne do sesji na wiele sposobów, ale jeśli nie zablokuje ono bieżącej sesji, nie będzie to „sesja”.

Dla konkretnych problemów, o których wspomniałeś, myślę, że powinieneś sprawdzić HttpContext.Current.Response.IsClientConnected . Może to być użyteczne, aby zapobiec niepotrzebnym wykonaniom i czeka na klienta, chociaż nie może całkowicie rozwiązać tego problemu, ponieważ można go użyć tylko w trybie puli, a nie asynchronizacji.

George Mavritsakis
źródło
10

Jeśli używasz zaktualizowanego Microsoft.Web.RedisSessionStateProvider(od 3.0.2), możesz dodać to do swojego, web.configaby umożliwić równoczesne sesje.

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

Źródło

rabz100
źródło
Nie jestem pewien, dlaczego było to na poziomie +1. Bardzo przydatne.
Pangamma
Czy to działa w puli aplikacji w trybie klasycznym? github.com/Azure/aspnet-redis-providers/issues/123
Rusty
czy działa to z domyślnym dostawcą usług stanu inProc lub Session?
Nick Chan Abdullah
Zauważ, że plakat odwołuje się, jeśli korzystasz z RedisSessionStateprovider, ale może on również współpracować z nowszymi dostawcami AspNetSessionState Async (dla SQL i Cosmos), ponieważ znajduje się również w ich dokumentacji: github.com/aspnet/AspNetSessionState Domyślam się, że będzie działać classicmode, jeśli SessionStateProvider już działa w trybie klasycznym, prawdopodobnie stan stanu sesji dzieje się w ASP.Net (nie w IIS). Z InProc może nie działać, ale byłby mniejszy problem, ponieważ rozwiązuje problem rywalizacji o zasoby, który jest większy w przypadku scenariuszy poza procesem.
madamission
4

W przypadku ASPNET MVC wykonaliśmy następujące czynności:

  1. Domyślnie SessionStateBehavior.ReadOnlynadpisuj wszystkie działania kontroleraDefaultControllerFactory
  2. W przypadku akcji kontrolera, które wymagają zapisu do stanu sesji, zaznacz atrybut, aby to ustawić SessionStateBehavior.Required

Utwórz niestandardowy obiekt ControllerFactory i zastąp 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
{ }

Podłącz utworzoną fabrykę kontrolerów w global.asax.cs

ControllerBuilder.Current.SetControllerFactory(typeof(DefaultReadOnlySessionStateControllerFactory));

Teraz możemy mieć zarówno stan sesji, jak read-onlyi read-writesesję w jednym 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);
    }
}

Uwaga: Stan sesji ASPNET może być nadal zapisywany nawet w trybie tylko do odczytu i nie wyrzuci żadnej formy wyjątku (po prostu nie blokuje się, aby zagwarantować spójność), dlatego musimy być ostrożni, aby zaznaczyć AcquireSessionLockdziałania kontrolera wymagające stanu zapisu.

Misterhex
źródło
3

Oznaczanie stanu sesji kontrolera jako tylko do odczytu lub wyłączone spowoduje rozwiązanie problemu.

Możesz ozdobić kontroler następującym atrybutem, aby oznaczyć go jako tylko do odczytu:

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

System.Web.SessionState.SessionStateBehavior enum ma następujące wartości:

  • Domyślna
  • Niepełnosprawny
  • Tylko czytać
  • wymagany
Michael King
źródło
0

Tylko po to, aby pomóc każdemu z tym problemem (blokowanie żądań podczas wykonywania innego z tej samej sesji) ...

Dzisiaj zacząłem rozwiązywać ten problem i po kilku godzinach badań rozwiązałem go, usuwając Session_Startmetodę (nawet jeśli pustą) z Global.asax pliku .

Działa to we wszystkich testowanych projektach.

graduenz
źródło
IDK, na jakim typie projektu to było, ale mój nie ma Session_Startmetody i nadal się blokuje
Denis G. Labrecque
0

Po zmaganiu się ze wszystkimi dostępnymi opcjami skończyłem pisać dostawcy SessionStore opartego na tokenach JWT (sesja podróżuje wewnątrz pliku cookie i nie jest potrzebne przechowywanie wewnętrznej bazy danych).

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

Zalety:

  • Wymiana drop-in, żadne zmiany w kodzie nie są potrzebne
  • Skaluj lepiej niż jakikolwiek inny scentralizowany sklep, ponieważ nie jest wymagany backend pamięci sesji.
  • Szybsze niż jakakolwiek inna pamięć sesji, ponieważ nie trzeba pobierać danych z pamięci sesji
  • Nie zużywa zasobów serwera do przechowywania sesji.
  • Domyślna implementacja nieblokująca: współbieżne żądanie nie blokuje się nawzajem i nie blokuje sesji
  • Skaluj aplikację w poziomie: ponieważ dane sesji przesyłane są wraz z samym żądaniem, możesz mieć wielu szefów stron internetowych, nie martwiąc się o udostępnianie sesji.
David
źródło