Excluir desempenho para dados LOB no SQL Server

16

Esta pergunta está relacionada a este tópico do fórum .

Executando o SQL Server 2008 Developer Edition em minha estação de trabalho e em um cluster de máquina virtual de dois nós do Enterprise Edition, onde me refiro ao "cluster alfa".

O tempo necessário para excluir linhas com uma coluna varbinária (máx) está diretamente relacionado ao comprimento dos dados nessa coluna. Isso pode parecer intuitivo no começo, mas após a investigação, entra em conflito com meu entendimento de como o SQL Server realmente exclui linhas em geral e lida com esse tipo de dados.

O problema decorre de um problema de tempo limite de exclusão (> 30 segundos) que estamos vendo em nosso aplicativo Web .NET, mas eu o simplifiquei para o propósito desta discussão.

Quando um registro é excluído, o SQL Server o marca como um fantasma a ser limpo por uma Tarefa de Limpeza de Fantasma posteriormente, depois que a transação é confirmada (consulte o blog de Paul Randal ). Em um teste para excluir três linhas com dados de 16 KB, 4 MB e 50 MB em uma coluna varbinária (máx), respectivamente, vejo isso acontecendo na página com a parte dos dados em linha, bem como na transação registro.

O que me parece estranho é que os bloqueios X são colocados em todas as páginas de dados LOB durante a exclusão e as páginas são desalocadas no PFS. Eu vejo isso no log de transações, bem como com sp_locke os resultados do dm_db_index_operational_statsDMV ( page_lock_count).

Isso cria um gargalo de E / S na minha estação de trabalho e em nosso cluster alfa, se essas páginas ainda não estiverem no cache do buffer. De fato, a page_io_latch_wait_in_msmesma DMV é praticamente toda a duração da exclusão e page_io_latch_wait_countcorresponde ao número de páginas bloqueadas. Para o arquivo de 50 MB na minha estação de trabalho, isso se traduz em mais de 3 segundos ao iniciar com um cache de buffer vazio ( checkpoint/ dbcc dropcleanbuffers), e não tenho dúvida de que seria mais demorado para fragmentação pesada e sob carga.

Tentei me certificar de que não estava apenas alocando espaço no cache, ocupando esse tempo. Li 2 GB de dados de outras linhas antes de executar a exclusão em vez do checkpointmétodo, que é mais do que o que é alocado no processo do SQL Server. Não tenho certeza se esse é um teste válido ou não, pois não sei como o SQL Server embaralha os dados. Eu supus que sempre empurraria o velho a favor do novo.

Além disso, ele nem modifica as páginas. Isso eu posso ver com dm_os_buffer_descriptors. As páginas são limpas após a exclusão, enquanto o número de páginas modificadas é menor que 20 para todas as três exclusões pequenas, médias e grandes. Também comparei a saída de DBCC PAGEpara uma amostra das páginas consultadas e não houve alterações (apenas o ALLOCATEDbit foi removido do PFS). Apenas os desaloca.

Para provar ainda mais que as pesquisas / desalocações da página estão causando o problema, tentei o mesmo teste usando uma coluna de fluxo de arquivos em vez de vanilla varbinary (max). As exclusões eram de tempo constante, independentemente do tamanho do LOB.

Então, primeiro minhas perguntas acadêmicas:

  1. Por que o SQL Server precisa procurar todas as páginas de dados LOB para bloquear X? Isso é apenas um detalhe de como os bloqueios são representados na memória (armazenados de alguma forma na página)? Isso faz com que o impacto de E / S dependa fortemente do tamanho dos dados, se não for completamente armazenado em cache.
  2. Por que o X bloqueia, apenas para desalocá-los? Não é suficiente bloquear apenas a folha de índice com a parte em linha, pois a desalocação não precisa modificar as páginas? Existe alguma outra maneira de obter os dados LOB contra os quais o bloqueio protege?
  3. Por que desalocar as páginas de antemão, já que já existe uma tarefa em segundo plano dedicada a esse tipo de trabalho?

E talvez mais importante, minha pergunta prática:

  • Existe alguma maneira de fazer exclusões operar de maneira diferente? Meu objetivo é que o tempo seja excluído independentemente do tamanho, semelhante ao fluxo de arquivos, onde qualquer limpeza ocorre em segundo plano após o fato. É uma coisa de configuração? Estou armazenando coisas estranhamente?

Aqui está como reproduzir o teste descrito (executado através da janela de consulta do SSMS):

CREATE TABLE [T] (
    [ID] [uniqueidentifier] NOT NULL PRIMARY KEY,
    [Data] [varbinary](max) NULL
)

DECLARE @SmallID uniqueidentifier
DECLARE @MediumID uniqueidentifier
DECLARE @LargeID uniqueidentifier

SELECT @SmallID = NEWID(), @MediumID = NEWID(), @LargeID = NEWID()
-- May want to keep these IDs somewhere so you can use them in the deletes without var declaration

INSERT INTO [T] VALUES (@SmallID, CAST(REPLICATE(CAST('a' AS varchar(max)), 16 * 1024) AS varbinary(max)))
INSERT INTO [T] VALUES (@MediumID, CAST(REPLICATE(CAST('a' AS varchar(max)), 4 * 1024 * 1024) AS varbinary(max)))
INSERT INTO [T] VALUES (@LargeID, CAST(REPLICATE(CAST('a' AS varchar(max)), 50 * 1024 * 1024) AS varbinary(max)))

-- Do this before test
CHECKPOINT
DBCC DROPCLEANBUFFERS
BEGIN TRAN

