sql >> Base de Datos >  >> RDS >> PostgreSQL

Errores de replicación lógica de PostgreSQL

PostgreSQL 10 vino con la bienvenida adición de la replicación lógica rasgo. Esto proporciona un medio más flexible y sencillo para replicar sus tablas que el mecanismo de replicación de transmisión regular. Sin embargo, tiene algunas limitaciones que pueden o no impedirle emplearlo para la replicación. Siga leyendo para obtener más información.

¿Qué es la replicación lógica de todos modos?

Replicación de transmisión

Antes de v10, la única forma de replicar los datos que residen en un servidor era replicar los cambios en el nivel WAL. Durante su funcionamiento, un servidor PostgreSQL (el principal ) genera una secuencia de archivos WAL. La idea básica es pasar estos archivos a otro servidor PostgreSQL (el standby ) que toma estos archivos y los "reproduce" para recrear los mismos cambios que ocurren en el servidor principal. El servidor en espera permanece en un modo de solo lectura llamado modo de recuperación , y cualquier cambio en el servidor en espera no permitido (es decir, solo se permiten transacciones de solo lectura).

El proceso de envío de los archivos WAL del primario al de reserva se llama logshipping , y se puede hacer manualmente (secuencias de comandos para sincronizar los cambios desde el $PGDATA/pg_wal principal directorio al secundario) o a través de replicación de transmisión .Varias características como ranuras de replicación , comentarios en espera y conmutación por error se agregaron con el tiempo para mejorar la confiabilidad y la utilidad de la replicación de transmisión.

Una gran "característica" de la replicación de transmisión es que es todo o nada. Todos los cambios a todos los objetos de todas las bases de datos en el primario deben enviarse al standby, y el standby tiene que importar cada cambio. No es posible replicar selectivamente una parte de su base de datos.

Replicación lógica

Replicación lógica , agregado en v10, permite hacer precisamente eso:replicar solo un conjunto de tablas en otros servidores. Se explica mejor con un ejemplo. Tomemos una base de datos llamada src en un servidor, y cree una tabla en él:

src=> CREATE TABLE t (col1 int, col2 int);
CREATE TABLE
src=> INSERT INTO t VALUES (1,10), (2,20), (3,30);
INSERT 0 3

También vamos a crear una publicación en esta base de datos (tenga en cuenta que necesita tener privilegios de superusuario para hacer esto):

src=# CREATE PUBLICATION mypub FOR ALL TABLES;
CREATE PUBLICATION

Ahora vamos a una base de datos dst en otro servidor y crea una tabla similar:

dst=# CREATE TABLE t (col1 int, col2 int, col3 text NOT NULL DEFAULT 'foo');
CREATE TABLE

Y ahora configuramos una suscripción aquí que se conectará a la publicación en la fuente y comenzará a extraer los cambios. (Tenga en cuenta que debe tener un usuario repuser en el servidor de origen con privilegios de replicación y acceso de lectura a las tablas).

dst=# CREATE SUBSCRIPTION mysub CONNECTION 'user=repuser password=reppass host=127.0.0.1 port=5432 dbname=src' PUBLICATION mypub;
NOTICE:  created replication slot "mysub" on publisher
CREATE SUBSCRIPTION

Los cambios se sincronizan y puede ver las filas en el lado de destino:

dst=# SELECT * FROM t;
 col1 | col2 | col3
------+------+------
    1 |   10 | foo
    2 |   20 | foo
    3 |   30 | foo
(3 rows)

La tabla de destino tiene una columna adicional "col3", que no se toca con la replicación. Los cambios se replican "lógicamente", por lo tanto, siempre que sea posible insertar una fila con t.col1 y t.col2 solo, el proceso de replicación se dosificará.

En comparación con la replicación de transmisión, la función de replicación lógica es perfecta para replicar, por ejemplo, un solo esquema o un conjunto de tablas en una base de datos específica a otro servidor.

Replicación de cambios de esquema

Suponga que tiene una aplicación Django con su conjunto de tablas viviendo en la base de datos de origen. Es fácil y eficiente configurar la replicación lógica para llevar todas estas tablas a otro servidor, donde puede ejecutar informes, análisis, trabajos por lotes, aplicaciones de soporte para desarrolladores/clientes y similares sin tocar los datos "reales" y sin afectar la aplicación de producción.

Posiblemente, la mayor limitación de Logical Replication actualmente es que no replica los cambios de esquema:cualquier comando DDL ejecutado en la base de datos de origen no provoca un cambio similar en la base de datos de destino, a diferencia de la replicación de transmisión. Por ejemplo, si hacemos esto en la base de datos fuente:

src=# ALTER TABLE t ADD newcol int;
ALTER TABLE
src=# INSERT INTO t VALUES (-1, -10, -100);
INSERT 0 1

esto se registra en el archivo de registro de destino:

ERROR:  logical replication target relation "public.t" is missing some replicated columns

y la replicación se detiene. La columna debe agregarse "manualmente" en el destino, momento en el que se reanuda la replicación:

dst=# SELECT * FROM t;
 col1 | col2 | col3
------+------+------
    1 |   10 | foo
    2 |   20 | foo
    3 |   30 | foo
(3 rows)

dst=# ALTER TABLE t ADD newcol int;
ALTER TABLE
dst=# SELECT * FROM t;
 col1 | col2 | col3 | newcol
------+------+------+--------
    1 |   10 | foo  |
    2 |   20 | foo  |
    3 |   30 | foo  |
   -1 |  -10 | foo  |   -100
(4 rows)

Esto significa que si su aplicación Django ha agregado una nueva característica que necesita nuevas columnas o tablas, y debe ejecutar django-admin migrate en la base de datos de origen, la configuración de la replicación se interrumpe.

Solución alternativa

Su mejor opción para solucionar este problema sería pausar la suscripción en el destino, migrar primero el destino, luego el origen y luego reanudar la suscripción. Puede pausar y reanudar suscripciones de esta manera:

-- pause replication (destination side)
ALTER SUBSCRIPTION mysub DISABLE;

-- resume replication
ALTER SUBSCRIPTION mysub ENABLE;

Si se agregan nuevas tablas y su publicación no es "PARA TODAS LAS TABLAS", deberá agregarlas a la publicación manualmente:

ALTER PUBLICATION mypub ADD TABLE newly_added_table;

También deberá "actualizar" la suscripción en el lado de destino para decirle a Postgres que comience a sincronizar las nuevas tablas:

dst=# ALTER SUBSCRIPTION mysub REFRESH PUBLICATION;
ALTER SUBSCRIPTION

Secuencias

Considere esta tabla en la fuente, que tiene una secuencia:

src=# CREATE TABLE s (a serial PRIMARY KEY, b text);
CREATE TABLE
src=# INSERT INTO s (b) VALUES ('foo'), ('bar'), ('baz');
INSERT 0 3
src=# SELECT * FROM s;
 a |  b
---+-----
 1 | foo
 2 | bar
 3 | baz
(3 rows)

src=# SELECT currval('s_a_seq'), nextval('s_a_seq');
 currval | nextval
---------+---------
       3 |       4
(1 row)

La secuencia s_a_seq fue creado para respaldar el a columna, de serial type. Esto genera los valores de incremento automático para s.a . Ahora vamos a replicar esto en dst e inserte otra fila:

dst=# SELECT * FROM s;
 a |  b
---+-----
 1 | foo
 2 | bar
 3 | baz
(3 rows)

dst=# INSERT INTO s (b) VALUES ('foobaz');
ERROR:  duplicate key value violates unique constraint "s_pkey"
DETAIL:  Key (a)=(1) already exists.
dst=#  SELECT currval('s_a_seq'), nextval('s_a_seq');
 currval | nextval
---------+---------
       1 |       2
(1 row)

Vaya, ¿qué acaba de pasar? El destino intentó iniciar la secuencia desde cero y generó un valor de 1 para a . Esto se debe a que la replicación lógica no replica los valores de las secuencias, ya que el siguiente valor de la secuencia no se almacena en la propia tabla.

Solución alternativa

Si lo piensa lógicamente, no puede modificar el mismo valor de "autoincremento" desde dos lugares sin sincronización bidireccional. Si realmente necesita un número incremental en cada fila de una tabla y necesita insertar en esa tabla desde varios servidores, podría:

  • use una fuente externa para el número, como ZooKeeper o etcd,
  • use rangos que no se superpongan; por ejemplo, el primer servidor genera e inserta números en el rango de 1 a 1 millón, el segundo en el rango de 1 a 2 millones, y así sucesivamente.

Tablas sin filas únicas

Intentemos crear una tabla sin una clave principal y replicarla:

src=# CREATE TABLE nopk (foo text);
CREATE TABLE
src=# INSERT INTO nopk VALUES ('new york');
INSERT 0 1
src=# INSERT INTO nopk VALUES ('boston');
INSERT 0 1

Y las filas ahora también están en el destino:

dst=# SELECT * FROM nopk;
   foo
----------
 new york
 boston
(2 rows)

Ahora intentemos eliminar la segunda fila en la fuente:

src=# DELETE FROM nopk WHERE foo='boston';
ERROR:  cannot delete from table "nopk" because it does not have a replica identity and publishes deletes
HINT:  To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE.

Esto sucede porque el destino no podrá identificar de forma exclusiva la fila que debe eliminarse (o actualizarse) sin una clave principal.

Solución alternativa

Por supuesto, puede modificar el esquema para incluir una clave principal. En caso de que no quiera hacer eso, ALTER TABLE y establezca la "identificación de réplica" en la fila completa o en un índice único. Por ejemplo:

src=# ALTER TABLE nopk REPLICA IDENTITY FULL;
ALTER TABLE
src=# DELETE FROM nopk WHERE foo='boston';
DELETE 1

La eliminación ahora tiene éxito, y la replicación también:

dst=# SELECT * FROM nopk;
   foo
----------
 new york
(1 row)

Si su tabla realmente no tiene forma de identificar filas de manera única, entonces está un poco atascado. Consulte la sección IDENTIDAD DE LA RÉPLICA de ALTERABLE para obtener más información.

Destinos con particiones diferentes

¿No sería bueno tener una fuente dividida de una manera y un destino de otra manera? Por ejemplo, en el origen podemos mantener particiones para cada mes, y en el destino para cada año. Presumiblemente, el destino es una máquina más grande, y necesitamos mantener datos históricos, pero rara vez necesitamos esos datos.

Vamos a crear una tabla con particiones mensuales en la fuente:

src=# CREATE TABLE measurement (
src(#     logdate         date not null,
src(#     peaktemp        int
src(# ) PARTITION BY RANGE (logdate);
CREATE TABLE
src=#
src=# CREATE TABLE measurement_y2019m01 PARTITION OF measurement
src-# FOR VALUES FROM ('2019-01-01') TO ('2019-02-01');
CREATE TABLE
src=#
src=# CREATE TABLE measurement_y2019m02 PARTITION OF measurement
src-# FOR VALUES FROM ('2019-02-01') TO ('2019-03-01');
CREATE TABLE
src=#
src=# GRANT SELECT ON measurement, measurement_y2019m01, measurement_y2019m02 TO repuser;
GRANT

E intente crear una tabla con particiones anuales en el destino:

dst=# CREATE TABLE measurement (
dst(#     logdate         date not null,
dst(#     peaktemp        int
dst(# ) PARTITION BY RANGE (logdate);
CREATE TABLE
dst=#
dst=# CREATE TABLE measurement_y2018 PARTITION OF measurement
dst-# FOR VALUES FROM ('2018-01-01') TO ('2019-01-01');
CREATE TABLE
dst=#
dst=# CREATE TABLE measurement_y2019 PARTITION OF measurement
dst-# FOR VALUES FROM ('2019-01-01') TO ('2020-01-01');
CREATE TABLE
dst=#
dst=# ALTER SUBSCRIPTION mysub REFRESH PUBLICATION;
ERROR:  relation "public.measurement_y2019m01" does not exist
dst=#

Postgres se queja de que necesita la tabla de particiones para enero de 2019, que no tenemos intención de crear en el destino.

Esto sucede porque la replicación lógica no funciona en el nivel de la tabla base, sino en el nivel de la tabla secundaria. No existe una solución real para esto:si usa particiones, la jerarquía de particiones debe ser la misma en ambos lados de la configuración de replicación lógica.

Objetos grandes

Los objetos grandes no se pueden replicar mediante la replicación lógica. Esto probablemente no sea un gran problema hoy en día, ya que almacenar objetos grandes no es una práctica común en la actualidad. También es más fácil almacenar una referencia a un objeto grande en algún almacenamiento redundante externo (como NFS, S3, etc.) y replicar esa referencia en lugar de almacenar y replicar el objeto mismo.