À partir des DMV, pouvez-vous dire si une connexion a utilisé ApplicationIntent = ReadOnly?

23

J'ai un groupe de disponibilité Always On configuré et je veux m'assurer que mes utilisateurs utilisent ApplicationIntent = ReadOnly dans leurs chaînes de connexion.

À partir de SQL Server via DMV (ou événements étendus ou autre), puis-je savoir si un utilisateur connecté avec ApplicationIntent = ReadOnly dans sa chaîne de connexion?

Veuillez ne pas répondre à la façon de PRÉVENIR les connexions - ce n'est pas le sujet de cette question. Je ne peux pas simplement arrêter les connexions, car nous avons des applications existantes qui se connectent sans la bonne chaîne, et j'ai besoin de savoir lesquelles elles sont afin que je puisse travailler avec les développeurs et les utilisateurs pour les réparer progressivement au fil du temps.

Supposons que les utilisateurs ont plusieurs applications. Par exemple, Bob se connecte à SQL Server Management Studio et à Excel. Il se connecte à SSMS lorsqu'il doit effectuer des mises à jour et à Excel lorsqu'il doit effectuer des lectures. Je dois m'assurer qu'il utilise ApplicationIntent = ReadOnly lorsqu'il se connecte à Excel. (Ce n'est pas le scénario exact, mais il est assez proche pour illustrer.)

Brent Ozar
la source
Je pense que la lecture seule est décidée au moment du routage TDS. Une fois routé vers un secondaire lisible, les informations ne sont plus nécessaires, donc elles ne parviennent probablement pas dans le moteur.
Remus Rusanu
2
"le routage en lecture seule se connecte d'abord au primaire puis cherche le meilleur secondaire lisible disponible" il semble que le secondaire le verrait comme une connexion ordinaire. Si un événement XEvent est déclenché, ce sera sur le serveur principal. Je ne sais pas de quoi je parle, mais je spécule.
Remus Rusanu
1
@RemusRusanu parlez-vous sqlserver.read_only_route_completecar il est déclenché uniquement sur le primaire.
Kin Shah
@Kin là vous allez, exactement comme je l'aurais codé;)
Remus Rusanu
2
@RemusRusanu Je jouais avec et je suppose que c'est le plus proche que vous pouvez obtenir avec gotchas - l'URL en lecture seule est configurée correctement et il n'y a aucun problème de connectivité. Dans les deux cas, cet événement réussira.
Kin Shah

Réponses:

10

Reprenant l' sqlserver.read_only_route_completeévénement prolongé mentionné par Kin et Remus, il est un bon débogage événement, mais il ne porte pas beaucoup d'informations avec - juste route_port(par exemple 1433) et route_server_name(par exemple sqlserver-0.contoso.com) par défaut . Cela aiderait également à déterminer quand une connexion intentionnelle en lecture seule a réussi. Il y a un read_only_route_failévénement mais je n'ai pas pu le déclencher, peut-être qu'en cas de problème avec l'URL de routage, il ne semblait pas se déclencher lorsque l'instance secondaire n'était pas disponible / arrêtée pour autant que je sache.

J'ai cependant eu un certain succès en joignant cela à l' sqlserver.loginévénement et au suivi de la causalité activé, ainsi que certaines actions (comme sqlserver.username) pour le rendre utile.

Étapes à reproduire

Créez une session d'événements étendus pour suivre les événements pertinents, ainsi que des actions utiles et suivre le lien de 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
)

