Moving all Availability Groups in the Event of a Single Availability Group Failover

Environment

Blog30_1.PNG

-> Disaster Recovery for a SQL Server 2016 Standard Edition Database  Server was implemented using Alwayson Basic Availability group.

-> One of the availability group failed over and that caused application outage, as other databases required by the application were residing on a different server.  Implementing a Listener doesn’t makes sense in this scenario as the application is dependent on bunch of databases and creating Listener for each Availability group doesn’t seem practical and the application cannot use multiple Listeners in their connection string.

-> We decided that in an event of a failover for any Availability group. All other availability groups will also be failed over, Just to make sure that all the availability groups are on the same replica.

-> We decided to implement a Powershell script to perform this. This solution will work only for Basic Availability groups.

-> Create a Linked Server on each of the 2 SQL Server nstance to access the master database of other Database Instance. For security reasons we can use “Be made using the login’s current security context’.

-> Make sure all the Availability groups are at the same instance. Execute the below query on both Primary and secondary instance on Master Database only once.

IF EXISTS(SELECT * FROM sysobjects where name ='AG_role')
DROP TABLE dbo.AG_role
create table AG_role (AGName varchar(30)
,PrimaryReplica varchar(30)
,DBName varchar(30)
,Role int)
set nocount on
Declare @AGName varchar(30)
Declare @PrimaryReplica varchar(30)
Declare @DBName varchar(30)
DECLARE AG_Cursor CURSOR STATIC FOR
SELECT
AG.name AS [AvailabilityGroupName],
ISNULL(agstates.primary_replica, '') AS [PrimaryReplicaServerName],
dbcs.database_name AS [DatabaseName]
FROM master.sys.availability_groups AS AG
LEFT OUTER JOIN master.sys.dm_hadr_availability_group_states as agstates
ON AG.group_id = agstates.group_id
INNER JOIN master.sys.availability_replicas AS AR
ON AG.group_id = AR.group_id
INNER JOIN master.sys.dm_hadr_availability_replica_states AS arstates
ON AR.replica_id = arstates.replica_id AND arstates.is_local = 1
INNER JOIN master.sys.dm_hadr_database_replica_cluster_states AS dbcs
ON arstates.replica_id = dbcs.replica_id
LEFT OUTER JOIN master.sys.dm_hadr_database_replica_states AS dbrs
ON dbcs.replica_id = dbrs.replica_id AND dbcs.group_database_id = dbrs.group_database_id
ORDER BY AG.name ASC, dbcs.database_name
OPEN AG_Cursor
FETCH NEXT FROM AG_Cursor into @AGName, @PrimaryReplica, @DBName
WHILE @@FETCH_STATUS = 0
BEGIN
insert into AG_role values(@AGName, @PrimaryReplica, @DBName,sys.fn_hadr_is_primary_replica(@DBName))
FETCH NEXT FROM AG_Cursor into @AGName, @PrimaryReplica, @DBName
END
CLOSE AG_Cursor
DEALLOCATE AG_Cursor

-> Save the below file as a powershell script on both Primary and secondary Database server. Create a Windows task on both servers to execute the script every 1 min on both servers. Please make sure $PrimaryReplica is same as the server where the powershell script is saved. The $SecondaryReplica will be the other server that is part of the Alwayson Availability group.

