Trabajos del Agente SQL Server y Grupos de disponibilidad


37

Estoy buscando las mejores prácticas para tratar los trabajos programados del Agente SQL Server en los grupos de disponibilidad de SQL Server 2012. Tal vez me perdí algo, sin embargo, en el estado actual, siento que el Agente SQL Server no está realmente integrado con esta gran característica de SQL2012.

¿Cómo puedo hacer que un trabajo de agente SQL programado sea consciente de un cambio de nodo? Por ejemplo, tengo un trabajo ejecutándose en el nodo primario que carga datos cada hora. Ahora, si el primario se cae, ¿cómo puedo activar el trabajo en el secundario que ahora se convierte en primario?

Si programo el trabajo siempre en el secundario, falla porque entonces el secundario es de solo lectura.


Respuestas:


40

Dentro de su trabajo del Agente SQL Server, tenga alguna lógica condicional para comprobar si la instancia actual cumple el rol particular que está buscando en su grupo de disponibilidad:

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

Todo lo que hace es extraer el rol actual de la réplica local, y si está en el PRIMARYrol, puede hacer lo que sea que su trabajo deba hacer si es la réplica principal. El ELSEbloque es opcional, pero es para manejar la lógica posible si su réplica local no es primaria.

Por supuesto, cambie 'YourAvailabilityGroupName'la consulta anterior a su nombre de grupo de disponibilidad real.

No confunda grupos de disponibilidad con instancias de clúster de conmutación por error. Si la instancia es la réplica primaria o secundaria para un grupo de disponibilidad determinado no afecta a los objetos a nivel de servidor, como los trabajos del Agente SQL Server, etc.


14

En lugar de hacer esto por trabajo (verificando cada trabajo por el estado del servidor antes de decidir continuar), he creado un trabajo ejecutándose en ambos servidores para verificar en qué estado se encuentra el servidor.

  • Si es primario, habilite cualquier trabajo que tenga un paso dirigido a una base de datos en la AG.
  • Si el servidor es secundario, desactive cualquier trabajo dirigido a una base de datos en el AG.