Exécutez la session XE (pensez à l'échantillonnage car il s'agit d'un événement de débogage) et collectez quelques connexions:

connexions sqlcmd

Notez ici que sqlserver-0 est mon secondaire lisible et sqlserver-1 le principal. Ici, j'utilise le -Kcommutateur de sqlcmdpour simuler les connexions d'intention d'application en lecture seule et certaines connexions SQL. L'événement en lecture seule se déclenche sur une connexion d'intention en lecture seule réussie.

En mettant en pause ou en arrêtant la session, je peux l'interroger et tenter de lier les deux événements, par exemple:

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 requête doit afficher les connexions avec et sans intention de lecture seule de l'application:

Résultats de la requête

  • read_only_route_completeest un événement de débogage, utilisez-le avec parcimonie. Prenons l'exemple de l'échantillonnage.
  • les deux événements ainsi que le lien de causalité offrent la possibilité de répondre à vos besoins - des tests supplémentaires sont nécessaires sur cette plate-forme simple
  • J'ai remarqué que si le nom de la base de données n'était pas spécifié dans la connexion, les choses ne semblaient pas fonctionner
  • J'ai essayé de faire fonctionner la pair_matchingcible mais j'ai manqué de temps. Il y a un potentiel de développement ici, quelque chose comme:

    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
la source
5

Non, il ne semble pas y avoir de propriété de connexion exposée DMV (soit dans sys.dm_exec_connections ou sys.dm_exec_sessions ) ni même CONNECTIONPROPERTY qui se rapporte au ApplicationIntentmot clé ConnectionString.

Cependant, il peut être utile de demander, via Microsoft Connect, que cette propriété soit ajoutée au sys.dm_exec_connectionsDMV car elle semble être une propriété de la connexion qui est stockée quelque part dans la mémoire de SQL Server, en fonction des informations suivantes trouvées dans la page MSDN pour Prise en charge de SqlClient pour la haute disponibilité, la reprise après sinistre (accentuation en italique):

Spécification de l'intention de l'application

Lorsque ApplicationIntent = ReadOnly , le client demande une charge de travail de lecture lors de la connexion à une base de données activée AlwaysOn. Le serveur appliquera l'intention au moment de la connexion et pendant une instruction de base de données USE, mais uniquement à une base de données toujours activée.

Si une USEdéclaration peut être vérifiée, alors les ApplicationIntentbesoins doivent exister au-delà de la tentative de connexion initiale. Cependant, je n'ai pas personnellement vérifié ce comportement.


PS J'avais pensé que nous pourrions utiliser les faits qui:

  • un réplica principal peut être défini pour interdire l'accès en lecture seule à une ou plusieurs bases de données, et
  • "l'intention" sera appliquée lors de l'exécution d'une USEinstruction.

L'idée était de créer une nouvelle base de données uniquement dans le but de tester et de suivre ce paramètre. La nouvelle base de données serait utilisée dans un nouveau groupe de disponibilité qui serait défini pour autoriser uniquement les READ_WRITEconnexions. La théorie était qu'à l'intérieur d'un déclencheur d'ouverture de session, un EXEC(N'USE [ReadWriteOnly]; INSERT INTO LogTable...;');dans une TRY...CATCHconstruction, avec essentiellement rien dans le CATCHbloc, ne produirait aucune erreur pour les connexions ReadWrite (qui se connecteraient dans la nouvelle base de données), ou l' USEerreur sur les connexions ReadOnly, mais alors rien ne se passerait puisque l'erreur est détectée et ignorée (et la INSERTdéclaration ne serait jamais atteinte). Dans les deux cas, l'événement de connexion réel ne sera pas empêché / refusé. Le code d'ouverture de session serait effectivement:

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;

Malheureusement, lors du test de l'effet de l'émission d'une USEinstruction dans un EXEC()dans un TRY...CATCHintérieur d'une transaction, j'ai constaté que la violation d'accès était un abandon au niveau du lot, pas un abandon au niveau de l'instruction. Et le réglage XACT_ABORT OFFn'a rien changé. J'ai même créé une procédure stockée SQLCLR simple à utiliser Context Connection = true;, puis appelée SqlConnection.ChangeDatabase()dans un try...catchet la transaction était toujours abandonnée. Et vous ne pouvez pas utiliser Enlist=falsela connexion contextuelle. Et l'utilisation d'une connexion régulière / externe dans SQLCLR pour sortir de la transaction n'aiderait pas car ce serait une toute nouvelle connexion.

Il y a une possibilité très, très mince que HAS_DBACCESS puisse être utilisé à la place de la USEdéclaration, mais je n'ai vraiment pas grand espoir qu'il puisse incorporer les informations de connexion actuelles dans ses vérifications. Mais je n'ai aucun moyen de le tester non plus.

Bien sûr, s'il existe un indicateur de trace qui peut empêcher la violation d'accès d'être annulée par lots, le plan mentionné ci-dessus devrait fonctionner ;-).

Solomon Rutzky
la source
Malheureusement, je ne peux pas les nier - les autres répliques lisibles pourraient être en panne. J'ai toujours besoin des requêtes de lecture pour travailler sur le primaire - je dois juste savoir quand elles se produisent.
Brent Ozar
@BrentOzar J'ai mis à jour ma réponse pour inclure une nouvelle étape 3 qui vérifiera cette condition et s'il n'y a pas de Secondaires disponibles, alors il permettra la connexion. De plus, si l'intention est toujours de "savoir quand ça se passe", alors la même configuration peut être utilisée, il suffit de changer le ROLLBACKdéclencheur de connexion en un INSERTdans une table de log :-)
Solomon Rutzky
1
c'est une excellente réponse, mais ce n'est pas pour cette question. Je n'ai pas besoin d'arrêter les utilisateurs, je dois surveiller quand cela se produit. Nous avons des applications existantes que nous devons identifier et corriger progressivement. Si j'arrêtais les utilisateurs de se connecter, cela provoquerait une révolte immédiate. Si vous souhaitez créer une question distincte pour cela et y poster votre réponse, ce serait bien - mais veuillez concentrer votre réponse ici sur ma vraie question. Merci.
Brent Ozar
@BrentOzar Désolé, j'ai mal compris votre commentaire à Tom comme signifiant quelque chose d'un peu plus fort que le simple suivi / enregistrement. J'ai supprimé la partie de ma réponse qui portait sur la prévention de l'accès.
Solomon Rutzky
@BrentOzar J'ai ajouté quelques notes sous la ligne (dans la section PS) qui étaient presque une solution, mais contrecarrées à la toute fin. J'ai posté ces notes au cas où cela déclencherait une idée en vous (ou quelqu'un d'autre) pour trouver la pièce manquante, ou même quelque chose de complètement différent, qui pourrait résoudre ce puzzle.
Solomon Rutzky
2

