ti-enxame.com

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

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

Caso as implicações disso sejam perdidas em você, como foi para mim no início, isso basicamente significa o seguinte:

  • A qualquer momento uma página ASP.Net está demorando muito para carregar (talvez devido a uma chamada de banco de dados lenta ou qualquer outra coisa), eo usuário decide que quer navegar para uma página diferente, porque eles estão cansados ​​de esperar, eles não podem! O bloqueio de sessão ASP.Net força a nova solicitação de página a aguardar até que a solicitação original tenha terminado sua carga dolorosamente lenta. Arrrgh.

  • Sempre que um UpdatePanel é carregado lentamente, e o usuário decide navegar para uma página diferente antes do UpdatePanel terminar de atualizar ... Eles não podem! O bloqueio de sessão do asp.net força a nova solicitação de página para aguardar até que a solicitação original tenha terminado sua carga dolorosamente lenta. Double Arrrgh!

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

  • Implemente um SessionStateDataStore personalizado, que o ASP.Net suporta. Eu não encontrei muitos por aí para copiar, e parece um tipo de alto risco e fácil de estragar.
  • Mantenha o controle de todas as solicitações em andamento e, se uma solicitação chegar do mesmo usuário, cancele a solicitação original. Parece meio que extremo, mas funcionaria (acho).
  • Não use a sessão! Quando eu preciso de algum tipo de estado para o usuário, eu poderia usar apenas Cache, e os 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 Microsoft ASP.Net teria deixado um gargalo de desempenho tão grande no quadro na versão 4.0! Estou faltando alguma coisa óbvia? Quão difícil seria usar uma coleção ThreadSafe para a sessão?

269
James

OK, tão grande Props para Joel Muller por toda sua contribuição. Minha melhor solução foi usar o Custom SessionStateModule detalhado no final deste artigo do MSDN:

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

Isso foi:

  • Muito rápido para implementar (na verdade parecia mais fácil do que ir a rota do provedor)
  • Usou muito da sessão ASP.Net padrão, pronta para uso (através da classe SessionStateUtility)

Isso fez uma enorme diferença para a sensação de "aborrecimento" para o nosso aplicativo. Eu ainda não consigo acreditar que a implementação personalizada do ASP.Net Session bloqueia a sessão para toda a solicitação. Isso adiciona uma quantidade tão grande de lentidão aos sites. A julgar pela quantidade de pesquisas on-line que tive que fazer (e conversas com vários desenvolvedores ASP.Net realmente experientes), muitas pessoas passaram por essa questão, mas muito poucas pessoas chegaram ao fundo da causa. Talvez eu escreva uma carta para Scott Gu ...

Espero que isso ajude algumas pessoas por aí!

82
James

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

<% @Page EnableSessionState="ReadOnly" %>

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

<% @Page EnableSessionState="False" %>

Se nenhuma das 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 thread-safe, se ele não usa bloqueios?

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

Eu sugiro que você tente minimizar a modificação de variáveis ​​de sessão, uma vez que elas tenham sido definidas, se possível. Isso permitiria que você criasse a maioria das páginas de sessão somente leitura, aumentando a chance de que várias solicitações simultâneas do mesmo usuário não se bloqueassem.

199
Joel Mueller

Comecei usando o AngiesList.Redis.RedisSessionStateModule , que além de usar o (muito rápido) servidor Redis para armazenamento (estou usando o porta do windows - embora também exista um porta MSOpenTech ), não há absolutamente nenhum bloqueio na sessão.

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

O MS que decide que todas as sessões do ASP.NET devem ser bloqueadas por padrão apenas para lidar com um projeto de aplicativos ruim é uma decisão ruim, na minha opinião. Especialmente porque parece que a maioria dos desenvolvedores não percebeu/nem sequer percebeu que as sessões foram bloqueadas, muito menos que os aplicativos aparentemente precisam ser estruturados para que você possa fazer o estado da sessão somente leitura o máximo possível (desativação, quando possível) .

31
gregmac

Eu preparei uma biblioteca baseada em links postados neste tópico. Ele usa os exemplos do MSDN e do CodeProject. Graças a James.

Também fiz modificações recomendadas 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 na produção. Também precisa melhorar o tratamento de erros. Exceções não são detectadas na implementação atual.

Alguns provedores de sessão sem bloqueio usando o Redis:

20
Der_Meister

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

  1. Não use sessão nada
  2. Use a sessão como está e faça um ajuste fino como joel mencionou.

A sessão não é apenas thread-safe mas também state-safe, de uma forma que você saiba que até que a solicitação atual seja concluída, toda variável de sessão não mudará de outra requisição ativa. Para que isso aconteça, você deve garantir que sessão será bloqueado até que a solicitação atual seja concluída.

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

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 ele não consiga resolver esse problema completamente, já que isso pode ser usado apenas por um modo de pooling e não por assíncrono.

11
George Mavritsakis

Se você estiver usando o Microsoft.Web.RedisSessionStateProvider atualizado (iniciando em 3.0.2), você poderá adicioná-lo ao seu web.config para permitir sessões simultâneas.

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

fonte

6
rabz100

Para o ASPNET MVC, fizemos o seguinte:

  1. Por padrão, defina SessionStateBehavior.ReadOnly na ação de todos os controladores, sobrescrevendo DefaultControllerFactory
  2. Em ações do controlador que precisam gravar no estado da sessão, marque com o atributo para defini-lo como SessionStateBehaviour.Required

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

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

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

Agora, podemos ter o estado da sessão read-only e read-write 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 ASPNET ainda pode ser gravado no modo somente leitura e não lançará nenhuma forma de exceção (apenas não bloqueia para garantir a consistência), portanto temos que ter cuidado para marcar AcquireSessionLock nas ações do controlador que exigem gravação do estado da sessão .

3
Misterhex

Marcar o estado de 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)]

the System.Web.SessionState.SessionStateBehavior enum tem os seguintes valores:

  • Default
  • Desativado
  • Somente leitura
  • Requeridos
2
Michael King

Apenas para ajudar alguém com este problema (bloquear pedidos ao executar outro da mesma sessão) ...

Hoje eu comecei a resolver este problema e, depois de algumas horas de pesquisa, eu resolvi isso removendo o método Session_Start (mesmo se vazio) do arquivo Global.asax.

Isso funciona em todos os projetos que testei.

0
graduenz