$PrimaryReplica = "JBSERVER1";
$SecondaryReplica = "JBSERVER2";
Invoke-Sqlcmd "create table AGFailover(AGSQL nvarchar(max),flag int default 0);" -ServerInstance $PrimaryReplica;
Invoke-Sqlcmd "
Declare @Ag_Current int
Declare @Ag_Previous int
Declare @AGName varchar(30)
Declare @PrimaryReplica varchar(30)
Declare @DBName varchar(30)
Declare @Role int
Declare @Flag int
DECLARE AG_Cursor CURSOR STATIC FOR
SELECT * from AG_Role
OPEN AG_Cursor
FETCH NEXT FROM AG_Cursor into @AGName, @PrimaryReplica, @DBName, @Role
WHILE @@FETCH_STATUS = 0
BEGIN
set @Ag_Current = (select sys.fn_hadr_is_primary_replica(@DBName))
set @Ag_Previous = (select Role from Ag_Role where DBName=@DBName)
if(@Ag_Current = @Ag_Previous)
BEGIN
print 'No Failover occured'
insert into AGFailover (AGSQL)
select 'ALTER AVAILABILITY GROUP ['+@AGName+'] FAILOVER;'
END
ELSE
BEGIN
--print 'Failover occured'
if (@Ag_Previous =1)
BEGIN
set @flag=1
END
END
FETCH NEXT FROM AG_Cursor into @AGName, @PrimaryReplica, @DBName, @Role
END
CLOSE AG_Cursor
DEALLOCATE AG_Cursor;
if (@flag=1)
begin
update AGFailover set flag=1
end" -ServerInstance $PrimaryReplica;
Invoke-Sqlcmd "
DECLARE @AGSql NVARCHAR(MAX) = N'';
SELECT
@AGSql = @AGSql + AGSQL FROM $PrimaryReplica.master.dbo.AGFailover where flag=1;
EXEC sp_executesql @AGSql;" -ServerInstance $SecondaryReplica;
Invoke-Sqlcmd "drop table AGFailover;" -ServerInstance $PrimaryReplica;
Invoke-Sqlcmd "delete from AG_role;" -ServerInstance $PrimaryReplica;
Invoke-Sqlcmd "
set nocount on
Declare @AGName varchar(30)
Declare @PrimaryReplica varchar(30)
Declare @DBName varchar(30)
DECLARE AG_Cursor CURSOR STATIC FOR
SELECT
AG.name AS [AvailabilityGroupName],
ISNULL(agstates.primary_replica, '') AS [PrimaryReplicaServerName],
dbcs.database_name AS [DatabaseName]
FROM master.sys.availability_groups AS AG
LEFT OUTER JOIN master.sys.dm_hadr_availability_group_states as agstates
ON AG.group_id = agstates.group_id
INNER JOIN master.sys.availability_replicas AS AR
ON AG.group_id = AR.group_id
INNER JOIN master.sys.dm_hadr_availability_replica_states AS arstates
ON AR.replica_id = arstates.replica_id AND arstates.is_local = 1
INNER JOIN master.sys.dm_hadr_database_replica_cluster_states AS dbcs
ON arstates.replica_id = dbcs.replica_id
LEFT OUTER JOIN master.sys.dm_hadr_database_replica_states AS dbrs
ON dbcs.replica_id = dbrs.replica_id AND dbcs.group_database_id = dbrs.group_database_id
ORDER BY AG.name ASC, dbcs.database_name
OPEN AG_Cursor
FETCH NEXT FROM AG_Cursor into @AGName, @PrimaryReplica, @DBName
WHILE @@FETCH_STATUS = 0
BEGIN
insert into AG_role values(@AGName, @PrimaryReplica, @DBName,sys.fn_hadr_is_primary_replica(@DBName))
FETCH NEXT FROM AG_Cursor into @AGName, @PrimaryReplica, @DBName
END
CLOSE AG_Cursor
DEALLOCATE AG_Cursor;" -ServerInstance $PrimaryReplica;

-> Whenever a failover of any of the availability group happens, the script executing from the task scheduler will check for the failover and fails over all other Availability groups.

Thank You,
Vivek Janakiraman

Disclaimer:
The views expressed on this blog are mine alone and do not reflect the views of my company or anyone else. All postings on this blog are provided “AS IS” with no warranties, and confers no rights.

Difference between Readable Secondary setting Yes and Read Intent only

-> Readable Secondary setting in AlwaysON is discussed in article “https://docs.microsoft.com/en-us/sql/database-engine/availability-groups/windows/configure-read-only-access-on-an-availability-replica-sql-server” is as below,

No
No user connections are allowed to secondary databases of this replica. They are not available for read access. This is the default setting.

