Desde los DMV, ¿puede decir si una conexión usó ApplicationIntent = ReadOnly?

23

Tengo configurado un grupo de disponibilidad Always On y quiero asegurarme de que mis usuarios estén usando ApplicationIntent = ReadOnly en sus cadenas de conexión.

Desde el servidor SQL a través de DMV (o eventos extendidos o lo que sea), ¿puedo saber si un usuario se conectó con ApplicationIntent = ReadOnly en su cadena de conexión?

No responda cómo PREVENIR las conexiones; de eso no se trata esta pregunta. No puedo simplemente detener las conexiones, porque tenemos aplicaciones existentes que se conectan sin la cadena correcta, y necesito saber cuáles son para poder trabajar con los desarrolladores y usuarios para solucionarlo gradualmente con el tiempo.

Suponga que los usuarios tienen múltiples aplicaciones. Por ejemplo, Bob se conecta con SQL Server Management Studio y con Excel. Se conecta con SSMS cuando necesita hacer actualizaciones, y Excel cuando necesita hacer lecturas. Necesito asegurarme de que esté usando ApplicationIntent = ReadOnly cuando se conecta con Excel. (Ese no es el escenario exacto, pero está lo suficientemente cerca como para ilustrarlo).

Brent Ozar
fuente
Creo que solo lectura se decide en el momento del enrutamiento TDS. Una vez que se enruta a un secundario legible, la información ya no es necesaria, por lo que probablemente no llegue al motor.
Remus Rusanu
2
"el enrutamiento de solo lectura primero se conecta al primario y luego busca el mejor secundario legible disponible" parece que el secundario lo vería como una conexión ordinaria. Si hay algún XEvent activado, sería en el primario. No sé de qué estoy hablando, pero estoy especulando.
Remus Rusanu
1
@RemusRusanu estás hablando sqlserver.read_only_route_completeya que se activa solo en primaria.
Kin Shah
@Kin allí tienes, exactamente como lo habría codificado;)
Remus Rusanu
2
@RemusRusanu Estaba jugando con él y supongo que es lo más cercano que se puede obtener con las trampas: la URL de solo lectura está configurada correctamente y no hay problemas de conectividad. En ambos casos, ese evento tendrá éxito.
Kin Shah

Respuestas:

10

Retomando el sqlserver.read_only_route_completeevento extendido mencionado por Kin y Remus, es un buen evento de depuración , pero no lleva mucha información, solo route_port(por ejemplo, 1433) y route_server_name(por ejemplo, sqlserver-0.contoso.com) por defecto . Esto también ayudaría a determinar cuándo una conexión de intención de solo lectura fue exitosa. Hay un read_only_route_failevento pero no pude activarlo, tal vez si hubiera un problema con la URL de enrutamiento, no parece que se active cuando la instancia secundaria no estaba disponible / apagado por lo que pude ver.

Sin embargo, he tenido cierto éxito al unir eso con el sqlserver.loginevento y el seguimiento de causalidad habilitado, junto con algunas acciones (como sqlserver.username) para que sea útil.

Pasos para reproducir

Cree una sesión de eventos extendidos para rastrear eventos relevantes, además de acciones útiles y rastrear causalidad:

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
)

Ejecute la sesión XE (considere el muestreo ya que este es un evento de depuración) y recopile algunos inicios de sesión:

conexiones sqlcmd

Tenga en cuenta que sqlserver-0 es mi secundaria legible y sqlserver-1 es la primaria. Aquí estoy usando el -Kinterruptor de sqlcmdpara simular inicios de sesión de intención de aplicación de solo lectura y algunos inicios de sesión SQL. El evento de solo lectura se dispara en un inicio de sesión exitoso de solo lectura.

Al pausar o detener la sesión, puedo consultarla e intentar vincular los dos eventos, por ejemplo:

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 consulta debe mostrar los inicios de sesión con y sin intención de solo lectura de la aplicación:

