从DMV中,您能否确定连接是否使用了ApplicationIntent = ReadOnly?


23

我设置了一个Always On可用性组,我想确保我的用户在其连接字符串中使用ApplicationIntent = ReadOnly。

从SQL Server通过DMV(或扩展事件或其他),我能否确定用户的连接字符串中是否与ApplicationIntent = ReadOnly连接?

请不要回答如何防止连接-这不是这个问题。我不能简单地停止连接,因为我们现有的应用程序在连接时没有正确的字符串,我需要知道它们是哪个,以便我与开发人员和用户一起逐步解决问题。

假设用户有多个应用程序。例如,鲍勃与SQL Server Management Studio和Excel连接。当他需要更新时,他与SSMS连接;当他需要读取时,他与Excel连接。我需要确保他与Excel连接时正在使用ApplicationIntent = ReadOnly。(这不是确切的情况,但是足够接近以说明问题。)


认为只读是在TDS路由时间决定的。一旦路由到可读的辅助站点,就不再需要信息,因此可能不会使它进入引擎。
Remus Rusanu

2
“只读路由首先连接到主服务器,然后寻找最佳可用的可读辅助服务器”,似乎辅助服务器将其视为普通连接。如果有任何XEvent触发,它将在主事件上。我不知道我在说什么,但我在猜测。
Remus Rusanu

1
您正在谈论@RemusRusanu,sqlserver.read_only_route_complete因为它仅在主要对象上触发。
Kin Shah

@Kin到了,就跟我要编码的一样;)
Remus Rusanu

2
@RemusRusanu我在玩它,我想它可能最接近使用陷阱–正确配置了只读URL,并且没有连接问题。在这两种情况下,该事件都将成功。
Kin Shah

Answers:


10

选择sqlserver.read_only_route_completeKin和Remus提到的扩展事件,这是一个不错的Debug事件,但是它并没有携带很多信息- 默认情况下仅为route_port(例如1433)和route_server_name(例如sqlserver-0.contoso.com) 。这也只会帮助确定只读意图连接何时成功。有一个read_only_route_fail事件,但是我无法触发,也许,如果路由URL出现问题,据我所知,当辅助实例不可用/关闭时,它似乎没有触发。

但是,在sqlserver.login启用事件和因果关系跟踪以及一些sqlserver.username使其有用的操作(如)之后,我取得了一些成功。

重现步骤

创建扩展事件会话以跟踪相关事件,以及有用的操作和因果关系:

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
)

运行XE会话(请考虑采样,因为这是Debug事件),并收集一些登录信息:

sqlcmd连接

注意这里sqlserver-0是我可读的辅助服务器,sqlserver-1是我的辅助服务器。在这里,我使用的-K开关sqlcmd来模拟只读的应用程序意图登录和一些SQL登录。成功的只读意图登录将触发readonly事件。

在暂停或停止会话时,我可以查询它并尝试链接两个事件,例如:

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

该查询应显示具有和不具有应用程序只读意图的登录名:

查询结果

  • read_only_route_complete是Debug事件,请谨慎使用。例如考虑采样。
  • 这两个事件以及跟踪因果关系提供了满足您要求的潜力-在此简单钻机上需要进行进一步测试
  • 我确实注意到,如果在连接中未指定数据库名称,则似乎无法正常工作
  • 我试图让pair_matching目标工作,但时间用完了。这里有一些发展潜力,例如:

    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

不,它似乎没有任何DMV公开的连接属性(在sys.dm_exec_connectionssys.dm_exec_sessions中),甚至没有与ConnectionString关键字相关的CONNECTIONPROPERTYApplicationIntent

但是,可能值得通过Microsoft Connect请求将此属性添加到sys.dm_exec_connectionsDMV,因为它似乎是存储在SQL Server内存中某个位置的连接的属性,基于以下MSDN页面中的以下信息:SqlClient对高可用性,灾难恢复的支持(斜体为我的强调):

指定申请意向

ApplicationIntent = ReadOnly时,客户端在连接到启用了AlwaysOn的数据库时请求读取工作负载。服务器将在连接时和USE数据库语句期间强制实施该意图,但仅对启用了Always On的数据库强制实施该意图。

如果USE可以验证一条语句,则ApplicationIntent除了初始连接尝试之外,还存在其他需求。但是,我尚未亲自验证此行为。


PS我一直在想,我们可以利用以下事实:

  • 可以将主副本设置为禁止只读访问一个或多个数据库,并且
  • 执行USE语句时将强制执行“意图” 。