Este enfoque proporciona una serie de cosas

  • funciona en servidores donde no hay bases de datos en AG (o una combinación de Db's dentro / fuera de AG)
  • cualquiera puede crear un nuevo trabajo y no tener que preocuparse por si la base de datos está en un AG (aunque sí deben recordar agregar el trabajo al otro servidor)
  • Permite que cada trabajo tenga un correo electrónico fallido que sigue siendo útil (todos sus trabajos tienen correos electrónicos fallidos, ¿verdad?)
  • Al ver el historial de un trabajo, realmente puede ver si el trabajo realmente se ejecutó e hizo algo (este es el principal), en lugar de ver una larga lista de éxitos que en realidad no ejecutó nada (en el secundario)

el script verifica la base de datos en el campo a continuación si esta base de datos está en un grupo de disponibilidad, el script tomará alguna medida

Este proceso se ejecuta cada 15 minutos en cada servidor. (tiene la ventaja adicional de agregar un comentario para informar a las personas por qué se deshabilitó el trabajo)

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

No es infalible, pero para cargas nocturnas y trabajos por hora, hace el trabajo.

Incluso mejor que tener este procedimiento ejecutado en un horario, en lugar de eso, ejecútelo en respuesta a Alert 1480 (alerta de cambio de rol de AG).


9

Soy consciente de dos conceptos para lograr esto.

Requisito previo: Basado en la respuesta de Thomas Stringer, creé dos funciones en la base de datos maestra de nuestros dos 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. Hacer que un trabajo finalice si no se ejecuta en la réplica principal

    Para este caso, cada trabajo en ambos servidores necesita cualquiera de los siguientes dos fragmentos de código como el Paso 1:

    Verificar por nombre de grupo:

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

    Verificar por nombre de base de datos:

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

    Sin embargo, si usa este segundo, tenga cuidado con las bases de datos del sistema; por definición, no pueden ser parte de ningún grupo de disponibilidad, por lo que siempre fallará para ellos.

    Ambos funcionan de forma inmediata para los usuarios administradores. Para los usuarios que no son administradores, debe agregar permisos adicionales, uno de ellos sugerido aquí :

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

    Si configura la acción de falla para Salir del informe de trabajo exitoso en este primer paso, no obtendrá el registro de trabajo lleno de feos signos de cruz roja, para el trabajo principal se convertirán en señales de advertencia amarillas.

    Desde nuestra experiencia, esto no es ideal. Al principio adoptamos este enfoque, pero rápidamente perdimos la noción de encontrar trabajos que realmente tuvieran un problema, porque todos los trabajos de réplica secundaria saturaban el registro de trabajos con mensajes de advertencia.

    Lo que luego buscamos es:

  2. Trabajos proxy

    Si adopta este concepto, en realidad necesitará crear dos trabajos por tarea que desee realizar. El primero es el "trabajo proxy" que comprueba si se está ejecutando en la réplica principal. Si es así, comienza el "trabajo de trabajo", de lo contrario, finaliza con gracia sin saturar el registro con mensajes de advertencia o error.

    Si bien personalmente no me gusta la idea de tener dos trabajos por tarea en cada servidor, creo que es definitivamente más fácil de mantener, y no tiene que establecer la acción de falla del paso para Salir del informe de éxito , lo cual es un poco torpe.

    Para los trabajos, adoptamos un esquema de nombres. El trabajo proxy solo se llama {put jobname here}. El trabajo del trabajador se llama {put jobname here} worker. Esto hace posible automatizar el inicio del trabajo del trabajador desde el proxy. Para hacerlo, agregué el siguiente procedimiento a ambos dbs maestros:

    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

    Esto utiliza la svf_AgReplicaStatefunción que se muestra arriba, puede cambiarla fácilmente para verificar usando el nombre de la base de datos llamando a la otra función.

    Desde el único paso del trabajo proxy, lo llamas así:

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

    Esto utiliza tokens como se muestra aquí y aquí para obtener la identificación del trabajo actual. El procedimiento luego obtiene el nombre del trabajo actual de msdb, lo agrega  workery comienza a usar el trabajo del trabajador sp_start_job.

    Si bien esto aún no es ideal, mantiene los registros de trabajo más ordenados y fáciles de mantener que la opción anterior. Además, siempre puede ejecutar el trabajo proxy con un usuario sysadmin, por lo que no es necesario agregar permisos adicionales.


3

Si el proceso de carga de datos es una simple consulta o llamada de procedimiento, puede crear el trabajo en ambos nodos y dejar que determine si es el nodo primario en función de la propiedad Updateability de la base de datos, antes de que ejecute el proceso de carga de datos:

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

-- Data Load code goes under here

END

1

Siempre es mejor crear un nuevo Paso de trabajo que verifique si es una Réplica primaria, entonces todo está bien para continuar con la ejecución del trabajo, de lo contrario, si es una Réplica secundaria, luego detenga el trabajo. No falle el trabajo, de lo contrario seguirá enviando notificaciones innecesarias. En su lugar, detenga el trabajo para que se cancele y no se envíen notificaciones cada vez que estos trabajos se ejecuten en la réplica secundaria.

A continuación se muestra el script para agregar un primer paso para un trabajo específico.

Nota para ejecutar el script:

  • Reemplace 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' con Job_ID
  • Reemplace 'YYYYYYYYYYYYYYYYYYYYYYYYYY' con Job_Name
  • Si hay varios grupos de disponibilidad, establezca el nombre de AG en la variable @AGNameToCheck_IfMoreThanSingleAG en cuanto a qué AG se debe verificar su estado de réplica.

  • También tenga en cuenta que este script debería funcionar bien incluso en aquellos servidores que no tienen grupos de disponibilidad. Se ejecutará solo para las versiones de SQL Server 2012 y 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

0

Otra forma es insertar un paso en cada trabajo, que debe ejecutarse primero, con el siguiente 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

Establezca este paso para continuar con el siguiente paso en caso de éxito y para salir del trabajo informando éxito en caso de error.

Me parece más limpio agregar un paso adicional en lugar de agregar lógica adicional a un paso existente.


0

Otra opción más nueva es usar master.sys.fn_hadr_is_primary_replica ('DbName'). He encontrado esto súper útil cuando uso el Agente SQL para realizar el mantenimiento de la base de datos (junto con un cursor que he usado durante años) y también cuando ejecuto una ETL u otra tarea específica de la base de datos. El beneficio es que destaca la base de datos en lugar de mirar todo el Grupo de disponibilidad ... si eso es lo que necesita. También hace que sea mucho más improbable que un comando se ejecute contra una base de datos que "estaba" en el primario, pero digamos que ocurrió una conmutación por error automática durante la ejecución del trabajo, y ahora está en una réplica secundaria. Los métodos anteriores que miran la réplica principal echan un vistazo y no se actualizan. Tenga en cuenta que esta es solo una forma diferente de lograr resultados muy similares y brindar un control más granular, si lo necesita. Además, la razón por la que este método no se discutió cuando se hizo esta pregunta es porque Microsoft no lanzó esta función hasta después de que se lanzó SQL 2014. A continuación hay algunos ejemplos de cómo se puede usar esta función:

   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 desea usar esto para el mantenimiento de la base de datos de usuarios, esto es lo que 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 este sea un consejo útil!


0

Yo uso esto:

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

end
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.