Acabei de descobrir por que todos os sites ASP.Net são lentos e estou tentando descobrir o que fazer sobre isso.

275

Acabei de descobrir que cada solicitação em um aplicativo Web ASP.Net obtém um bloqueio de sessão no início de uma solicitação e a libera no final da solicitação!

Caso as implicações disso sejam perdidas para você, como era para mim a princípio, isso basicamente significa o seguinte:

  • Sempre que uma página da Web ASP.Net está demorando muito para ser carregada (talvez devido a uma chamada lenta do banco de dados ou qualquer outra coisa), e o usuário decide que deseja navegar para uma página diferente porque está cansado de esperar, ELES NÃO PODEM! O bloqueio de sessão do ASP.Net força a nova solicitação de página a aguardar até que a solicitação original termine sua carga lenta e dolorosa. Arrrgh.

  • Sempre que um UpdatePanel estiver carregando lentamente, e o usuário decidir navegar para uma página diferente antes que o UpdatePanel termine de atualizar ... ELES NÃO PODEM! O bloqueio de sessão do ASP.net força a nova solicitação de página a aguardar até que a solicitação original termine sua carga lenta e dolorosa. Arrrgh dobro!

Então, quais são as opções? Até agora eu vim com:

  • Implemente um SessionStateDataStore personalizado, suportado pelo ASP.Net. Eu não encontrei muitos por aí para copiar, e parece meio alto risco e fácil de estragar.
  • Acompanhe todas as solicitações em andamento e, se uma solicitação vier do mesmo usuário, cancele a solicitação original. Parece meio extremo, mas funcionaria (eu acho).
  • Não use Session! Quando eu precisar de algum tipo de estado para o usuário, eu poderia apenas usar o Cache e itens-chave no nome de usuário autenticado, ou algo assim. Mais uma vez parece meio extremo.

Eu realmente não posso acreditar que a equipe da ASP.Net Microsoft teria deixado um gargalo de desempenho tão grande na estrutura na versão 4.0! Estou perdendo algo óbvio? Quão difícil seria usar uma coleção ThreadSafe para a sessão?

James
fonte
40
Você percebe que este site é construído sobre o .NET. Dito isto, acho que é bem dimensionado.
wheaties
7
OK, então eu estava sendo um pouco ridícula com o meu título. Ainda assim, IMHO o choque de desempenho que a implementação imediata da sessão impõe é surpreendente. Além disso, aposto que os caras do Stack Overflow tiveram que fazer um bom desenvolvimento altamente personalizado para obter o desempenho e a escalabilidade que alcançaram - e parabéns a eles. Por fim, o Stack Overflow é um MVC APP, não o WebForms, que eu aposto que ajuda (embora, reconhecidamente, ele ainda tenha usado a mesma infraestrutura de sessão).
James
4
Se Joel Mueller forneceu as informações para corrigir seu problema, por que você não marcou a resposta dele como a resposta correta? Apenas um pensamento.
Ars265 26/10/12
1
@ ars265 - Joel Muller forneceu muitas informações boas, e eu queria agradecer a ele por isso. No entanto, eu finalmente segui uma rota diferente da sugerida em seu post. Portanto, marcar uma postagem diferente como resposta.
James

Respostas:

201

Se sua página não modificar nenhuma variável de sessão, você poderá desativar a maior parte desse bloqueio.

<% @Page EnableSessionState="ReadOnly" %>

Se sua página não ler nenhuma variável de sessão, você poderá desativar totalmente esse bloqueio para essa página.

<% @Page EnableSessionState="False" %>

Se nenhuma de suas páginas usar variáveis ​​de sessão, basta desativar o estado da sessão no web.config.

<sessionState mode="Off" />

Estou curioso, o que você acha que "uma coleção ThreadSafe" faria para se tornar segura para threads, se não usar bloqueios?

Edit: Eu provavelmente deveria explicar o que quero dizer com "desativar a maior parte desse bloqueio". Qualquer número de páginas de sessão somente leitura ou sem sessão pode ser processado para uma determinada sessão ao mesmo tempo sem se bloquear. No entanto, uma página de leitura-gravação-sessão não pode iniciar o processamento até que todas as solicitações somente leitura tenham sido concluídas e, enquanto estiver em execução, deve ter acesso exclusivo à sessão desse usuário para manter a consistência. O bloqueio de valores individuais não funcionaria, porque e se uma página alterar um conjunto de valores relacionados como um grupo? Como você garantiria que outras páginas em execução ao mesmo tempo obtivessem uma visualização consistente das variáveis ​​de sessão do usuário?

Eu sugeriria que você tentasse minimizar a modificação de variáveis ​​de sessão depois que elas fossem definidas, se possível. Isso permitiria que você aproveitasse a maioria das suas páginas como sessões de somente leitura, aumentando a chance de várias solicitações simultâneas do mesmo usuário não se bloquearem.

