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

Actualizaciones personalizadas basadas en activadores para PostgreSQL

1.ª REGLA: No actualiza PostgreSQL con la replicación basada en activadores
2.ª REGLA: NO actualice PostgreSQL con la replicación basada en activadores
TERCERA REGLA: Si actualiza PostgreSQL con la replicación basada en disparadores, prepárese para sufrir. Y prepárate bien.

Debe haber una razón muy seria para no usar pg_upgrade para actualizar PostgreSQL.

Bien, digamos que no puede permitirse más de unos segundos de tiempo de inactividad. Usa pglogical entonces.

OK, digamos que ejecuta 9.3 y, por lo tanto, no puede usar pglogical. Usa Londiste.

¿No puede encontrar el archivo README legible? Usa SLONY.

¿Demasiado complicado? Use la replicación de transmisión:promueva el esclavo y ejecute pg_upgrade en él, luego cambie las aplicaciones para que funcionen con el nuevo servidor promocionado.

¿Su aplicación es relativamente intensiva en escritura todo el tiempo? ¿Examinó todas las soluciones posibles y aún desea configurar una replicación personalizada basada en disparadores? Hay cosas a las que debes prestar atención entonces:

  • Todas las tablas necesitan PK. No debe confiar en ctid (incluso con autovacuum deshabilitado)
  • Deberá habilitar el activador para todas las tablas enlazadas con restricciones (y es posible que necesite FK diferido)
  • Las secuencias necesitan sincronización manual
  • Los permisos no se replican (a menos que también configure un activador de eventos)
  • Los disparadores de eventos pueden ayudar con la automatización del soporte para tablas nuevas, pero es mejor no complicar demasiado un proceso que ya es complicado. (como crear un activador y una tabla externa en la creación de la tabla, también crear la misma tabla en un servidor externo o alterar la tabla del servidor remoto con el mismo cambio, lo hace en la base de datos anterior)
  • Para cada declaración, el disparador es menos confiable pero probablemente más simple
  • Debe imaginar vívidamente su proceso de migración de datos preexistente
  • Debe planificar la accesibilidad limitada de las tablas al configurar y habilitar la replicación basada en activadores
  • Debe conocer absolutamente todas las dependencias y restricciones de sus relaciones antes de seguir por este camino.

¿Suficientes advertencias? ¿Quieres jugar ya? Entonces, comencemos con algo de código.

Antes de escribir cualquier disparador, tenemos que crear un conjunto de datos simulados. ¿Por qué? ¿No sería mucho más fácil tener un activador antes de tener datos? Entonces, ¿los datos se replicarían en el clúster de "actualización" a la vez? Seguro que lo haría. Pero entonces, ¿qué queremos actualizar? Simplemente cree un conjunto de datos en una versión más nueva. Entonces, sí, si planea actualizar a una versión superior y necesita agregar alguna tabla, cree disparadores de replicación antes de colocar los datos, eliminará la necesidad de sincronizar los datos no replicados más adelante. Pero estas nuevas tablas son, podemos decir, una parte fácil. Así que primero simulemos el caso cuando tengamos datos antes de decidir actualizar.

Supongamos que un servidor desactualizado se llama p93 (el más antiguo admitido) y el que replicamos se llama p10 (el 11 está en camino este trimestre, pero aún no sucedió):

\c PostgreSQL
select pg_terminate_backend(pid) from pg_stat_activity where datname in ('p93','p10');
drop database if exists p93;
drop database if exists p10;

Aquí uso psql, por lo que puedo usar el metacomando \c para conectarme a otra base de datos. Si desea seguir este código con otro cliente, deberá volver a conectarse. Por supuesto, no necesita este paso si lo ejecuta por primera vez. Tuve que recrear mi caja de arena varias veces, así que guardé declaraciones...

create database p93; --old db (I use 9.3 as oldest supported ATM version)
create database p10; --new db 

Entonces creamos dos bases de datos nuevas. Ahora me conectaré al que queremos actualizar y crearé varios tipos de datos de funkey y los usaré para completar una tabla que consideraremos como preexistente más adelante:

\c p93
create type myenum as enum('a', 'b');--adding some complex types
create type mycomposit as (a int, b text); --and again...
create table t(i serial not null primary key, ts timestamptz(0) default now(), j json, t text, e myenum, c mycomposit);
insert into t values(0, now(), '{"a":{"aa":[1,3,2]}}', 'foo', 'b', (3,'aloha'));
insert into t (j,e) values ('{"b":null}', 'a');
insert into t (t) select chr(g) from generate_series(100,240) g;--add some more data
delete from t where i > 3 and i < 142; --mockup activity and mix tuples to be not sequential
insert into t (t) select null;

