Pergunta

Estamos vendo algumas condições de impasse perniciosas, mas raras, no banco de dados Stack Overflow SQL Server 2005.

Anexei o profiler, configurei um perfil de rastreamento usando este excelente artigo sobre solução de impasses, e capturou vários exemplos.O estranho é que a gravação de impasse é sempre o mesmo:

UPDATE [dbo].[Posts]
SET [AnswerCount] = @p1, [LastActivityDate] = @p2, [LastActivityUserId] = @p3
WHERE [Id] = @p0

A outra declaração de impasse varia, mas geralmente é algum tipo de declaração trivial e simples. ler da tabela de postagens.Este sempre morre no impasse.Aqui está um exemplo

SELECT
[t0].[Id], [t0].[PostTypeId], [t0].[Score], [t0].[Views], [t0].[AnswerCount], 
[t0].[AcceptedAnswerId], [t0].[IsLocked], [t0].[IsLockedEdit], [t0].[ParentId], 
[t0].[CurrentRevisionId], [t0].[FirstRevisionId], [t0].[LockedReason],
[t0].[LastActivityDate], [t0].[LastActivityUserId]
FROM [dbo].[Posts] AS [t0]
WHERE [t0].[ParentId] = @p0

Para ser perfeitamente claro, não estamos vendo impasses de gravação/gravação, mas de leitura/gravação.

Temos uma mistura de consultas LINQ e SQL parametrizadas no momento.Nós adicionamos with (nolock) para todas as consultas SQL.Isso pode ter ajudado alguns.Também tivemos uma única consulta de crachá (muito) mal escrita que corrigi ontem, que levava mais de 20 segundos para ser executada todas as vezes e, além disso, estava sendo executada a cada minuto.Eu esperava que essa fosse a origem de alguns dos problemas de bloqueio!

Infelizmente, recebi outro erro de impasse há cerca de 2 horas.Exatos os mesmos sintomas, exatamente a mesma escrita do culpado.

O que é realmente estranho é que a instrução SQL de gravação de bloqueio que você vê acima faz parte de um caminho de código muito específico.Isso é apenas executado quando uma nova resposta é adicionada a uma pergunta - atualiza a pergunta pai com a nova contagem de respostas e a última data/usuário.Obviamente, isso não é tão comum em relação ao grande número de leituras que estamos fazendo!Pelo que sei, não estamos fazendo um grande número de gravações em nenhum lugar do aplicativo.

Sei que NOLOCK é uma espécie de martelo gigante, mas a maioria das consultas que executamos aqui não precisa ser tão precisa.Você se importará se seu perfil de usuário estiver alguns segundos desatualizado?

Usar NOLOCK com Linq é um pouco mais difícil, pois Scott Hanselman discute aqui.

Estamos flertando com a ideia de usar

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

no contexto do banco de dados base para que todas as nossas consultas LINQ tenham esse conjunto.Sem isso, teríamos que agrupar cada chamada LINQ que fazemos (bem, as de leitura simples, que é a grande maioria delas) em um bloco de código de transação de 3 a 4 linhas, o que é feio.

Acho que estou um pouco frustrado porque leituras triviais no SQL 2005 podem travar nas gravações.Eu pude ver que os impasses de gravação/gravação são um grande problema, mas lê? Não estamos administrando um site bancário aqui, não precisamos de precisão perfeita sempre.

Ideias?Pensamentos?


Você está instanciando um novo objeto LINQ to SQL DataContext para cada operação ou talvez esteja compartilhando o mesmo contexto estático para todas as suas chamadas?

Jeremy, estamos compartilhando na maior parte um contexto de dados estático no controlador base:

private DBContext _db;
/// <summary>
/// Gets the DataContext to be used by a Request's controllers.
/// </summary>
public DBContext DB
{
    get
    {
        if (_db == null)
        {
            _db = new DBContext() { SessionName = GetType().Name };
            //_db.ExecuteCommand("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED");
        }
        return _db;
    }
}

Você recomenda que criemos um novo contexto para cada controlador, ou por página, ou ..mais frequentemente?

Foi útil?

Solução

De acordo com o MSDN:

http://msdn.microsoft.com/en-us/library/ms191242.aspx

Quando as opções de banco de dados de isolamento de isolamento de instantâneos ou permitir que as opções de banco de dados estivessem ativadas, cópias lógicas (versões) são mantidas para todas as modificações de dados realizadas no banco de dados.Toda vez que uma linha é modificada por uma transação específica, a instância do mecanismo de banco de dados armazena uma versão da imagem comprometida anteriormente da linha no tempdb.Cada versão é marcada com o número da sequência de transações da transação que fez a alteração.As versões das linhas modificadas são encadeadas usando uma lista de links.O mais recente valor da linha é sempre armazenado no banco de dados atual e acorrentado às linhas de versão armazenadas no tempdb.