Resultados de consultas

  • read_only_route_completees un evento de depuración, así que utilícelo con moderación. Considere el muestreo, por ejemplo.
  • Los dos eventos, junto con la causalidad de la pista, ofrecen el potencial para cumplir con sus requisitos: se necesitan más pruebas en este sencillo equipo
  • Noté que si el nombre de la base de datos no se especificaba en la conexión, las cosas no parecían funcionar
  • Traté de conseguir que el pair_matchingobjetivo funcionara pero se me acabó el tiempo. Aquí hay un potencial de desarrollo, 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
fuente
5

No, no parece que haya ninguna propiedad de conexión expuesta al DMV (ya sea en sys.dm_exec_connections o sys.dm_exec_sessions ) o incluso CONNECTIONPROPERTY que se relaciona con la ApplicationIntentpalabra clave ConnectionString.

Sin embargo, podría valer la pena solicitar, a través de Microsoft Connect, que esta propiedad se agregue al sys.dm_exec_connectionsDMV, ya que parece ser una propiedad de la conexión que se almacena en algún lugar de la memoria de SQL Server, según la siguiente información que se encuentra en la página de MSDN para Soporte de SQL Client para alta disponibilidad, recuperación ante desastres (énfasis en cursiva mío):

Especificar la intención de la aplicación

Cuando ApplicationIntent = ReadOnly , el cliente solicita una carga de trabajo de lectura cuando se conecta a una base de datos habilitada para AlwaysOn. El servidor impondrá la intención en el momento de la conexión y durante una declaración de base de datos USE pero solo a una base de datos habilitada Always On.

Si USEse puede verificar una declaración, entonces ApplicationIntentdebe existir más allá del intento de conexión inicial. Sin embargo, no he verificado personalmente este comportamiento.


PD: había estado pensando que podríamos hacer uso de los hechos que:

  • Se puede configurar una réplica principal para no permitir el acceso de solo lectura a una o más bases de datos, y
  • la "intención" se aplicará cuando USEse ejecute una declaración.

La idea era crear una nueva base de datos con el único fin de probar y rastrear esta configuración. La nueva base de datos se usaría en un nuevo grupo de disponibilidad que se establecería para permitir solo READ_WRITEconexiones. La teoría era que dentro de un Logon Trigger, EXEC(N'USE [ReadWriteOnly]; INSERT INTO LogTable...;');dentro de una TRY...CATCHconstrucción, esencialmente sin nada en el CATCHbloque, no produciría ningún error para las conexiones ReadWrite (que se registrarían en el nuevo DB), o el USEerror en las conexiones ReadOnly, pero entonces no pasaría nada ya que el error se detecta y se ignora (y la INSERTdeclaración nunca se alcanzará). En cualquier caso, el evento de inicio de sesión real no se evitaría / denegaría. El código Logon Trigger sería efectivamente:

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;

Por desgracia, cuando se prueba el efecto de la emisión de una USEdeclaración dentro de un EXEC()plazo de un TRY...CATCHinterior de una transacción, he encontrado que la violación de acceso era un aborto a nivel de lote, no un aborto a nivel de declaración. Y la configuración XACT_ABORT OFFno cambió nada. Incluso creé un procedimiento almacenado SQLCLR simple para usar Context Connection = true;y luego llamé SqlConnection.ChangeDatabase()dentro de ay try...catchla transacción aún se canceló. Y no puede usar Enlist=falseen la conexión de contexto. Y usar una conexión regular / externa en SQLCLR para salir de la transacción no ayudaría, ya que sería una conexión completamente nueva.

Existe una posibilidad muy, muy escasa de que HAS_DBACCESS pueda usarse en lugar de la USEdeclaración, pero realmente no tengo muchas esperanzas de que pueda incorporar la información actual de Connection en sus comprobaciones. Pero tampoco tengo forma de probarlo.

Por supuesto, si hay un indicador de seguimiento que puede hacer que la infracción de acceso no se cancele por lotes, entonces el plan mencionado anteriormente debería funcionar ;-).

