Lavori di SQL Server Agent e gruppi di disponibilità


37

Sto cercando le migliori pratiche per gestire i processi pianificati di SQL Server Agent nei gruppi di disponibilità di SQL Server 2012. Forse ho perso qualcosa, tuttavia allo stato attuale sento che SQL Server Agent non è realmente integrato con questa fantastica funzionalità SQL2012.

Come posso rendere un processo dell'agente SQL pianificato consapevole di uno switch di nodo? Ad esempio, ho un lavoro in esecuzione sul nodo primario che carica i dati ogni ora. Ora se il primario scende, come posso attivare il lavoro sul secondario che ora diventa primario?

Se pianifico il lavoro sempre sul secondario non riesce perché il secondario è di sola lettura.


Risposte:


40

All'interno del processo di SQL Server Agent, è necessario verificare la logica condizionale per verificare se l'istanza corrente ricopre il ruolo specifico che si sta cercando nel proprio gruppo di disponibilità:

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

Tutto ciò che fa è estrarre il ruolo corrente della replica locale e, se è nel PRIMARYruolo, puoi fare tutto ciò che il tuo lavoro deve fare se è la replica primaria. Il ELSEblocco è facoltativo, ma è per gestire la logica possibile se la replica locale non è primaria.

Naturalmente, modifica 'YourAvailabilityGroupName'la query sopra con il nome del tuo gruppo di disponibilità effettivo.

Non confondere i gruppi di disponibilità con le istanze del cluster di failover. Il fatto che l'istanza sia la replica primaria o secondaria per un determinato gruppo di disponibilità non influisce sugli oggetti a livello di server, come i processi di SQL Server Agent e così via.


14

Piuttosto che farlo su una base per lavoro (controllando ogni lavoro per lo stato del server prima di decidere di continuare), ho creato un lavoro in esecuzione su entrambi i server per verificare in quale stato si trova il server.

  • Se è primario, abilitare qualsiasi lavoro che abbia una fase destinata a un database nell'AG.
  • Se il server è secondario, disabilitare qualsiasi lavoro destinato a un database nell'AG.