¿Ahora qué tenemos?

  ctid   |  i  |           ts           |          j           |  t  | e |     c     
---------+-----+------------------------+----------------------+-----+---+-----------
 (0,1)   |   0 | 2018-07-08 08:03:00+03 | {"a":{"aa":[1,3,2]}} | foo | b | (3,aloha)
 (0,2)   |   1 | 2018-07-08 08:03:00+03 | {"b":null}           |     | a | 
 (0,3)   |   2 | 2018-07-08 08:03:00+03 |                      | d   |   | 
 (0,4)   |   3 | 2018-07-08 08:03:00+03 |                      | e   |   | 
 (0,143) | 142 | 2018-07-08 08:03:00+03 |                      | ð   |   | 
 (0,144) | 143 | 2018-07-08 08:03:00+03 |                      |     |   | 
(6 rows)

Bien, algunos datos:¿por qué inserté y luego eliminé tanto? Bueno, tratamos de simular un conjunto de datos que existió por un tiempo. Así que estoy tratando de hacer que se disperse un poco. Movamos una fila más (0,3) al final de la página (0,145):

update t set j = '{}' where i =3; --(0,4)

Ahora supongamos que usaremos PostgreSQL_fdw (usar dblink aquí sería básicamente lo mismo y probablemente más rápido para 9.3, así que hágalo si lo desea).

create extension PostgreSQL_fdw;
create server p10 foreign data wrapper PostgreSQL_fdw options (host 'localhost', dbname 'p10'); --I know it's the same 9.3 server - change host to other version and use other cluster if you wish. It's not important for the sandbox...
create user MAPPING FOR vao SERVER p10 options(user 'vao', password 'tsun');

Ahora podemos usar pg_dump -s para obtener el DDL, pero solo lo tengo arriba. Tenemos que crear la misma tabla en el clúster de la versión superior para replicar los datos en:

\c p10
create type myenum as enum('a', 'b');--adding some complex types
create type mycomposit as (a int, b text); --and again...
create table t(i serial not null primary key, ts timestamptz(0) default now(), j json, t text, e myenum, c mycomposit);

Ahora volvemos a 9.3 y usamos tablas foráneas para la migración de datos (usaré f_ convención para nombres de tablas aquí, f significa extranjero):

\c p93
create foreign table f_t(i serial, ts timestamptz(0) default now(), j json, t text, e myenum, c mycomposit) server p10 options (TABLE_name 't');

¡Por fin! Creamos una función de inserción y disparamos.

create or replace function tgf_i() returns trigger as $$
begin
  execute format('insert into %I select ($1).*','f_'||TG_RELNAME) using NEW;
  return NEW;
end;
$$ language plpgsql;

Aquí y más adelante usaré enlaces para código más largo. Primero, para que el texto hablado no se hundiera en lenguaje máquina. En segundo lugar, porque utilizo varias versiones de las mismas funciones para reflejar cómo debe evolucionar el código bajo demanda.

--OK - first table ready - lets try logical trigger based replication on inserts:
insert into t (t) select 'one';
--and now transactional:
begin;
  insert into t (t) select 'two';
  select ctid, * from f_t;
  select ctid, * from t;
rollback;
select ctid, * from f_t where i > 143;
select ctid, * from t where i > 143;

Resultando:

INSERT 0 1
BEGIN
INSERT 0 1
 ctid  |  i  |           ts           | j |  t  | e | c 
-------+-----+------------------------+---+-----+---+---
 (0,1) | 144 | 2018-07-08 08:27:15+03 |   | one |   | 
 (0,2) | 145 | 2018-07-08 08:27:15+03 |   | two |   | 
(2 rows)

  ctid   |  i  |           ts           |          j           |  t  | e |     c     
---------+-----+------------------------+----------------------+-----+---+-----------
 (0,1)   |   0 | 2018-07-08 08:27:15+03 | {"a":{"aa":[1,3,2]}} | foo | b | (3,aloha)
 (0,2)   |   1 | 2018-07-08 08:27:15+03 | {"b":null}           |     | a | 
 (0,3)   |   2 | 2018-07-08 08:27:15+03 |                      | d   |   | 
 (0,143) | 142 | 2018-07-08 08:27:15+03 |                      | ð   |   | 
 (0,144) | 143 | 2018-07-08 08:27:15+03 |                      |     |   | 
 (0,145) |   3 | 2018-07-08 08:27:15+03 | {}                   | e   |   | 
 (0,146) | 144 | 2018-07-08 08:27:15+03 |                      | one |   | 
 (0,147) | 145 | 2018-07-08 08:27:15+03 |                      | two |   | 
