Je viens de découvrir pourquoi tous les sites Web ASP.Net sont lents et j'essaie de savoir quoi faire à ce sujet

275

Je viens de découvrir que chaque demande dans une application Web ASP.Net obtient un verrou de session au début d'une demande, puis le libère à la fin de la demande!

Au cas où les implications de cela seraient perdues pour vous, comme c'était le cas pour moi au début, cela signifie essentiellement ce qui suit:

  • Chaque fois qu'une page Web ASP.Net prend beaucoup de temps à charger (peut-être en raison d'un appel lent à la base de données ou autre), et l'utilisateur décide qu'il souhaite naviguer vers une autre page parce qu'il est fatigué d'attendre, IL NE PEUT PAS! Le verrou de session ASP.Net force la nouvelle demande de page à attendre que la demande d'origine ait terminé son chargement douloureusement lent. Arrrgh.

  • Chaque fois qu'un UpdatePanel se charge lentement, et l'utilisateur décide de naviguer vers une autre page avant la fin de la mise à jour du UpdatePanel ... IL NE PEUT PAS! Le verrou de session ASP.net force la nouvelle demande de page à attendre que la demande d'origine ait terminé son chargement douloureusement lent. Double Arrrgh!

Quelles sont donc les options? Jusqu'à présent, j'ai trouvé:

  • Implémentez un SessionStateDataStore personnalisé, pris en charge par ASP.Net. Je n'en ai pas trouvé trop à copier, et cela semble assez risqué et facile à gâcher.
  • Gardez une trace de toutes les demandes en cours et, si une demande provient du même utilisateur, annulez la demande d'origine. Semble un peu extrême, mais cela fonctionnerait (je pense).
  • N'utilisez pas Session! Lorsque j'ai besoin d'une sorte d'état pour l'utilisateur, je peux simplement utiliser le cache à la place et les éléments clés sur le nom d'utilisateur authentifié, ou quelque chose de ce genre. Cela semble encore un peu extrême.

Je ne peux vraiment pas croire que l'équipe ASP.Net Microsoft aurait laissé un goulot d'étranglement de performances aussi énorme dans le cadre de la version 4.0! Suis-je en train de manquer quelque chose d'évident? À quel point serait-il difficile d'utiliser une collection ThreadSafe pour la session?