Questo approccio fornisce una serie di cose

  • funziona su server in cui non ci sono database in AG (o un mix di Db in / out di AG)
  • chiunque può creare un nuovo lavoro e non doversi preoccupare se il db si trova in una AG (anche se devono ricordare di aggiungere il lavoro all'altro server)
  • Consente a ogni lavoro di avere un'e-mail di errore che rimane utile (tutti i lavori hanno e-mail di errore, giusto?)
  • Quando si visualizza la cronologia di un lavoro, si arriva effettivamente a vedere se il lavoro è stato effettivamente eseguito e fatto qualcosa (questo è il principale), piuttosto che vedere un lungo elenco di successi che in realtà non ha eseguito nulla (sul secondario)

lo script controlla il database nel campo sottostante se questo database si trova in un gruppo di disponibilità, lo script prenderà alcune misure

Questo proc viene eseguito ogni 15 minuti su ciascun server. (ha l'ulteriore vantaggio di aggiungere un commento per informare le persone sul perché il lavoro è stato disabilitato)

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

Non è infallibile, ma per carichi notturni e lavori orari fa il lavoro.

Ancora meglio che avere questa procedura eseguita su una pianificazione, invece eseguirla in risposta all'Avviso 1480 (avviso di cambio ruolo AG).


9

Sono consapevole di due concetti per raggiungere questo obiettivo.

Prerequisito: in base alla risposta di Thomas Stringer, ho creato due funzioni nel master db dei nostri due server:

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. Fare terminare un lavoro se non viene eseguito sulla replica primaria

    In questo caso, ogni lavoro su entrambi i server richiede uno dei due seguenti frammenti di codice come Passaggio 1:

    Controlla per nome del gruppo:

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

    Verifica per nome del database:

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

    Se usi questo secondo, fai attenzione ai database di sistema - per definizione non possono far parte di nessun gruppo di disponibilità, quindi falliranno sempre per quelli.

    Entrambi funzionano immediatamente per gli utenti amministratori. Per gli utenti non amministratori, devi aggiungere ulteriori autorizzazioni, una delle quali suggerita qui :

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

    Se imposti l'azione di errore su Esci dal processo di segnalazione del successo in questo primo passaggio, non otterrai il registro lavori pieno di brutti segni della croce rossa, per il lavoro principale si trasformeranno invece in segnali di avvertimento gialli.

    Dalla nostra esperienza, questo non è l'ideale. Inizialmente abbiamo adottato questo approccio, ma abbiamo perso rapidamente traccia per quanto riguarda la ricerca di lavori che in realtà avevano un problema, perché tutti i lavori di replica secondari ingombravano il registro dei lavori con messaggi di avviso.

    Ciò che abbiamo scelto è:

  2. Processi proxy

    Se adotti questo concetto, dovrai effettivamente creare due lavori per ogni attività che desideri eseguire. Il primo è il "processo proxy" che controlla se viene eseguito sulla replica primaria. In tal caso, avvia il "lavoro di lavoro", in caso contrario, termina semplicemente senza ingombrare il registro con messaggi di avviso o di errore.

    Mentre personalmente non mi piace l'idea di avere due lavori per attività su ogni server, penso che sia sicuramente più gestibile e non devi impostare l'azione di errore del passaggio per chiudere il processo di segnalazione dei lavori , che è un po ' imbarazzante.

    Per i lavori, abbiamo adottato uno schema di denominazione. Il processo proxy è appena chiamato {put jobname here}. Viene chiamato il lavoro lavoratore {put jobname here} worker. Ciò consente di automatizzare l'avvio del processo di lavoro dal proxy. Per fare ciò, ho aggiunto la seguente procedura a entrambi i master dbs:

    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

    Questo utilizza la svf_AgReplicaStatefunzione mostrata sopra, puoi facilmente cambiarla per controllare usando il nome del database invece chiamando l'altra funzione.

    Dall'unico passaggio del processo proxy, lo si chiama in questo modo:

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

    Questo utilizza i token come mostrato qui e qui per ottenere l'ID del lavoro corrente. La procedura quindi ottiene il nome del lavoro corrente da msdb, lo aggiunge  workere avvia il lavoro di lavoro utilizzando sp_start_job.

    Sebbene ciò non sia ancora ideale, mantiene i registri lavori più ordinati e gestibili rispetto all'opzione precedente. Inoltre, è sempre possibile eseguire il processo proxy con un utente sysadmin, quindi non è necessario aggiungere ulteriori autorizzazioni.


3

Se il processo di caricamento dei dati è una semplice query o chiamata di procedura, è possibile creare il processo su entrambi i nodi e lasciare che determini se si tratta del nodo primario in base alla proprietà Aggiornabilità del database, prima di eseguire il processo di caricamento dei dati:

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

-- Data Load code goes under here

END

1

È sempre meglio creare un nuovo passaggio di lavoro che controlla se si tratta di una replica primaria, quindi tutto va bene per continuare l'esecuzione del lavoro altrimenti se si tratta di una replica secondaria, quindi interrompere il lavoro. Non fallire il lavoro altrimenti continuerà a inviare notifiche non necessarie. Invece interrompere il lavoro in modo che venga annullato e non vengano inviate notifiche ogni volta che questi lavori vengono eseguiti sulla replica secondaria.

Di seguito è riportato lo script per aggiungere un primo passaggio per un lavoro specifico.

Nota per eseguire lo script:

  • Sostituire 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' con Job_ID
  • Sostituisci "AAAAAAAAAAAAAAAAAAAAAAAAAAA" con Job_Name
  • Se esistono più gruppi di disponibilità, impostare il nome AG nella variabile @AGNameToCheck_IfMoreThanSingleAG su quale AG deve essere verificato per il suo stato di replica.

  • Si noti inoltre che questo script dovrebbe funzionare bene anche su quei server che non dispongono di gruppi di disponibilità. Verrà eseguito solo per le versioni di SQL Server 2012 e successive.

            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

Un altro modo è inserire un passaggio in ciascun lavoro, che dovrebbe essere eseguito per primo, con il seguente codice:

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

Impostare questo passaggio per continuare con il passaggio successivo in caso di successo e per chiudere il processo riportando il successo in caso di errore.

Trovo più pulito aggiungere un passaggio aggiuntivo anziché aggiungere ulteriore logica a un passaggio esistente.


0

Un'altra opzione più recente è l'utilizzo di master.sys.fn_hadr_is_primary_replica ('DbName'). L'ho trovato super utile quando si utilizza SQL Agent per eseguire la manutenzione del database (associato a un cursore che utilizzo da anni) e anche quando si esegue un ETL o un'altra attività specifica del database. Il vantaggio è che individua il database invece di guardare l'intero gruppo di disponibilità ... se è quello che ti serve. Inoltre, rende molto più improbabile che un comando venga eseguito su un database che "era" sul primario, ma diciamo che si è verificato un failover automatico durante l'esecuzione del processo, ed è ora su una replica secondaria. I metodi sopra che osservano la replica primaria danno un'occhiata e non si aggiornano. Tieni presente che questo è solo un modo diverso per ottenere risultati molto simili e dare un controllo più granulare, se necessario. Inoltre, il motivo per cui questo metodo non è stato discusso quando è stata posta questa domanda è perché Microsoft non ha rilasciato questa funzione fino al rilascio di SQL 2014. Di seguito sono riportati alcuni esempi di come è possibile utilizzare questa funzione:

   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 si desidera utilizzare questo per la manutenzione del database utente, questo è quello che 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

Spero che questo sia un consiglio utile!


0

Io lo uso questo:

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

end
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.