(8 rows)

ROLLBACK
 ctid  |  i  |           ts           | j |  t  | e | c 
-------+-----+------------------------+---+-----+---+---
 (0,1) | 144 | 2018-07-08 08:27:15+03 |   | one |   | 
(1 row)

  ctid   |  i  |           ts           | j |  t  | e | c 
---------+-----+------------------------+---+-----+---+---
 (0,146) | 144 | 2018-07-08 08:27:15+03 |   | one |   | 
(1 row)

¿Qué vemos aquí? Vemos que los datos recién insertados se replican en la base de datos p10 con éxito. Y, en consecuencia, se revierte si la transacción falla. Hasta aquí todo bien. Pero no podría no notar (sí, sí, no) que la tabla en p93 es mucho más grande:los datos antiguos no se replicaron. ¿Cómo lo conseguimos allí? Bien sencillo:

insert into … select local.* from ...outer join foreign where foreign.PK is null 

haría. Y esta no es la principal preocupación aquí:debería preocuparse por cómo administrará los datos preexistentes en las actualizaciones y eliminaciones, porque las declaraciones que se ejecutan con éxito en la versión inferior de la base de datos fallarán o simplemente afectarán cero filas en la superior, solo porque no hay datos preexistentes ! Y aquí llegamos a la frase de segundos de tiempo de inactividad. (Si fuera una película, por supuesto aquí tendríamos un flashback, pero, por desgracia, si la frase "segundos de tiempo de inactividad" no te llamó la atención antes, tendrás que ir arriba y buscar la frase...)

Para habilitar todos los disparadores de declaraciones, debe congelar la tabla, copiar todos los datos y luego habilitar los disparadores, de modo que las tablas en las bases de datos de versiones inferiores y superiores estén sincronizadas y todas las declaraciones tengan el mismo (o muy similar, porque el físico). la distribución diferirá, nuevamente mire arriba el primer ejemplo para la columna ctid) afecta. Pero ejecutar tal "activar la replicación" en la tabla en una transacción biiiiiiig no será segundos de tiempo de inactividad. Potencialmente, hará que el sitio sea de solo lectura durante horas. Especialmente si la mesa está aproximadamente unida por FK con otras mesas grandes.

Bueno, solo lectura no es un tiempo de inactividad completo. Pero luego intentaremos dejar todas las SELECCIONES y algunas INSERTAR, ELIMINAR, ACTUALIZAR funcionando (en datos nuevos, fallando en datos antiguos). Mover la tabla o la transacción a solo lectura se puede hacer de muchas maneras, ya sea con algún enfoque de PostgreSQL, o nivel de aplicación, o incluso revocando temporalmente los permisos correspondientes. Estos enfoques en sí mismos pueden ser un tema para su propio blog, por lo que solo lo mencionaré.

De todas formas. Volvamos a los disparadores. Para realizar la misma acción, que requiere trabajar en una fila distinta (ACTUALIZAR, ELIMINAR) en la tabla remota como lo hace en la tabla local, necesitamos usar claves principales, ya que la ubicación física será diferente. Y las claves principales se crean en diferentes tablas con diferentes columnas, por lo tanto, debemos crear una función única para cada tabla o intentar escribir algo genérico. Supongamos (para simplificar) que solo tenemos PK de una columna, entonces esta función debería ayudar. ¡Así que finalmente! Tengamos una función de actualización aquí. Y obviamente un disparador:

create trigger tgu before update on t for each row execute procedure tgf_u();
Descargue el documento técnico hoy Gestión y automatización de PostgreSQL con ClusterControl Obtenga información sobre lo que necesita saber para implementar, monitorear, administrar y escalar PostgreSQLDescargar el documento técnico

Y veamos si funciona:

begin;
        update t set j = '{"updated":true}' where i = 144;
        select * from t where i = 144;
        select * from f_t where i = 144;
Rollback;

Resultando en:

BEGIN
psql:blog.sql:71: INFO:  (144,"2018-07-08 09:09:20+03","{""updated"":true}",one,,)
UPDATE 1
  i  |           ts           |        j         |  t  | e | c 
-----+------------------------+------------------+-----+---+---
 144 | 2018-07-08 09:09:20+03 | {"updated":true} | one |   | 
(1 row)

  i  |           ts           |        j         |  t  | e | c 
-----+------------------------+------------------+-----+---+---
 144 | 2018-07-08 09:09:20+03 | {"updated":true} | one |   | 
