Trabalhos do SQL Server Agent e grupos de disponibilidade

37

Estou procurando as melhores práticas para lidar com trabalhos agendados do SQL Server Agent em grupos de disponibilidade do SQL Server 2012. Talvez eu tenha perdido algo, no entanto, no estado atual, sinto que o SQL Server Agent não está realmente integrado com esse ótimo recurso do SQL2012.

Como posso informar um trabalho agendado do agente SQL sobre uma opção de nó? Por exemplo, tenho um trabalho em execução no nó principal que carrega dados a cada hora. Agora, se o primário cair, como posso ativar o trabalho no secundário, que agora se torna primário?

Se eu agendar o trabalho sempre no secundário, ele falhará porque o secundário será somente leitura.

nojetlag
fonte

Respostas:

40

Na tarefa do SQL Server Agent, tenha alguma lógica condicional para testar se a instância atual está cumprindo a função específica que você está procurando no seu grupo de disponibilidade:

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

Tudo o que isso faz é extrair a função atual da réplica local e, se ela estiver na PRIMARYfunção, você poderá fazer o que for necessário para o seu trabalho, se for a réplica primária. O ELSEbloco é opcional, mas é possível manipular a lógica possível se sua réplica local não for primária.

Obviamente, altere 'YourAvailabilityGroupName'a consulta acima para o nome do seu grupo de disponibilidade real.

Não confunda grupos de disponibilidade com instâncias de cluster de failover. Se a instância é a réplica primária ou secundária de um determinado grupo de disponibilidade não afeta os objetos no nível do servidor, como os trabalhos do SQL Server Agent e assim por diante.

Thomas Stringer
fonte
14

Em vez de fazer isso por tarefa (verificando todos os trabalhos para o estado do servidor antes de decidir continuar), criei um trabalho em execução nos dois servidores para verificar em que estado o servidor está.

  • Se for primário, ative qualquer trabalho que tenha uma etapa visando um banco de dados no AG.
  • Se o servidor for secundário, desative qualquer trabalho destinado a um banco de dados no AG.

Essa abordagem fornece várias coisas

  • funciona em servidores onde não há bancos de dados no AG (ou uma mistura de DBs dentro / fora dos AGs)
  • qualquer um pode criar um novo trabalho e não precisa se preocupar se o banco de dados está em um AG (embora eles tenham que se lembrar de adicionar o trabalho ao outro servidor)
  • Permite que cada trabalho tenha um email de falha que permanece útil (todos os seus trabalhos têm emails de falha, certo?)
  • Ao visualizar o histórico de um trabalho, você realmente vê se o trabalho realmente foi executado e fez alguma coisa (sendo a principal), em vez de ver uma longa lista de sucessos que realmente não executaram nada (no secundário)

o script verifica o banco de dados no campo abaixo se esse banco de dados estiver em um grupo de disponibilidade, o script executará alguma ação

Este processo é executado a cada 15 minutos em cada servidor. (tem o bônus adicional de anexar um comentário para informar às pessoas por que o trabalho foi desativado)