Para transações de curta duração, uma versão de uma linha modificada pode ser armazenada em cache no pool de buffers sem ser escrito nos arquivos de disco do banco de dados tempdb.Se a necessidade da linha versionada tiver vida curta, ela simplesmente será descartada do pool de buffer e poderá não incorrer necessariamente na sobrecarga de E/S.

Parece haver uma pequena penalidade de desempenho pela sobrecarga extra, mas pode ser insignificante.Devemos testar para ter certeza.

Tente definir esta opção e REMOVER todos os NOLOCKs das consultas de código, a menos que seja realmente necessário.NOLOCKs ou o uso de métodos globais no manipulador de contexto do banco de dados para combater os níveis de isolamento das transações do banco de dados são soluções para o problema.NOLOCKS mascarará problemas fundamentais com nossa camada de dados e possivelmente levará à seleção de dados não confiáveis, onde a seleção/atualização automática de versão de linha parece ser a solução.

ALTER Database [StackOverflow.Beta] SET READ_COMMITTED_SNAPSHOT ON

Outras dicas

NÃO FECHE e LEIA NÃO COMPROMETIDO são uma ladeira escorregadia.Você nunca deve usá-los, a menos que entenda primeiro por que o impasse está acontecendo.Eu ficaria preocupado se você dissesse: "Adicionamos with (nolock) a todas as consultas SQL".Precisando adicionar COM NOLOCK em todos os lugares é um sinal claro de que você tem problemas em sua camada de dados.

A declaração de atualização em si parece um pouco problemática.Você determina a contagem no início da transação ou apenas extrai-a de um objeto? AnswerCount = AnswerCount+1 quando uma pergunta é adicionada é provavelmente a melhor maneira de lidar com isso.Assim, você não precisa de uma transação para obter a contagem correta e não precisa se preocupar com o problema de simultaneidade ao qual está potencialmente se expondo.

Uma maneira fácil de contornar esse tipo de problema de impasse sem muito trabalho e sem permitir leituras sujas é usar "Snapshot Isolation Mode" (novo no SQL 2005) que sempre fornecerá uma leitura limpa dos últimos dados não modificados.Você também pode capturar e tentar novamente instruções em conflito com bastante facilidade se quiser tratá-las normalmente.

A questão do OP era perguntar por que esse problema ocorreu.Este post espera responder a isso, deixando possíveis soluções para serem elaboradas por outros.

Este é provavelmente um problema relacionado ao índice.Por exemplo, digamos que a tabela Posts tenha um índice não clusterizado X que contém o ParentID e um (ou mais) campo(s) sendo atualizado(s) (AnswerCount, LastActivityDate, LastActivityUserId).

Um deadlock ocorreria se o cmd SELECT fizesse um bloqueio de leitura compartilhada no índice X para pesquisar pelo ParentId e então precisasse fazer um bloqueio de leitura compartilhada no índice clusterizado para obter as colunas restantes enquanto o cmd UPDATE faz uma gravação exclusiva bloquear no índice clusterizado e precisar obter um bloqueio exclusivo de gravação no índice X para atualizá-lo.

Agora você tem uma situação em que A bloqueou X e está tentando obter Y, enquanto B bloqueou Y e está tentando obter X.

Claro, precisaremos que o OP atualize sua postagem com mais informações sobre quais índices estão em jogo para confirmar se essa é realmente a causa.

Estou bastante desconfortável com esta pergunta e o atendente responde.Tem muito “experimente esse pó mágico!Não, esse pó mágico!"

Não consigo ver em nenhum lugar que você tenha analisado os bloqueios capturados e determinado que tipo exato de bloqueios está bloqueado.

Tudo o que você indicou é que ocorrem alguns bloqueios - não o que é um impasse.

No SQL 2005 você pode obter mais informações sobre quais bloqueios estão sendo removidos usando:

DBCC TRACEON (1222, -1)

para que quando ocorrer o impasse você tenha melhores diagnósticos.

Você está instanciando um novo objeto LINQ to SQL DataContext para cada operação ou talvez esteja compartilhando o mesmo contexto estático para todas as suas chamadas?Originalmente, tentei a última abordagem e, pelo que me lembro, causou bloqueio indesejado no banco de dados.Agora crio um novo contexto para cada operação atômica.

