Eu tenho uma configuração de AG de 4 nós da seguinte forma:
Configuração de hardware de VM de todos os nós:
- Microsoft SQL Server 2017 Enterprise Edition (RTM-CU14) (KB4484710)
- 16 vCPUs
- 356 GB de RAM (longa história para este...)
- grau máximo de paralelismo: 1 (conforme exigido pelo fornecedor do aplicativo)
- limite de custo para paralelismo: 50
- memória máxima do servidor (MB): 338944 (331 GB)
Configuração AG:
- Nó 1: Secundário não legível de confirmação primária ou síncrona, configurado para failover automático
- Nó 2: Secundário não legível de confirmação primária ou síncrona, configurado para failover automático
- Nó 3: Conjunto secundário legível com confirmação assíncrona, configurado para failover manual
- Nó 4: Conjunto secundário legível com confirmação assíncrona, configurado para failover manual
A consulta em questão:
Não há nada de muito louco nessa consulta, ela fornece um resumo dos itens de trabalho pendentes em várias filas dentro do aplicativo. Você pode ver o código em um dos links do plano de execução abaixo.
Comportamento de execução no nó primário:
Quando executado no nó Primário, o tempo de execução é geralmente em torno da marca de 1 segundo. Aqui está o plano de execução e abaixo estão as estatísticas capturadas de STATISTICS IO e STATISTICS TIME do nó primário:
(347 rows affected)
Table 'Worktable'. Scan count 647, logical reads 2491, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'workitemlc'. Scan count 300, logical reads 7125, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'schedulertask'. Scan count 1, logical reads 29, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'wfschedulertask'. Scan count 1, logical reads 9, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'schedulerservice'. Scan count 1, logical reads 12, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'schedulerworkerpool'. Scan count 1, logical reads 3, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'itemlc'. Scan count 1, logical reads 26372, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
(1 row affected)
SQL Server Execution Times:
CPU time = 500 ms, elapsed time = 656 ms.
SQL Server parse and compile time:
CPU time = 0 ms, elapsed time = 0 ms.
Comportamento de execução no nó secundário somente leitura:
Ao executar em um nó secundário somente leitura (ou seja, nó 3 ou nó 4), esta consulta usa o mesmo plano de execução (este é um link de plano diferente) e aproximadamente as mesmas estatísticas de execução são mostradas (por exemplo, pode haver mais algumas páginas verificações, pois esses resultados estão sempre mudando), mas com exceção do tempo de CPU, eles são muito semelhantes. Aqui estão as estatísticas capturadas de STATISTICS IO e STATISTICS TIME do nó secundário somente leitura:
(347 rows affected)
Table 'Worktable'. Scan count 647, logical reads 2491, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'workitemlc'. Scan count 300, logical reads 7125, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'schedulertask'. Scan count 1, logical reads 29, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'wfschedulertask'. Scan count 1, logical reads 9, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'schedulerservice'. Scan count 1, logical reads 12, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'schedulerworkerpool'. Scan count 1, logical reads 3, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'itemlc'. Scan count 1, logical reads 26372, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
(1 row affected)
SQL Server Execution Times:
CPU time = 55719 ms, elapsed time = 56335 ms.
SQL Server parse and compile time:
CPU time = 0 ms, elapsed time = 0 ms.
Outros detalhes:
Também executei o script sp_WhoIsActive
de Paul RandalWaitingTasks.sql
no secundário enquanto essa consulta está sendo executada, mas não vejo nenhuma espera ocorrendo, o que é francamente frustrante:
Isso também não parece ser um caso de latência de AG, pois o status de sincronização é realmente muito bom:
--https://sqlperformance.com/2015/08/monitoring/availability-group-replica-sync
SELECT
ar.replica_server_name,
adc.database_name,
ag.name AS ag_name,
drs.is_local,
drs.synchronization_state_desc,
drs.synchronization_health_desc,
--drs.last_hardened_lsn,
--drs.last_hardened_time,
drs.last_redone_time,
drs.redo_queue_size,
drs.redo_rate,
(drs.redo_queue_size / drs.redo_rate) / 60.0 AS est_redo_completion_time_min,
drs.last_commit_lsn,
drs.last_commit_time
FROM sys.dm_hadr_database_replica_states AS drs
INNER JOIN sys.availability_databases_cluster AS adc
ON drs.group_id = adc.group_id AND
drs.group_database_id = adc.group_database_id
INNER JOIN sys.availability_groups AS ag
ON ag.group_id = drs.group_id
INNER JOIN sys.availability_replicas AS ar
ON drs.group_id = ar.group_id AND
drs.replica_id = ar.replica_id
ORDER BY
ag.name,
ar.replica_server_name,
adc.database_name;
Esta consulta parece ser o pior ofensor. Outras consultas que também levam menos de um segundo no nó primário podem levar de 1 a 5 segundos no nó secundário e, embora o comportamento não seja tão grave, parece estar causando problemas.
Por fim, também examinei os servidores e verifiquei processos externos, como varreduras de A/V, trabalhos externos gerando E/S inesperadas, etc. e cheguei de mãos vazias. Não acho que isso esteja sendo causado por algo fora do processo do SQL Server.
A questão:
É apenas meio-dia onde estou e já foi um longo dia, então suspeito que estou perdendo algo óbvio aqui. Ou isso ou temos algo mal configurado, o que é possível, pois tivemos várias chamadas para o fornecedor e MS relacionadas a esse ambiente.
Para toda a minha investigação, eu simplesmente não consigo encontrar o que está causando essa diferença no desempenho. Eu esperaria ver algum tipo de espera ocorrendo nos nós secundários, mas nada. Como posso solucionar ainda mais isso para identificar a causa raiz? Alguém já viu esse comportamento antes e encontrou uma maneira de resolvê-lo?
ATUALIZAÇÃO #1
Depois de trocar os estados do terceiro nó (uma das réplicas somente leitura) para não legível e depois de volta para legível como um teste, essa réplica ainda está sendo mantida por uma transação aberta, com todas as consultas do cliente exibindo o HADR_DATABASE_WAIT_FOR_TRANSITION_TO_VERSIONING
esperar.
A execução de um DBCC OPENTRAN
comando produz os seguintes resultados:
Oldest active transaction:
SPID (server process ID): 420s
UID (user ID) : -1
Name : QDS nested transaction
LSN : (941189:33148:8)
Start time : May 7 2019 12:54:06:753PM
SID : 0x0
DBCC execution completed. If DBCC printed error messages, contact your system administrator.
Ao pesquisar este SPID em sp_who2
, ele o mostra como um BACKGROUND
processo QUERY STORE BACK
listado como o comando.
Embora possamos fazer backups do TLog, suspeito que estamos enfrentando uma funcionalidade semelhante a esse bug resolvido , então pretendo abrir um tíquete com a MS sobre esse problema específico hoje.
Dependendo do resultado desse ticket, tentarei capturar um rastreamento de pilha de chamadas de acordo com a sugestão de Joe e ver para onde vamos.
Atualização final (problema resolvido automaticamente)
Depois de eclipsar a marca de 52 horas da transação do Query Store sendo aberta (conforme identificado acima), o AG decidiu fazer o failover automaticamente. Antes que isso acontecesse, eu puxei algumas métricas adicionais. Por este link , fornecido por Sean, o banco de dados em questão tinha um armazenamento de versão muito grande dedicado a este banco de dados, especificamente em um ponto eu havia registrado 1651360 páginas no reserved_page_count
campo e 13210880 para o reserved_space_kb
valor.
De acordo com os ERRORLOGs, o failover ocorreu após um dilúvio de 5 minutos de falhas de proteção de transações relacionadas a QDS base transaction
e QDS nested transaction
transações.
O failover causou uma interrupção de cerca de 10 minutos no meu caso. O banco de dados tem ~6 TB de tamanho e é muito ativo, então isso foi realmente muito bom na minha opinião. Enquanto o novo nó primário estava online durante esse período, nenhuma consulta de cliente pôde ser concluída, pois todos estavam aguardando o QDS_LOADDB
tipo de espera.
Após o failover, os números de armazenamento de versão foram reduzidos para 176 reserved_page_count
e 1408 para reserved_space_kb
. As consultas nas réplicas somente leitura secundárias também começaram a ser executadas tão rapidamente quanto se fossem executadas a partir do primário, portanto, parece que o comportamento desapareceu completamente, como resultado do failover.
Isenção de responsabilidade: não sei nada sobre grupos de disponibilidade, mas sei um pouco sobre como solucionar problemas de consultas que parecem usar mais CPU do que deveriam.
Você tem um problema de CPU em que você está usando muito dele. Uma coisa importante a dizer sobre esperas é que quase todas elas não estão ocupadas com a CPU. Quando um trabalhador entra em um estado de espera, ele cedeu e não está mais em execução no agendador no SQLOS. Portanto, se você tiver uma consulta MAXDOP 1 com as seguintes estatísticas de execução:
Você atingiu quase 99% de utilização da CPU para a consulta. Por que deve haver estatísticas de espera significativas para essa consulta? Você pode ver alguns se tiver algumas esperas ocupadas da CPU, como esperas externas ou preventivas, mas isso também não é garantido. A conclusão é que as estatísticas de espera podem não ser tão úteis aqui.
Há algumas coisas para verificar em ordem aproximada (a ordem depende do que você sabe sobre o meio ambiente):
A maioria dos itens acima é bem abordada em várias postagens de blog e documentação, mas vou expandir o rastreamento de ETW. Se você quiser saber por que o SQL Server está usando tanta CPU para uma consulta específica e tiver acesso ao host, sempre poderá fazer o rastreamento ETW para visualizar as pilhas de chamadas e ver quanta CPU várias pilhas de chamadas estão fazendo. Em outras palavras, o sistema operacional host fica feliz em dizer para que CPU está sendo usado, se você souber como perguntar. Os métodos comuns de rastreamento de ETW incluem o Windows Performance Recorder e o PerfView .
Dar sentido a esses resultados requer profundo conhecimento interno e é fácil chegar a uma conclusão errada. Em muitos casos, é melhor coletar os dados brutos e pedir a especialistas para examiná-los. Ao executar um rastreamento, você deseja que o mínimo de atividade possível ocorra no SQL Server. Abaixo estão algumas respostas postadas aqui que usam o rastreamento ETW para tirar conclusões sobre o SQL Server:
Suspeito que, no seu caso, se você conseguir coletar pilhas de chamadas enquanto a consulta de 45 segundos for executada, obterá algumas pistas muito úteis sobre a natureza do problema.
Esta resposta é uma adição à resposta de Joe, pois não posso ter 100% de certeza de que é o armazenamento de versões, no entanto, há evidências suficientes até agora para implicar que isso seja parte do problema.
Quando uma réplica secundária é marcada como legível, um bom estado estável para as informações de versão precisa ser obtido primeiro para que haja um ponto de partida conhecido e bom para todas as operações de leitura no secundário. Quando estiver aguardando a transição e ainda houver transações abertas no primário, isso se manifestará como
HADR_DATABASE_WAIT_FOR_TRANSITION_TO_VERSIONING
e também é um bom indicador de que o primário passa por uma grande quantidade de rotatividade de dados (ou pelo menos alguém tem uma transação aberta muito longa que também não é bom). Quanto mais tempo as transações estiverem abertas e quanto mais alterações de dados houver, mais controle de versão ocorrerá.As réplicas secundárias atingem o status legível usando o isolamento de instantâneo nos bastidores da sessão, mesmo que, se você verificar as informações da sessão, elas apareçam na leitura padrão confirmada. Como o isolamento de instantâneos é otimista e usa o armazenamento de versão, todas as alterações precisarão ser versionadas. Isso é agravado quando há muitas consultas em execução (e possivelmente de longa duração) no secundário enquanto a rotatividade de dados é alta no primário. Geralmente, isso se manifesta apenas em algumas tabelas para um sistema OLTP, mas é completamente dependente do aplicativo e da carga de trabalho.
O próprio armazenamento de versão é medido em gerações e quando uma consulta é executada que requer o uso do armazenamento de versão, o ponteiro de registro de versão é usado para apontar para a cadeia TempDB dessa linha. Eu digo chain, porque é uma lista de versões para aquela linha e toda a chain deve ser percorrida sequencialmente para encontrar a versão apropriada com base no timestamp inicial da transação para que os resultados estejam alinhados com os dados naquele momento.
Se o armazenamento de versão tiver muitas gerações para essas linhas devido a transações de longa execução nas réplicas primárias e secundárias, isso causará tempos mais longos do que a média para a execução das consultas e geralmente na forma de CPU mais alta, enquanto todos os outros itens aparentemente permanecem exatamente os mesmos - como plano de execução, estatísticas, linhas retornadas, etc. A caminhada da cadeia é quase uma operação puramente de CPU, portanto, quando as cadeias se tornam muito longas e a quantidade de linhas retornadas é muito alta, você obtém um (não linear, mas pode ser próximo) aumenta o tempo para a consulta.
A única coisa que pode ser feita é limitar o comprimento das transações no primário e no secundário para garantir que o armazenamento de versão não esteja se tornando muito grande no TempDB enquanto tem muitas gerações. As tentativas de limpar o armazenamento de versões acontecem aproximadamente uma vez por minuto, no entanto, a limpeza exige que todas as versões da mesma geração não sejam mais necessárias antes de serem removidas e todas as versões futuras não podem ser limpas até que a versão mais antiga não seja mais necessária. Assim, uma consulta de longa duração pode causar a incapacidade de limpar efetivamente muitas gerações não utilizadas.
Alternar a réplica para dentro e para fora do modo legível também limpará o armazenamento de versão, pois não é mais legível.
Existem outros itens que também podem estar em jogo, mas este parece ser o mais plausível, dados os dados atuais e a forma como a réplica estava reagindo.
DMVs de versionamento TempDB (não confundir com versionamento ADR).
Como o problema se resolveu, resta-me especular sua causa (rima não intencional). Com base na postagem de Sean e no fato de que uma transação aberta do Query Store parece ter sido a causa raiz do aumento do tamanho do meu armazenamento de versão (por exemplo, a causa das
HADR_DATABASE_WAIT_FOR_TRANSITION_TO_VERSIONING
esperas), só posso supor que o Query Store teve uma parte no comportamento que foi apresentado. Esse banco de dados é maior (~6 TB), bastante ativo e a maioria das consultas que o atingem são geradas no cliente e não parametrizadas (ou seja, consultas ad-hoc), então não acredito que o Query Store se preste a fornecer muito uso neste cenário. Por isso, desabilitaremos o Query Store neste ambiente durante uma futura janela de manutenção, após a qual suspeito que não veremos esse comportamento novamente.Abrimos um ticket com a Microsoft, mas o tempo não estava a nosso favor, pois o problema foi resolvido antes que pudéssemos fazer qualquer análise detalhada por meio de um rastreamento PSSDIAG ou algo semelhante. Espero que eles possam fazer alguns testes localizados e replicar esse problema, caso seja um bug que encontramos. Se forem identificadas outras atualizações em uma resolução mais permanente, também atualizarei esta resposta.