James
la source
40
Vous vous rendez compte que ce site est construit sur .NET. Cela dit, je pense que cela évolue très bien.
Wheaties du
7
OK, donc j'étais un peu facétieux avec mon titre. Pourtant, à mon humble avis, les performances choquantes que la mise en œuvre prête à l'emploi de la session impose sont surprenantes. En outre, je parie que les gars de Stack Overflow ont dû faire un bon développement hautement personnalisé pour obtenir les performances et l'évolutivité qu'ils ont obtenues - et bravo à eux. Enfin, Stack Overflow est une application MVC, pas WebForms, ce qui, je parie, aide (bien qu'il soit vrai que cela utilisait toujours la même infrastructure de session).
James
4
Si Joel Mueller vous a donné les informations pour résoudre votre problème, pourquoi n'avez-vous pas marqué sa réponse comme la bonne réponse? Juste une pensée.
ars265
1
@ ars265 - Joel Muller a fourni beaucoup de bonnes informations, et je voulais l'en remercier. Cependant, j'ai finalement opté pour un itinéraire différent de celui suggéré dans son message. Par conséquent, marquer un message différent comme réponse.
James

Réponses:

201

Si votre page ne modifie aucune variable de session, vous pouvez désactiver la plupart de ce verrou.

<% @Page EnableSessionState="ReadOnly" %>

Si votre page ne lit aucune variable de session, vous pouvez désactiver complètement ce verrou pour cette page.

<% @Page EnableSessionState="False" %>

Si aucune de vos pages n'utilise de variables de session, désactivez simplement l'état de la session dans le web.config.

<sessionState mode="Off" />

Je suis curieux, que pensez-vous qu'une "collection ThreadSafe" ferait pour devenir thread-safe, si elle n'utilise pas de verrous?

Edit: je devrais probablement expliquer par ce que je veux dire par "désactiver la plupart de ce verrou". N'importe quel nombre de pages en lecture seule ou sans session peut être traité pour une session donnée en même temps sans se bloquer. Cependant, une page de session en lecture-écriture ne peut pas commencer le traitement tant que toutes les demandes en lecture seule ne sont pas terminées et, pendant son exécution, elle doit avoir un accès exclusif à la session de cet utilisateur afin de maintenir la cohérence. Le verrouillage des valeurs individuelles ne fonctionnerait pas, car que se passe-t-il si une page modifie un ensemble de valeurs associées en tant que groupe? Comment garantiriez-vous que d'autres pages exécutées en même temps obtiennent une vue cohérente des variables de session de l'utilisateur?

Je vous suggère d'essayer de minimiser la modification des variables de session une fois qu'elles ont été définies, si possible. Cela vous permettrait de créer la majorité de vos pages en lecture seule, augmentant ainsi les chances que plusieurs demandes simultanées du même utilisateur ne se bloquent pas.

Joel Mueller
la source
2
Salut Joel Merci pour votre temps sur cette réponse. Ce sont de bonnes suggestions et quelques pistes de réflexion. Je ne comprends pas votre raisonnement pour dire que toutes les valeurs d'une session doivent être exclusivement verrouillées sur l'ensemble de la demande. Les valeurs du cache ASP.Net peuvent être modifiées à tout moment par n'importe quel thread. Pourquoi cela devrait-il être différent pour la session? En aparté - un problème que j'ai avec l'option en lecture seule est que si un développeur ajoute une valeur à la session lorsqu'elle est en mode lecture seule, elle échoue silencieusement (sans exception). En fait, il conserve la valeur pour le reste de la demande - mais pas au-delà.
James
5
@James - Je devine simplement les motivations des concepteurs ici, mais j'imagine qu'il est plus courant que plusieurs valeurs dépendent les unes des autres dans une session d'un seul utilisateur que dans un cache qui peut être purgé par manque d'utilisation ou faible. des raisons de mémoire à tout moment. Si une page définit 4 variables de session liées et qu'une autre les lit après que seulement deux ont été modifiées, cela pourrait facilement conduire à des bogues très difficiles à diagnostiquer. J'imagine que les concepteurs ont choisi de voir "l'état actuel de la session d'un utilisateur" comme une seule unité à des fins de verrouillage pour cette raison.
Joel Mueller
2
Alors, développer un système qui s'adresse aux programmeurs au plus petit dénominateur commun qui ne peuvent pas comprendre le verrouillage? Le but est-il d'activer les batteries de serveurs Web qui partagent un magasin de sessions entre des instances IIS? Pouvez-vous donner un exemple de quelque chose que vous stockeriez dans une variable de session? Je ne pense à rien.
Jason Goemaat
2
Oui, c'est l'un des objectifs. Repensez les différents scénarios lorsque l'équilibrage de charge et la redondance sont mis en œuvre dans l'infrastructure. Lorsque l'utilisateur travaille sur la page Web, c'est-à-dire qu'il saisit des données dans le formulaire, pendant, disons, 5 minutes, et quelque chose dans la ferme Web se bloque - la source d'alimentation d'un nœud se gonfle - l'utilisateur ne devrait PAS le remarquer. Il ne peut pas être renvoyé de la session simplement parce que sa session a été perdue, simplement parce que son processus de travail n'existe plus. Cela signifie que pour gérer un équilibrage / redondance parfait, les sessions doivent être externalisées à partir des nœuds de travail.
quetzalcoatl
7
Un autre niveau utile de désactivation est <pages enableSessionState="ReadOnly" />dans web.config et utilisez @Page pour activer l'écriture sur des pages spécifiques uniquement.
MattW
84

OK, donc gros accessoires à Joel Muller pour toutes ses contributions. Ma solution ultime a été d'utiliser le SessionStateModule personnalisé détaillé à la fin de cet article MSDN:

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

C'était:

  • Très rapide à mettre en œuvre (semblait en fait plus facile que de suivre la voie du fournisseur)
  • Utilisé une grande partie de la gestion de session ASP.Net standard prête à l'emploi (via la classe SessionStateUtility)

Cela a fait une énorme différence dans le sentiment de "snapiness" à notre application. Je n'arrive toujours pas à croire que l'implémentation personnalisée de ASP.Net Session verrouille la session pour toute la demande. Cela ajoute une telle quantité de lenteur aux sites Web. À en juger par la quantité de recherches en ligne que j'ai eu à faire (et les conversations avec plusieurs développeurs ASP.Net vraiment expérimentés), beaucoup de gens ont rencontré ce problème, mais très peu de gens sont allés au fond de la cause. J'écrirai peut-être une lettre à Scott Gu ...

J'espère que cela aide quelques personnes!

James
la source
19
Cette référence est une découverte intéressante, mais je dois vous mettre en garde sur certaines choses - l'exemple de code a quelques problèmes: tout d'abord, ReaderWriterLocka été déconseillé en faveur de ReaderWriterLockSlim- vous devriez l'utiliser à la place. Deuxièmement, lock (typeof(...))a également été déconseillé - vous devez plutôt verrouiller sur une instance d'objet statique privé. Troisièmement, l'expression "Cette application n'empêche pas les requêtes Web simultanées d'utiliser le même identifiant de session" est un avertissement, pas une fonctionnalité.
Joel Mueller
3
Je pense que vous pouvez faire cela, mais vous devez remplacer l'utilisation de SessionStateItemCollectiondans l'exemple de code par une classe thread-safe (peut-être basée sur ConcurrentDictionary) si vous voulez éviter les erreurs difficiles à reproduire sous charge.
Joel Mueller
3
Je viens de m'intéresser un peu plus à cela, et malheureusement, ISessionStateItemCollectionla Keyspropriété doit être de type System.Collections.Specialized.NameObjectCollectionBase.KeysCollection- qui n'a pas de constructeurs publics. Gee, merci les gars. C'est très pratique.
Joel Mueller
2
OK, je crois que j'ai enfin une implémentation de verrouillage de threadsafe complète et non lisible du fonctionnement de Session. Les dernières étapes ont consisté à implémenter une collection SessionStateItem threadsafe personnalisée, qui était basée sur l'article MDSN lié dans le commentaire ci-dessus. La dernière pièce du puzzle avec cela était la création d'un énumérateur threadsafe basé sur cet excellent article: codeproject.com/KB/cs/safe_enumerable.aspx .
James
26
James - c'est évidemment un sujet assez ancien, mais je me demandais si vous pouviez partager votre solution ultime? J'ai essayé de suivre en utilisant le fil des commentaires ci-dessus, mais jusqu'à présent, je n'ai pas pu obtenir de solution de travail. Je suis assez certain qu'il n'y a rien de fondamental dans notre utilisation limitée de la session qui nécessiterait un verrouillage.
bsiegel
31

J'ai commencé à utiliser le AngiesList.Redis.RedisSessionStateModule , qui en plus d'utiliser le serveur Redis (très rapide) pour le stockage (j'utilise le port Windows - bien qu'il y ait aussi un port MSOpenTech ), il ne bloque absolument pas la session .

À mon avis, si votre application est structurée de manière raisonnable, ce n'est pas un problème. Si vous avez réellement besoin de données verrouillées et cohérentes dans le cadre de la session, vous devez spécifiquement implémenter vous-même un contrôle de verrouillage / simultané.

À mon avis, décider que chaque session ASP.NET devrait être verrouillée par défaut juste pour gérer une mauvaise conception d'application est une mauvaise décision. Surtout parce qu'il semble que la plupart des développeurs ne réalisent / ne réalisent même pas que les sessions ont été verrouillées, et encore moins que les applications doivent apparemment être structurées afin que vous puissiez faire un état de session en lecture seule autant que possible (opt-out, si possible) .

gregmac
la source
Votre lien GitHub semble être mort à 404. bibliothèques.io/ github/ angieslist/ AL- Redis semble être la nouvelle URL?
Uwe Keim
On dirait que l'auteur a voulu supprimer la bibliothèque, même du deuxième lien. J'hésiterais à utiliser une bibliothèque abandonnée, mais il y a une fourchette ici: github.com/PrintFleet/AL-Redis et une bibliothèque alternative liée à partir d'ici: stackoverflow.com/a/10979369/12534
Christian Davén
21

J'ai préparé une bibliothèque basée sur les liens publiés dans ce fil. Il utilise les exemples de MSDN et CodeProject. Merci à James.

J'ai également apporté des modifications conseillées par Joel Mueller.

Le code est ici:

https://github.com/dermeister0/LockFreeSessionState

Module HashTable:

Install-Package Heavysoft.LockFreeSessionState.HashTable

Module ScaleOut StateServer:

Install-Package Heavysoft.LockFreeSessionState.Soss

Module personnalisé:

Install-Package Heavysoft.LockFreeSessionState.Common

Si vous souhaitez implémenter la prise en charge de Memcached ou Redis, installez ce package. Héritez ensuite la classe LockFreeSessionStateModule et implémentez des méthodes abstraites.

Le code n'est pas encore testé en production. Il faut également améliorer la gestion des erreurs. Les exceptions ne sont pas prises dans l'implémentation actuelle.

Certains fournisseurs de sessions sans verrouillage utilisant Redis:

Der_Meister
la source
Il nécessite des bibliothèques de la solution ScaleOut, qui n'est pas gratuite?
Hoàng Long
1
Oui, j'ai créé l'implémentation pour SOSS uniquement. Vous pouvez utiliser les fournisseurs de sessions Redis mentionnés, c'est gratuit.
Der_Meister
Peut-être que Hoàng Long a manqué le point que vous avez le choix entre l'implémentation HashTable en mémoire et ScaleOut StateServer.
David De Sloovere
Merci pour votre contribution :) Je vais essayer de voir comment il agit sur les quelques cas d'utilisation que nous avons avec SESSION impliqués.
Agustin Garzon le
Étant donné que de nombreuses personnes ont mentionné l'obtention d'un verrou sur un élément spécifique de la session, il pourrait être utile de souligner que l'implémentation de sauvegarde doit renvoyer une référence commune à la valeur de la session via les appels get afin d'activer le verrouillage (et même qui ne fonctionnera pas avec des serveurs à charge équilibrée). Selon la façon dont vous utilisez l'état de la session, il existe un potentiel de conditions de course ici. De plus, il me semble qu'il y a des verrous dans votre implémentation qui ne font rien du tout car ils n'encapsulent qu'un seul appel en lecture ou en écriture (corrigez-moi si je me trompe ici).
nw.
11

