Bre*_*zar 24 sql-server availability-groups
我设置了一个 Always On Availability Group,我想确保我的用户在他们的连接字符串中使用 ApplicationIntent=ReadOnly。
从 SQL Server 通过 DMV(或扩展事件或其他),我能否判断用户是否在其连接字符串中使用 ApplicationIntent=ReadOnly 进行连接?
请不要回答如何防止连接 - 这不是这个问题的内容。我不能简单地停止连接,因为我们现有的应用程序在没有正确字符串的情况下进行连接,我需要知道它们是哪些,以便我可以与开发人员和用户合作,随着时间的推移逐渐修复它。
假设用户有多个应用程序。例如,Bob 与 SQL Server Management Studio 和 Excel 连接。他需要更新时使用 SSMS,需要读取时使用 Excel。我需要确保他在与 Excel 连接时使用 ApplicationIntent=ReadOnly。(这不是确切的场景,但足以说明。)
wBo*_*Bob 11
拿起sqlserver.read_only_route_completeKin 和 Remus 提到的扩展事件,这是一个很好的调试事件,但它没有携带大量信息 -默认情况下只有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
)
Run Code Online (Sandbox Code Playgroud)
运行 XE 会话(考虑采样,因为这是一个 Debug 事件),并收集一些登录信息:
注意这里 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
Run Code Online (Sandbox Code Playgroud)
查询应该显示有和没有应用程序只读意图的登录:
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'
)
Run Code Online (Sandbox Code Playgroud)不,似乎没有任何 DMV 公开的连接属性(在sys.dm_exec_connections或sys.dm_exec_sessions 中)甚至与ConnectionString 关键字相关的CONNECTIONPROPERTYApplicationIntent。
但是,根据 MSDN 页面中的以下信息,可能值得通过 Microsoft Connect 请求将此属性添加到sys.dm_exec_connectionsDMV,因为它似乎是存储在 SQL Server 内存中某处的连接的属性SqlClient 对高可用性、灾难恢复的支持(斜体强调我的):
指定应用程序意图
当ApplicationIntent=ReadOnly 时,客户端在连接到启用 AlwaysOn 的数据库时请求读取工作负载。服务器将在连接时和 USE 数据库语句期间强制执行该意图,但仅适用于启用了 Always On 的数据库。
如果USE可以验证语句,则ApplicationIntent需要在初始连接尝试之外存在。但是,我没有亲自验证这种行为。
PS 我一直认为我们可以利用以下事实:
USE语句时将强制执行“意图” 。这个想法是为了测试和跟踪这个设置而创建一个新的数据库。新数据库将用于新的可用性组,该组将设置为仅允许READ_WRITE连接。理论是,在登录触发器内部,EXEC(N'USE [ReadWriteOnly]; INSERT INTO LogTable...;');在TRY...CATCH结构中,CATCH块中基本上没有任何内容,要么不会为 ReadWrite 连接产生错误(它将自己登录到新的数据库中),要么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;
Run Code Online (Sandbox Code Playgroud)
不幸的是,在测试在事务内部的内部发出USE语句的效果时,我发现访问冲突是批处理级别的中止,而不是语句级别的中止。并且设置没有改变任何东西。我什至创建了一个简单的 SQLCLR 存储过程来使用,然后在 a 中调用并且事务仍然中止。并且您不能在上下文连接上使用。并且在 SQLCLR 中使用常规/外部连接来退出事务也无济于事,因为这将是一个全新的连接。EXEC()TRY...CATCHXACT_ABORT OFFContext Connection = true;SqlConnection.ChangeDatabase()try...catchEnlist=false
可以使用HAS_DBACCESS代替USE语句的可能性非常非常小,但我真的不希望它能够将当前的连接信息合并到它的检查中。但我也没有办法测试它。
当然,如果有一个跟踪标志可以导致访问冲突不能批量中止,那么上面提到的计划应该可行;-)。
小智 -1
您的应用程序是否使用一个服务帐户或多个服务帐户?如果是这样,请使用扩展事件来监控您的登录流量,但排除您的主永远在线服务器上的服务帐户。您现在应该能够看到谁正在登录主永远在线服务器并且没有使用只读辅助连接字符串。我正准备安装永远在线,这就是我要做的,除非你告诉我这行不通。