Antes de incendiar a casa para pegar uma mosca com NOLOCK por toda parte, você pode dar uma olhada naquele gráfico de impasse que deveria ter capturado com o Profiler.

Lembre-se que um impasse requer (pelo menos) 2 bloqueios.A Conexão 1 possui o Bloqueio A, deseja o Bloqueio B - e vice-versa para a Conexão 2.Esta é uma situação insolúvel e alguém tem que ceder.

O que você mostrou até agora é resolvido com um bloqueio simples, que o Sql Server tem prazer em fazer o dia todo.

Eu suspeito que você (ou LINQ) está iniciando uma transação com essa instrução UPDATE e selecionando alguma outra informação de antemão.Mas, você realmente precisa voltar atrás no gráfico de impasse para encontrar os bloqueios mantido por cada thread e, em seguida, retroceda através do Profiler para encontrar as instruções que fizeram com que esses bloqueios fossem concedidos.

Espero que haja pelo menos 4 instruções para completar este quebra-cabeça (ou uma instrução que requer vários bloqueios - talvez haja um gatilho na tabela Postagens?).

Você se importará se seu perfil de usuário estiver alguns segundos desatualizado?

Não - isso é perfeitamente aceitável.Definir o nível básico de isolamento da transação é provavelmente a melhor/mais limpa maneira de fazer isso.

O impasse típico de leitura/gravação vem do acesso à ordem do índice.Read (T1) localiza a linha no índice A e, em seguida, procura a coluna projetada no índice B (geralmente agrupada).Write (T2) altera o índice B (o cluster) e então precisa atualizar o índice A.T1 tem S-Lck em A, quer S-Lck em B, T2 tem X-Lck em B, quer U-Lck em A.Impasse, puff.T1 é morto.Isso prevalece em ambientes com tráfego OLTP pesado e índices um pouco demais :).A solução é fazer com que a leitura não precise pular de A para B (ou seja,coluna incluída em A ou remover coluna da lista projetada) ou T2 não precisa pular de B para A (não atualizar coluna indexada).Infelizmente, o linq não é seu amigo aqui ...

@Jeff - Definitivamente não sou um especialista nisso, mas obtive bons resultados instanciando um novo contexto em quase todas as chamadas.Acho que é semelhante a criar um novo objeto Connection em cada chamada com ADO.A sobrecarga não é tão ruim quanto você imagina, já que o pool de conexões ainda será usado de qualquer maneira.

Eu apenas uso um auxiliar estático global como este:

public static class AppData
{
    /// <summary>
    /// Gets a new database context
    /// </summary>
    public static CoreDataContext DB
    {
        get
        {
            var dataContext = new CoreDataContext
            {
                DeferredLoadingEnabled = true
            };
            return dataContext;
        }
    }
}

e então eu faço algo assim:

var db = AppData.DB;

var results = from p in db.Posts where p.ID = id select p;

E eu faria a mesma coisa para atualizações.De qualquer forma, não tenho tanto tráfego quanto você, mas definitivamente estava conseguindo algum bloqueio quando usei um DataContext compartilhado no início com apenas alguns usuários.Não há garantias, mas pode valer a pena tentar.

Atualizar:Então, novamente, olhando para o seu código, você está compartilhando apenas o contexto de dados durante o tempo de vida daquela instância específica do controlador, o que basicamente parece bom, a menos que seja de alguma forma usado simultaneamente por várias chamadas dentro do controlador.Em um tópico sobre o assunto, ScottGu disse:

Os controladores vivem apenas para uma única solicitação - portanto, no final do processamento de uma solicitação, eles são coletados como lixo (o que significa que o DataContext é coletado) ...

De qualquer forma, pode não ser isso, mas, novamente, provavelmente vale a pena tentar, talvez em conjunto com alguns testes de carga.

Q.Por que você está armazenando o AnswerCount no Posts mesa em primeiro lugar?

Uma abordagem alternativa é eliminar o "write back" para o Posts tabela por não armazenar o AnswerCount na tabela, mas para calcular dinamicamente o número de respostas à postagem conforme necessário.

Sim, isso significa que você está executando uma consulta adicional:

SELECT COUNT(*) FROM Answers WHERE post_id = @id

ou mais normalmente (se você estiver exibindo isso na página inicial):

SELECT p.post_id, 
     p.<additional post fields>,
     a.AnswerCount
FROM Posts p
    INNER JOIN AnswersCount_view a
    ON <join criteria>
WHERE <home page criteria>