Sauf si votre application a des besoins particuliers, je pense que vous avez 2 approches:

  1. Ne pas utiliser de session du tout
  2. Utilisez la session telle quelle et effectuez le réglage fin comme l'a mentionné Joel.

La session est non seulement sécurisée pour les threads mais également pour les états, de manière à ce que, jusqu'à ce que la demande en cours soit terminée, chaque variable de session ne changera pas d'une autre demande active. Pour que cela se produise, vous devez vous assurer que la session SERA VERROUILLÉE jusqu'à ce que la demande en cours soit terminée.

Vous pouvez créer une session comme un comportement de plusieurs façons, mais si elle ne verrouille pas la session en cours, elle ne sera pas «session».

Pour les problèmes spécifiques que vous avez mentionnés, je pense que vous devriez vérifier HttpContext.Current.Response.IsClientConnected . Cela peut être utile pour éviter les exécutions et les attentes inutiles sur le client, bien qu'il ne puisse pas résoudre ce problème entièrement, car cela ne peut être utilisé que par une méthode de regroupement et non par async.

George Mavritsakis
la source
10

Si vous utilisez la mise à jour Microsoft.Web.RedisSessionStateProvider(à partir de 3.0.2), vous pouvez l'ajouter à votre web.configpour autoriser les sessions simultanées.

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

