Nas DMVs, você pode dizer se uma conexão usou ApplicationIntent = ReadOnly?

23

Eu tenho um grupo de disponibilidade AlwaysOn configurado e quero garantir que meus usuários estejam usando ApplicationIntent = ReadOnly em suas cadeias de conexão.

No SQL Server via DMVs (ou Eventos estendidos ou o que for), posso saber se um usuário conectado a ApplicationIntent = ReadOnly em sua cadeia de conexão?

Por favor, não responda como impedir conexões - não é sobre isso que se trata. Não posso simplesmente interromper as conexões, porque temos aplicativos existentes que estão se conectando sem a string correta e preciso saber quais são eles para poder trabalhar com os desenvolvedores e usuários para corrigi-los gradualmente ao longo do tempo.

Suponha que os usuários tenham vários aplicativos. Por exemplo, Bob se conecta ao SQL Server Management Studio e ao Excel. Ele se conecta ao SSMS quando precisa fazer atualizações e ao Excel quando precisa fazer leituras. Eu preciso ter certeza de que ele está usando ApplicationIntent = ReadOnly quando ele se conectar ao Excel. (Esse não é o cenário exato, mas é próximo o suficiente para ilustrar.)

Brent Ozar
fonte
Acho que somente leitura é decidida no tempo de roteamento do TDS. Uma vez roteado para um secundário legível, as informações não são mais necessárias e, provavelmente, não entram no mecanismo.
Remus Rusanu
2
"o roteamento somente leitura primeiro se conecta ao primário e depois procura o melhor secundário legível disponível" , parece que o secundário o veria como uma conexão comum. Se houver algum XEvent acionado, ele estaria no primário. Não sei do que estou falando, mas estou especulando.
Remus Rusanu
1
@RemusRusanu você está falando sqlserver.read_only_route_complete, pois é acionado apenas no primário.
Kin Shah
@Kin lá vai você, exatamente como eu tinha código-lo;)
Remus Rusanu
2
@RemusRusanu Eu estava brincando com ele e acho que é o mais próximo possível das dicas - o URL somente leitura está configurado corretamente e não há problemas de conectividade. Nos dois casos, esse evento será bem-sucedido.
Kin Shah

Respostas:

10

Captar o sqlserver.read_only_route_completeevento estendido mencionado por Kin e Remus, é um evento agradável de depuração , mas não carrega muita informação - apenas route_port(por exemplo, 1433) e route_server_name(por exemplo, sqlserver-0.contoso.com) por padrão . Isso também ajudaria a determinar quando uma conexão de intenção somente leitura foi bem-sucedida. Há um read_only_route_failevento, mas eu não consegui acioná-lo, talvez se houvesse um problema com o URL de roteamento, ele não parecia acionar quando a instância secundária estava indisponível / desligada, tanto quanto eu sabia.

No entanto, tive algum sucesso ao associar isso ao sqlserver.loginrastreamento de eventos e causalidade ativado, juntamente com algumas ações (como sqlserver.username) para torná-lo útil.

Passos para reproduzir

Crie uma sessão de Eventos estendidos para rastrear eventos relevantes, além de ações úteis e rastrear causalidade:

CREATE EVENT SESSION [xe_watchLoginIntent] ON SERVER 
ADD EVENT sqlserver.login
    ( ACTION ( sqlserver.username ) ),
ADD EVENT sqlserver.read_only_route_complete
    ( ACTION ( 
        sqlserver.client_app_name,
        sqlserver.client_connection_id,
        sqlserver.client_hostname,
        sqlserver.client_pid,
        sqlserver.context_info,
        sqlserver.database_id,
        sqlserver.database_name,
        sqlserver.username 
        ) ),
ADD EVENT sqlserver.read_only_route_fail
    ( ACTION ( 
        sqlserver.client_app_name,
        sqlserver.client_connection_id,
        sqlserver.client_hostname,
        sqlserver.client_pid,
        sqlserver.context_info,
        sqlserver.database_id,
        sqlserver.database_name,
        sqlserver.username 
        ) )
ADD TARGET package0.event_file( SET filename = N'xe_watchLoginIntent' )
WITH ( 
    MAX_MEMORY = 4096 KB, 
    EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS, 
    MAX_DISPATCH_LATENCY = 30 SECONDS,
    MAX_EVENT_SIZE = 0 KB, 
    MEMORY_PARTITION_MODE = NONE, 
    TRACK_CAUSALITY = ON,   --<-- relate events
    STARTUP_STATE = ON      --<-- ensure sessions starts after failover
)