(1 row)

ROLLBACK

ESTÁ BIEN. Y mientras todavía está caliente, agreguemos la función de activación de borrado y la replicación también:

create trigger tgd before delete on t for each row execute procedure tgf_d();

Y comprueba:

begin;
        delete from t where i = 144;
        select * from t where i = 144;
        select * from f_t where i = 144;
Rollback;

Dando:

DELETE 1
 i | ts | j | t | e | c 
---+----+---+---+---+---
(0 rows)

 i | ts | j | t | e | c 
---+----+---+---+---+---
(0 rows)

Como recordamos (¡quién podría olvidar esto!), no estamos convirtiendo el soporte de "replicación" en una transacción. Y deberíamos hacerlo si queremos datos consistentes. Como se dijo anteriormente, TODOS los disparadores de declaraciones en TODAS las tablas relacionadas con FK deben habilitarse en una transacción, previamente preparada mediante la sincronización de datos. De lo contrario podríamos caer en:

begin;
        select * from t where i = 3;
        delete from t where i = 3;
        select * from t where i = 3;
        select * from f_t where i = 3;
Rollback;

Dando:

p93=# begin;
BEGIN
p93=#         select * from t where i = 3;
 i |           ts           | j  | t | e | c 
---+------------------------+----+---+---+---
 3 | 2018-07-08 09:16:27+03 | {} | e |   | 
(1 row)

p93=#         delete from t where i = 3;
DELETE 1
p93=#         select * from t where i = 3;
 i | ts | j | t | e | c 
---+----+---+---+---+---
(0 rows)

p93=#         select * from f_t where i = 3;
 i | ts | j | t | e | c 
---+----+---+---+---+---
(0 rows)

p93=# rollback;

¡Yayki! ¡Eliminamos una fila en la base de datos de la versión inferior y no en la más nueva! Solo porque no estaba allí. Esto no sucedería si lo hiciéramos de la manera correcta (begin;sync;enable trigger;end;). ¡Pero la forma correcta haría que las tablas fueran de solo lectura durante mucho tiempo! El lector más empedernido diría incluso "¿por qué haría usted entonces una replicación basada en disparadores?".

Puede hacerlo con pg_upgrade como lo haría la gente "normal". Y en el caso de la replicación de transmisión, puede hacer que todos los conjuntos sean de solo lectura. Pausa la reproducción de xlog y actualiza el maestro mientras la aplicación sigue siendo RO la esclava.

¡Exactamente! ¿No empecé con eso?

La replicación basada en disparadores aparece en el escenario cuando necesita algo muy especial. Por ejemplo, puede intentar permitir SELECT y algunas modificaciones en datos recién creados, no solo RO. Supongamos que tiene un cuestionario en línea:el usuario se registra, responde, obtiene su bonificación-puntos-gratis-otros-nadie-necesita-cosas geniales y se va. Con tal estructura, puede prohibir modificaciones en los datos que aún no están en una versión superior, lo que permite el flujo completo de datos para los nuevos usuarios.

Por lo tanto, abandona a pocas personas que trabajan en cajeros automáticos en línea, permitiendo que los recién llegados trabajen sin siquiera darse cuenta de que está en medio de una actualización. Suena horrible, pero ¿no dije hipotéticamente? yo no? Bueno, lo dije en serio.

No importa cuál sea el caso de la vida real, veamos cómo puede implementarlo. Las funciones de eliminación y actualización cambiarán. Y veamos ahora el último escenario:

BEGIN
psql:blog.sql:86: ERROR:  This data is not replicated yet, thus can't be deleted
psql:blog.sql:87: ERROR:  current transaction is aborted, commands ignored until end of transaction block
psql:blog.sql:88: ERROR:  current transaction is aborted, commands ignored until end of transaction block
ROLLBACK

La fila no se eliminó en la versión inferior porque no se encontró en la superior. Lo mismo ocurriría con actualizado. Inténtalo tú mismo. Ahora puede iniciar la sincronización de datos sin detener muchas modificaciones en la tabla que incluye en la replicación basada en disparadores.

¿Es mejor? ¿Peor? Es diferente:tiene muchos defectos y algunos beneficios sobre el sistema global de RO. Mi objetivo era demostrar por qué alguien querría usar un método tan complicado en lugar de lo normal:obtener habilidades específicas en un proceso estable y bien conocido. A algún costo, por supuesto...

Entonces, ahora que nos sentimos un poco más seguros en cuanto a la consistencia de los datos y mientras nuestros datos preexistentes en la tabla t se sincronizan con p10, podemos hablar sobre otras tablas. ¿Cómo funcionaría todo con FK (después de todo, mencioné FK tantas veces, tengo que incluirlo en la muestra). Bueno, ¿por qué esperar?

