Dai DMV, puoi sapere se una connessione ha utilizzato ApplicationIntent = ReadOnly?


23

Ho impostato un gruppo di disponibilità Always On e desidero assicurarmi che i miei utenti utilizzino ApplicationIntent = ReadOnly nelle loro stringhe di connessione.

Da SQL Server tramite DMV (o Eventi estesi o altro), posso sapere se un utente si è connesso con ApplicationIntent = ReadOnly nella sua stringa di connessione?

Si prega di non rispondere con come PREVENIRE le connessioni - non è di questo che si tratta. Non posso semplicemente interrompere le connessioni, perché abbiamo applicazioni esistenti che si connettono senza la stringa giusta e devo sapere quali sono in modo da poter lavorare con gli sviluppatori e gli utenti per risolverlo gradualmente nel tempo.

Supponiamo che gli utenti abbiano più applicazioni. Ad esempio, Bob si connette con SQL Server Management Studio e con Excel. Si connette con SSMS quando deve eseguire gli aggiornamenti e Excel quando deve eseguire le letture. Devo assicurarmi che stia usando ApplicationIntent = ReadOnly quando si connette con Excel. (Questo non è lo scenario esatto, ma è abbastanza vicino da illustrare.)


Penso che la sola lettura sia decisa al momento del routing TDS. Una volta che viene indirizzato a un secondario leggibile, le informazioni non sono più necessarie, quindi probabilmente non vengono inserite nel motore.
Remus Rusanu,

2
"il routing di sola lettura si collega prima al primario e quindi cerca il secondario leggibile migliore disponibile" sembra che il secondario lo veda come una normale connessione. Se c'è XEvent attivato, sarebbe sul primario. Non so di cosa sto parlando, ma sto speculando.
Remus Rusanu,

1
@RemusRusanu stai parlando sqlserver.read_only_route_completein quanto viene attivato solo sulla primaria.
Kin Shah,

@Kin ecco, esattamente come avrei voluto codificarlo;)
Remus Rusanu,

2
@RemusRusanu Ci stavo giocando e credo che sia il più vicino che puoi ottenere con i gotchas - l'URL di sola lettura è configurato correttamente e non ci sono problemi di connettività. In entrambi i casi, quell'evento avrà successo.
Kin Shah,

Risposte:


10

Raccogliendo l' sqlserver.read_only_route_completeEvento esteso menzionato da Kin e Remus, è un evento di debug piacevole , ma non contiene molte informazioni - solo route_port(ad esempio 1433) e route_server_name(ad esempio sqlserver-0.contoso.com) per impostazione predefinita . Ciò contribuirebbe anche solo a determinare quando una connessione di intento di sola lettura ha avuto esito positivo. C'è un read_only_route_failevento, ma non sono riuscito a attivarlo, forse se si è verificato un problema con l'URL di routing, non sembrava attivarsi quando l'istanza secondaria non era disponibile / spegnimento per quanto ne sapevo.

Ho comunque avuto un po 'di successo unendomi a questo con il sqlserver.logintracciamento dell'evento e della causalità abilitato, insieme ad alcune azioni (come sqlserver.username) per renderlo utile.

I passaggi per riprodurre

Crea una sessione Eventi estesi per tenere traccia degli eventi rilevanti, oltre ad azioni utili e tenere traccia della causalità:

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
)

Esegui la sessione XE (considera il campionamento poiché si tratta di un evento di debug) e raccogli alcuni accessi:

connessioni sqlcmd

Nota qui sqlserver-0 è il mio secondario leggibile e sqlserver-1 il primario. Qui sto usando l' -Kinterruttore di sqlcmdper simulare accessi intento applicazione di sola lettura e alcuni accessi SQL. L'evento di sola lettura si attiva con un accesso con intento di sola lettura riuscito.

Durante la pausa o l'interruzione della sessione, posso interrogarla e tentare di collegare i due eventi, ad esempio:

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

La query dovrebbe mostrare gli accessi con e senza intento di sola lettura dell'applicazione:

Risultati della query

  • read_only_route_completeè un evento di debug, quindi usalo con parsimonia. Prendi ad esempio il campionamento.
  • i due eventi insieme alla causalità traccia offrono il potenziale per soddisfare le vostre esigenze - ulteriori test necessari su questo semplice rig
  • Ho notato che se il nome del database non è stato specificato nella connessione, le cose non sembrano funzionare
  • Ho cercato di far funzionare l' pair_matchingobiettivo ma non ho più tempo. C'è un potenziale di sviluppo qui, qualcosa come:

    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'
        )

