Today I want to show you another powerful way to use your Central Management Server. The CMS can also be used to execute OPENROWSET queries over your complete SQL Server Farm. Now I can hear you all say: “Why would you want to do that?” and “OPENROWSET, you know that you shouldn’t implement that for queries that have to run frequently!”. And to be honest, in most cases you’d be right. But it can be extremely useful in some situations.
Imagine a hosted environment with extremely strict and rigid release rules. Because of the importance and stability requirements of this environment, every component of the environment must follow the complete DTAP (Development/Test/Acceptance/Production) workflow. Off course this includes the Production SQL Servers and their instances.
Enter the CMS, because a CMS is rarely considered crucial for operations, we have introduced the CMS as a DBA Tool, granted, a VERY powerful one, but still “just” a tool. By doing this we made the CMS the responsibility of the DBA team so that we can make changes to the CMS without having to go via QA/Testing/The whole change process and version control. (We never told our QA officer what we actually CAN do with a CMS though).
As we all know, for licensing compliance it can be especially important to know on what server our active nodes of the deployed availability groups are located. This becomes even more important if you have several large servers, each serving multiple instances.
For license (read cost) reasons this environment runs in a complete active-passive setup. So, it is particularly important that at any given time all active listeners are running on the preferred primary. This setting is easily configured in the Windows Cluster manager but conform with the SQL Server best practices you don’t want automatic failback. So, if an instance is discovered to run on the “wrong” server we want to know this. But how can we check and report this at any given point in time without having to check multiple (100+) availability groups?
In this environment SQL Instance patching happens during the nightly maintenance window. A predefined list of instances was patched by the hosting using their own procedure for rolling updates. But unfortunately, any detailed report mentioning where they started and how they implemented this is missing. So, we’d love to know the listeners’ situation as soon as possible.
Luckily (Cough Cough) our CMS or Central Management Server comes to the rescue, so we can use a centralized query to get this information from our servers in an efficient manner and even keep historical information.
OK let’s go through the steps needed to build this pull report, getting this crucial information only using our CMS.
Because in this case our production instances (and the QA officer) are rather allergic to undocumented and untested changes we don’t want to implement anything (let alone a linked server) on the Instance side. This means that we must make sure that our CMS can execute Ad Hoc distributed queries. (This will be the only one instance change needed for the whole SQL Server Farm)
sp_configure 'show advanced options', 1; RECONFIGURE; sp_configure 'Ad Hoc Distributed Queries', 1; RECONFIGURE; GO
Once connected to our trusty CMS, it’s easy to get the complete server list using the following query:
select s.server_name as name from msdb.dbo.sysmanagement_shared_registered_servers s
You could also include the groups, and add an extra filter if you want to, but for this blogpost I’ll keep this part straightforward.
Then we write the query that will fetch all the needed information (and some other interesting titbits) from our hadr groups and their servers.
select hadrc.cluster_name, SERVERPROPERTY('MachineName') AS [ServerName], SERVERPROPERTY('InstanceName') AS [Instance], SERVERPROPERTY('Edition') AS [Edition], SERVERPROPERTY('ProductVersion') AS [ProductVersion], Left(@@Version, Charindex('-', @@version) - 2) As VersionName, lip.state_desc, ip_address, AGC.name as AG_Name, isnull(l.dns_name, @@SERVERNAME )+':'+(CAST(port as varchar(8))) as Listener, case WHEN is_conformant is null then 0 ELSE is_conformant END as is_conformant, CASE WHEN role=1 THEN replica_server_name ELSE (select replica_server_name from sys.dm_hadr_availability_replica_cluster_states RCS INNER JOIN sys.dm_hadr_availability_replica_states AS ARS ON ARS.replica_id = RCS.replica_id where role=1) END as active_node, CASE WHEN role=2 THEN replica_server_name ELSE (select top 1 replica_server_name from sys.dm_hadr_availability_replica_cluster_states RCS INNER JOIN sys.dm_hadr_availability_replica_states AS ARS ON ARS.replica_id = RCS.replica_id where role=2) END as passive_node, hadrc.quorum_state_desc, hadrc.quorum_type_desc, CAST(CASE AGC.failure_condition_level WHEN 1 THEN 'SQL Server service is down/SQL Server AlwaysOn Lease Timeout' WHEN 2 THEN 'Instance of SQL Server does not connect to cluster/availability replica is in failed state' WHEN 3 THEN 'Critical SQL Server internal errors / Default' WHEN 4 THEN 'Moderate SQL Server internal errors' WHEN 5 THEN 'Any qualified failure conditions/Exhaustion of SQL Engine worker-threads/Detection of an unsolvable' END as varchar(110)) as AutomaticFailoverCondition, RCS.join_state_desc, ARS.role_desc, ARS.connected_state_desc, ARS.operational_state_desc, ARS.synchronization_health_desc, CAST(Getdate() as date) as [Timestamp], CHECKSUM(CAST(SERVERPROPERTY('MachineName') as varchar(128))+ AGC.name +CONVERT(varchar(10),getdate(),102))as LookupKey FROM sys.availability_groups_cluster AS AGC INNER JOIN sys.dm_hadr_availability_replica_cluster_states AS RCS ON RCS.group_id = AGC.group_id INNER JOIN sys.dm_hadr_availability_replica_states AS ARS ON ARS.replica_id = RCS.replica_id LEFT OUTER JOIN sys.availability_group_listeners l ON l.group_id = ARS.group_id LEFT OUTER join sys.availability_group_listener_ip_addresses lip on lip.listener_id=l.listener_id outer apply (select * from sys.dm_hadr_cluster) as hadrc WHERE replica_server_name = SERVERPROPERTY('ServerName')
Ok now we have a good query, so let’s create the table that will store this information, so that we have a solid base to create our reporting/reports.
DECLARE @CMD nvarchar(4000) DECLARE @CMD2 NVARCHAR(4000) DECLARE @ServerName sysname SET NOCOUNT ON; SET @CMD='''select hadrc.cluster_name, SERVERPROPERTY(''''MachineName'''') AS [ServerName], SERVERPROPERTY(''''InstanceName'''') AS [Instance], SERVERPROPERTY(''''Edition'''') AS [Edition], SERVERPROPERTY(''''ProductVersion'''') AS [ProductVersion], Left(@@Version, Charindex(''''-'''', @@version) - 2) As VersionName, lip.state_desc, ip_address, AGC.name as AG_Name, isnull(l.dns_name, @@SERVERNAME )+'''':''''+(CAST(port as varchar(8))) as Listener, case WHEN is_conformant is null then 0 ELSE is_conformant END as is_conformant, CASE WHEN role=1 THEN replica_server_name ELSE (select replica_server_name from sys.dm_hadr_availability_replica_cluster_states RCS INNER JOIN sys.dm_hadr_availability_replica_states AS ARS ON ARS.replica_id = RCS.replica_id where role=1) END as active_node, CASE WHEN role=2 THEN replica_server_name ELSE (select top 1 replica_server_name from sys.dm_hadr_availability_replica_cluster_states RCS INNER JOIN sys.dm_hadr_availability_replica_states AS ARS ON ARS.replica_id = RCS.replica_id where role=2) END as passive_node, hadrc.quorum_state_desc, hadrc.quorum_type_desc, CAST(CASE AGC.failure_condition_level WHEN 1 THEN ''''SQL Server service is down/SQL Server AlwaysOn Lease Timeout'''' WHEN 2 THEN ''''Instance of SQL Server does not connect to cluster/availability replica is in failed state'''' WHEN 3 THEN ''''Critical SQL Server internal errors / Default'''' WHEN 4 THEN ''''Moderate SQL Server internal errors'''' WHEN 5 THEN ''''Any qualified failure conditions/Exhaustion of SQL Engine worker-threads/Detection of an unsolvable'''' END as varchar(110)) as AutomaticFailoverCondition, RCS.join_state_desc, ARS.role_desc, ARS.connected_state_desc, ARS.operational_state_desc, ARS.synchronization_health_desc, CAST(Getdate() as date) as [Timestamp], CHECKSUM(CAST(SERVERPROPERTY(''''ServerName'''') as varchar(128))+AGC.name+CONVERT(varchar(10),getdate(),102))as LookupKey FROM sys.availability_groups_cluster AS AGC INNER JOIN sys.dm_hadr_availability_replica_cluster_states AS RCS ON RCS.group_id = AGC.group_id INNER JOIN sys.dm_hadr_availability_replica_states AS ARS ON ARS.replica_id = RCS.replica_id LEFT OUTER JOIN sys.availability_group_listeners l ON l.group_id = ARS.group_id LEFT OUTER join sys.availability_group_listener_ip_addresses lip on lip.listener_id=l.listener_id outer apply (select * from sys.dm_hadr_cluster) as hadrc WHERE replica_server_name = SERVERPROPERTY(''''ServerName'''')''' DECLARE ServerList Cursor FOR select s.server_name as name from msdb.dbo.sysmanagement_shared_registered_servers s OPEN ServerList FETCH NEXT FROM ServerList into @ServerName WHILE @@FETCH_STATUS = 0 BEGIN SET @CMD2 = 'Delete from MNGDB.Reporting.AG_ClusterInfo where ServerName='''+@ServerName+''' and Timestamp = CAST(Getdate() as date)' exec sp_executesql @CMD2 SET @CMD2='INSERT INTO MNGDB.Reporting.AG_ClusterInfo SELECT a.* FROM OPENROWSET(''SQLNCLI'', ''Server='+@ServerName+';Trusted_Connection=yes;'','+@CMD+') AS a;' exec sp_executesql @CMD2 FETCH NEXT FROM ServerList into @ServerName END CLOSE ServerList DEALLOCATE ServerLis Ok hit F5
After the previous run, you’ll have a table filled with all the information we need to be able to create multiple interesting reports using the same dataset. Please note that you don’t want to run this query multiple times (OPENDATASET queries like this shouldn’t be used too frequently) so by storing it in a table on the CMS, you can get all the info you need without having to hammer all your instances.
So, let’s query the data:
;WITH ClusterResources as (Select DISTINCT cluster_name, [Listener], Timestamp FROM MNGDB.Reporting.AG_ClusterInfo WHERE Timestamp = CAST(Getdate() as date)) Select C.cluster_name, C.[Listener], CIA.ServerName as Primary_Server, CIA.[Instance] as Primary_Instance, CIA.[ProductVersion] as Primary_Version, CIP.ServerName as Secondary_Server, CIP.[Instance] as Secondary_Instance, CIP.[ProductVersion] as Secondary_Version, (Select count(*) FROM ClusterResources d where d.cluster_name=C.cluster_name) as Aantal_AGroups_InCluster, (Select count(*) FROM MNGDB.Reporting.AG_ClusterInfo e where e.[role_desc]='PRIMARY' AND CIA.[connected_state_desc]='CONNECTED' and e.cluster_name=C.cluster_name AND CAST(e.ServerName as varchar(128))=CAST(CIA.ServerName as varchar(128))) as Aantal_Active_AG_On_Active_Node, (Select count(*) FROM MNGDB.Reporting.AG_ClusterInfo e where e.[role_desc]!='PRIMARY' AND CIA.[connected_state_desc]='CONNECTED' and e.cluster_name=C.cluster_name AND CAST(e.ServerName as varchar(128))=CAST(CIP.ServerName as varchar(128))) as Aantal_Passive_AG_On_Passive_Node, CASE
© 2023 Kohera
Crafted by
© 2022 Kohera
Crafted by
Cookie | Duration | Description |
---|---|---|
ARRAffinity | session | ARRAffinity cookie is set by Azure app service, and allows the service to choose the right instance established by a user to deliver subsequent requests made by that user. |
ARRAffinitySameSite | session | This cookie is set by Windows Azure cloud, and is used for load balancing to make sure the visitor page requests are routed to the same server in any browsing session. |
cookielawinfo-checkbox-advertisement | 1 year | Set by the GDPR Cookie Consent plugin, this cookie records the user consent for the cookies in the "Advertisement" category. |
cookielawinfo-checkbox-analytics | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Analytics". |
cookielawinfo-checkbox-functional | 11 months | The cookie is set by GDPR cookie consent to record the user consent for the cookies in the category "Functional". |
cookielawinfo-checkbox-necessary | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Necessary". |
cookielawinfo-checkbox-others | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Other. |
cookielawinfo-checkbox-performance | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Performance". |
CookieLawInfoConsent | 1 year | CookieYes sets this cookie to record the default button state of the corresponding category and the status of CCPA. It works only in coordination with the primary cookie. |
elementor | never | The website's WordPress theme uses this cookie. It allows the website owner to implement or change the website's content in real-time. |
viewed_cookie_policy | 11 months | The cookie is set by the GDPR Cookie Consent plugin and is used to store whether or not user has consented to the use of cookies. It does not store any personal data. |
Cookie | Duration | Description |
---|---|---|
__cf_bm | 30 minutes | Cloudflare set the cookie to support Cloudflare Bot Management. |
pll_language | 1 year | Polylang sets this cookie to remember the language the user selects when returning to the website and get the language information when unavailable in another way. |
Cookie | Duration | Description |
---|---|---|
_ga | 1 year 1 month 4 days | Google Analytics sets this cookie to calculate visitor, session and campaign data and track site usage for the site's analytics report. The cookie stores information anonymously and assigns a randomly generated number to recognise unique visitors. |
_ga_* | 1 year 1 month 4 days | Google Analytics sets this cookie to store and count page views. |
_gat_gtag_UA_* | 1 minute | Google Analytics sets this cookie to store a unique user ID. |
_gid | 1 day | Google Analytics sets this cookie to store information on how visitors use a website while also creating an analytics report of the website's performance. Some of the collected data includes the number of visitors, their source, and the pages they visit anonymously. |
ai_session | 30 minutes | This is a unique anonymous session identifier cookie set by Microsoft Application Insights software to gather statistical usage and telemetry data for apps built on the Azure cloud platform. |
CONSENT | 2 years | YouTube sets this cookie via embedded YouTube videos and registers anonymous statistical data. |
vuid | 1 year 1 month 4 days | Vimeo installs this cookie to collect tracking information by setting a unique ID to embed videos on the website. |
Cookie | Duration | Description |
---|---|---|
ai_user | 1 year | Microsoft Azure sets this cookie as a unique user identifier cookie, enabling counting of the number of users accessing the application over time. |
VISITOR_INFO1_LIVE | 5 months 27 days | YouTube sets this cookie to measure bandwidth, determining whether the user gets the new or old player interface. |
YSC | session | Youtube sets this cookie to track the views of embedded videos on Youtube pages. |
yt-remote-connected-devices | never | YouTube sets this cookie to store the user's video preferences using embedded YouTube videos. |
yt-remote-device-id | never | YouTube sets this cookie to store the user's video preferences using embedded YouTube videos. |
yt.innertube::nextId | never | YouTube sets this cookie to register a unique ID to store data on what videos from YouTube the user has seen. |
yt.innertube::requests | never | YouTube sets this cookie to register a unique ID to store data on what videos from YouTube the user has seen. |
Cookie | Duration | Description |
---|---|---|
WFESessionId | session | No description available. |