Joel Mueller
fonte
2
Oi Joel Obrigado pelo seu tempo com esta resposta. Estas são algumas boas sugestões e um pouco de reflexão. Não entendo seu raciocínio para dizer que todos os valores de uma sessão devem ser bloqueados exclusivamente em toda a solicitação. Os valores do cache do ASP.Net podem ser alterados a qualquer momento por qualquer thread. Por que isso deve ser diferente para a sessão? Como um aparte - um problema que tenho com a opção somente leitura é que, se um desenvolvedor adiciona um valor à sessão quando está no modo somente leitura, ela falha silenciosamente (sem exceção). De fato, mantém o valor para o restante da solicitação - mas não além.
James
5
@ James - Estou apenas adivinhando as motivações dos designers aqui, mas imagino que seja mais comum ter vários valores dependentes um do outro na sessão de um único usuário do que em um cache que possa ser eliminado por falta de uso ou baixa motivos de memória a qualquer momento. Se uma página define 4 variáveis ​​de sessão relacionadas e outra as lê depois que apenas duas foram modificadas, isso pode facilmente levar a alguns erros muito difíceis de diagnosticar. Imagino que os designers optaram por visualizar "o estado atual da sessão de um usuário" como uma única unidade para fins de bloqueio por esse motivo.
Joel Mueller
2
Então, desenvolva um sistema que atenda aos programadores de menor denominador comum que não conseguem descobrir o bloqueio? O objetivo é habilitar farms da Web que compartilham um armazenamento de sessão entre instâncias do IIS? Você pode dar um exemplo de algo que você armazenaria em uma variável de sessão? Não consigo pensar em nada.
Jason Goemaat
2
Sim, esse é um dos propósitos. Repensar os vários cenários quando o balanceamento de carga e a redundância são implementados na infraestrutura. Quando o usuário trabalha na página da Web, ou seja, ele está inserindo dados no formulário, por, digamos, 5 minutos, e algo no webfarm trava - a fonte de energia de um nó é inchada - o usuário NÃO deve perceber isso. Ele não pode ser expulso da sessão apenas porque sua sessão foi perdida, apenas porque seu processo de trabalho não existe mais. Isto significa que para lidar com perfeito equilíbrio / redundância, as sessões devem ser exteriorizado de nós trabalhadores ..
Quetzalcoatl
7
Outro nível útil de exclusão está <pages enableSessionState="ReadOnly" />no web.config e use @Page para ativar a gravação apenas em páginas específicas.
MattW
84

OK, grandes adereços para Joel Muller por todas as suas contribuições. Minha solução definitiva foi usar o SessionStateModule personalizado, detalhado no final deste artigo do MSDN:

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

Isto foi:

  • Implementação muito rápida (na verdade, parecia mais fácil do que seguir a rota do provedor)
  • Utilizou grande parte da sessão padrão do ASP.Net, pronta para uso (por meio da classe SessionStateUtility)

Isso fez uma enorme diferença no sentimento de "agitação" em nossa aplicação. Ainda não consigo acreditar que a implementação personalizada da sessão do ASP.Net bloqueie a sessão para toda a solicitação. Isso adiciona uma quantidade enorme de lentidão aos sites. A julgar pela quantidade de pesquisas on-line que eu tinha que fazer (e pelas conversas com vários desenvolvedores ASP.Net realmente experientes), muitas pessoas experimentaram esse problema, mas poucas pessoas chegaram ao fundo da causa. Talvez eu escreva uma carta para Scott Gu ...

Espero que isso ajude algumas pessoas por aí!

James
fonte
19
Essa referência é uma descoberta interessante, mas devo alertá-lo sobre algumas coisas - o código de exemplo tem alguns problemas: Primeiro, ReaderWriterLockfoi preterido em favor de ReaderWriterLockSlim- você deve usá-lo. Segundo, lock (typeof(...))também foi preterido - você deve bloquear em uma instância de objeto estático particular. Terceiro, a frase "Este aplicativo não impede que solicitações simultâneas da Web usem o mesmo identificador de sessão" é um aviso, não um recurso.
Joel Mueller
3
Eu acho que você pode fazer isso funcionar, mas você deve substituir o uso do SessionStateItemCollectioncódigo de exemplo por uma classe de thread-safe (talvez baseada em ConcurrentDictionary) se você quiser evitar erros difíceis de reproduzir sob carga.
Joel Mueller
3
Eu apenas examinei isso um pouco mais e, infelizmente, ISessionStateItemCollectionexige que a Keyspropriedade seja do tipo System.Collections.Specialized.NameObjectCollectionBase.KeysCollection- que não possui construtores públicos. Puxa, obrigado pessoal. Isso é muito conveniente.
Joel Mueller
2
OK, acredito que finalmente tenho uma implementação de bloqueio de leitura completa e sem segurança do Session trabalhando. As últimas etapas envolveram a implementação de uma coleção SessionStateItem com segurança de thread personalizada, baseada no artigo do MDSN vinculado no comentário acima. A parte final do quebra-cabeça com isso foi a criação de um enumerador de thread-safe com base neste ótimo artigo: codeproject.com/KB/cs/safe_enumerable.aspx .
James James
26
James - obviamente esse é um tópico bastante antigo, mas eu queria saber se você poderia compartilhar sua solução definitiva? Tentei acompanhar o uso dos tópicos acima, mas até agora não consegui obter uma solução funcional. Estou bastante certo de que não há nada fundamental em nosso uso limitado da sessão que exija bloqueio.
precisa saber é o seguinte
31