Execute a sessão XE (considere a amostragem, pois este é um evento de Depuração) e colete alguns logins:

conexões sqlcmd

Observe aqui que o sqlserver-0 é meu secundário legível e o sqlserver-1 o principal. Aqui, estou usando a -Kopção de sqlcmdpara simular logons de intenção de aplicativo somente leitura e alguns logons SQL. O evento somente leitura é acionado em um login de intenção somente leitura bem-sucedido.

Ao pausar ou interromper a sessão, posso consultá-la e tentar vincular os dois eventos, por exemplo:

DROP TABLE IF EXISTS #tmp

SELECT IDENTITY( INT, 1, 1 ) rowId, file_offset, CAST( event_data AS XML ) AS event_data
INTO #tmp
FROM sys.fn_xe_file_target_read_file( 'xe_watchLoginIntent*.xel', NULL, NULL, NULL )

ALTER TABLE #tmp ADD PRIMARY KEY ( rowId );
CREATE PRIMARY XML INDEX _pxmlidx_tmp ON #tmp ( event_data );


-- Pair up the login and read_only_route_complete events via xxx
DROP TABLE IF EXISTS #users

SELECT
    rowId,
    event_data.value('(event/@timestamp)[1]', 'DATETIME2' ) AS [timestamp],
    event_data.value('(event/action[@name="username"]/value/text())[1]', 'VARCHAR(100)' ) AS username,
    event_data.value('(event/action[@name="attach_activity_id_xfer"]/value/text())[1]', 'VARCHAR(100)' ) AS attach_activity_id_xfer,
    event_data.value('(event/action[@name="attach_activity_id"]/value/text())[1]', 'VARCHAR(100)' ) AS attach_activity_id
INTO #users
FROM #tmp l
WHERE l.event_data.exist('event[@name="login"]') = 1
  AND l.event_data.exist('(event/action[@name="username"]/value/text())[. = "SqlUserShouldBeReadOnly"]') = 1


DROP TABLE IF EXISTS #readonly

SELECT *,
    event_data.value('(event/@timestamp)[1]', 'DATETIME2' ) AS [timestamp],
    event_data.value('(event/data[@name="route_port"]/value/text())[1]', 'INT' ) AS route_port,
    event_data.value('(event/data[@name="route_server_name"]/value/text())[1]', 'VARCHAR(100)' ) AS route_server_name,
    event_data.value('(event/action[@name="username"]/value/text())[1]', 'VARCHAR(100)' ) AS username,
    event_data.value('(event/action[@name="client_app_name"]/value/text())[1]', 'VARCHAR(100)' ) AS client_app_name,
    event_data.value('(event/action[@name="attach_activity_id_xfer"]/value/text())[1]', 'VARCHAR(100)' ) AS attach_activity_id_xfer,
    event_data.value('(event/action[@name="attach_activity_id"]/value/text())[1]', 'VARCHAR(100)' ) AS attach_activity_id
INTO #readonly
FROM #tmp
WHERE event_data.exist('event[@name="read_only_route_complete"]') = 1


SELECT *
FROM #users u
    LEFT JOIN #readonly r ON u.attach_activity_id_xfer = r.attach_activity_id_xfer

SELECT u.username, COUNT(*) AS logins, COUNT( DISTINCT r.rowId ) AS records
FROM #users u
    LEFT JOIN #readonly r ON u.attach_activity_id_xfer = r.attach_activity_id_xfer
GROUP BY u.username

A consulta deve mostrar os logins com e sem a intenção somente leitura do aplicativo:

Resultados da consulta

  • read_only_route_completeé um evento de depuração, portanto use com moderação. Considere amostragem, por exemplo.
  • os dois eventos, juntamente com a causalidade da faixa, oferecem o potencial de atender às suas necessidades - são necessários mais testes neste equipamento simples
  • Notei que se o nome do banco de dados não estivesse especificado na conexão, as coisas não pareciam funcionar
  • Tentei fazer o pair_matchingalvo funcionar, mas o tempo acabou. Há algum potencial de desenvolvimento aqui, algo como:

    ALTER EVENT SESSION [xe_watchLoginIntent] ON SERVER
    ADD TARGET package0.pair_matching ( 
        SET begin_event = N'sqlserver.login',
            begin_matching_actions = N'sqlserver.username',
            end_event = N'sqlserver.read_only_route_complete',
            end_matching_actions = N'sqlserver.username'
        )
