От DMV, можете ли вы сказать, использовало ли соединение ApplicationIntent = ReadOnly?

23

У меня настроена группа доступности всегда, и я хочу убедиться, что мои пользователи используют ApplicationIntent = ReadOnly в своих строках подключения.

С сервера SQL через DMV (или расширенные события или что-то еще), могу ли я узнать, был ли пользователь, связанный с ApplicationIntent = ReadOnly в своей строке подключения?

Пожалуйста, не отвечайте с тем, как ПРЕДОТВРАТИТЬ соединения - это не тот вопрос, о котором идет речь. Я не могу просто прекратить соединения, потому что у нас есть приложения, которые соединяются без правильной строки, и мне нужно знать, какие из них, поэтому я могу работать с разработчиками и пользователями, чтобы постепенно исправить это с течением времени.

Предположим, что у пользователей есть несколько приложений. Например, Боб соединяется как с SQL Server Management Studio, так и с Excel. Он подключается к SSMS, когда ему нужно сделать обновления, и к Excel, когда ему нужно сделать чтение. Мне нужно убедиться, что он использует ApplicationIntent = ReadOnly, когда он соединяется с Excel. (Это не точный сценарий, но он достаточно близок для иллюстрации.)

Брент Озар
источник
Я думаю, что только для чтения определяется во время маршрутизации TDS. После того, как информация направлена ​​на читаемый вторичный объект, информация больше не нужна, поэтому, вероятно, она не попадет в движок.
Ремус Русану
2
«Маршрутизация только для чтения сначала подключается к первичному серверу, а затем ищет лучшего доступного для чтения вторичного устройства». Похоже, вторичное устройство увидит его как обычное соединение. Если есть какое-либо событие XEvent, оно будет на первичном. Я не знаю о чем говорю, но я размышляю.
Ремус Русану
1
@RemusRusanu вы говорите о том, sqlserver.read_only_route_completeкак это происходит только на первичном.
Кин Шах
@ Кин, ты идешь, именно так, как я бы его написал;)
Ремус Русану
2
@RemusRusanu Я играл с ним, и я думаю, что он самый близкий, который вы можете получить с помощью getchas - URL только для чтения настроен правильно и нет проблем с подключением. В обоих случаях это событие будет успешным.
Кин Шах

Ответы:

10

Подбирая sqlserver.read_only_route_completeрасширенное событие, упомянутое Kin и 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
)

Запустите сеанс 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является событием отладки, поэтому используйте его экономно. Рассмотрим выборку для примера.
  • два события вместе с причинно-следственной связью дают возможность удовлетворить ваши требования - необходимы дополнительные испытания на этой простой установке
  • Я заметил, что если имя базы данных не было указано в соединении, кажется, что вещи не работают
  • Я пытался заставить 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'
        )
wBob
источник
5

Нет, не похоже, что существует какое-либо свойство соединения, доступное для DMV (в sys.dm_exec_connections или sys.dm_exec_sessions ) или даже CONNECTIONPROPERTY, которое относится к ApplicationIntentключевому слову ConnectionString.

Однако, возможно, стоит запросить через Microsoft Connect, чтобы это свойство было добавлено в sys.dm_exec_connectionsDMV, поскольку оно , по-видимому, является свойством соединения, которое хранится где-то в памяти SQL Server, на основе следующей информации, найденной на странице MSDN для: Поддержка SqlClient для высокой доступности, аварийного восстановления (выделено курсивом):

Указание намерения приложения

Когда ApplicationIntent = ReadOnly , клиент запрашивает рабочую нагрузку для чтения при подключении к базе данных с включенным AlwaysOn. Сервер будет применять намерение во время соединения и во время оператора базы данных USE, но только для базы данных с поддержкой Always On.

Если USEутверждение может быть проверено, то ApplicationIntentпосле первоначальной попытки подключения он должен существовать. Однако я лично не проверял это поведение.


PS Я думал, что мы могли бы использовать факты, которые:

  • Первичная реплика может быть настроена так, чтобы запретить доступ ReadOnly к одной или нескольким базам данных, и
  • «намерение» будет применено при выполнении 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;

К сожалению, при тестировании эффекта выдачи USEоператора EXEC()внутри TRY...CATCHвнутренней части транзакции я обнаружил, что нарушение доступа было прерыванием на уровне пакета, а не прерыванием на уровне оператора. И настройка XACT_ABORT OFFничего не изменила. Я даже создал простую хранимую процедуру SQLCLR для использования, Context Connection = true;а затем вызвал ее SqlConnection.ChangeDatabase()внутри, try...catchи транзакция все еще была прервана. И вы не можете использовать Enlist=falseв контексте подключения. И использование обычного / внешнего соединения в SQLCLR для выхода за пределы транзакции не поможет, так как это будет совершенно новое соединение.

