Travaux de l'agent SQL Server et groupes de disponibilité

37

Je recherche les meilleures pratiques pour traiter les travaux d'agent SQL Server planifiés dans les groupes de disponibilité SQL Server 2012. Peut-être ai-je manqué quelque chose, mais à l'état actuel, je pense que SQL Server Agent n'est pas vraiment intégré à cette excellente fonctionnalité SQL2012.

Comment puis-je rendre un travail d'agent SQL planifié conscient d'un commutateur de noeud? Par exemple, j'ai un travail en cours sur le nœud principal qui charge les données toutes les heures. Maintenant, si le primaire tombe en panne, comment puis-je activer le travail sur le secondaire qui devient maintenant principal?

Si je planifie le travail toujours sur le secondaire, il échoue car le secondaire est en lecture seule.

nojetlag
la source

Réponses:

40

Dans votre travail d'agent SQL Server, disposez d'une logique conditionnelle à tester pour déterminer si l'instance actuelle remplit le rôle particulier que vous recherchez dans votre groupe de 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

Tout cela consiste à extraire le rôle actuel du réplica local et, s'il en fait partie PRIMARY, vous pouvez faire ce que votre travail doit faire s'il s'agit du réplica principal. leELSE bloc est facultatif, mais il permet de gérer la logique possible si votre réplica local n'est pas primaire.

Bien entendu, remplacez 'YourAvailabilityGroupName'la requête ci-dessus par le nom de votre groupe de disponibilité réel.

Ne confondez pas les groupes de disponibilité avec les instances de cluster de basculement. Que l'instance soit le réplica principal ou secondaire d'un groupe de disponibilité donné n'affecte pas les objets de niveau serveur, tels que les travaux de l'Agent SQL Server, etc.

Thomas Stringer
la source
14