Solomon Rutzky
fuente
Desafortunadamente, no puedo negarlos: las otras réplicas legibles podrían estar inactivas. Todavía necesito las consultas de lectura para trabajar en el primario; solo necesito saber cuándo están sucediendo.
Brent Ozar
@BrentOzar He actualizado mi respuesta para incluir un nuevo Paso 3 que verificará esa condición y si no hay Secundarios disponibles, entonces permitirá la Conexión. Además, si la intención es simplemente "saber cuándo estás sucediendo", entonces se puede usar la misma configuración, simplemente cambia el ROLLBACKINSERT
Activador de
1
Esta es una gran respuesta, pero no es para esta pregunta. No necesito detener a los usuarios, necesito monitorear cuando está sucediendo. Tenemos aplicaciones existentes que necesitamos identificar y corregir gradualmente. Si impedía que los usuarios iniciaran sesión, provocaría una revuelta inmediata. Si desea crear una pregunta separada para esto y publicar su respuesta allí, sería genial, pero centre su respuesta aquí en mi pregunta real. Gracias.
Brent Ozar
@BrentOzar Lo siento, entendí mal su comentario a Tom como algo algo más fuerte que solo el seguimiento / registro. He eliminado la parte de mi respuesta que trataba sobre la prevención del acceso.
Solomon Rutzky
@BrentOzar Agregué algunas notas debajo de la línea (en la sección de PS) que estaba cerca de ser una solución, pero frustradas al final. Publiqué esas notas en caso de que surja una idea en ti (o en otra persona) para encontrar la pieza que falta, o incluso algo completamente diferente, que pueda resolver este rompecabezas.
Solomon Rutzky
2

¿Qué tan enfermo quieres estar? La transmisión TDS no es tan difícil de proxy, lo hicimos para nuestra aplicación SaaS. El bit que está buscando (literalmente un bit) está en el mensaje login7. Puede hacer que sus usuarios se conecten a través de un proxy y registren / apliquen el bit allí. Demonios, incluso podrías encenderlo para ellos. :)

Walden Leverich
fuente
Eso es definitivamente más enfermo de lo que quiero estar, pero gracias, jajaja.
Brent Ozar
-1

¿Su aplicación utiliza una cuenta de servicio o quizás varias cuentas de servicio? Si es así, use el evento extendido para monitorear su tráfico de inicio de sesión pero excluya sus cuentas de servicio en su servidor primario siempre activo. Ahora debería poder ver quién inicia sesión en el servidor primario siempre activo y no utiliza la cadena de conexión secundaria de solo lectura. Me estoy preparando para instalar Always-On y esto es lo que voy a hacer a menos que me digas que esto no funcionará.

ArmorDba
fuente
1
Tom: suponga que los usuarios tienen múltiples aplicaciones. Por ejemplo, Bob se conecta con SQL Server Management Studio y con Excel. Se conecta con SSMS cuando necesita hacer actualizaciones, y Excel cuando necesita hacer lecturas. Necesito asegurarme de que esté usando ApplicationIntent = ReadOnly cuando se conecta con Excel. (Ese no es el escenario exacto, pero está lo suficientemente cerca como para ilustrar).
Brent Ozar
También tengo personas conectadas a mi servidor de producción con Excel con acceso muy limitado. Se conectan con sus derechos. Espero poder verlos. Traeremos nuestro Always On en breve.
ArmorDba
-1

Desafortunadamente, no tengo el entorno para probar lo siguiente, y sin duda hay varios puntos en los que puede fallar, pero lo arrojaré por lo que vale.

Un procedimiento almacenado CLR tiene acceso a la conexión actual a través de la new SqlConnection("context connection=true")construcción (tomada de aquí ). El tipo SqlConnection expone una propiedad ConnectionString . Dado que ApplicationIntent está en la cadena de conexión inicial, supongo que estará disponible en esta propiedad y se puede analizar. Hay muchas transferencias en esa cadena, por supuesto, así que muchas oportunidades para que todo tenga forma de pera.

Esto se ejecutaría desde un Logon Trigger y los valores requeridos persistieron según sea necesario.

Michael Green
fuente
1
Esto no funcionaria. El código SQLCLR no tiene acceso a la conexión actual, tiene acceso a la sesión actual a través de la conexión de contexto. El objeto SqlConnection en el código .NET no está aprovechando la conexión real realizada desde el software del cliente original en SQL Server. Esas son dos cosas separadas.
Solomon Rutzky
Oh bueno, no importa entonces.
Michael Green
No, esto no funciona.
Brent Ozar