mas isso normalmente resulta em um INDEX SCAN e pode ser mais eficiente no uso de recursos do que usar READ ISOLATION.

Há mais de uma maneira de esfolar um gato.A desnormalização prematura de um esquema de banco de dados pode introduzir problemas de escalabilidade.

Você definitivamente deseja que READ_COMMITTED_SNAPSHOT esteja ativado, o que não é por padrão.Isso fornece a semântica do MVCC.É a mesma coisa que a Oracle usa por padrão.Ter um banco de dados MVCC é incrivelmente útil, NÃO usá-lo é uma loucura.Isso permite que você execute o seguinte dentro de uma transação:

Atualizar USUÁRIOS Set FirstName = 'foobar';//decide dormir por um ano.

enquanto isso, sem cometer o que foi dito acima, todos podem continuar selecionando nessa tabela perfeitamente.Se você não estiver familiarizado com o MVCC, ficará chocado ao saber que algum dia conseguiu viver sem ele.Seriamente.

Definir seu padrão para leitura não confirmada não é uma boa ideia.Sem dúvida, você introduzirá inconsistências e acabará com um problema pior do que o que você tem agora.O isolamento de instantâneo pode funcionar bem, mas é uma mudança drástica na forma como o Sql Server funciona e coloca um enorme carregar em tempdb.

Aqui está o que você deveria fazer:use try-catch (em T-SQL) para detectar a condição de deadlock.Quando isso acontecer, basta executar novamente a consulta.Esta é uma prática padrão de programação de banco de dados.

Existem bons exemplos desta técnica no livro de Paul Nielson Bíblia SQL Server 2005.

Aqui está um modelo rápido que eu uso:

-- Deadlock retry template

declare @lastError int;
declare @numErrors int;

set @numErrors = 0;

LockTimeoutRetry:

begin try;

-- The query goes here

return; -- this is the normal end of the procedure

end try begin catch
    set @lastError=@@error
    if @lastError = 1222 or @lastError = 1205 -- Lock timeout or deadlock
    begin;
        if @numErrors >= 3 -- We hit the retry limit
        begin;
            raiserror('Could not get a lock after 3 attempts', 16, 1);
            return -100;
        end;

        -- Wait and then try the transaction again
        waitfor delay '00:00:00.25';
        set @numErrors = @numErrors + 1;
        goto LockTimeoutRetry;

    end;

    -- Some other error occurred
    declare @errorMessage nvarchar(4000), @errorSeverity int
    select    @errorMessage = error_message(),
            @errorSeverity = error_severity()

    raiserror(@errorMessage, @errorSeverity, 1)

    return -100
end catch;    

Uma coisa que funcionou para mim no passado é garantir que todas as minhas consultas e atualizações acessem recursos (tabelas) na mesma ordem.

Ou seja, se uma consulta for atualizada na ordem Tabela1, Tabela2 e uma consulta diferente a atualizar na ordem Tabela2, Tabela1, você poderá ver conflitos.

Não tenho certeza se é possível alterar a ordem das atualizações, já que você está usando o LINQ.Mas é algo para se olhar.

Você se importará se seu perfil de usuário estiver alguns segundos desatualizado?

Alguns segundos seriam definitivamente aceitáveis.De qualquer forma, não parece que demoraria tanto, a menos que um grande número de pessoas envie respostas ao mesmo tempo.

Eu concordo com Jeremy neste ponto.Você pergunta se deve criar um novo contexto de dados para cada controlador ou por página - costumo criar um novo para cada consulta independente.

No momento, estou construindo uma solução que costumava implementar o contexto estático como você, e quando lancei toneladas de solicitações na fera de um servidor (mais de um milhão) durante os testes de estresse, também recebi bloqueios de leitura/gravação aleatoriamente.

Assim que mudei minha estratégia para usar um contexto de dados diferente no nível LINQ por consulta e confiei que o SQL Server poderia fazer sua mágica de pool de conexões, os bloqueios pareceram desaparecer.

É claro que eu estava sob alguma pressão de tempo, então tentei várias coisas ao mesmo tempo, então não posso ter 100% de certeza de que foi isso que resolveu o problema, mas tenho um alto nível de confiança - vamos colocar dessa forma .

Você deve implementar leituras sujas.

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

Se você não precisa absolutamente de integridade transacional perfeita com suas consultas, você deve usar leituras sujas ao acessar tabelas com alta simultaneidade.Presumo que sua tabela Posts seja uma dessas.

Isso pode fornecer as chamadas "leituras fantasmas", que ocorrem quando sua consulta atua sobre dados de uma transação que não foi confirmada.

