Decifrando Key e Page WaitResource em deadlocks e bloqueios

Se você utiliza o relatório de bloqueios (blocked process report) ou coleta informações de deadlocks fornecidas pelo SQL Server, ocasionalmente, você encontrará coisas como estas:

waitresource="PAGE: 6:3:70133"

waitresource="KEY: 6:72057594041991168 (ce52f92a058c)"

Às vezes, naquele imenso XML que você está analisando, haverá mais informações (as informações de deadlocks contêm uma lista de recursos que ajudam a identificar nomes de objeto e índice), mas nem sempre.

Este texto ajudará a decifrá-las.

Toda a informação contida aqui está disponível na internet em vários lugares, mas está bastante dispersa! Quero reunir tudo — do DBCC PAGE ao hobt_id e às funções não documentadas %%physloc%% e %%lockres%%.

Primeiro, vamos falar sobre esperas em bloqueios de PAGE e depois vamos abordar bloqueios de KEY.

1) waitresource="PAGE: 6:3:70133" = Database_Id: FileId: PageNumber

Se sua consulta está aguardando um bloqueio de PAGE, o SQL Server fornecerá o endereço dessa página.

Decompondo "PAGE: 6:3:70133" obtemos:

  • database_id = 6
  • data_file_id = 3
  • page_number = 70133

1.1) Decifrando database_id

Vamos encontrar o nome do banco de dados usando a consulta:

SELECT 
    name 
FROM sys.databases 
WHERE database_id=6;
GO

Este é o público BD WideWorldImporters no meu SQL Server.

1.2) Procurando o nome do arquivo de dados — se você estiver interessado

Vamos usar o data_file_id na próxima etapa para encontrar o nome da tabela. Você pode pular para a próxima etapa, mas se você está interessado no nome do arquivo, pode encontrá-lo executando a consulta no contexto do banco de dados encontrado, substituindo data_file_id nesta consulta:

USE WideWorldImporters;
GO
SELECT 
    name, 
    physical_name
FROM sys.database_files
WHERE file_id = 3;
GO

No BD WideWorldImporters, este é o arquivo chamado WWI_UserData e ele está recuperado no meu C:MSSQLDATAWideWorldImporters_UserData.ndf. (Ops, você me pegou colocando arquivos no disco do sistema! Não! Que situação desconfortável).

1.3) Obtenção do nome do objeto a partir do DBCC PAGE

Agora sabemos que a página #70133 no arquivo de dados 3 pertence ao BD WorldWideImporters. Podemos examinar o conteúdo dessa página usando o DBCC PAGE não documentado e a trace flag 3604.
Nota: prefiro usar o DBCC PAGE em uma cópia restaurada de um backup em outro servidor, porque isso é uma coisa não documentada. Em alguns casos, ela pode levar à criação de um dump (nota do tradutor — o link, infelizmente, leva a lugar nenhum, mas pelo que parece no url, trata-se de índices filtrados.).

/* This trace flag makes DBCC PAGE output go to our Messages tab
instead of the SQL Server Error Log file */
DBCC TRACEON (3604);
GO
/* DBCC PAGE (DatabaseName, FileNumber, PageNumber, DumpStyle)*/
DBCC PAGE ('WideWorldImporters',3,70133,2);
GO

Rolando para os resultados, pode-se encontrar object_id e index_id.
Decifrando Key e Page WaitResource em deadlocks e bloqueios
Quase pronto! Agora você pode encontrar os nomes da tabela e do índice usando a consulta:

USE WideWorldImporters;
GO
SELECT 
    sc.name as schema_name, 
    so.name as object_name, 
    si.name as index_name
FROM sys.objects as so 
JOIN sys.indexes as si on 
    so.object_id=si.object_id
JOIN sys.schemas AS sc on 
    so.schema_id=sc.schema_id
WHERE 
    so.object_id = 94623380
    and si.index_id = 1;
GO

E aqui vemos que a espera por bloqueio foi no índice PK_Sales_OrderLines da tabela Sales.OrderLines.

Nota: no SQL Server 2014 e versões posteriores, o nome do objeto também pode ser encontrado usando o DMO não documentado sys.dm_db_database_page_allocations. Mas você terá que consultar cada página no banco de dados, o que não parece nada legal para bancos de dados grandes, por isso eu usei DBCC PAGE.

1.4) Posso ver os dados naquela página que estava bloqueada?

