В предыдущей статье серии я дал абрис базовой логики, которая потребуется для организации регулярных проверок синхронизации заданий агента SQL Server для серверов, где были развернуты группы доступности AlwaysOn. На этот раз я подробно опишу этапы организации таких проверок, а также код, необходимый для их осуществления.
Шаг 1. Начинаем с прилинкованных серверов
Чтобы описываемая в статье логика заработала, потребуется установить настройки прилинкованных серверов для всех серверов, на которых размещаются ваши группы доступности AlwaysOn. В части 9 я описал ряд относящихся к настройкам прилинкованных серверов ключевых проблем, которые вам предстоит решить перед тем, как представленные ниже коды или логика начнут работать.
Шаг 2. С помощью определяемой пользователем функции UDF установите хост, где размещается основная реплика
При проведении проверок важно знать, на каком сервере (или экземпляре) в данный момент размещается основная реплика для данной группы доступности.
Как отмечалось в части 7 («Развилка на дороге»), если вы используете подход, в соответствии с которым решение об активации или деактивации заданий принимается в зависимости от того, будут ли они выполняться на том же сервере, где находится основная реплика (реплики), без этой логики вам просто не обойтись. С другой стороны, если в шагах задания вы используете логику if-else, вам при осуществлении проверок синхронизации тоже будет полезно знать, на каком сервере размещается основная реплика в целевой группе доступности — на этот раз для того, чтобы упростить проведение проверок и осуществлять их лишь на основном сервере или с основного сервера, как будет показано ниже.
Теоретически существует два способа выяснить, «кто главный» в той или иной группе доступности: по имени базы данных (например, находится ли конкретная база данных на сервере, который в данный момент является основным) или по имени группы доступности (содержит ли на данный момент та или иная группа доступности основные реплики на определенном сервере). Сценарии для обоих случаев приводятся в листинге 1.
Соглашение об именовании, к которому относятся эти определяемые пользователем функции, скопировано с примера sys.fn_hadr_backup_is_preferred_replica. Как и в случае с данной функцией, если вы укажете имена упомянутых выше параметров @DBName или @AGName некорректно, то получите результат FALSE, а не сообщение об ошибке (это сделано намеренно).
Шаг 3. Чтобы проверить задания уровня сервера, разверните хранимую процедуру
Закончив настройку прилинкованных серверов, переходите к проверке заданий уровня сервера — заданий агента SQL Server, которые выполняются на уровне сервера либо имеют в качестве целевых детали уровня сервера, такие как проверка имеющегося дискового пространства, опрос журналов регистрации ошибок или очистка предыстории msdb. Эта логика, вероятно, также будет использоваться для проверки операций резервного копирования.
Идея, положенная в основу данного кода, довольно проста. Он запрашивает детали заданий агента SQL Server на одном сервере, считывает детали заданий на другом, а затем, сопоставляя соответствующие фрагменты данных, определяет, есть ли в них какие-либо отличия. По крайней мере, так можно определить задачу этого кода, если не вдаваться в подробности (см. листинг 2).
При этом незаметно для постороннего глаза происходит кое-что еще. К примеру, имеется параметр @PrimaryAGAfinity, который используется для указания «сходства» или корреляции относительно указанной группы доступности, размещенной на ваших серверах. Идея состоит в том, что если вы укажете данное значение (например, MyProductionDbs) и SQL Server обнаружит, что группа доступности определена именно на том хосте, где выполняется этот код, и что группа доступности в данный момент не содержит основных реплик, тогда данная логика проверки не будет применяться, ибо она будет исходить из того, что мы намерены выполнять проверки на сервере, где размещается основная реплика (основные реплики).
Точно так же существуют параметры для MailProfileName (или имя Mail Profile, которое необходимо будет использовать в случае обнаружения вопросов в плане синхронизации), и имя оператора, которого потребуется известить.
Наконец, имеется параметр @IngoredJobs — в нем вы можете с помощью запятых-разделителей сформировать список имен заданий, которые хотите проигнорировать (или не подвергать проверке по синхронизации), а также флаг @ConsoleOnly, который вы можете использовать для выполнения или тестирования данной хранимой процедуры внутри SSMS, где она не будет беспокоить вас, направляя предупреждение по электронной почте, что было бы сделано в случае составления графика.
Шаг 4. Разверните хранимую процедуру для проверки заданий группы доступности AlwaysOn
Надо сказать, что приведенная выше хранимая процедура выполняет еще одну ключевую задачу: она игнорирует любое задание агента SQL Server, если назначенное этому заданию имя категории задания совпадает с именем размещенной на хосте группы доступности — в соответствии с соглашением, описанным в части 7 («Проблемы активации и деактивации пакетных заданий»). Нам потребуется дополнительная логика для проверки заданий уровня группы доступности и состояния их синхронизации.
В конечном счете эта логика во многом подобна логике, описанной выше: мы будем считывать детали задания с одного сервера и сопоставлять их с деталями, полученными на другом сервере. Единственное отличие состоит в том, что в данном случае мы будем считывать детали только тех заданий, которые относятся к определенной категории заданий агента SQL Server (иначе говоря, к определенной группе доступности), и при желании будем определять, существует ли необходимость переключать состояние этих заданий из позиции «активировано» в позицию «деактивировано» и наоборот — в зависимости от того, где они в данный момент размещаются и находятся ли на том же хосте, что и основная реплика.
При этом используется код, приведенный в листинге 3.
Если вы решили управлять «пакетными заданиями» (как они определены в части 7) с использованием подхода «активировать или деактивировать», приведенный в листинге 3 код будет работать без дополнительной подстройки и попытается активировать или деактивировать задания на ваших серверах по мере необходимости. С этой целью он будет вызывать следующую хранимую процедуру (которая взаимодействует с таблицей состояния заданий, описанной в части 8; см. листинг 4).
Однако если вы решите действовать в соответствии с логикой IF/ELSE, реализованной в ваших пакетных заданиях, то целесообразно деактивировать (то есть закомментировать) код в «родительской» хранимой процедуре, которая пытается выполнить эти операции. Необходимо отметить, что весь код в данной статье предназначен для случаев, когда группа доступности содержит всего лишь два сервера (отсюда обращения к «партнерскому» связанному серверу; см. часть 9). Если ваша топология включает более двух серверов, базовая логика, представленная в этих статьях, будет работать, но вам придется соответствующим образом модифицировать ее (эту тему мы затронем в одной из следующих статей).
Шаг 5. Создайте задание агента SQL Server, которое будет выполнять регулярные проверки
Итак, код готов, и теперь вы можете приступать к составлению задания, которое будет выполнять регулярные проверки синхронизации. Базовая логика формирования этого задания такова. Сначала нужно составить расписание выполнения повторяющихся проверок, затем добавить отдельный шаг задания для выполнения проверок «уровня сервера», после чего добавить дополнительный шаг задания для каждой группы доступности AlwaysOn, в отношении которой вы намереваетесь осуществлять проверки. Так, если на ваших серверах размещается одна группа доступности, задание будет состоять из двух шагов, а если таких групп доступности будет три, вы получите задание из четырех шагов (один для проверок уровня сервера и по одному шагу задания, иначе говоря, по одному обращению к dba_SyncCheckupForAGLevelJobs для каждой имеющейся группы доступности).
В следующей статье серии мы более подробно рассмотрим эталонное задание по синхронизации, варианты действий и рекомендации по составлению графиков. Кроме того, я расскажу о том, как эти проверки себя ведут, так сказать, в «естественной среде обитания».
USE master;
GO
IF OBJECT_ID('dbo.fn_hadr_database_is_primary','FN') IS NOT NULL
DROP FUNCTION dbo.fn_hadr_database_is_primary;
GO
CREATE FUNCTION dbo.fn_hadr_database_is_primary (@DBName sysname)
RETURNS bit
AS
BEGIN
DECLARE @description sysname;
SELECT
@description = hars.role_desc
FROM
sys.databases d
INNER JOIN sys.dm_hadr_availability_replica_states hars ON d.replica_id = hars.replica_id
WHERE
database_id = DB_ID(@DBName);
IF @description = 'PRIMARY'
RETURN 1;
RETURN 0;
END;
GO
USE master;
GO
IF OBJECT_ID('dbo.fn_hadr_group_is_primary','FN') IS NOT NULL
DROP FUNCTION dbo.fn_hadr_group_is_primary;
GO
CREATE FUNCTION dbo.fn_hadr_group_is_primary (@AGName sysname)
RETURNS bit
AS
BEGIN
DECLARE @PrimaryReplica sysname;
SELECT @PrimaryReplica = hags.primary_replica
FROM
sys.dm_hadr_availability_group_states hags
INNER JOIN sys.availability_groups ag ON ag.group_id = hags.group_id
WHERE
ag.name = @AGName;
IF UPPER(@PrimaryReplica) = UPPER(@@SERVERNAME)
RETURN 1; -- основной
RETURN 0; -- не основной
END;
GO
USE master;
GO
IF OBJECT_ID('dbo.dba_SyncCheckupForInstanceLevelJobs','P') IS NOT NULL
DROP PROC dbo.dba_SyncCheckupForInstanceLevelJobs
GO
CREATE PROC dbo.dba_SyncCheckupForInstanceLevelJobs
@MailProfileName sysname = NULL, -- напр., 'General'
@OperatorName sysname = NULL, -- напр., 'Alerts',
@PrimaryAGAfinity sysname = NULL, -- см. комментарии выше.
@IgnoredJobs nvarchar(MAX) = ‘’,
@ConsoleOnly bit = 0 -- выводится только
на консоль - не направляйте предупреждения по электронной
почте (об отладке или выполнении в ручном режиме и т.д.).
AS
SET NOCOUNT ON;
IF @PrimaryAGAfinity IS NOT NULL BEGIN
IF (SELECT dbo.fn_hadr_group_is_primary(@PrimaryAGAfinity))
= 0 BEGIN
PRINT 'Server is Not Primary.'
RETURN 0;
END
END
-- если мы не выполняем данную операцию в ручном режиме,
удостоверьтесь, что сервер является основным:
IF @ConsoleOnly = 0 BEGIN - если мы не выполняем данную
операцию в ручном режиме, удостоверьтесь, что мы
располагаем всеми параметрами:
IF ISNULL(@MailProfileName, '') = '' BEGIN
RAISERROR('Procedure expects parameter
''@MailProfileName'' which was not provided.', 16, 4);
RETURN -2;
END
IF ISNULL(@OperatorName, '') = '' BEGIN
RAISERROR('Procedure expects parameter
''@OperatorName'' which was not provided.', 16, 4);
RETURN -3;
END
END;
CREATE TABLE #IgnoredJobs (
job_name sysname
);
-- однострочная или встроенная ‘разделенная’ функция:
DECLARE @DeserializedJobs nvarchar(MAX) = N'SELECT '
+ REPLACE(REPLACE(REPLACE(N'''{0}''','{0}',@IgnoredJobs),
',', ''','''), ',', ' UNION SELECT ');
INSERT INTO #IgnoredJobs
EXEC(@DeserializedJobs);
DECLARE @LocalServerName sysname = @@SERVERNAME;
DECLARE @RemoteServerName sysname;
SET @RemoteServerName = (SELECT TOP 1 name
FROM PARTNER.master.sys.servers WHERE server_id = 0);
----------------------------------------------------------------------------
-- Начинаем с проверки всех заданий, не сопоставленных
с категориями, представляющими их имя ...
DECLARE @AvailabilityGroupDatabases TABLE (
name sysname NOT NULL
);
INSERT INTO @AvailabilityGroupDatabases
SELECT name FROM sys.availability_groups
UNION
SELECT name FROM PARTNER.master.sys.availability_groups;
CREATE TABLE #LocalJobs (
job_id uniqueidentifier,
name sysname,
[enabled] tinyint,
[description] nvarchar(512),
start_step_id int,
owner_sid varbinary(85),
notify_level_email int,
notify_email_operator_id int
);
CREATE TABLE #RemoteJobs (
job_id uniqueidentifier,
name sysname,
[enabled] tinyint,
[description] nvarchar(512),
start_step_id int,
owner_sid varbinary(85),
notify_level_email int,
notify_email_operator_id int
);
INSERT INTO #LocalJobs
SELECT
sj.job_id,
sj.name,
sj.[enabled],
sj.[description],
sj.start_step_id,
sj.owner_sid,
sj.notify_level_email,
sj.notify_email_operator_id
FROM
msdb.dbo.sysjobs sj
INNER JOIN msdb.dbo.syscategories sc ON sj.category_id =
sc.category_id
WHERE
-- categoryName рассматриваемого задания != an AG Group Name
UPPER(sc.name) NOT IN (SELECT UPPER(name)
FROM @AvailabilityGroupDatabases);
INSERT INTO #RemoteJobs
SELECT
sj.job_id,
sj.name,
sj.[enabled],
sj.[description],
sj.start_step_id,
sj.owner_sid,
sj.notify_level_email,
sj.notify_email_operator_id
FROM
PARTNER.msdb.dbo.sysjobs sj
INNER JOIN PARTNER.msdb.dbo.syscategories sc ON
sj.category_id = sc.category_id
WHERE
UPPER(sc.name) NOT IN (SELECT UPPER(name)
FROM @AvailabilityGroupDatabases);
CREATE TABLE #Divergence (
name sysname,
diff_type sysname
);
INSERT INTO #Divergence
SELECT
name,
'Job Exists on ' + @LocalServerName + ' Only.'
FROM
#LocalJobs
WHERE
name NOT IN (SELECT name FROM #RemoteJobs);
INSERT INTO #Divergence
SELECT
name,
'Job Exists on ' + @RemoteServerName + ' Only.'
FROM
#RemoteJobs
WHERE
name NOT IN (SELECT name FROM #LocalJobs);
INSERT INTO #Divergence
SELECT
lj.name,
'Job-Level Differences (enabled, start-step, notification, etc)'
FROM
#LocalJobs lj
INNER JOIN #RemoteJobs rj ON rj.name = lj.name
WHERE
lj.[enabled] != rj.[enabled]
OR lj.start_step_id != rj.start_step_id
OR lj.notify_email_operator_id != rj.notify_email_operator_id
OR lj.notify_level_email != rj.notify_level_email;
CREATE TABLE #LocalJobSteps (
step_id int,
[checksum] int
);
CREATE TABLE #RemoteJobSteps (
step_id int,
[checksum] int
);
CREATE TABLE #LocalJobSchedules (
schedule_name sysname,
[checksum] int
);
CREATE TABLE #RemoteJobSchedules (
schedule_name sysname,
[checksum] int
);
-- Теперь (сопоставляя одно задание с другим) пройдем
по всем данным и проверим расписание, а также шаги заданий:
DECLARE checker FAST_FORWARD FOR
SELECT
lj.job_id local_job_id,
rj.job_id remote_job_id,
lj.name
FROM
#LocalJobs lj
INNER JOIN #RemoteJobs rj ON lj.name = rj.name;
DECLARE @LocalJobId uniqueidentifier,
@RemoteJobId uniqueidentifier, @JobName sysname;
DECLARE @LocalCount int, @RemoteCount int;
OPEN checker;
FETCH NEXT FROM checker INTO @LocalJobId, @RemoteJobId,
@JobName;
WHILE @@FETCH_STATUS = 0 BEGIN
-- сначала проверяются шаги заданий:
DELETE FROM #LocalJobSteps;
DELETE FROM #RemoteJobSteps;
INSERT INTO #LocalJobSteps
SELECT
step_id,
BINARY_CHECKSUM(step_name + subsystem + command
+ STR(on_success_action) + STR(on_fail_action)
+ database_name) [detail]
FROM msdb.dbo.sysjobsteps
WHERE job_id = @LocalJobId;
INSERT INTO #RemoteJobSteps
SELECT
step_id,
BINARY_CHECKSUM(step_name + subsystem + command
+ STR(on_success_action) + STR(on_fail_action)
+ database_name) [detail]
FROM PARTNER.msdb.dbo.sysjobsteps
WHERE job_id = @RemoteJobId;
SELECT @LocalCount = COUNT(*) FROM #LocalJobSteps;
SELECT @RemoteCount = COUNT(*) FROM #RemoteJobSteps;
IF @LocalCount != @RemoteCount
INSERT INTO #Divergence VALUES (@JobName,
'Different Job Step Counts between Servers');
ELSE BEGIN
INSERT INTO #Divergence
SELECT
@JobName,
'Different Job Step Details between Servers'
FROM
#LocalJobSteps ljs
INNER JOIN #RemoteJobSteps rjs ON rjs.step_id =
ljs.step_id
WHERE
ljs.[checksum] != rjs.[checksum];
END;
-- Теперь проверяем расписания:
DELETE FROM #LocalJobSchedules;
DELETE FROM #RemoteJobSchedules;
INSERT INTO #LocalJobSchedules
SELECT
ss.name,
BINARY_CHECKSUM(ss.[enabled] + ss.freq_type
+ ss.freq_interval + ss.freq_subday_type +
ss.freq_subday_interval + ss.freq_relative_interval
+ ss.freq_recurrence_factor +
ss.active_start_date + ss.active_end_date
+ ss.active_start_date + ss.active_end_time) [details]
FROM
msdb.dbo.sysjobschedules sjs
INNER JOIN msdb.dbo.sysschedules ss ON ss.schedule_id =
sjs.schedule_id
WHERE
sjs.job_id = @LocalJobId;
INSERT INTO #RemoteJobSchedules
SELECT
ss.name,
BINARY_CHECKSUM(ss.[enabled] + ss.freq_type
+ ss.freq_interval + ss.freq_subday_type +
ss.freq_subday_interval + ss.freq_relative_interval
+ ss.freq_recurrence_factor +
ss.active_start_date + ss.active_end_date
+ ss.active_start_date + ss.active_end_time) [details]
FROM
PARTNER.msdb.dbo.sysjobschedules sjs
INNER JOIN PARTNER.msdb.dbo.sysschedules ss
ON ss.schedule_id = sjs.schedule_id
WHERE
sjs.job_id = @RemoteJobId;
SELECT @LocalCount = COUNT(*) FROM #LocalJobSchedules;
SELECT @RemoteCount = COUNT(*) FROM #RemoteJobSchedules;
IF @LocalCount != @RemoteCount
INSERT INTO #Divergence VALUES (@JobName,
'Different Job Schedule Counts between Servers');
ELSE BEGIN
INSERT INTO #Divergence
SELECT
@JobName,
'Different Schedule Details between Servers.'
FROM
#LocalJobSchedules ljs
INNER JOIN #RemoteJobSchedules rjs
ON rjs.schedule_name = ljs.schedule_name
WHERE
ljs.[checksum] != rjs.[checksum];
END;
FETCH NEXT FROM checker INTO @LocalJobId,
@RemoteJobId, @JobName;
END;
CLOSE checker;
DEALLOCATE checker;
IF(SELECT COUNT(*) FROM #Divergence WHERE name
NOT IN(SELECT job_name FROM #IgnoredJobs)) > 0 BEGIN
DECLARE @subject sysname = 'SQL Server Agent Job
Synchronization Problems';
DECLARE @crlf char(2) = CHAR(13) + CHAR(10);
DECLARE @tab char(1) = CHAR(9);
DECLARE @message nvarchar(MAX) = 'Problems detected
with the following SQL Server Agent Jobs: '
+ @crlf;
SELECT
@message = @message + @tab + name + ': ' + @crlf
+ @tab + @tab + diff_type + @crlf
FROM
#Divergence
WHERE
name NOT IN (SELECT job_name FROM #IgnoredJobs
WHERE job_name != '')
ORDER BY
NAME;
SELECT @message += @crlf + @crlf + 'Jobs can be synchronized
by scripting them on the Primary and running scripts
on the Seconary.'
+ @crlf + @tab + 'To Script Multiple Jobs at once,
SSMS > SQL Server Agent Jobs > F7 -> then shift/ctrl
+ click to select multiple jobs simultaneously.'
IF @ConsoleOnly = 1 BEGIN
-- распечатываем детали:
PRINT 'SUBJECT: ' + @subject;
PRINT 'BODY: ' + @crlf + @message;
END
ELSE BEGIN
-- отправляем послание:
EXEC msdb..sp_notify_operator
@profile_name = @MailProfileName,
@name = @OperatorName,
@subject = @subject,
@body = @message;
END;
END;
DROP TABLE #LocalJobs;
DROP TABLE #RemoteJobs;
DROP TABLE #Divergence;
DROP TABLE #LocalJobSteps;
DROP TABLE #RemoteJobSteps;
DROP TABLE #LocalJobSchedules;
DROP TABLE #RemoteJobSchedules;
DROP TABLE #IgnoredJobs;
RETURN 0;
Листинг 3. Проверка заданий уровня группы доступности
USE master;
GO
IF OBJECT_ID('dbo.dba_SyncCheckupForAGLevelJobs','P') IS NOT NULL
DROP PROC dbo.dba_SyncCheckupForAGLevelJobs;
GO
CREATE PROC dbo.dba_SyncCheckupForAGLevelJobs
@AGName sysname, -- Необходимо
@JobsTableDBName sysname, -- Необходимо (извещает
нас о том, в какой базе
данных размещается список
активированных/
деактивированных заданий).
@MailProfileName sysname = NULL, -- факультативно -
требуется лишь если
@ConsoleOnly = 0
@OperatorName sysname = NULL, -- см. выше
@IgnoredJobs nvarchar(MAX) = NULL, -- игнорируемые задания
(в категории с именем
MirroredDB)
@ConsoleOnly bit = 0 - передает результаты
на консоль (1), или
направляет
предупреждения/
результаты по каналам
электронной почты (0).
AS
SET NOCOUNT ON;
IF ISNULL(@AGName, '') = '' BEGIN
RAISERROR('Procedure expects parameter ''@AGName''
which was not provided.', 16, 4);
RETURN -1;
END
IF ISNULL(@JobsTableDBName, ‘’) = ‘’ BEGIN
RAISERROR(‘Procedure expects parameter ‘’@JobsTableDBName’’ which was not provided.’, 16, 4);
RETURN -2;
END
IF @ConsoleOnly = 0 BEGIN - если мы не выполняем процедуру
‘вручную’ - удостоверьтесь, что вы располагаем всеми параметрами:
IF ISNULL(@MailProfileName, '') = '' BEGIN
RAISERROR('Procedure expects parameter
''@MailProfileName'' which was not provided.', 16, 4);
RETURN -5;
END
IF ISNULL(@OperatorName, '') = '' BEGIN
RAISERROR('Procedure expects parameter
''@OperatorName'' which was not provided.', 16, 4);
RETURN -6;
END
END;
-- Начните с выполнения задания, которое прежде всего
синхронизирует или обновляет состояние заданий
«активировано/деактивировано»:
EXEC master.dbo.dba_EnsureAGLevelJobEnabledStatuses
@AGName, @JobsTableDBName;
-- Остальные проверки и отчеты необходимо выполнять только
на основном сервере.
IF(SELECT master.dbo.fn_hadr_group_is_primary(@AGName)) =
0 BEGIN
PRINT 'Server is Not Primary';
RETURN 0;
END
CREATE TABLE #IgnoredJobs (
job_name sysname
);
-- однострочная или встроенная ‘разделенная’ функция:
DECLARE @DeserializedJobs nvarchar(MAX) = N'SELECT '
+ REPLACE(REPLACE(REPLACE(N'''{0}''','{0}',@IgnoredJobs),
',', ''','''), ',', ' UNION SELECT ');
INSERT INTO #IgnoredJobs
EXEC(@DeserializedJobs);
CREATE TABLE #LocalJobs (
job_id uniqueidentifier,
name sysname,
[enabled] tinyint,
[description] nvarchar(512),
start_step_id int,
owner_sid varbinary(85),
notify_level_email int,
notify_email_operator_id int
);
CREATE TABLE #RemoteJobs (
job_id uniqueidentifier,
name sysname,
[enabled] tinyint,
[description] nvarchar(512),
start_step_id int,
owner_sid varbinary(85),
notify_level_email int,
notify_email_operator_id int
);
INSERT INTO #LocalJobs
SELECT
sj.job_id,
sj.name,
sj.[enabled],
sj.[description],
sj.start_step_id,
sj.owner_sid,
sj.notify_level_email,
sj.notify_email_operator_id
FROM
msdb.dbo.sysjobs sj
INNER JOIN msdb.dbo.syscategories sc ON sj.category_id =
sc.category_id
WHERE
UPPER(sc.name) = UPPER(@AGName);
INSERT INTO #RemoteJobs
SELECT
sj.job_id,
sj.name,
sj.[enabled],
sj.[description],
sj.start_step_id,
sj.owner_sid,
sj.notify_level_email,
sj.notify_email_operator_id
FROM
PARTNER.msdb.dbo.sysjobs sj
INNER JOIN PARTNER.msdb.dbo.syscategories sc ON
sj.category_id = sc.category_id
WHERE
UPPER(sc.name) = UPPER(@AGName);
-- Теперь начнем сопоставление различий:
CREATE TABLE #Divergence (
name sysname,
diff_type nvarchar(300)
);
INSERT INTO #Divergence
SELECT
name,
'Exists on Primary Server Only'
FROM
#LocalJobs
WHERE
name NOT IN (SELECT name FROM #RemoteJobs);
INSERT INTO #Divergence
SELECT
name,
'Exists on Secondary Server Only'
FROM
#RemoteJobs
WHERE
name NOT IN (SELECT name FROM #LocalJobs);
-- проверим свойства мета-данных:
INSERT INTO #Divergence
SELECT
lj.name,
'Job-Level Differences (owner, start-step, notification, etc)'
FROM
#LocalJobs lj
INNER JOIN #RemoteJobs rj ON rj.name = lj.name
WHERE
lj.owner_sid != rj.owner_sid
OR lj.start_step_id != rj.start_step_id
OR lj.notify_email_operator_id != rj.notify_email_operator_id
OR lj.notify_level_email != rj.notify_level_email;
-- Для подтверждения состояния «задание активировано» мы
имеем две проверки:
-- A: Ни одно задание, у которого Job.CategoryName =
@AGName, не должно быть активировано
на ВТОРИЧНОМ сервере.
-- B: мы не можем сделать такое же заключение
применительно к ОСНОВНОМУ серверу. Поэтому если
Job.CategoryName = @AGName и
-- enabled/disabled != Enabled битовому флагу из
таблицы _JobEnabledStates, тогда нам нужно
-- инициировать ошибку, поскольку задание, состояние
которого должно быть «активировано», на деле
НЕ является таковым (или наоборот).
-- A) Проверка заданий на удаленном сервере:
INSERT INTO #Divergence
SELECT
name,
'Job Enabled on SECONDARY SERVER (Jobs should only be
enabled on PRIMARY).'
FROM
#RemoteJobs
WHERE
enabled = 1;
-- B) Проверьте задания на локальном сервере:
DECLARE @Jobs TABLE (
JobName sysname NOT NULL,
[Enabled] bit NOT NULL
)
DECLARE @sql nvarchar(MAX) = N'SELECT JobName,
[Enabled] FROM ' + @JobsTableDBName + '.dbo.[' + @AGName
+ '_JobEnabledStates];'
INSERT INTO @Jobs
EXEC(@sql);
-- Сообщите о незадокументированных заданиях:
INSERT INTO #Divergence
SELECT
lj.name,
'Job Enabled/Disabled Status is not currently defined.
Please check job Enabled/Disabled status then run «EXEC
master.dbo.dba_DocumentJobEnabledStatuses ''' + @AGName
+ ''';» to document Job Enabled/Disabled state.'
FROM
#LocalJobs lj
WHERE
lj.name NOT IN (SELECT JobName FROM @Jobs);
-- Сообщите о заданиях, которые активированы, но не должны
быть таковыми:
INSERT INTO #Divergence
SELECT
lj.name,
'Job is ENABLED on Primary Replica Server but ' +
@JobsTableDBName + '.dbo.[' + @AGName +
'_JobEnabledStates] specifies that Job should be DISABLED.
(Update Table or Job Status to correct this issue.)'
FROM
#LocalJobs lj
LEFT OUTER JOIN @Jobs j ON lj.name = j.JobName
WHERE
ISNULL(j.[Enabled],0) = 0 AND lj.[enabled] = 1; --
задание зарегистрировано или установлено по умолчанию
как деактивированное, но в настоящее время активировано.
-- Сообщите о заданиях, которые деактивированы,
но не должны быть в этом состоянии:
INSERT INTO #Divergence
SELECT
lj.name,
'Job is DISABLED on Primary Replica Server but '
+ @JobsTableDBName + '.dbo.[' + @AGName +
'_JobEnabledStates] specifies that Job should
be _ENABLED_ (Running). (Update Table or Job Status
to correct this issue.)'
FROM
#LocalJobs lj
LEFT OUTER JOIN @Jobs j ON lj.name = j.JobName
WHERE
ISNULL(j.[Enabled], 0) = 1 AND lj.[enabled] = 0;
-- задание зарегистрировано или установлено по умолчанию
как активированное, но в настоящее время деактивировано
-- Теперь (сопоставляя задание с заданием) пройдитесь
по всем данным и проверьте графики, а также шаги заданий:
CREATE TABLE #LocalJobSteps (
step_id int,
[checksum] int
);
CREATE TABLE #RemoteJobSteps (
step_id int,
[checksum] int
);
CREATE TABLE #LocalJobSchedules (
schedule_name sysname,
[checksum] int
);
CREATE TABLE #RemoteJobSchedules (
schedule_name sysname,
[checksum] int
);
DECLARE checker FAST_FORWARD FOR
SELECT
lj.job_id local_job_id,
rj.job_id remote_job_id,
lj.name
FROM
#LocalJobs lj
INNER JOIN #RemoteJobs rj ON lj.name = rj.name;
DECLARE @LocalJobId uniqueidentifier, @RemoteJobId
uniqueidentifier, @JobName sysname;
DECLARE @LocalCount int, @RemoteCount int;
OPEN checker;
FETCH NEXT FROM checker INTO @LocalJobId, @RemoteJobId,
@JobName;
WHILE @@FETCH_STATUS = 0 BEGIN
-- прежде всего, проверьте шаги заданий:
DELETE FROM #LocalJobSteps;
DELETE FROM #RemoteJobSteps;
INSERT INTO #LocalJobSteps
SELECT
step_id,
BINARY_CHECKSUM(step_name + subsystem + command
+ STR(on_success_action) + STR(on_fail_action)
+ database_name) [detail]
FROM msdb.dbo.sysjobsteps
WHERE job_id = @LocalJobId;
INSERT INTO #RemoteJobSteps
SELECT
step_id,
BINARY_CHECKSUM(step_name + subsystem + command
+ STR(on_success_action) + STR(on_fail_action)
+ database_name) [detail]
FROM PARTNER.msdb.dbo.sysjobsteps
WHERE job_id = @RemoteJobId;
SELECT @LocalCount = COUNT(*) FROM #LocalJobSteps;
SELECT @RemoteCount = COUNT(*) FROM #RemoteJobSteps;
IF @LocalCount != @RemoteCount
INSERT INTO #Divergence VALUES (@JobName,
'Different Job Step Counts between Servers');
ELSE BEGIN
INSERT INTO #Divergence
SELECT
@JobName,
'Different Job Step Details between Servers'
FROM
#LocalJobSteps ljs
INNER JOIN #RemoteJobSteps rjs ON rjs.step_id =
ljs.step_id
WHERE
ljs.[checksum] != rjs.[checksum];
END;
-- Теперь проверьте графики:
DELETE FROM #LocalJobSchedules;
DELETE FROM #RemoteJobSchedules;
INSERT INTO #LocalJobSchedules
SELECT
ss.name,
BINARY_CHECKSUM(ss.[enabled] + ss.freq_type
+ ss.freq_interval + ss.freq_subday_type +
ss.freq_subday_interval + ss.freq_relative_interval
+ ss.freq_recurrence_factor +
ss.active_start_date + ss.active_end_date
+ ss.active_start_date + ss.active_end_time) [details]
FROM
msdb.dbo.sysjobschedules sjs
INNER JOIN msdb.dbo.sysschedules ss ON ss.schedule_id =
sjs.schedule_id
WHERE
sjs.job_id = @LocalJobId;
INSERT INTO #RemoteJobSchedules
SELECT
ss.name,
BINARY_CHECKSUM(ss.[enabled] + ss.freq_type
+ ss.freq_interval + ss.freq_subday_type +
ss.freq_subday_interval + ss.freq_relative_interval
+ ss.freq_recurrence_factor +
ss.active_start_date + ss.active_end_date
+ ss.active_start_date + ss.active_end_time) [details]
FROM
PARTNER.msdb.dbo.sysjobschedules sjs
INNER JOIN PARTNER.msdb.dbo.sysschedules ss
ON ss.schedule_id = sjs.schedule_id
WHERE
sjs.job_id = @RemoteJobId;
SELECT @LocalCount = COUNT(*) FROM #LocalJobSchedules;
SELECT @RemoteCount = COUNT(*) FROM #RemoteJobSchedules;
IF @LocalCount != @RemoteCount
INSERT INTO #Divergence VALUES (@JobName,
'Different Job Schedule Counts between Servers');
ELSE BEGIN
INSERT INTO #Divergence
SELECT
@JobName,
'Different Schedule Details between servers.'
FROM
#LocalJobSchedules ljs
INNER JOIN #RemoteJobSchedules rjs ON
rjs.schedule_name = ljs.schedule_name
WHERE
ljs.[checksum] != rjs.[checksum];
END;
FETCH NEXT FROM checker INTO @LocalJobId,
@RemoteJobId, @JobName;
END;
CLOSE checker;
DEALLOCATE checker;
IF(SELECT COUNT(*) FROM #Divergence WHERE name
NOT IN(SELECT job_name FROM #IgnoredJobs)) > 0 BEGIN
DECLARE @subject sysname = 'SQL Server Agent
Job Synchronization Problems';
DECLARE @crlf char(2) = CHAR(13) + CHAR(10);
DECLARE @tab char(1) = CHAR(9);
DECLARE @message nvarchar(MAX) = 'Problems detected
with the following SQL Server Agent Jobs: '
+ @crlf;
SELECT
@message = @message + @tab + name + ': ' + @crlf
+ @tab + @tab + diff_type + @crlf
FROM
#Divergence
WHERE
name NOT IN (SELECT job_name FROM #IgnoredJobs
WHERE job_name != '')
ORDER BY
NAME;
SELECT @message += @crlf + @crlf + 'Jobs can be synchronized
by scripting them on the Primary and running scripts
on the Seconary.'
+ @crlf + @tab + 'To Script Multiple Jobs at once,
SSMS > SQL Server Agent Jobs > F7 -> then shift/ctrl
+ click to select multiple jobs simultaneously.'
IF @ConsoleOnly = 1 BEGIN
-- распечатайте детали:
PRINT 'SUBJECT: ' + @subject;
PRINT 'BODY: ' + @crlf + @message;
END
ELSE BEGIN
-- отправьте сообщение:
EXEC msdb..sp_notify_operator
@profile_name = @MailProfileName,
@name = @OperatorName,
@subject = @subject,
@body = @message;
END;
END;
DROP TABLE #LocalJobs;
DROP TABLE #RemoteJobs;
DROP TABLE #Divergence;
DROP TABLE #LocalJobSteps;
DROP TABLE #RemoteJobSteps;
DROP TABLE #LocalJobSchedules;
DROP TABLE #RemoteJobSchedules;
DROP TABLE #IgnoredJobs;
RETURN 0;
GO
USE master;
GO
IF OBJECT_ID('dbo.dba_EnsureAGLevelJobEnabledStatuses','P')
IS NOT NULL
DROP PROC dbo.dba_EnsureAGLevelJobEnabledStatuses
GO
CREATE PROC dbo.dba_EnsureAGLevelJobEnabledStatuses
@AGName sysname, -- имя группы доступности, на которую будет осуществлено аварийное переключение.
@JobsTableDBName sysname - имя базы данных, которая содержит таблицу с деталями активированных заданий
AS
SET NOCOUNT ON;
IF(SELECT master.dbo.fn_hadr_group_is_primary(@AGName)) = 0 BEGIN
-- Если мы находимся не на ОСНОВНОМ сервере,
позаботьтесь о том, чтобы все задания с именем категории
categoryName = @AGName были деактивированы
DECLARE disabler FAST_FORWARD FOR
SELECT
j.job_id,
j.name
FROM
msdb.dbo.sysjobs j
INNER JOIN msdb.dbo.syscategories c ON c.category_id =
j.category_id
WHERE
j.[enabled] = 1 AND
c.name = @AGName;
DECLARE @JobId uniqueidentifier;
DECLARE @JobName sysname;
OPEN disabler;
FETCH NEXT FROM disabler INTO @JobId, @JobName;
WHILE @@FETCH_STATUS = 0 BEGIN
EXEC msdb.dbo.sp_update_job
@job_id = @JobId,
@enabled = 0;
PRINT 'Disabled Job: [' + @JobName + '] ON Non-Primary Server.';
FETCH NEXT FROM disabler INTO @JobId, @JobName;
END;
CLOSE disabler;
DEALLOCATE disabler;
END ;
ELSE BEGIN
-- иначе, если мы НАХОДИМСЯ на основном сервере,
мы должны АКТИВИРОВАТЬ задания, которые нужно
активировать.
DECLARE @Error sysname;
DECLARE @Jobs TABLE (
JobName sysname NOT NULL,
[Enabled] bit NOT NULL
);
DECLARE @sql nvarchar(MAX) = N'SELECT JobName,
[Enabled] FROM ' + @JobsTableDBName + '.dbo.['
+ @AGName + '_JobEnabledStates];'
INSERT INTO @Jobs
EXEC(@sql);
DECLARE enabler FAST_FORWARD FOR
SELECT
JobName
FROM
@Jobs
WHERE
[Enabled] = 1;
OPEN enabler;
FETCH NEXT FROM enabler INTO @JobName;
WHILE @@FETCH_STATUS = 0 BEGIN
SELECT @JobId = job_id FROM msdb.dbo.sysjobs
WHERE name = @JobName;
IF @JobId IS NULL BEGIN
SET @Error = 'Unable to Locate (and set ENABLED)
on Job: ' + @JobName + '.';
THROW 50001, @Error, 1;
RETURN -1; -- готово. Должны ли мы отправить
предупреждение по электронной почте? Возможно.
END
EXEC msdb.dbo.sp_update_job
@job_id = @JobId,
@enabled = 1; -- активируйте задание здесь...
FETCH NEXT FROM enabler INTO @JobName;
END;
CLOSE enabler;
DEALLOCATE enabler;
END;
RETURN 0;
GO