Read-intent only
Only read-only connections are allowed to secondary databases of this replica. The secondary database(s) are all available for read access.

Yes
All connections are allowed to secondary databases of this replica, but only for read access. The secondary database(s) are all available for read access.

Environment

Blog29_1.PNG

-> Creating the Read-Only routing list using below query,

ALTER AVAILABILITY GROUP AG1
MODIFY REPLICA ON
N'JBSERVER1' WITH
(SECONDARY_ROLE (ALLOW_CONNECTIONS = READ_ONLY));
ALTER AVAILABILITY GROUP AG1
MODIFY REPLICA ON
N'JBSERVER1' WITH
(SECONDARY_ROLE (READ_ONLY_ROUTING_URL = N'TCP://JBSERVER1.JBS.COM:1433'));
ALTER AVAILABILITY GROUP AG1
MODIFY REPLICA ON
N'JBSERVER2' WITH
(SECONDARY_ROLE (ALLOW_CONNECTIONS = READ_ONLY));
ALTER AVAILABILITY GROUP AG1
MODIFY REPLICA ON
N'JBSERVER2' WITH
(SECONDARY_ROLE (READ_ONLY_ROUTING_URL = N'TCP://JBSERVER2.JBS.COM:1433'));
ALTER AVAILABILITY GROUP AG1
MODIFY REPLICA ON
N'JBSERVER3' WITH
(SECONDARY_ROLE (ALLOW_CONNECTIONS = READ_ONLY));
ALTER AVAILABILITY GROUP AG1
MODIFY REPLICA ON
N'JBSERVER3' WITH
(SECONDARY_ROLE (READ_ONLY_ROUTING_URL = N'TCP://JBSERVER3.JBS.COM:1433'));
ALTER AVAILABILITY GROUP AG1
MODIFY REPLICA ON
N'JBSERVER1' WITH
(PRIMARY_ROLE (READ_ONLY_ROUTING_LIST=('JBSERVER3','JBSERVER2')));
ALTER AVAILABILITY GROUP AG1
MODIFY REPLICA ON
N'JBSERVER2' WITH
(PRIMARY_ROLE (READ_ONLY_ROUTING_LIST=('JBSERVER3','JBSERVER1')));
ALTER AVAILABILITY GROUP AG1
MODIFY REPLICA ON
N'JBSERVER3' WITH
(PRIMARY_ROLE (READ_ONLY_ROUTING_LIST=('JBSERVER2','JBSERVER1')));

-> The Readable Secondary is set to “Read-intent only”,

Blog29_2.PNG

-> Connecting to the secondary Replica JBSERVER3 on database JB1 directly and trying a select query,

TITLE: Connect to Database Engine
ADDITIONAL INFORMATION:
The target database (‘JB1’) is in an availability group and is currently accessible for connections when the application intent is set to read only. For more information about application intent, see SQL Server Books Online. (Microsoft SQL Server, Error: 978)

For help, click: http://go.microsoft.com/fwlink?ProdName=Microsoft%20SQL%20Server&EvtSrc=MSSQLServer&EvtID=978&LinkId=20476

Blog29_3

-> It is pretty clear that we are not able to make a connection to database JB1 on Server JBSERVER3 directly. Let us try a connection using ApplicationIntent=ReadOnly and it connects fine,

Blog29_4
Blog29_5
Blog29_6

-> This advises us that we cannot read the readable secondaries without a ApplicationIntent = ReadOnly.

-> The Readable Secondary is set to “Yes”,

Blog29_7.PNG

-> Connecting to the secondary Replica JBSERVER3 on database JB1 directly and trying a select query. It works fine without any issues,

Blog29_8.PNG

-> In short, Readable Secondary = “Read Intent only” requires ApplicationIntent=ReadOnly to execute a select query on the secondary. Readable Secondary = “Yes” allows us to execute a select query on the secondary directly.

Thank You,
Vivek Janakiraman

Disclaimer:
The views expressed on this blog are mine alone and do not reflect the views of my company or anyone else. All postings on this blog are provided “AS IS” with no warranties, and confers no rights.