Não estamos administrando um site bancário aqui, não precisamos de precisão perfeita sempre

Use leituras sujas.Você está certo ao dizer que eles não fornecerão uma precisão perfeita, mas devem resolver seus problemas de bloqueio morto.

Sem isso, teríamos que agrupar cada chamada LINQ que fazemos (bem, as de leitura simples, que é a grande maioria delas) em um bloco de código de transação de 3 a 4 linhas, o que é feio

Se você implementar leituras sujas no "contexto do banco de dados base", poderá sempre agrupar suas chamadas individuais usando um nível de isolamento mais alto se precisar da integridade transacional.

Então, qual é o problema em implementar um mecanismo de nova tentativa?Sempre haverá a possibilidade de ocorrer um impasse, então por que não ter alguma lógica para identificá-lo e tentar novamente?

Pelo menos algumas das outras opções não introduzirão penalidades de desempenho que são aplicadas o tempo todo, quando um sistema de nova tentativa raramente entra em ação?

Além disso, não se esqueça de algum tipo de registro quando ocorrer uma nova tentativa, para que você não entre nessa situação de raro tornar-se frequente.

Agora que vi a resposta de Jeremy, lembro-me de ter ouvido que a prática recomendada é usar um novo DataContext para cada operação de dados.Rob Conery escreveu vários posts sobre DataContext e sempre os atualiza em vez de usar um singleton.

Aqui está o padrão que usamos para Video.Show (link para visualização de origem no CodePlex):

using System.Configuration;
namespace VideoShow.Data
{
  public class DataContextFactory
  {
    public static VideoShowDataContext DataContext()
    {
        return new VideoShowDataContext(ConfigurationManager.ConnectionStrings["VideoShowConnectionString"].ConnectionString);
    }
    public static VideoShowDataContext DataContext(string connectionString)
    {
        return new VideoShowDataContext(connectionString);
    }
  }
}

Depois, no nível de serviço (ou ainda mais granular, para atualizações):

private VideoShowDataContext dataContext = DataContextFactory.DataContext();

public VideoSearchResult GetVideos(int pageSize, int pageNumber, string sortType)
{
  var videos =
  from video in DataContext.Videos
  where video.StatusId == (int)VideoServices.VideoStatus.Complete
  orderby video.DatePublished descending
  select video;
  return GetSearchResult(videos, pageSize, pageNumber);
}

Eu teria que concordar com Greg, desde que definir o nível de isolamento para leitura não confirmada não tenha nenhum efeito negativo em outras consultas.

Eu estaria interessado em saber, Jeff, como defini-lo no nível do banco de dados afetaria uma consulta como a seguinte:

Begin Tran
Insert into Table (Columns) Values (Values)
Select Max(ID) From Table
Commit Tran

Para mim, tudo bem se meu perfil estiver alguns minutos desatualizado.

Você está tentando novamente a leitura depois que ela falha?Certamente é possível, ao disparar uma tonelada de leituras aleatórias, que algumas acertem quando não conseguem ler.A maioria dos aplicativos com os quais trabalho são muito poucas gravações em comparação com o número de leituras e tenho certeza de que as leituras não estão nem perto do número que você está obtendo.

Se a implementação de "READ UNCOMMITTED" não resolver o seu problema, será difícil ajudar sem saber muito mais sobre o processamento.Pode haver alguma outra opção de ajuste que ajudaria nesse comportamento.A menos que algum guru do MSSQL venha em socorro, recomendo enviar o problema ao fornecedor.

Eu continuaria afinando tudo;como está o desempenho do subsistema de disco?Qual é o comprimento médio da fila de disco?Se as E/S estiverem fazendo backup, o problema real pode não ser essas duas consultas que estão travando, pode ser outra consulta que está causando gargalos no sistema;você mencionou uma consulta que leva 20 segundos e foi ajustada, existem outras?

Concentre-se em encurtar as consultas de longa duração, aposto que os problemas de impasse desaparecerão.

Teve o mesmo problema e não pode usar o "IsolationLevel = IsolationLevel.ReadUncommitted" no TransactionScope porque o servidor não tem DTS habilitado (!).

Foi o que fiz com um método de extensão:

public static void SetNoLock(this MyDataContext myDS)
{
    myDS.ExecuteCommand("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED");
}

Então, para selects que utilizam tabelas de simultaneidade crítica, habilitamos o "nolock" assim:

using (MyDataContext myDS = new MyDataContext())
{
   myDS.SetNoLock();

   //  var query = from ...my dirty querys here...
}

Sugestões são bem-vindas!

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top