wBob
fonte
5

Não, não parece haver nenhuma propriedade de conexão exposta ao DMV (em sys.dm_exec_connections ou sys.dm_exec_sessions ) ou mesmo CONNECTIONPROPERTY que esteja relacionado aoApplicationIntent palavra-chave ConnectionString.

No entanto, pode valer a pena solicitar, via Microsoft Connect, que essa propriedade seja adicionada à sys.dm_exec_connectionsDMV, pois parece ser uma propriedade da conexão armazenada em algum lugar na memória do SQL Server, com base nas seguintes informações encontradas na página MSDN para Suporte do SqlClient para alta disponibilidade e recuperação de desastres (ênfase em itálico):

Especificando a intenção do aplicativo

Quando ApplicationIntent = ReadOnly , o cliente solicita uma carga de trabalho de leitura ao se conectar a um banco de dados ativado pelo AlwaysOn. O servidor aplicará a intenção no momento da conexão e durante uma instrução de banco de dados USE, mas apenas para um banco de dados ativado sempre.

Se uma USEinstrução puder ser verificada, é ApplicationIntentnecessário que exista além da tentativa de conexão inicial. No entanto, eu não verifiquei pessoalmente esse comportamento.


PS Eu estava pensando que poderíamos fazer uso dos fatos que:

  • uma réplica primária pode ser configurada para impedir o acesso ReadOnly a um ou mais bancos de dados e
  • a "intenção" será aplicada quando uma USEinstrução for executada.

A idéia era criar um novo banco de dados apenas com o objetivo de testar e rastrear essa configuração. O novo banco de dados seria usado em um novo grupo de disponibilidade que seria definido para permitir apenas READ_WRITEconexões. A teoria era que dentro de um gatilho de logon, um EXEC(N'USE [ReadWriteOnly]; INSERT INTO LogTable...;');dentro de uma TRY...CATCHconstrução, com essencialmente nada no CATCHbloco, não produziria nenhum erro para conexões ReadWrite (que se registrariam no novo banco de dados) ou o USEerro nas conexões ReadOnly, mas então nada aconteceria desde que o erro foi detectado e desconsiderado (e a INSERTdeclaração nunca seria alcançada). Nos dois casos, o evento de logon real não seria impedido / negado. O código do acionador de logon seria efetivamente:

BEGIN TRY
    EXEC(N'
        USE [ApplicationIntentTracking];
        INSERT INTO dbo.ReadWriteLog (column_list)
          SELECT sess.some_columns, conn.other_columns
          FROM   sys.dm_exec_connections conn
          INNER JOIN sys.dm_exec_sessions sess
                  ON sess.[session_id] = conn.[session_id]
          WHERE   conn.[session_id] = @@SPID;
        ');
END TRY
BEGIN CATCH
    DECLARE @DoNothing INT;
END CATCH;

Infelizmente, ao testar o efeito da emissão de uma USEdeclaração dentro de uma EXEC()dentro de um TRY...CATCHinterior de uma Transação, descobri que a violação de acesso foi um abort-nível de lote, e não um abort-nível de instrução. E configuração XACT_ABORT OFFnão mudou nada. Até criei um procedimento armazenado SQLCLR simples para usar Context Connection = true;e depois chamei SqlConnection.ChangeDatabase()dentro de a try...catche a transação ainda foi abortada. E você não pode usar Enlist=falsena conexão de contexto. E usar uma conexão regular / externa no SQLCLR para sair da Transação não ajudaria, pois seria uma Conexão totalmente nova.

Há uma possibilidade muito, muito pequena, de que o HAS_DBACCESS possa ser usado no lugar da USEinstrução, mas eu realmente não tenho grandes esperanças de que seja capaz de incorporar as informações atuais da Conexão em suas verificações. Mas também não tenho como testá-lo.

Obviamente, se houver um sinalizador de rastreamento que possa causar a violação de acesso sem interrupção de lote, o plano mencionado acima deverá funcionar ;-).

