Uno de los problemas que podemos tener con el uso de replicación de SQL Server es la aparición de problemas inesperados por falta de conocimiento o planificación adecuada. La replicación de SQL Server debe verse como una funcionalidad transversal que nos afectará a muchos más niveles de los que podríamos esperar inicialmente de una replicación de datos. La mayoría de DBAs que tengan que lidiar con replicación de SQL Server conocerán esto de primera mano. En este artículo vamos a centrarnos en cómo realizar un backup de una arquitectura con replicación transaccional.
Copias de seguridad
Cuando tenemos una arquitectura replicada tenemos que considerar nuestro publicador, distribuidor y subscriptor como un meta-servidor sobre el que realizamos copias de seguridad. En el caso de la replicación transaccional tenemos distintos agentes que deberemos tener en cuenta para poder tener una sincronización correcta cuando restauremos las bases de datos. El primero de ellos es el lector del log de transacciones que es el responsable de extraer del log de transacciones aquellas operaciones que deban replicarse y escribirlas en la base de datos de distribución. El segundo de ellos es el agente de distribución que es el responsable de leer estas operaciones de la base de datos de distribución y replicarlas al subscriptor.
Imaginemos por ejemplo que nuestras copias de seguridad están programadas para las 17:00 aproximadamente en todos nuestros servidores y que tras un desastre deseamos restaurar nuestros sistemas al estado que tenían a las 17 horas. Por motivos de diferencias de sincronización de relojes, duración de los backups, etc. la hora efectiva de finalización de los backups es la siguiente:
Escenario | Backup del publicador | Backup del distribuidor | Backup del subscriptor |
A |
17:00 |
17:01 |
17:02 |
B |
16:58 |
17:00 |
17:01 |
C |
17:00 |
17:01 |
16:58 |
D |
17:00 |
16:58 |
17:02 |
Vamos a trabajar sobre un supuesto en que nuestra última transacción replicada (un insert por ejemplo) se ejecutó el publicador a las 16:59 y que nuestra replicación está funcionando correctamente, sin latencias excesivas que puedan implicar retrasos de más de unos pocos segundos en la replicación de los cambios.
Escenario A
Una vez realizados los backups procederemos a detener los agentes de réplica antes de proceder con la restauración de las bases de datos. Para detenerlos utilizaremos el monitor de replicación o bien desactivaremos los jobs correspondientes:
Una vez tenemos los agentes parados procederemos a restaurar las bases de datos respaldadas sin tener que seguir ningún orden particular para este proceso. Una vez restauradas las bases de datos pondremos en marcha los agentes e introducimos un nuevo dato en la tabla replicada. Vemos como el agente del log de transacciones es capaz de llevarlo a la base de distribución sin problemas y el agente de distribución de aplicar el cambio en el subscriptor:
En resumen, realizando este backup de forma escalonada y teniendo asegurado que el último cambio ocurrió antes del backup del publicador y que todos los cambios se sincronizaron correctamente (sin latencias) la recuperación ha tenido éxito. Como podemos ver son muchas condiciones que en entornos reales probablemente no se pueden garantizar. Tampoco podemos determinar el instante exacto de finalización del backup ya que dependerá de muchos factores que están fuera de nuestro control.
Escenario B
Realizaremos los backups de forma que el backup del publicador termine antes del último cambio replicado. Dicho cambio sí pasará por el distribuidor y el subscriptor. Detendremos los agentes de réplica como en el caso anterior y restauraremos las bases de datos.
Una vez restauradas veremos cómo los datos se encuentran desincronizados, existiendo datos en el subscriptor que no existen en el publicador:
select * from origen.dbo.tabla
select * from destino.dbo.tabla
Además el lector del log de transacciones se nos quejará de que realmente hemos hecho “un viaje en el tiempo no autorizado”:
Básicamente en la base de datos de distribución tenemos almacenado el último LSN sincronizado y al intentarlo utilizar nos encontramos con que es un valor que no existe.
Escenario C
En este escenario será el backup del subscriptor el que termine antes que el último cambio replicado. Quedará registrado dicho cambio como replicado tanto en el publicador como en el distribuidor. Inicialmente antes de la sincronización las bases de datos estarán desincronizadas:
select * from origen.dbo.tabla
select * from destino.dbo.tabla
Después de unos segundos, las tablas se sincronizarán:
En este caso lo que hemos experimentado sería similar a si tenemos una desconexión temporal del subscriptor. Es decir, si desconectamos de la red el subscriptor y se generan cambios en el publicador llegarán al distribuidor y quedarán ahí almacenados tanto tiempo como periodo de retención tengamos configurado. Por lo tanto mientras no excedamos la “ventana” que nos da el periodo de retención podremos hacer el “replay” desde el punto en el que nos hubiésemos quedado.
Escenario D
En este escenario el backup del distribuidor terminará antes que el del publicador y el del subscriptor. Una vez tenemos los backups restaurados vemos que tenemos los datos sincronizados inicialmente:
select * from origen.dbo.tabla
select * from destino.dbo.tabla
Sin embargo, si insertamos algún dato nuevo en el publicador veremos como el agente del log de transacciones no es capaz de llevar dicho cambio hasta el distribuidor:
De nuevo el problema es un “viaje en el tiempo” en este caso del distribuidor que hace que cuando intente continuar por el LSN que tenía anotado éste se encuentre “por detrás” de la parte activa del log de transacciones del publicador.
Resumen de resultados
La siguiente tabla resume los resultados obtenidos en los distintos escenarios:
Escenario | Backup del publicador | Backup del distribuidor | Backup del subscriptor | Resultado |
A |
17:00 |
17:01 |
17:02 |
Sincronizado si no existe actividad |
B |
16:58 |
17:00 |
17:01 |
Error en el agente lector del log de |
C |
17:00 |
17:01 |
16:58 |
Es posible llegar a la sincronización si el backup |
D |
17:00 |
16:58 |
17:02 |
Error en el agente lector del log de |
Restauración consistente – Escenario E
En un escenario de desastre nos encontraremos con el dilema de si realizamos una restauración consistente o no. La restauración consistente nos aporta beneficios de cara a mantener el funcionamiento de la réplica imperturbado a cambio de una mayor complejidad. La restauración inconsistente puede llevarnos a problemas de sincronización que nos fuercen a tener que reinicializar la réplica. Será por tanto este coste de reinicialización el que decida muy probablemente cual es nuestra mejor estrategia.
En el caso que deseemos tener un entorno consistente al restaurar deberemos buscar un mecanismo que nos asegure esto. Inicialmente se podría pensar en realizar una copia completa de seguridad seguida por un backup del log de transacciones en cada uno de los servidores pasados unos minutos desde el fin de cada uno de los backups. Esto nos permitiría encontrar un punto en el tiempo donde podamos restaurar todas las bases de datos utilizando la cláusula “STOPAT” del comando restore log. El inconveniente de esta propuesta es que la utilización de marcas de tiempo puede que no sean lo suficientemente precisas si en el momento en el que se realizaron los backups los relojes de los distintos servidores no se encontraban perfectamente sincronizados. Debemos encontrar una forma de asegurar que restauramos todas las bases de datos a un punto consistente independientemente del reloj.
Una forma de conseguir esto es utilizar transacciones distribuidas marcadas. Una transacción marcada es una transacción que se inicia con la cláusla WITH MARK ‘nombre marca’. Si hacemos dicha transacción distribuida entre los tres servidores conseguiremos tener una marca consistente a la que podremos recurrir cuando restauremos un backup del log de transacciones con la cláusula WITH STOPATMARK. Desgraciadamente la “marca” no es automáticamente traspasada cuando realizamos una transacción distribuida. Para solucionar esto crearemos una tabla de marcado en cada una de las bases de datos implicadas y un procedimiento almacenado para el marcado. La tabla podría ser bien un tabla sencilla que simplemente contenga un valor numérico o bien utilizar alguna tabla ya existente :
CREATE TABLE MARCAS (A INT PRIMARY KEY)
INSERT INTO MARCAS VALUES (1)
Crearemos también un procedimiento de marcado en cada una de ellas:
CREATE PROCEDURE marcar @nombre_marca nvarchar (255)
AS
BEGIN TRANSACTION @nombre_marca WITH MARK
UPDATE marcas SET a=a+1
COMMIT TRANSACTION;
Finalmente para proceder al marcado deberemos ejecutar dentro de una misma transacción distribuida estos tres procedimientos. Para ello crearemos en cualquiera de los servidores el siguiente procedimiento:
CREATE PROCEDURE marcar_todos @nombre_marca nvarchar (255)
AS
BEGIN TRANSACTION
EXEC publicador.origen.dbo.marcar @nombre_marca
EXEC distribuidor.distribution.dbo.marcar @nombre_marca
EXEC subscriptor.destino.dbo.marcar @nombre_marca
COMMIT TRANSACTION;
Antes de realizar el marcado deberemos realizar un backup completo de cada una de las bases de datos que nos sirva de origen para la cadena de log de cada uno de ellos. Una vez realizado el backup completo, procederemos al marcado ejecutando el procedimiento anterior:
EXEC marcar_todos ‘Marca replica’
Una vez la marca esté en el log de transacciones de cada una de las bases de datos, realizaremos un backup del log de cada una de las bases de datos. Este backup del log contendrá la marca que hemos realizado y será la que utilicemos para la restauración.
Cuando procedamos a la restauración de cada base de datos comenzaremos realizando el RESTORE del backup completo con la opción NORECOVERY para cada una de las bases de datos:
restore database origen from disk = ‘c:E_origen.bak’ with replace, norecovery
restore database distribution from disk = ‘c:E_distribution.bak’ with replace, norecovery
restore database destino from disk = ‘c:E_destino.bak’ with replace, norecovery
Una vez restaurados los backups completos, restauraremos los backups del log utilizando la marca creada como punto de sincronización:
restore database origen from disk = ‘c:E_origen_log.bak’ with STOPATMARK=‘Marca replica’
restore database distribution from disk = ‘c:E_distribution_log.bak’ with STOPATMARK=‘Marca replica’
restore database destino from disk = ‘c:E_destino_log.bak’ with STOPATMARK=‘Marca replica’
Tras esta restauración todas las bases de datos se encontrarán consistentes en el mismo punto por lo que no tendremos problema alguno cuando iniciemos los agentes de la réplica. Existe la posibilidad de que inicialmente los datos no se encuentren sincronizados si en el momento en el tiempo en el que realizamos el marcado éstos no se encontraban sincronizados. Por ejemplo si cuando lanzamos la marca el agente del log de transacciones aún no ha leído una transacción recién confirmada. Sin embargo esto no será un problema ya que los agentes podrán continuar exactamente por el punto en el que se encontraban sin riesgos a inconsistencias de LSNs ni de datos.