Plutôt que de procéder de la sorte à chaque travail (en vérifiant l'état de chaque serveur pour chaque travail avant de décider de continuer), j'ai créé un travail en cours d'exécution sur les deux serveurs afin de vérifier l'état du serveur.

  • Si c'est le cas, activez tout travail comportant une étape visant une base de données dans l'AG.
  • Si le serveur est secondaire, désactivez tout travail ciblant une base de données dans l'AG.

Cette approche fournit un certain nombre de choses

  • cela fonctionne sur des serveurs où il n'y a pas de bases de données dans AG (ou un mélange de Db entrant / sortant d'AG)
  • n'importe qui peut créer un nouveau travail et ne pas avoir à se soucier de savoir si la base de données se trouve dans un AG (bien qu'ils ne doivent pas oublier d'ajouter le travail à l'autre serveur)
  • Permet à chaque travail d'avoir un email d'échec qui reste utile (tous vos emplois ont des emails d'échec, n'est-ce pas?)
  • Lorsque vous visualisez l'historique d'un travail, vous voyez réellement si le travail a été exécuté et a accompli quelque chose (il s'agit du principal), plutôt que de voir une longue liste de succès qui n'a en réalité pas fonctionné (au secondaire).

le script vérifie la base de données dans le champ ci-dessous si cette base de données est dans un groupe de disponibilité, le script effectue une action

Ce proc est exécuté toutes les 15 minutes sur chaque serveur. (avec en prime l'ajout d'un commentaire pour informer les gens des raisons pour lesquelles l'emploi a été désactivé)

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

Ce n'est pas infaillible, mais pour les chargements de nuit et les travaux horaires, le travail est fait.

Mieux encore que d'exécuter cette procédure selon un planning, exécutez-la plutôt en réponse à l'alerte 1480 (alerte de changement de rôle de l'AG).

Trubs
la source
9

Je suis conscient de deux concepts pour accomplir cela.

Prérequis: Sur la base de la réponse de Thomas Stringer, j'ai créé deux fonctions dans la base de données principale de nos deux serveurs:

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. Mettre fin à un travail s'il n'est pas exécuté sur le réplica principal

    Dans ce cas, chaque travail sur les deux serveurs nécessite l'un des deux extraits de code suivants en tant qu'Étape 1:

    Vérifier par nom de groupe:

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

    Vérifier par nom de base de données:

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

    Cependant, si vous utilisez ce second, méfiez-vous des bases de données système. Par définition, elles ne peuvent faire partie d'aucun groupe de disponibilité. Par conséquent, elles échoueront toujours.

    Ces deux solutions sont prêtes à l'emploi pour les utilisateurs administrateurs. Pour les utilisateurs non-administrateurs, vous devez ajouter des autorisations supplémentaires, l'une d'entre elles suggérée ici :

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

    Si vous définissez l'action d'échec sur Quitter le signalement du travail lors de cette première étape, le journal des travaux ne sera pas rempli de signes de croix rouges, mais pour le travail principal, ils deviendront des signes d'alerte jaunes.

    D'après notre expérience, ce n'est pas idéal. Nous avons d’abord adopté cette approche, mais avons rapidement perdu le fil de la recherche des travaux qui posaient problème, car tous les travaux de réplicas secondaires encombraient le journal des travaux avec des messages d’avertissement.

    Nous sommes ensuite allés chercher:

  2. Proxy jobs

    Si vous adoptez ce concept, vous devrez en fait créer deux tâches par tâche à exécuter. Le premier est le "travail proxy" qui vérifie s'il est exécuté sur le réplica principal. Si tel est le cas, il lance le "job worker", sinon, il se termine normalement sans encombrer le journal de messages d'avertissement ou d'erreur.

    Personnellement, je n’aime pas l’idée d’avoir deux tâches par tâche sur chaque serveur, mais je pense que c’est nettement plus facile à gérer, et vous n’aurez pas à définir l’action en cas d’échec de l’étape Fin du rapport de tâche , ce qui est un peu gênant.

    Pour les emplois, nous avons adopté un schéma de nommage. Le travail proxy est appelé {put jobname here}. Le travail de travailleur s'appelle {put jobname here} worker. Cela permet d'automatiser le démarrage du travail de travail à partir du proxy. Pour ce faire, j'ai ajouté la procédure suivante aux deux dbs maîtres:

    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

    Ceci utilise la svf_AgReplicaStatefonction montrée ci-dessus, vous pouvez facilement changer cela pour vérifier en utilisant le nom de la base de données à la place en appelant l'autre fonction.

    À partir de la seule étape du travail de proxy, vous l'appelez comme suit:

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

    Ceci utilise les jetons comme indiqué ici et ici pour obtenir l'identifiant du travail en cours. La procédure récupère ensuite le nom du travail actuel auprès de msdb, y est ajoutée  workeret démarre le travail de travail avec sp_start_job.

    Bien que cela ne soit toujours pas idéal, les journaux de travail sont plus propres et faciles à gérer que l’option précédente. En outre, le travail de proxy peut toujours être exécuté avec un utilisateur sysadmin. Il n'est donc pas nécessaire d'ajouter des autorisations supplémentaires.

takrl
la source
3

Si le processus de chargement de données est un simple appel de requête ou de procédure, vous pouvez créer le travail sur les deux nœuds et le laisser déterminer s'il s'agit du nœud principal en fonction de la propriété Updateability de la base de données, avant d'exécuter le processus de chargement de données:

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

-- Data Load code goes under here

END
Yasin
la source
1

Il est toujours préférable de créer une nouvelle étape de travail qui vérifie s'il s'agit d'un réplica principal, alors tout va bien pour continuer l'exécution du travail, s'il s'agit d'un réplica secondaire, puis arrêter le travail. N'échouez pas le travail sinon il continuera d'envoyer des notifications inutiles. Au lieu de cela, arrêtez le travail pour qu'il soit annulé et qu'aucune notification ne soit envoyée chaque fois que ces travaux sont exécutés sur le réplica secondaire.

Voici le script pour ajouter une première étape pour un travail spécifique.

Note pour exécuter le script:

  • Remplacer 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' par Job_ID
  • Remplacez "YYYYYYYYYYYYYYYYYYYYYYYYYYYY" par Job_Name
  • S'il existe plusieurs groupes de disponibilité, définissez le nom de l'AG dans la variable @AGNameToCheck_IfMoreThanSingleAG afin de déterminer quel AG doit être vérifié pour son état de réplique.

  • Notez également que ce script devrait bien fonctionner même sur les serveurs ne disposant pas de groupes de disponibilité. S'exécutera uniquement pour les versions SQL Server 2012 et ultérieures.

            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
la source
0

Une autre méthode consiste à insérer une étape dans chaque travail, qui doit s'exécuter en premier, avec le code suivant:

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

Définissez cette étape pour passer à l'étape suivante en cas de succès et pour quitter le travail en signalant le succès en cas d'échec.

Je trouve plus propre d'ajouter une étape supplémentaire au lieu d'ajouter de la logique à une étape existante.

KoeKk
la source
0

Une autre option, plus récente, utilise master.sys.fn_hadr_is_primary_replica ('DbName'). J'ai trouvé cela très utile lors de l'utilisation de SQL Agent pour la maintenance de la base de données (associée à un curseur que j'avais utilisé pendant des années), ainsi que lors de l'exécution d'une tâche ETL ou d'une autre tâche spécifique à la base de données. L'avantage est que la base de données est isolée plutôt que de regarder l'ensemble du groupe de disponibilité ... si c'est ce dont vous avez besoin. Cela rend également beaucoup plus improbable qu'une commande soit exécutée sur une base de données qui "était" sur la base principale, mais supposons qu'un basculement automatique s'est produit lors de l'exécution du travail et qu'il s'agit maintenant d'un réplica secondaire. Les méthodes ci-dessus, qui examinent le réplica principal, ne font qu'une mise à jour. N'oubliez pas qu'il s'agit simplement d'une manière différente d'obtenir des résultats très similaires et de donner un contrôle plus granulaire, si vous en avez besoin. De plus, la raison pour laquelle cette méthode n'a pas été abordée lorsque cette question a été posée est que Microsoft n'a pas publié cette fonction avant la publication de SQL 2014. Voici quelques exemples d'utilisation de cette fonction:

   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

Si vous souhaitez utiliser ceci pour la maintenance de la base de données utilisateur, voici ce que j'utilise:

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

J'espère que c'est un conseil utile!

SQL_Hacker
la source
0

J'utilise ceci:

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

end
Aleksey Vitsko
la source