5

No, non sembra che ci sia alcuna proprietà di connessione esposta a DMV (o in sys.dm_exec_connections o sys.dm_exec_sessions ) o anche CONNECTIONPROPERTY che si riferisce alApplicationIntent parola chiave ConnectionString.

Tuttavia, potrebbe valere la pena richiedere, tramite Microsoft Connect, che questa proprietà venga aggiunta al sys.dm_exec_connectionsDMV in quanto sembra essere una proprietà della connessione archiviata da qualche parte nella memoria di SQL Server, in base alle seguenti informazioni trovate nella pagina MSDN per SqlClient Support for High Availability, Disaster Recovery (corsivo sottolineato mio):

Specifica dell'intento dell'applicazione

Quando ApplicationIntent = ReadOnly , il client richiede un carico di lavoro in lettura durante la connessione a un database abilitato AlwaysOn. Il server imporrà l'intento al momento della connessione e durante un'istruzione del database USE, ma solo a un database abilitato Always On.

Se è USEpossibile verificare un'istruzione, è ApplicationIntentnecessario che esista oltre il tentativo di connessione iniziale. Tuttavia, non ho verificato personalmente questo comportamento.


PS Stavo pensando che potremmo sfruttare i fatti che:

  • una replica primaria può essere impostata per impedire l'accesso ReadOnly a uno o più database e
  • l '"intento" verrà applicato quando USEviene eseguita un'istruzione.

L'idea era di creare un nuovo database al solo scopo di testare e tracciare questa impostazione. Il nuovo DB verrebbe utilizzato in un nuovo gruppo di disponibilità impostato per consentire solo le READ_WRITEconnessioni. La teoria era che all'interno di un Logon Trigger, un EXEC(N'USE [ReadWriteOnly]; INSERT INTO LogTable...;');all'interno di un TRY...CATCHcostrutto, praticamente senza nulla nel CATCHblocco, non avrebbe prodotto alcun errore per le connessioni ReadWrite (che si sarebbero registrati nel nuovo DB), oppure l' USEerrore sarebbe sulle connessioni ReadOnly, ma allora non accadrebbe nulla poiché l'errore viene colto e ignorato (e la INSERTdichiarazione non verrebbe mai raggiunta). In entrambi i casi, l'evento di accesso effettivo non verrebbe impedito / negato. Il codice del trigger di accesso sarebbe effettivamente:

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;

Purtroppo, quando si verifica l'effetto del rilascio di una USEdichiarazione all'interno di una EXEC()all'interno di un TRY...CATCHinterno di una transazione, ho scoperto che la violazione di accesso è stata un'interruzione a livello di gruppo, non un abort-livello di istruzione. E l'impostazione XACT_ABORT OFFnon ha cambiato nulla. Ho anche creato una semplice Stored procedure SQLCLR da usare Context Connection = true;e poi chiamata SqlConnection.ChangeDatabase()all'interno di un try...catche la transazione è stata ancora interrotta. E non è possibile utilizzare Enlist=falsesulla connessione di contesto. E l'utilizzo di una connessione regolare / esterna in SQLCLR per uscire dalla Transazione non sarebbe di aiuto in quanto sarebbe una connessione completamente nuova.

Esiste una possibilità molto, molto ridotta, che HAS_DBACCESS possa essere utilizzato al posto diUSE , ma non ho grandi speranze che sia in grado di incorporare le informazioni di connessione correnti nei suoi controlli. Ma non ho nemmeno modo di testarlo.

Naturalmente, se esiste un flag di traccia che può causare l'interruzione in batch della violazione di accesso, il piano sopra menzionato dovrebbe funzionare ;-).


Sfortunatamente, non posso negarli: le altre repliche leggibili potrebbero essere inattive. Ho ancora bisogno delle query di lettura per funzionare sul primario, ho solo bisogno di sapere quando stanno accadendo.
Brent Ozar,