create table c (i serial, t int references t(i), x text);
--and accordingly a foreign table - the one on newer version...
\c p10
create table c (i serial, t int references t(i), x text);
\c p93
create foreign table f_c(i serial, t int, x text) server p10 options (TABLE_name 'c');
--let’s pretend it had some data before we decided to migrate with triggers to a higher version
insert into c (t,x) values (1,'FK');
--- so now we add triggers to replicate DML:
create trigger tgi before insert on c for each row execute procedure tgf_i();
create trigger tgu before update on c for each row execute procedure tgf_u();
create trigger tgd before delete on c for each row execute procedure tgf_d();

Seguramente vale la pena envolver esos tres en una función con el objetivo de "activar" muchas tablas. Pero no lo haré. Como no voy a agregar más tablas, ¡dos bases de datos de relaciones referenciadas ya son una red tan desordenada!

--now, what would happen if we tr inserting referenced FK, that does not exist on remote db?..
insert into c (t,x) values (2,'FK');
/* it fails with:
psql:blog.sql:139: ERROR:  insert or update on table "c" violates foreign key constraint "c_t_fkey"
a new row isn't inserted neither on remote, nor local db, so we have safe data consistencyy, but inserts are blocked?..
Yes untill data that existed untill trigerising gets to remote db - ou cant insert FK with before triggerising keys, yet - a new (both t and c tables) data will be accepted:
*/
insert into t(i) values(4); --I use gap we got by deleting data above, so I dont need to "returning" and know the exact id -less coding in sample script
insert into c(t) values(4);
select * from c;
select * from f_c;

Resultado en:

psql:blog.sql:109: ERROR:  insert or update on table "c" violates foreign key constraint "c_t_fkey"
DETAIL:  Key (t)=(2) is not present in table "t".
CONTEXT:  Remote SQL command: INSERT INTO public.c(i, t, x) VALUES ($1, $2, $3)
SQL statement "insert into f_c select ($1).*"
PL/pgSQL function tgf_i() line 3 at EXECUTE statement
INSERT 0 1
INSERT 0 1
 i | t | x  
---+---+----
 1 | 1 | FK
 3 | 4 | 
(2 rows)

 i | t | x 
---+---+---
 3 | 4 | 
(1 row)

Otra vez. Parece que la consistencia de datos está en su lugar. También puede comenzar a sincronizar datos para la nueva tabla c...

¿Cansado? Definitivamente lo soy.

Conclusión

En conclusión, me gustaría resaltar algunos errores que cometí al analizar este enfoque. Mientras creaba la declaración de actualización, enumerando dinámicamente todas las columnas de pg_attribute, perdí bastante hora. ¡Imagínese lo decepcionado que estaba al descubrir más tarde que me olvidé por completo de la construcción ACTUALIZAR (lista) =(lista)! Y la función llegó a un estado mucho más corto y más legible.

Así que el error número uno fue:tratar de construir todo usted mismo, solo porque parece tan accesible. Todavía lo es, pero como siempre, probablemente alguien ya lo haya hecho mejor:pasar dos minutos solo para verificar si es así puede ahorrarle una hora de pensar más tarde.

Y en segundo lugar, me pareció mucho más simple donde resultaron ser mucho más profundos, y compliqué demasiado muchos casos que están perfectamente controlados por el modelo de transacción de PostgreSQL.

Entonces, solo después de intentar construir la caja de arena, obtuve una comprensión algo clara de las estimaciones de este enfoque.

Por lo tanto, la planificación es obviamente necesaria, pero no planifique más de lo que realmente puede hacer.

La experiencia viene con la práctica.

Mi caja de arena me recordó una estrategia informática:te sientas después del almuerzo y piensas:"ajá, aquí construyo Pyramyd, allí consigo tiro con arco, luego me convierto en Sons of Ra y construyo 20 arqueros largos, y aquí ataco al patético vecinos Dos horas de gloria. Y DE REPENTE te encuentras a la mañana siguiente, dos horas antes del trabajo con “¿Cómo llegué aquí? ¿Por qué tengo que firmar esta alianza humillante con bárbaros no lavados para salvar a mi último arquero y realmente necesito vender mi Pirámide tan duramente construida por ella?”

Lecturas:

  • https://www.PostgreSQL.org/docs/current/static/diferente-replicación-soluciones.html
  • https://stackoverflow.com/questions/15343075/update-multiple-columns-in-a-trigger-function-in-plpgsql