这个想法是创建一个新的数据库,仅用于测试和跟踪此设置。新的数据库将在新的可用性组中使用,该可用性组将设置为仅允许READ_WRITE连接。从理论上讲,在Logon Trigger的EXEC(N'USE [ReadWriteOnly]; INSERT INTO LogTable...;');内部,TRY...CATCH构造内的CATCH块中几乎没有任何内容,或者不会对ReadWrite连接产生错误(将自己记录在新的DB中),或者USE对ReadOnly连接产生错误,但是那么什么也不会发生,因为错误被发现并被忽略了(并且INSERT永远不会到达该语句)。无论哪种情况,都不会阻止/拒绝实际的登录事件。登录触发代码实际上是:

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;

不幸的是,测试发出的效果时,USE内声明EXEC()一个内部TRY...CATCH事务的里面,我发现访问冲突是一个批次级中止,而不是语句级中止。而且设置XACT_ABORT OFF没有任何改变。我什至创建了一个简单的SQLCLR存储过程来使用Context Connection = true;,然后在中调用SqlConnection.ChangeDatabase()try...catch并且该事务仍被中止。并且您不能Enlist=false在上下文连接上使用。并且在SQLCLR中使用常规/外部连接来移出Transaction并没有帮助,因为它将是一个全新的连接。

可以使用HAS_DBACCESS代替该USE语句的可能性非常小,但是我对将当前的Connection信息合并到其检查中的希望并不高。但是我也无法对其进行测试。

当然,如果有一个跟踪标志可能导致访问冲突不被中止,那么上述计划应该起作用;-)。


不幸的是,我不能否认他们-其他可读副本可能已关闭。我仍然需要在主数据库上使用读取查询-我只需要知道它们何时发生。
布伦特·奥扎尔

@BrentOzar我已经将答案更新为包括新的步骤3,该步骤将检查该条件,如果没有可用的辅助,则将允许连接。另外,如果意图仍然只是“知道什么时候发生”,则可以使用相同的设置,只需将ROLLBACK“登录触发器” 中的更改为一个INSERT到日志表中即可:-)
Solomon Rutzky

1
这是一个很好的答案,但不是这个问题。我不需要停止用户,我需要监视它的发生时间。我们需要逐步定位和修复的现有应用程序。如果我停止用户登录,将立即引起反抗。如果您想为此创建一个单独的问题,然后在此处发布答案,那很好-但请在此处将您的答案集中在我的实际问题上。谢谢。
布伦特·奥扎尔

@BrentOzar抱歉,我误解了您对Tom的评论,意思是比跟踪/记录要强一些。我已经删除了我的回答中涉及阻止访问的部分。
所罗门·鲁兹基

@BrentOzar我在(在PS部分中)下面的行中添加了一些注释,这些注释已接近解决方案,但最终却受到了挫败。我张贴了这些笔记,以防它在您(或其他人)中引起一个想法,以提出缺失的片断,甚至是完全不同的片断,以解决这个难题。
所罗门·鲁兹基

2

你想生病吗?TDS流并不难代理,我们是为SaaS应用程序完成的。您要查找的位(实际上是位)在login7消息中。您可以让您的用户通过代理连接,然后在其中登录/强制执行。地狱,你甚至可以为他们打开它。:)


那绝对比我想的要病得多,但是,谢谢,哈哈哈。
布伦特·奥扎尔

-1

您的应用程序使用一个服务帐户还是可能使用多个服务帐户?如果是这样,请使用扩展事件来监视您的登录流量,但在主要的始终在线服务器上排除您的服务帐户。现在,您应该能够看到谁正在登录到始终在线的主服务器,而不使用只读的次要连接字符串。我已经准备好安装Always-On,这就是我要做的,除非您告诉我这将不起作用。


1
汤姆-假设用户有多个应用程序。例如,鲍勃与SQL Server Management Studio和Excel连接。当他需要更新时,他与SSMS连接;当他需要读取时,他与Excel连接。我需要确保他与Excel连接时正在使用ApplicationIntent = ReadOnly。(这不是确切的情况,但是足够接近了。)
Brent Ozar

我也有一些人使用访问权限非常有限的Excel连接到我的生产服务器。他们与自己的权利联系在一起。希望我能看到他们。我们将很快启用“始终在线”。
ArmorDba

-1

不幸的是,我没有测试以下内容的环境,毫无疑问,它可能会失败几个方面,但是我将把它丢在一边,以求物有所值。

CLR存储过程可以通过该new SqlConnection("context connection=true")构造访问当前连接(从此处获取)。SqlConnection类型公开ConnectionString属性。由于ApplicationIntent在初始连接字符串中,因此我想它可以在此属性中使用并且可以解析。当然,这条链上有很多交接,所以有很多机会变成梨形。

这将从登录触发器运行,并且所需的值将根据需要持久保存。


1
这行不通。SQLCLR代码无权访问当前的连接,它可以通过上下文连接访问当前的会话。.NET代码中的SqlConnection对象未利用从原始客户端软件到SQL Server的实际连接。那是两件事。
所罗门·鲁兹基

哦,那没关系。
Michael Green

不,这不起作用。
布伦特·奥扎尔
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.