@BrentOzar Ho aggiornato la mia risposta per includere un nuovo passaggio 3 che verificherà tale condizione e se non ci sono secondari disponibili, consentirà la connessione. Inoltre, se l'intento è ancora quello di "sapere quando stai accadendo", è possibile utilizzare la stessa configurazione, basta cambiare il ROLLBACKtrigger di accesso in un INSERTin una tabella di registro :-)
Solomon Rutzky

1
questa è un'ottima risposta, ma non è per questa domanda. Non ho bisogno di fermare gli utenti, devo monitorare quando sta accadendo. Abbiamo app esistenti che dobbiamo individuare e correggere gradualmente. Se interrompessi l'accesso degli utenti, provocherebbe una rivolta immediata. Se desideri creare una domanda separata per questo, e pubblicare la tua risposta lì, sarebbe fantastico - ma ti preghiamo di focalizzare la tua risposta qui sulla mia domanda reale. Grazie.
Brent Ozar,

@BrentOzar Siamo spiacenti, ho frainteso il tuo commento a Tom come qualcosa di un po 'più forte del semplice monitoraggio / registrazione. Ho rimosso la parte della mia risposta relativa alla prevenzione dell'accesso.
Solomon Rutzky,

@BrentOzar Ho aggiunto alcune note sotto la riga (nella sezione PS) che era vicino ad essere una soluzione, ma alla fine mi ha sventato. Ho pubblicato quelle note nel caso in cui ti venga in mente un'idea (o qualcun altro) di trovare il pezzo mancante, o anche qualcosa di completamente diverso, che potrebbe risolvere questo enigma.
Solomon Rutzky,

2

Quanto malato vuoi essere? Il flusso TDS non è così difficile da proxy, lo abbiamo fatto per la nostra app SaaS. Il bit che stai cercando (letteralmente un po ') è nel messaggio login7. Potresti avere i tuoi utenti connessi tramite un proxy e registrare / applicare il bit lì. Cavolo, potresti persino accenderlo per loro. :)


È decisamente più malato di quanto io voglia essere, ma grazie, hahaha.
Brent Ozar,

-1

L'applicazione utilizza un account di servizio o forse più account di servizio? In tal caso, utilizzare l'Evento esteso per monitorare il traffico di accesso ma escludere gli account del servizio sul server sempre attivo principale. Ora dovresti essere in grado di vedere chi accede al server sempre attivo primario e non utilizza la stringa di connessione secondaria di sola lettura. Mi sto preparando per installare Always-On e questo è quello che farò a meno che tu non mi dica che non funzionerà.


1
Tom: supponi che gli utenti abbiano più applicazioni. Ad esempio, Bob si connette con SQL Server Management Studio e con Excel. Si connette con SSMS quando deve eseguire gli aggiornamenti e Excel quando deve eseguire le letture. Devo assicurarmi che stia usando ApplicationIntent = ReadOnly quando si connette con Excel. (Questo non è lo scenario esatto, ma è abbastanza vicino da illustrare.)
Brent Ozar il

Ho anche persone che si collegano al mio server di produzione con Excel con accesso molto limitato. Si connettono con i loro diritti. Spero di poterli vedere. A breve porteremo il nostro Always On.
ArmorDba,

-1

Sfortunatamente non ho l'ambiente per testare quanto segue, e ci sono senza dubbio diversi punti in cui potrebbe fallire, ma lo lancerò fuori per quello che vale.

Una procedura memorizzata CLR ha accesso alla connessione corrente tramite il new SqlConnection("context connection=true")costrutto (preso da qui ). Il tipo SqlConnection espone un ConnectionString proprietà . Poiché ApplicationIntent si trova nella stringa di connessione iniziale, suppongo che sarà disponibile in questa proprietà e possa essere analizzato. Ovviamente ci sono molte ripercussioni in quella catena, quindi molte opportunità per farlo diventare a forma di pera.

Ciò verrebbe eseguito da un trigger di accesso e i valori richiesti persistessero come necessario.


1
Questo non funzionerebbe. Il codice SQLCLR non ha accesso alla connessione corrente, ha accesso alla sessione corrente tramite la connessione al contesto. L'oggetto SqlConnection nel codice .NET non sta toccando la connessione effettiva effettuata dal software client originale in SQL Server. Quelle sono due cose separate.
Solomon Rutzky,

Oh bene, non importa allora.
Michael Green,

No, questo non funziona.
Brent Ozar,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.