-- Do one of these deletes to measure results or profile
DELETE FROM [T] WHERE ID = @SmallID
DELETE FROM [T] WHERE ID = @MediumID
DELETE FROM [T] WHERE ID = @LargeID

-- Do this after test
ROLLBACK

Aqui estão alguns resultados da criação de perfil das exclusões na minha estação de trabalho:

| Tipo de coluna | Excluir tamanho | Duração (ms) | Lê | Escreve | CPU
-------------------------------------------------- ------------------
| VarBinary | 16 KB | 40 13 2 0
| VarBinary | 4 MB | 952 2318 2 0
| VarBinary | 50 MB | 2976 28594 1 | 62
-------------------------------------------------- ------------------
| FileStream | 16 KB | 1 | 12 1 | 0
| FileStream | 4 MB | 0 9 0 0
| FileStream | 50 MB | 1 | 9 0 0

Em vez disso, não podemos apenas usar o filtro de arquivos porque:

  1. Nossa distribuição de tamanho de dados não garante isso.
  2. Na prática, adicionamos dados em vários blocos e o fluxo de arquivos não suporta atualizações parciais. Nós precisaríamos projetar em torno disso.

Atualização 1

Testou uma teoria de que os dados estão sendo gravados no log de transações como parte da exclusão, e isso não parece ser o caso. Estou testando isso incorretamente? Ver abaixo.

SELECT MAX([Current LSN]) FROM fn_dblog(NULL, NULL)
--0000002f:000001d9:0001

BEGIN TRAN
DELETE FROM [T] WHERE ID = @ID

SELECT
    SUM(
        DATALENGTH([RowLog Contents 0]) +
        DATALENGTH([RowLog Contents 1]) +
        DATALENGTH([RowLog Contents 3]) +
        DATALENGTH([RowLog Contents 4])
    ) [RowLog Contents Total],
    SUM(
        DATALENGTH([Log Record])
    ) [Log Record Total]
FROM fn_dblog(NULL, NULL)
WHERE [Current LSN] > '0000002f:000001d9:0001'

Para um arquivo com mais de 5 MB, isso retornou 1651 | 171860.

Além disso, eu esperaria que as páginas estivessem sujas se os dados fossem gravados no log. Apenas as desalocações parecem estar registradas, o que corresponde ao que está sujo após a exclusão.

Atualização 2

Recebi uma resposta de Paul Randal. Ele afirmou que precisa ler todas as páginas para percorrer a árvore e encontrar quais páginas desalocar e afirmou que não há outra maneira de procurar quais páginas. Esta é uma meia resposta para 1 e 2 (embora não explique a necessidade de bloqueios em dados fora de linha, mas isso é pequeno).

A pergunta 3 ainda está aberta: por que desalocar as páginas com antecedência se já existe uma tarefa em segundo plano para limpar as exclusões?

E, claro, a questão mais importante: existe uma maneira de mitigar diretamente (ou seja, não contornar) esse comportamento de exclusão dependente do tamanho? Eu acho que esse seria um problema mais comum, a menos que realmente sejamos os únicos a armazenar e excluir linhas de 50 MB no SQL Server? Todo mundo lá fora resolve isso com algum tipo de trabalho de coleta de lixo?

Jeremy Rosenberg
fonte
Eu gostaria que houvesse uma solução melhor, mas não a encontrei. Tenho uma situação de registrar grandes volumes de linhas de tamanhos variados, até 1 MB +, e tenho um processo de "limpeza" para excluir registros antigos. Como as exclusões eram muito lentas, tive que dividi-la em duas etapas - primeiro remova as referências entre as tabelas (o que é muito rápido) e depois exclua as linhas órfãs. O trabalho de exclusão teve uma média de ~ 2,2 segundos / MB para excluir dados. Então, é claro, tive que reduzir a contenção, portanto, tenho um procedimento armazenado com "DELETE TOP (250)" dentro de um loop até que nenhuma linha seja excluída mais.
Abacus

Respostas:

5

Não sei dizer por que exatamente seria muito mais ineficiente excluir um VARBINARY (MAX) em comparação com o fluxo de arquivos, mas uma idéia que você pode considerar se estiver apenas tentando evitar o tempo limite do aplicativo da web ao excluir esses LOBS. Você pode armazenar os valores VARBINARY (MAX) em uma tabela separada (vamos chamá-lo de tblLOB) referenciado pela tabela original (vamos chamar isso de tblParent).

A partir daqui, quando você exclui um registro, é possível excluí-lo do registro pai e, em seguida, ter um processo ocasional de coleta de lixo para limpar e limpar os registros na tabela LOB. Pode haver atividade adicional no disco rígido durante esse processo de coleta de lixo, mas será pelo menos separado da Web do front-end e poderá ser executado fora dos horários de pico.

Ian Chamberland
fonte
Obrigado. Essa é exatamente uma das nossas opções no quadro. A tabela é um sistema de arquivos e, atualmente, estamos separando os dados binários em um banco de dados completamente separado da meta hierarquia. Podemos fazer o que você disse e excluir a linha da hierarquia e fazer com que um processo de GC limpe as linhas LOB órfãs. Ou tenha um carimbo de data / hora de exclusão com os dados para atingir o mesmo objetivo. Este é o caminho que podemos seguir se não houver uma resposta satisfatória para o problema.
Jeremy Rosenberg
11
Gostaria de ter apenas um carimbo de hora para indicar que ele foi excluído. Isso funcionará, mas você terá muito espaço ocupado nas linhas ativas. Você precisará ter algum tipo de processo de gc em algum momento, dependendo de quanto é excluído, e será menos impactante excluir menos regularmente, em vez de lotes ocasionalmente.
Ian Chamberland