Solomon Rutzky
fonte
Infelizmente, não posso negá-los - as outras réplicas legíveis podem estar inativas. Ainda preciso que as consultas de leitura funcionem no primário - só preciso saber quando elas estão acontecendo.
Brent Ozar
@BrentOzar Atualizei minha resposta para incluir uma nova Etapa 3 que verificará essa condição e, se não houver Secundários disponíveis, ela permitirá a Conexão. Além disso, se a intenção é ainda apenas "saber quando thy're acontecendo", então a mesma configuração pode ser usado, basta alterar o ROLLBACKno Acesso de gatilho para um INSERTem uma tabela log :-)
Solomon Rutzky
1
Esta é uma ótima resposta, mas não é para esta pergunta. Não preciso parar os usuários, preciso monitorar quando está acontecendo. Temos aplicativos existentes que precisamos identificar e corrigir gradualmente. Se eu impedisse que os usuários fizessem login, isso causaria uma revolta imediata. Se você deseja criar uma pergunta separada para isso e postar sua resposta lá, seria ótimo - mas concentre sua resposta aqui na minha pergunta real. Obrigado.
Brent Ozar
@BrentOzar Desculpe, eu entendi mal o seu comentário a Tom como significando algo um pouco mais forte do que apenas rastrear / registrar. Eu removi a parte da minha resposta que tratava de impedir o acesso.
Solomon Rutzky
@BrentOzar Adicionei algumas notas abaixo da linha (na seção PS) que estavam perto de ser uma solução, mas foram frustradas no final. Publiquei essas anotações, caso isso acenda uma idéia em você (ou em outra pessoa) para apresentar a peça que está faltando, ou mesmo algo completamente diferente, que possa resolver esse quebra-cabeça.
Solomon Rutzky
2

Quão doente você quer estar? O fluxo TDS não é tão difícil de proxy, fizemos isso para nosso aplicativo SaaS. O bit que você está procurando (literalmente um pouco) está na mensagem de login7. Você pode fazer com que seus usuários se conectem por meio de um proxy e registrem / imponham o bit lá. Inferno, você pode até ligar para eles. :)

Walden Leverich
fonte
Definitivamente, é mais doente do que eu quero estar, mas obrigado, hahaha.
Brent Ozar
-1

Seu aplicativo usa uma conta de serviço ou talvez várias contas de serviço? Nesse caso, use o Evento estendido para monitorar o tráfego de login, mas exclua suas contas de serviço no servidor primário sempre ativo. Agora você deve poder ver quem está efetuando login no servidor primário sempre ativo e não está usando a cadeia de conexão secundária somente leitura. Estou me preparando para instalar o Always-On e é isso que vou fazer, a menos que você me diga que isso não funcionará.

ArmorDba
fonte
1
Tom - suponha que os usuários tenham vários aplicativos. Por exemplo, Bob se conecta ao SQL Server Management Studio e ao Excel. Ele se conecta ao SSMS quando precisa fazer atualizações e ao Excel quando precisa fazer leituras. Eu preciso ter certeza de que ele está usando ApplicationIntent = ReadOnly quando ele se conectar ao Excel. (Isso não é o cenário exato, mas é perto o suficiente para ilustrar.)
Brent Ozar
Também tenho pessoas conectando ao meu servidor de produção com o Excel com acesso muito limitado. Eles se conectam com seus direitos. Espero ser capaz de vê-los. Em breve, apresentaremos o nosso Always On.
ArmorDba
-1

Infelizmente, não tenho o ambiente para testar o seguinte, e há, sem dúvida, vários pontos nos quais ele pode falhar, mas eu o divulgarei pelo que vale a pena.

Um procedimento armazenado CLR tem acesso à conexão atual por meio da new SqlConnection("context connection=true")construção (obtida daqui ). O tipo SqlConnection expõe uma propriedade ConnectionString . Como ApplicationIntent está na cadeia de conexão inicial, suponho que ele esteja disponível nesta propriedade e possa ser analisado. É claro que existem muitas desvantagens nessa cadeia, portanto há muitas oportunidades para que tudo corra em forma de pêra.

Isso seria executado a partir de um gatilho de logon e os valores necessários persistiam conforme necessário.

Michael Green
fonte
1
Isso não funcionaria. O código SQLCLR não tem acesso à Conexão atual, ele tem acesso à Sessão atual através da Conexão de Contexto. O objeto SqlConnection no código .NET não está tocando na conexão real feita do software-cliente original no SQL Server. Essas são duas coisas separadas.
Solomon Rutzky
Oh bem, não importa então.
Michael Green
Não, isso não funciona.
Brent Ozar