/*
    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

Não é à prova de idiotas, mas para cargas noturnas e trabalhos por hora, ele faz o trabalho.

Ainda melhor do que executar esse procedimento em um agendamento, execute-o em resposta ao Alerta 1480 (alerta de alteração de função do AG).

Trubs
fonte
9

Estou ciente de dois conceitos para fazer isso.

Pré-requisito: Com base na resposta de Thomas Stringer, criei duas funções no banco de dados mestre de nossos dois servidores:

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. Tornar um trabalho encerrado se não for executado na réplica primária

    Nesse caso, todos os trabalhos nos dois servidores precisam de um dos dois trechos de código a seguir na Etapa 1:

    Verifique pelo nome do grupo:

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

    Verifique pelo nome do banco de dados:

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

    Se você usar este segundo, tenha cuidado com os bancos de dados do sistema - por definição, eles não podem fazer parte de nenhum grupo de disponibilidade; portanto, eles sempre falham.

    Ambos funcionam imediatamente para usuários administrativos. Para usuários não administradores, é necessário adicionar permissões extras, uma delas sugerida aqui :

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

    Se você definir a ação de falha como Sair do relatório de trabalhos com êxito nesta primeira etapa, não receberá o registro de trabalhos cheio de sinais feios de cruz vermelha; para o trabalho principal, eles se transformarão em sinais de aviso amarelos.

    De nossa experiência, isso não é o ideal. A princípio, adotamos essa abordagem, mas rapidamente perdemos o controle sobre a localização de tarefas que realmente tinham um problema, porque todas as tarefas de réplica secundária estavam repletas de logs de tarefas com mensagens de aviso.

    O que procuramos então é:

  2. Trabalhos de proxy

    Se você adotar esse conceito, precisará criar dois trabalhos por tarefa que deseja executar. O primeiro é o "trabalho de proxy" que verifica se está sendo executado na réplica primária. Nesse caso, inicia o "trabalho do trabalhador"; caso contrário, termina normalmente sem desorganizar o log com mensagens de aviso ou erro.

    Embora eu pessoalmente não goste da ideia de ter dois trabalhos por tarefa em todos os servidores, acho que é definitivamente mais sustentável, e você não precisa definir a ação de falha da etapa para Sair do sucesso do relatório de trabalhos , o que é um pouco desajeitado.

    Para os trabalhos, adotamos um esquema de nomeação. O trabalho de proxy é apenas chamado {put jobname here}. O trabalho do trabalhador é chamado {put jobname here} worker. Isso torna possível automatizar o início do trabalho do trabalhador a partir do proxy. Para fazer isso, adicionei o seguinte procedimento aos dois dbs principais:

    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

    Isso utiliza a svf_AgReplicaStatefunção mostrada acima, você pode facilmente mudar isso para verificar usando o nome do banco de dados, chamando a outra função.

    De dentro da única etapa do trabalho de proxy, você o chama assim:

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

    Isso utiliza Tokens, como mostrado aqui e aqui, para obter o ID do trabalho atual. O procedimento obtém o nome do trabalho atual do msdb, anexa  workera ele e inicia o trabalho do trabalhador usando sp_start_job.

    Embora isso ainda não seja o ideal, ele mantém os logs de tarefas mais organizados e com manutenção do que a opção anterior. Além disso, você sempre pode executar o trabalho de proxy com um usuário sysadmin, portanto, não é necessário adicionar permissões extras.

takrl
fonte
3

Se o processo de carregamento de dados for uma consulta simples ou chamada de procedimento, você poderá criar o trabalho nos dois nós e permitir que ele determine se é o nó principal com base na propriedade Updateability do banco de dados, antes de executar o processo de carregamento de dados:

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

-- Data Load code goes under here

END
Yasin
fonte
1

É sempre melhor criar uma nova etapa da tarefa que verifique se é uma réplica primária e está tudo bem para continuar com a execução da tarefa; caso seja uma réplica secundária, interrompa a tarefa. Não falhe no trabalho, pois ele continuará enviando notificações desnecessárias. Em vez disso, pare o trabalho para que ele seja cancelado e nenhuma notificação seja enviada sempre que esses trabalhos forem executados na réplica secundária.

Abaixo está o script para adicionar uma primeira etapa para um trabalho específico.

Nota para executar o script:

  • Substitua 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' por Job_ID
  • Substitua 'YYYYYYYYYYYYYYYYYYYYYYYY' por Job_Name
  • Se houver vários Grupos de Disponibilidade, defina o nome do AG na variável @AGNameToCheck_IfMoreThanSingleAG sobre qual AG deve ser verificado quanto ao seu estado de réplica.

  • Observe também que esse script deve funcionar bem mesmo nos servidores que não possuem grupos de disponibilidade. Será executado apenas para versões do SQL Server 2012 e posteriores.

            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
Masood Hashim
fonte
0

Outra maneira é inserir uma etapa em cada tarefa, que deve ser executada primeiro, com o seguinte código:

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

Defina esta etapa para continuar com a próxima etapa do sucesso e encerrar o trabalho que informa o sucesso em uma falha.

Acho mais limpo adicionar uma etapa extra em vez de adicionar lógica extra a uma etapa existente.

KoeKk
fonte
0

Outra opção mais recente é usar master.sys.fn_hadr_is_primary_replica ('DbName'). Eu achei isso super útil ao usar o SQL Agent para fazer a manutenção do banco de dados (juntamente com um cursor que eu usei há anos) e também ao executar uma ETL ou outra tarefa específica do banco de dados. O benefício é que ele seleciona o banco de dados em vez de examinar todo o Grupo de Disponibilidade ... se é isso que você precisa. Também torna muito mais improvável que um comando seja executado em um banco de dados que "estava" no primário, mas digamos que ocorreu um failover automático durante a execução do trabalho, e agora ele está em uma réplica secundária. Os métodos acima, que examinam a réplica primária, dão uma olhada e não são atualizados. Lembre-se de que essa é apenas uma maneira diferente de obter resultados muito semelhantes e fornecer um controle mais granular, se você precisar. Além disso, a razão pela qual esse método não foi discutido quando essa pergunta foi feita é porque a Microsoft não lançou essa função até o lançamento do SQL 2014. Abaixo estão alguns exemplos de como essa função pode ser usada:

   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

Se você deseja usar isso para manutenção do banco de dados do usuário, é isso que eu uso:

/*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

Espero que seja uma dica útil!

SQL_Hacker
fonte
0

Eu uso isso:

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

end
aleksey vitsko
fonte