La source

rabz100
la source
Je ne sais pas pourquoi c'était à 0. +1. Très utile.
Pangamma
Est-ce que cela fonctionne dans le pool d'applications en mode classique? github.com/Azure/aspnet-redis-providers/issues/123
Rusty
cela fonctionne-t-il avec le fournisseur de services d'état par défaut inProc ou Session?
Nick Chan Abdullah
Notez que l'affiche fait référence si vous utilisez RedisSessionStateprovider, mais cela peut également fonctionner avec ces nouveaux fournisseurs AspNetSessionState Async (pour SQL et Cosmos) car c'est également dans leur documentation: github.com/aspnet/AspNetSessionState Je pense que cela fonctionnerait dans classicmode si SessionStateProvider fonctionne déjà en mode classique, le problème d'état de session se produit probablement à l'intérieur d'ASP.Net (pas IIS). Avec InProc, cela peut ne pas fonctionner, mais ce serait moins un problème car il résout un problème de conflit de ressources qui est un problème plus important avec les scénarios hors proc.
madamission
4

Pour ASPNET MVC, nous avons fait ce qui suit:

  1. Par défaut, définissez SessionStateBehavior.ReadOnlytoutes les actions du contrôleur en remplaçantDefaultControllerFactory
  2. Sur les actions du contrôleur qui doivent être écrites dans l'état de session, marquez avec l'attribut pour le définir sur SessionStateBehavior.Required