Bem, sim. Mas… você tem certeza de que realmente precisa disso?
Isso é lento até mesmo em tabelas pequenas. Mas é legal, então, uma vez que você leu até aqui… vamos falar sobre %%physloc%%!

%%physloc%% é um pedaço de mágica não documentado que retorna o identificador físico para cada registro. Você pode usar %%physloc%% junto com sys.fn_PhysLocFormatter no SQL Server 2008 e versões superiores.

Agora que sabemos que queremos aplicar bloqueio na página em Sales.OrderLines, podemos ver todos os dados nesta tabela que estão armazenados no arquivo de dados #3 na página #70133, usando a seguinte consulta:

Use WideWorldImporters;
GO
SELECT 
    sys.fn_PhysLocFormatter (%%physloc%%),
    *
FROM Sales.OrderLines (NOLOCK)
WHERE sys.fn_PhysLocFormatter (%%physloc%%) like '(3:70133%'
GO

Como eu disse — isso é lento mesmo em tabelas minúsculas. Adicionei NOLOCK à consulta porque não temos garantia de que os dados que queremos olhar sejam exatamente os mesmos que estavam no momento em que o bloqueio foi detectado — então podemos fazer leituras sujas sem problemas.
Mas, uau, a consulta retorna exatamente aquelas 25 linhas pelas quais nossa consulta lutou.
Decifrando Key e Page WaitResource em deadlocks e bloqueios
Chega de bloqueios de PÁGINA. E se estivermos aguardando um bloqueio de CHAVE?

2) waitresource="KEY: 6:72057594041991168 (ce52f92a058c)" = Database_Id, HOBT_Id (um hash mágico que pode ser decifrado com %%lockres%%, se você realmente quiser)

Se sua consulta tentar aplicar um bloqueio a um registro no índice e acabar sendo bloqueada, você recebe um tipo completamente diferente de endereço.
Dividindo "6:72057594041991168 (ce52f92a058c)" em partes, obtemos:

  • database_id = 6
  • hobt_id = 72057594041991168
  • hash mágico = (ce52f92a058c)

2.1) Decifrando database_id

Isso funciona exatamente como no exemplo acima! Vamos encontrar o nome do BD com a consulta:

SELECT 
    name 
FROM sys.databases 
WHERE database_id=6;
GO

No meu caso, é o mesmo BD WideWorldImporters.

2.2) Decifrando hobt_id

No contexto do BD encontrado, precisamos executar uma consulta ao sys.partitions com um par de joins que ajudarão a determinar os nomes da tabela e do índice…

USE WideWorldImporters;
GO
SELECT 
    sc.name as schema_name, 
    so.name as object_name, 
    si.name as index_name
FROM sys.partitions AS p
JOIN sys.objects AS so ON 
    p.object_id=so.object_id
JOIN sys.indexes AS si ON 
    p.index_id=si.index_id AND 
    p.object_id=si.object_id
JOIN sys.schemas AS sc ON 
    so.schema_id=sc.schema_id
WHERE hobt_id = 72057594041991168;
GO

Ele me diz que a consulta estava aguardando no bloqueio Application.Countries, usando o índice PK_Application_Countries.

2.3) Agora um pouco de mágica %%lockres%% — se você quiser descobrir qual registro foi bloqueado

Se eu realmente quiser saber em qual linha o bloqueio foi necessário, posso descobrir isso com uma consulta à própria tabela. Podemos usar a função não documentada %%lockres%% para encontrar o registro que corresponde ao hash mágico.
Tenha em mente que esta consulta irá escanear toda a tabela, e em tabelas grandes isso pode não ser nada divertido:

SELECT
    *
FROM Application.Countries (NOLOCK)
WHERE %%lockres%% = '(ce52f92a058c)';
GO

Eu adicionei NOLOCK (a conselho de Klaus Aschenbrenner no Twitter) porque bloqueios podem se tornar um problema. Queremos apenas ver o que está lá agora, e não o que havia lá quando a transação começou — não acho que a consistência dos dados seja importante para nós.
Voilà, o registro pelo qual estávamos lutando!
Decifrando Key e Page WaitResource em deadlocks e bloqueios

Agradecimentos e leitura adicional

Não lembro quem primeiro descreveu muitas dessas coisas, mas aqui estão dois posts sobre as coisas menos documentadas que podem te interessar:

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster