SQL Server代理作业和可用性组


37

我正在寻找在SQL Server 2012可用性组中处理计划的SQL Server代理作业的最佳实践。也许我错过了一些东西,但是在当前状态下,我觉得SQL Server Agent并没有真正与这一出色的SQL2012功能集成在一起。

如何使计划的SQL代理作业知道节点切换?例如,我有一个在主节点上运行的作业,该作业每小时加载一次数据。现在,如果主服务器出现故障,我如何激活现在成为主服务器的辅助服务器上的作业?

如果我将作业始终安排在辅助服务器上,则它将失败,因为辅助服务器是只读的。


Answers:


40

在您的SQL Server代理作业中,具有一些条件逻辑来测试当前实例是否在您的可用性组中担任您正在寻找的特定角色:

if (select
        ars.role_desc
    from sys.dm_hadr_availability_replica_states ars
    inner join sys.availability_groups ag
    on ars.group_id = ag.group_id
    where ag.name = 'YourAvailabilityGroupName'
    and ars.is_local = 1) = 'PRIMARY'
begin
    -- this server is the primary replica, do something here
end
else
begin
    -- this server is not the primary replica, (optional) do something here
end

所有这一切都是拉扯本地副本的当前角色,如果它是PRIMARY角色,则可以执行本地副本的工作而需要做的任何事情。该ELSE块是可选的,但是如果您的本地副本不是主要副本,它将处理可能的逻辑。

当然,'YourAvailabilityGroupName'在上面的查询中更改为您的实际可用性组名称。

不要将可用性组与故障转移群集实例混淆。实例是给定可用性组的主要副本还是辅助副本,都不会影响服务器级别的对象,例如SQL Server代理作业等。


14

我不是在每个作业的基础上执行此操作(在决定继续之前检查每个作业的服务器状态),而是在两个服务器上创建了一个作业,以检查服务器处于什么状态。

  • 如果是主要任务,则启用任何具有针对AG中的数据库的步骤的作业。
  • 如果服务器是辅助服务器,请禁用任何针对AG中数据库的作业。

这种方法提供了很多东西

  • 它适用于AG中没有数据库的服务器(或Db进/出AG的混合)
  • 任何人都可以创建新作业,而不必担心数据库是否在AG中(尽管他们确实要记住将作业添加到其他服务器中)
  • 允许每个作业都有一封仍然有用的失败电子邮件(您的所有作业都有失败电子邮件吗?)
  • 查看工作的历史记录时,您实际上可以查看工作是否真正运行并做了某件事(这是主要的),而不是看到一长串实际上没有执行任何操作的成功列表(在次要上)

该脚本在下面的字段中检查数据库 如果此数据库位于可用性组中,则脚本将采取一些措施

此proc在每台服务器上每15分钟执行一次。(具有附加注释以告知人们该工作为何被禁用的额外好处)

/*
    This proc goes through all SQL Server agent jobs and finds any that refer to a database taking part in the availability Group 
    It will then enable/disable the job dependant on whether the server is the primary replica or not   
        Primary Replica = enable job
    It will also add a comment to the job indicating the job was updated by this proc
*/
CREATE PROCEDURE dbo.sp_HADRAgentJobFailover (@AGname varchar(200) = 'AG01' )
AS 

DECLARE @SQL NVARCHAR(MAX)

;WITH DBinAG AS (  -- This finds all databases in the AG and determines whether Jobs targeting these DB's should be turned on (which is the same for all db's in the AG)
SELECT  distinct
        runJobs = CASE WHEN role_desc = 'Primary' THEN 1 ELSE 0 END   --If this is the primary, then yes we want to run the jobs
        ,dbname = db.name
        ,JobDescription = CASE WHEN hars.role_desc = 'Primary'  -- Add the reason for the changing the state to the Jobs description
                THEN '~~~ [Enabled] using automated process (DBA_tools.dbo.sp_HADRAgentJobFailover) looking for jobs running against Primary Replica AG ~~~ '
                ELSE '~~~ [Diabled] using Automated process (DBA_tools.dbo.sp_HADRAgentJobFailover) because the job cant run on READ-ONLY Replica AG~~~ ' END 
FROM sys.dm_hadr_availability_replica_states hars
INNER JOIN sys.availability_groups ag ON ag.group_id = hars.group_id
INNER JOIN sys.Databases db ON  db.replica_id = hars.replica_id
WHERE is_local = 1
AND ag.Name = @AGname
) 

SELECT @SQL = (
SELECT DISTINCT N'exec msdb..sp_update_job @job_name = ''' + j.name + ''', @enabled = ' + CAST(d.runJobs AS VARCHAR) 
                + ',@description = ''' 
                + CASE WHEN j.description = 'No description available.' THEN JobDescription -- if there is no description just add our JobDescription
                       WHEN PATINDEX('%~~~%~~~',j.description) = 0 THEN j.description + '    ' + JobDescription  -- If our JobDescription is NOT there, add it
                       WHEN PATINDEX('%~~~%~~~',j.description) > 0 THEN SUBSTRING(j.description,1,CHARINDEX('~~~',j.description)-1) + d.JobDescription  --Replace our part of the job description with what we are doing.
                       ELSE d.JobDescription  -- Should never reach here...
                    END 
                + ''';'
FROM msdb.dbo.sysjobs j
INNER JOIN msdb.dbo.sysjobsteps s
INNER JOIN DBinAG d ON d.DbName =s.database_name     
ON j.job_id = s.job_id
WHERE j.enabled != d.runJobs   -- Ensure we only actually update the job, if it needs to change
FOR XML PATH ('')
)
PRINT REPLACE(@SQL,';',CHAR(10))
EXEC sys.sp_executesql @SQL

它不是万无一失的,但是对于过夜的负载和每小时的工作,它可以完成工作。

比按计划运行此过程更好,而是响应警报1480(AG角色更改警报)运行它。


9

我知道实现此目的的两个概念。

先决条件:根据Thomas Stringer的回答,我在两台服务器的主数据库中创建了两个函数:

CREATE FUNCTION [dbo].[svf_AgReplicaState](@availability_group_name sysname)
RETURNS bit
AS
BEGIN

if EXISTS(
    SELECT        ag.name
    FROM            sys.dm_hadr_availability_replica_states AS ars INNER JOIN
                             sys.availability_groups AS ag ON ars.group_id = ag.group_id
    WHERE        (ars.is_local = 1) AND (ars.role_desc = 'PRIMARY') AND (ag.name = @availability_group_name))

    RETURN 1

RETURN 0

END
GO

CREATE FUNCTION [dbo].[svf_DbReplicaState](@database_name sysname)
RETURNS bit
AS
BEGIN

IF EXISTS(
    SELECT        adc.database_name
    FROM            sys.dm_hadr_availability_replica_states AS ars INNER JOIN
                             sys.availability_databases_cluster AS adc ON ars.group_id = adc.group_id
    WHERE        (ars.is_local = 1) AND (ars.role_desc = 'PRIMARY') AND (adc.database_name = @database_name))

    RETURN 1
RETURN 0

END

GO


  1. 如果未在主副本上执行作业,则使其终止

    对于这种情况,两台服务器上的每个作业都需要以下两个代码片段之一,作为步骤1:

    按组名检查:

    IF master.dbo.svf_AgReplicaState('my_group_name')=0
      raiserror ('This is not the primary replica.',2,1)
    

    按数据库名称检查:

    IF master.dbo.svf_AgReplicaState('my_db_name')=0
      raiserror ('This is not the primary replica.',2,1)
    

    如果使用第二个数据库,则要当心系统数据库-根据定义,它们不能成为任何可用性组的一部分,因此对于这些可用性组来说,它总是会失败的。

    两者对于管理员用户而言都是开箱即用的。对于非管理员用户,您必须添加额外的权限,此处建议其中之一:

    GRANT VIEW SERVER STATE TO [user];
    GRANT VIEW ANY DEFINITION TO [user];
    

    如果在第一步中将失败操作设置为“ 退出作业报告成功 ”,您将不会获得充满丑陋的红叉标志的作业日志,对于主要作业,它们将变成黄色警告标志。

    根据我们的经验,这不是理想的。我们最初采用这种方法,但是很快就找不到有关实际存在问题的作业,因为所有辅助副本作业都在警告日志中打乱了作业日志。

    然后我们追求的是:

  2. 代理职位

    如果采用此概念,则实际上需要为每个要执行的任务创建两个作业。第一个是“代理作业”,用于检查它是否正在主副本上执行。如果是这样,它将启动“工作者作业”,如果不是,它将正常结束而不会在日志中显示警告或错误消息。

    虽然我个人不喜欢在每台服务器上每个任务都包含两个作业的想法,但我认为它显然更易于维护,您不必将步骤的失败操作设置为“ 退出作业报告成功”,这有点尴尬。

    对于工作,我们采用了命名方案。代理作业称为{put jobname here}。工人的工作称为{put jobname here} worker。这使从代理服务器自动启动工作者作业成为可能。为此,我向两个主数据库添加了以下过程:

    CREATE procedure [dbo].[procStartWorkerJob](@jobId uniqueidentifier, @availabilityGroup sysname, @postfix sysname = ' worker') as
    declare @name sysname
    
    if dbo.svf_AgReplicaState(@availabilityGroup)=0
        print 'This is not the primary replica.'
    else begin
        SELECT @name = name FROM msdb.dbo.sysjobs where job_id = @jobId
    
        set @name = @name + @postfix
        if exists(select name from msdb.dbo.sysjobs where name = @name)
            exec msdb.dbo.sp_start_job @name
        else begin
            set @name = 'Job '''+@name+''' not found.'
            raiserror (@name ,2,1)
        end
    end
    GO
    

    这利用了svf_AgReplicaState上面显示的功能,您可以轻松地将其更改为使用数据库名称进行检查,而不是调用其他功能。

    在代理作业的唯一步骤中,您可以这样称呼它:

    exec procStartWorkerJob $(ESCAPE_NONE(JOBID)), '{my_group_name}'

    这利用了此处此处所示的令牌来获取当前作业的ID。然后,该过程从msdb获取当前作业名称,将 worker其追加并使用来启动工作程序作业sp_start_job

    尽管这仍然不理想,但与以前的选项相比,它使作业日志更加整洁和可维护。此外,您始终可以使代理作业与sysadmin用户一起运行,因此不需要添加任何其他权限。


3

如果数据加载过程是一个简单的查询或过程调用,那么您可以在执行数据加载过程之前,在两个节点上创建作业,并使其基于数据库的Updateability属性确定其主节点是否为主要节点:

IF (SELECT CONVERT(sysname,DatabasePropertyEx(DB_NAME(),'Updateability'))) != 'READ_ONLY'
BEGIN

-- Data Load code goes under here

END

1

始终最好创建一个新的作业步骤,以检查它是否是主副本,然后一切正常以继续执行该作业,否则,如果它是第二副本,则停止该作业。不要使作业失败,否则它将继续发送不必要的通知。而是停止该作业,以便取消该作业,并且在辅助副本服务器上执行这些作业时,不会发送任何通知。

以下是为特定作业添加第一步的脚本。

注意执行脚本:

  • 用Job_ID替换“ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX”
  • 用Job_Name替换'YYYYYYYYYYYYYYYYYYYYYYYYYY'
  • 如果存在多个可用性组,则在变量@AGNameToCheck_IfMoreThanSingleAG中设置AG名称,以了解应检查哪个AG的副本状态。

  • 还要注意,即使在没有可用性组的服务器上,此脚本也应能很好地工作。仅对SQL Server 2012及更高版本执行。

            USE [msdb]
            GO
            EXEC msdb.dbo.sp_add_jobstep @job_id=N'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', @step_name=N'CheckForSecondaryReplica', 
                    @step_id=1, 
                    @cmdexec_success_code=0, 
                    @on_success_action=3, 
                    @on_fail_action=2, 
                    @retry_attempts=0, 
                    @retry_interval=0, 
                    @os_run_priority=0, @subsystem=N'TSQL', 
                    @command=N'
            DECLARE @AGNameToCheck_IfMoreThanSingleAG VARCHAR(100)
            SET @AGNameToCheck_IfMoreThanSingleAG = ''AGName_IfMoreThanOneAG'' -- If there are Multiple AGs, then a single server can have Primary of one AG and Secondary of other. So Job creator has to define as to which AG needs to verified before the job is automatically run on Primary.
    
            DECLARE @NumberofAGs INT
            SELECT @NumberofAGs = COUNT(group_id) FROM sys.availability_groups ags
    
    
            IF(@NumberofAGs < 2)
                IF EXISTS(Select * FROM sys.dm_hadr_availability_replica_states hars WHERE role_desc = ''Secondary'' AND hars.is_local = 1)                 
                                    EXEC msdb.dbo.sp_stop_job N''YYYYYYYYYYYYYYYYYYYYYYYYYY'' ;
                                    --RAISERROR(''This is a Secondary Replica'',16,1)
    
            IF(@NumberofAGs >= 2)
                IF EXISTS(SELECT 1 FROM sys.availability_groups WHERE name = @AGNameToCheck_IfMoreThanSingleAG)
                BEGIN
                            IF EXISTS(Select * from  sys.availability_groups ag
                                            JOIN sys.dm_hadr_availability_replica_states hars
                                                        ON ag.group_id = hars.group_id
                                                        Where role_desc = ''Secondary''
                                                        AND hars.is_local = 1
                                                        AND ag.name = @AGNameToCheck_IfMoreThanSingleAG)
                            BEGIN
                                    EXEC msdb.dbo.sp_stop_job N''YYYYYYYYYYYYYYYYYYYYYYYYYY'' ;
                                    --RAISERROR(''This is a Secondary Replica'',16,1)
                            END
                END
                ELSE
                            BEGIN
                                    RAISERROR(''The Defined AG in the Variable is not a part of this Server. Please Check!!!!!!!!!!!'',16,1)
                            END', 
                    @database_name=N'master', 
                    @flags=0
            GO
    

0

另一种方法是在每个作业中插入一个步骤,该步骤应首先运行,并使用以下代码:

IF (SELECT ars.role_desc
    FROM sys.dm_hadr_availability_replica_states ars
    INNER JOIN sys.availability_groups ag
    ON ars.group_id = ag.group_id
    AND ars.is_local = 1) <> 'PRIMARY'
BEGIN
   --We're on the secondary node, throw an error
   THROW 50001, 'Unable to execute job on secondary node',1
END

设置此步骤可继续进行下一步的成功,并在失败时退出报告成功的作业。

我发现它更干净,可以添加一个额外的步骤,而不是向现有步骤添加额外的逻辑。


0

另一个较新的选项是使用master.sys.fn_hadr_is_primary_replica('DbName')。当使用SQL Agent进行数据库维护(再加上我已经使用了多年的游标)以及执行ETL或其他特定于数据库的任务时,我发现这种超级有用。好处是,它只选择数据库,而不是查看整个可用性组...如果您需要的话。对于在主数据库上“存在”的数据库执行命令也变得更加不可能,但是可以说在执行作业期间发生了自动故障转移,并且该命令现在位于辅助副本上。上面的查看主副本的方法只是一看而已,不会更新。请记住,这只是获得非常相似的结果并根据需要提供更精细控制的另一种方式。同样,在询问此问题时未讨论此方法的原因是,Microsoft直到SQL 2014发布后才发布此功能。以下是如何使用此功能的一些示例:

   IF master.dbo.fn_hadr_database_is_primary_replica('Admin') = 1
    BEGIN 
        -- do whatever you were going to do in the Primary:
        PRINT 'Doing stuff in the Primary Replica';
    END
ELSE 
    BEGIN 
        -- we're not in the Primary - exit gracefully:
        PRINT 'This is not the primary replica - exiting with success';
    END

如果要使用它进行用户数据库维护,这就是我使用的方法:

/*Below evaluates all user databases in the instance and gives stubs to do work; must change to get anything other than print statements*/
declare @dbname varchar(1000)
declare @sql nvarchar(4000)

declare AllUserDatabases cursor for
    select [name] from master.sys.databases
    where database_id > 4 --this excludes all sysdbs; if all but tempdb is desired, change to <> 2
    and [state] = 0

open AllUserDatabases
fetch AllUserDatabases into @dbname

while (@@FETCH_STATUS = 0)
    begin
    --PRINT @dbname
        set @sql = '
            IF master.sys.fn_hadr_is_primary_replica(''' + @dbname + ''') = 1
                BEGIN 
                    -- do whatever you are going to do in the Primary:
                    PRINT ''Doing stuff in the Primary Replica''
                END
            ELSE 
                BEGIN 
                    -- not in the Primary - exit gracefully:
                    PRINT ''This is not the primary replica - exiting with success''
                END             
        '
        exec sp_executesql @sql
        fetch AllUserDatabases into @dbname
    end
close AllUserDatabases
deallocate AllUserDatabases

希望这对您有帮助!


0

我用这个:

if (select primary_replica from sys.dm_hadr_availability_group_states) = @@SERVERNAME begin
... paste your t-sql here ...

end
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.