Comment voulez-vous être malade? Le flux TDS n'est pas si difficile à proxy, nous l'avons fait pour notre application SaaS. Le bit que vous recherchez (littéralement un peu) se trouve dans le message login7. Vous pouvez demander à vos utilisateurs de se connecter via un proxy et d'enregistrer / appliquer le bit à cet endroit. Enfer, vous pouvez même l'activer pour eux. :)

Walden Leverich
la source
C'est définitivement plus malade que je ne veux l'être, mais merci, hahaha.
Brent Ozar
-1

Votre application utilise-t-elle un compte de service ou peut-être plusieurs comptes de service? Si c'est le cas, utilisez Extended Event pour surveiller votre trafic de connexion, mais excluez vos comptes de service sur votre serveur principal permanent. Vous devriez maintenant pouvoir voir qui se connecte au serveur principal permanent et n'utilise pas la chaîne de connexion secondaire en lecture seule. Je me prépare à installer Always-On et c'est ce que je vais faire, sauf si vous me dites que cela ne fonctionnera pas.

ArmorDba
la source
1
Tom - supposons que les utilisateurs ont plusieurs applications. Par exemple, Bob se connecte à SQL Server Management Studio et à Excel. Il se connecte à SSMS lorsqu'il doit effectuer des mises à jour et à Excel lorsqu'il doit effectuer des lectures. Je dois m'assurer qu'il utilise ApplicationIntent = ReadOnly lorsqu'il se connecte à Excel. (Ce n'est pas le scénario exact, mais il est assez proche pour l'illustrer.)
Brent Ozar
J'ai également des gens qui se connectent à mon serveur de production avec Excel avec un accès très limité. Ils se connectent avec leurs droits. J'espère que je pourrai les voir. Nous proposerons notre Always On sous peu.
ArmorDba
-1

Malheureusement, je n'ai pas l'environnement pour tester les éléments suivants, et il y a sans aucun doute plusieurs points où cela peut échouer, mais je vais le jeter là-bas pour ce qu'il vaut.

Une procédure stockée CLR a accès à la connexion actuelle via la new SqlConnection("context connection=true")construction (prise à partir d' ici ). Le type SqlConnection expose une propriété ConnectionString . Depuis ApplicationIntent est dans la chaîne de connexion initiale, je suppose qu'il sera disponible dans cette propriété et peut être analysé. Il y a beaucoup de transferts dans cette chaîne, bien sûr, donc de nombreuses opportunités pour que tout cela prenne la forme d'une poire.

Cela s'exécuterait à partir d'un déclencheur d'ouverture de session et les valeurs requises persisteraient selon les besoins.

Michael Green
la source
1
Ça ne marcherait pas. Le code SQLCLR n'a pas accès à la connexion actuelle, il a accès à la session en cours via la connexion contextuelle. L'objet SqlConnection dans le code .NET ne puise pas dans la connexion réelle établie à partir du logiciel client d'origine dans SQL Server. Ce sont deux choses distinctes.
Solomon Rutzky
Eh bien, tant pis alors.
Michael Green
Non, ça ne marche pas.
Brent Ozar