Comecei a usar o AngiesList.Redis.RedisSessionStateModule , que além de usar o servidor Redis (muito rápido) para armazenamento (estou usando a porta do Windows - embora também exista uma porta MSOpenTech ), ele não bloqueia absolutamente a sessão .

Na minha opinião, se o seu aplicativo estiver estruturado de maneira razoável, isso não será um problema. Se você realmente precisar de dados consistentes e bloqueados como parte da sessão, implemente especificamente uma verificação de bloqueio / simultaneidade por conta própria.

A Microsoft que decide que todas as sessões do ASP.NET devem ser bloqueadas por padrão apenas para lidar com o design inadequado de aplicativos é uma decisão ruim, na minha opinião. Especialmente porque parece que a maioria dos desenvolvedores nem sequer percebeu que as sessões estavam bloqueadas, sem falar no fato de que os aplicativos aparentemente precisam ser estruturados para que você possa executar o estado da sessão somente leitura o máximo possível (desativar, sempre que possível) .

gregmac
fonte
Seu link do GitHub parece estar 404 morto. libraries.io/github/angieslist/AL-Redis parece ser o novo URL?
Uwe Keim
Parece que o autor queria remover a biblioteca, mesmo a partir do segundo link. Eu hesitaria em usar uma biblioteca abandonada, mas há uma bifurcação aqui: github.com/PrintFleet/AL-Redis e uma biblioteca alternativa vinculada a partir daqui: stackoverflow.com/a/10979369/12534
Christian Davén
21

Eu preparei uma biblioteca com base nos links publicados neste tópico. Ele usa os exemplos do MSDN e CodeProject. Graças ao James.

Também fiz modificações aconselhadas por Joel Mueller.

O código está aqui:

https://github.com/dermeister0/LockFreeSessionState

Módulo HashTable:

Install-Package Heavysoft.LockFreeSessionState.HashTable

Módulo ScaleOut StateServer:

Install-Package Heavysoft.LockFreeSessionState.Soss

Módulo personalizado:

Install-Package Heavysoft.LockFreeSessionState.Common

Se você deseja implementar o suporte do Memcached ou Redis, instale este pacote. Em seguida, herde a classe LockFreeSessionStateModule e implemente métodos abstratos.

O código ainda não foi testado em produção. Também precisa melhorar o tratamento de erros. Exceções não são capturadas na implementação atual.

Alguns provedores de sessão sem bloqueio usando Redis:

Der_Meister
fonte
Requer bibliotecas da solução ScaleOut, o que não é gratuito?
Hoàng Long
1
Sim, eu criei a implementação apenas para o SOSS. Você pode usar os provedores de sessão Redis mencionados, é grátis.
Der_Meister
Talvez Hoàng Long tenha esquecido o ponto em que você tem uma escolha entre a implementação do HashTable na memória e o ScaleOut StateServer.
David De Sloovere 7/11
Obrigado pela sua contribuição :) Tentarei ver como ela age nos poucos casos de uso que temos com a SESSION envolvida.
Agustin Garzon
Como muitas pessoas mencionaram obter um bloqueio em um item específico na sessão, pode ser bom ressaltar que a implementação de backup precisa retornar uma referência comum ao valor da sessão através de chamadas get para ativar o bloqueio (e até que não funcionará com servidores com balanceamento de carga). Dependendo de como você usa o estado da sessão, há potencial para condições de corrida aqui. Além disso, parece-me que existem bloqueios na sua implementação que, na verdade, não fazem nada porque eles apenas envolvem uma única chamada de leitura ou gravação (corrija-me se estiver errado aqui).
agora.
11

A menos que seu aplicativo tenha necessidades especiais, acho que você tem 2 abordagens:

  1. Não use sessão nenhuma
  2. Use a sessão como está e execute o ajuste fino conforme mencionado em Joel.