Créez ControllerFactory personnalisé et remplacez 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
{ }

Connectez la fabrique de contrôleurs créée à global.asax.cs

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

Maintenant, nous pouvons avoir les deux read-onlyet l' read-writeétat de session en un seul 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);
    }
}

Remarque: l'état de session ASPNET peut toujours être écrit même en mode lecture seule et ne lèvera aucune forme d'exception (il ne se verrouille tout simplement pas pour garantir la cohérence), nous devons donc faire attention à marquer AcquireSessionLockdans les actions du contrôleur qui nécessitent l'écriture de l'état de session.

Misterhex
la source
3

Marquer l'état de session d'un contrôleur comme étant en lecture seule ou désactivé résoudra le problème.

Vous pouvez décorer un contrôleur avec l'attribut suivant pour le marquer en lecture seule:

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

l' énumération System.Web.SessionState.SessionStateBehavior a les valeurs suivantes:

  • Défaut
  • désactivé
  • Lecture seulement
  • Obligatoire
Michael King
la source
0

Juste pour aider quiconque avec ce problème (verrouillage des requêtes lors de l'exécution d'un autre depuis la même session) ...

Aujourd'hui, j'ai commencé à résoudre ce problème et, après quelques heures de recherche, je l'ai résolu en supprimant la Session_Startméthode (même si elle était vide) du fichier Global.asax .

Cela fonctionne dans tous les projets que j'ai testés.

graduenz
la source
IDK sur quel type de projet il s'agissait, mais le mien n'a pas de Session_Startméthode et se verrouille toujours
Denis G. Labrecque
0

Après avoir lutté avec toutes les options disponibles, j'ai fini par écrire un fournisseur SessionStore basé sur un jeton JWT (la session se déplace à l'intérieur d'un cookie, et aucun stockage backend n'est nécessaire).

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

Avantages:

  • Remplacement direct, aucune modification de votre code n'est nécessaire
  • Évoluez mieux que tout autre magasin centralisé, car aucun backend de stockage de session n'est nécessaire.
  • Plus rapide que tout autre stockage de session, car aucune donnée n'a besoin d'être récupérée à partir d'un stockage de session
  • Ne consomme aucune ressource de serveur pour le stockage de session.
  • Implémentation non bloquante par défaut: la demande simultanée ne se bloquera pas et ne bloquera pas la session
  • Faites évoluer votre application horizontalement: comme les données de session voyagent avec la demande elle-même, vous pouvez avoir plusieurs têtes Web sans vous soucier du partage de session.
David
la source