Существует очень, очень тонкая возможность, что HAS_DBACCESS может быть использован вместо USEоператора, но у меня действительно нет больших надежд на то, что он сможет включить текущую информацию о подключении в свои проверки. Но у меня нет возможности проверить это тоже.

Конечно, если есть флаг трассировки, который может привести к тому, что нарушение доступа не будет прерывать пакет, то упомянутый выше план должен сработать ;-).

Соломон Руцкий
источник
К сожалению, я не могу отрицать их - другие читаемые реплики могут быть недоступны. Мне все еще нужны запросы на чтение для работы с первичным сервером - мне просто нужно знать, когда они происходят.
Брент Озар
@BrentOzar Я обновил свой ответ, добавив новый шаг 3, который проверит это условие, и, если нет доступных вторичных серверов, он разрешит соединение. Кроме того, если намерение по-прежнему состоит в том, чтобы просто «знать, когда у вас происходит», то можно использовать ту же настройку, просто измените ROLLBACKв триггере INSERTвхода на таблицу регистрации :-)
Соломон Руцки,
1
это отличный ответ, но это не для этого вопроса. Мне не нужно останавливать пользователей, мне нужно отслеживать, когда это происходит. У нас есть приложения, которые мы должны постепенно определять и исправлять. Если я не позволю пользователям войти в систему, это вызовет немедленный бунт. Если вы хотите создать для этого отдельный вопрос и опубликовать свой ответ, это было бы замечательно, но, пожалуйста, сфокусируйте свой ответ здесь на моем реальном вопросе. Спасибо.
Брент Озар
@BrentOzar Извините, я неправильно понял ваш комментарий к Тому как нечто более сильное, чем просто отслеживание / ведение журнала. Я удалил часть своего ответа, касающуюся предотвращения доступа.
Соломон Руцкий
@BrentOzar Я добавил несколько примечаний под чертой (в разделе PS), которые были близки к решению, но сорваны в самом конце. Я разместил эти заметки на случай, если у вас (или у кого-то другого) возникнет идея найти недостающую часть или что-то совершенно другое, что может решить эту загадку.
Соломон Руцкий
2

Насколько больным ты хочешь быть? Прокси-поток не так сложно прокси, мы сделали это для нашего приложения SaaS. Бит, который вы ищете (буквально немного), находится в сообщении login7. Ваши пользователи могут подключаться через прокси и регистрировать / применять бит там. Черт, ты можешь даже включить это для них. :)

Уолден Леверих
источник
Это определенно более больное, чем я хочу быть, но спасибо, хахаха.
Брент Озар
-1

Использует ли ваше приложение служебную учетную запись или несколько учетных записей? Если это так, используйте расширенное событие для отслеживания трафика входа в систему, но исключите учетные записи служб на основном постоянном сервере. Теперь вы сможете увидеть, кто входит в систему на основном постоянно включенном сервере и не использует строку подключения только для чтения. Я готовлюсь к установке Always-On, и это то, что я собираюсь сделать, если вы не скажете мне, что это не будет работать.

ArmorDba
источник
1
Том - предположим, что у пользователей есть несколько приложений. Например, Боб соединяется как с SQL Server Management Studio, так и с Excel. Он подключается к SSMS, когда ему нужно сделать обновления, и к Excel, когда ему нужно сделать чтение. Мне нужно убедиться, что он использует ApplicationIntent = ReadOnly, когда он соединяется с Excel. (Это не точный сценарий, но он достаточно близок для иллюстрации.)
Брент Озар
У меня также есть люди, подключающиеся к моему производственному серверу с Excel с очень ограниченным доступом. Они связывают свои права. Я надеюсь, что смогу увидеть их. Мы скоро поднимет наш Always On.
ArmorDba
-1

К сожалению, у меня нет среды, чтобы протестировать следующее, и, несомненно, есть несколько моментов, в которых он может потерпеть неудачу, но я выложу его там, чтобы оно того стоило.

Хранимая процедура CLR имеет доступ к текущему соединению через new SqlConnection("context connection=true")конструкцию (взято отсюда ). Тип SqlConnection предоставляет свойство ConnectionString . Поскольку ApplicationIntent находится в исходной строке подключения, я предполагаю, что оно будет доступно в этом свойстве и может быть проанализировано. Конечно, в этой цепочке много раздач, так что у всех есть возможность сделать грушевидную форму.

Это запустится из триггера входа в систему, и необходимые значения сохранятся по мере необходимости.

Майкл Грин
источник
1
Это не сработает. Код SQLCLR не имеет доступа к текущему соединению, он имеет доступ к текущему сеансу через контекстное соединение. Объект SqlConnection в .NET-коде не подключается к фактическому соединению, установленному из исходного клиентского программного обеспечения в SQL Server. Это две разные вещи.
Соломон Руцкий,
Ну да ладно, тогда неважно.
Майкл Грин
Нет, это не работает
Брент Озар