A sessão não é apenas segura para threads, mas também segura para o estado, de maneira que você saiba que até que a solicitação atual seja concluída, todas as variáveis ​​de sessão não serão alteradas em relação a outra solicitação ativa. Para que isso aconteça, você deve garantir que a sessão SERÁ BLOQUEADA até que a solicitação atual seja concluída.

Você pode criar uma sessão como um comportamento de várias maneiras, mas se ela não bloquear a sessão atual, ela não será 'session'.

Para os problemas específicos que você mencionou, acho que você deve verificar HttpContext.Current.Response.IsClientConnected . Isso pode ser útil para evitar execuções e esperas desnecessárias no cliente, embora não possa resolver esse problema completamente, pois isso pode ser usado apenas por uma maneira de agrupamento e não por assinaturas.

George Mavritsakis
fonte
10

Se você estiver usando o atualizado Microsoft.Web.RedisSessionStateProvider(a partir de 3.0.2), poderá adicioná-lo ao seu web.configpara permitir sessões simultâneas.

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

Fonte

rabz100
fonte
Não sei por que isso foi em 0. +1. Muito útil.
Pangamma
Isso funciona no pool de aplicativos no modo clássico? github.com/Azure/aspnet-redis-providers/issues/123
Rusty
isso funciona com o provedor de serviços de estado padrão inProc ou Session?
Nick Chan Abdullah
Observe que o pôster faz referência se você estiver usando o RedisSessionStateprovider, mas também pode funcionar com esses provedores AspNetSessionState Async mais recentes (para SQL e Cosmos) porque também está na documentação deles: github.com/aspnet/AspNetSessionState Meu palpite é que ele funcionaria no classicmode se o SessionStateProvider já funcionar no modo clássico, provavelmente o material do estado da sessão acontece dentro do ASP.Net (não do IIS). Com o InProc, ele pode não funcionar, mas seria menos problemático, pois resolve um problema de contenção de recursos que é um problema maior com cenários fora do processo.
madamission
4

Para o ASPNET MVC, fizemos o seguinte:

  1. Por padrão, defina SessionStateBehavior.ReadOnlytodas as ações do controlador substituindoDefaultControllerFactory
  2. Nas ações do controlador que precisam gravar no estado da sessão, marque com atributo para configurá-lo como SessionStateBehavior.Required

Crie ControllerFactory personalizado e substitua 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
{ }

Conecte a fábrica de controladores criada em global.asax.cs

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

Agora, podemos ter os dois read-onlye read-writeo estado da sessão em um único 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);
    }
}

Nota: O estado da sessão do ASPNET ainda pode ser gravado no modo somente leitura e não gera nenhuma forma de exceção (ele simplesmente não trava para garantir a consistência); portanto, precisamos ter cuidado para marcar AcquireSessionLockas ações do controlador que exigem a gravação do estado da sessão.

Misterhex
fonte
3

Marcar o estado da sessão de um controlador como somente leitura ou desativado resolverá o problema.

Você pode decorar um controlador com o seguinte atributo para marcá-lo como somente leitura:

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

a enumeração System.Web.SessionState.SessionStateBehavior possui os seguintes valores:

  • Padrão
  • Desativado
  • Somente leitura
  • Requeridos
Michael King
fonte
0

Apenas para ajudar alguém com esse problema (bloqueio de solicitações ao executar outro da mesma sessão) ...

Hoje comecei a resolver esse problema e, após algumas horas de pesquisa, resolvi-o removendo o Session_Startmétodo (mesmo que vazio) do arquivo Global.asax .

Isso funciona em todos os projetos que testei.

graduenz
fonte
IDK em que tipo de projeto estava, mas o meu não possui um Session_Startmétodo e ainda bloqueia
Denis G. Labrecque
0

Depois de lutar com todas as opções disponíveis, acabei escrevendo um provedor SessionStore baseado em token JWT (a sessão viaja dentro de um cookie e não é necessário armazenamento de back-end).

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

Vantagens:

  • Substituição imediata, não são necessárias alterações no seu código
  • Dimensione melhor do que qualquer outro armazenamento centralizado, pois não é necessário nenhum back-end de armazenamento de sessão.
  • Mais rápido que qualquer outro armazenamento de sessão, pois nenhum dado precisa ser recuperado de nenhum armazenamento de sessão
  • Não consome recursos do servidor para armazenamento da sessão.
  • Implementação sem bloqueio padrão: a solicitação simultânea não se bloqueia e mantém um bloqueio na sessão
  • Dimensione horizontalmente seu aplicativo: como os dados da sessão viajam com a própria solicitação, você pode ter várias cabeças da Web sem se preocupar com o compartilhamento de sessões.
David
fonte