2009-06-09

Репликация PostgreSQL+Bucardo. Изменение структуры БД. Тестирование.

Итак. В рабочем варианте №1 докатились до того, что репликация поехала, но потом заткнулась на изменении структуры БД. Тема интересная и актуальная. Нужна методика поддержки репликации в рабочем состоянии в условиях постоянного изменения структуры БД. Приступим.

На своей локальной машине сделал БД alexsf_test и схему alexsf. Плюс ко всему:

ALTER ROLE alexsf SUPERUSER;
CREATE LANGUAGE plpgsql;
CREATE LANGUAGE plperlu;

и получаю: ERROR: не получилось загрузить библиотеку "/usr/lib/postgresql/plperl.so": libperl.so: невозможно открыть разделяемый объектный файл: Нет такого файла или каталога
SQL state: 58P01

Бл... Как всегда без приключений никуда... Такая библиотека есть, вот она:

/usr/lib/perl5/5.10.0/i586-linux-thread-multi/CORE/libperl.so

Че за херня!!! Как всегда отделываемся костылем:

ln -s /usr/lib/perl5/5.10.0/i586-linux-thread-multi/CORE/libperl.so /usr/lib/libperl.so

Теперь наполняем схему объектами и данными:

CREATE TABLE fio(
pk integer NOT NULL,
f character varying(100),
i character varying(100),
o character varying(100),
CONSTRAINT pk_fio PRIMARY KEY (pk)
)
WITH (OIDS=FALSE);
ALTER TABLE fio OWNER TO alexsf;

CREATE TABLE car(
pk integer NOT NULL,
model character varying(100),
nomer character varying(100),
CONSTRAINT pk_car PRIMARY KEY (pk)
)
WITH (OIDS=FALSE);
ALTER TABLE car OWNER TO alexsf;

CREATE TABLE telefon(
pk integer NOT NULL,
model character varying(100),
"operator" character varying(100),
nomer character varying(100),
CONSTRAINT pk_telefon PRIMARY KEY (pk)
)
WITH (OIDS=FALSE);
ALTER TABLE telefon OWNER TO alexsf;

insert into alexsf.fio VALUES('0','Пупкин0','Серафим0','Опанасович0');
insert into alexsf.fio VALUES('1','Пупкин1','Серафим1','Опанасович1');
insert into alexsf.fio VALUES('2','Пупкин2','Серафим2','Опанасович2');
insert into alexsf.fio VALUES('3','Пупкин3','Серафим3','Опанасович3');
insert into alexsf.fio VALUES('4','Пупкин4','Серафим4','Опанасович4');
insert into alexsf.fio VALUES('5','Пупкин5','Серафим5','Опанасович5');
insert into alexsf.fio VALUES('6','Пупкин6','Серафим6','Опанасович6');
insert into alexsf.fio VALUES('7','Пупкин7','Серафим7','Опанасович7');
insert into alexsf.fio VALUES('8','Пупкин8','Серафим8','Опанасович8');
insert into alexsf.fio VALUES('9','Пупкин9','Серафим9','Опанасович9');

insert into alexsf.car VALUES('0','Жигули0','0');
insert into alexsf.car VALUES('1','Жигули1','1');
insert into alexsf.car VALUES('2','Жигули2','2');
insert into alexsf.car VALUES('3','Жигули3','3');
insert into alexsf.car VALUES('4','Жигули4','4');
insert into alexsf.car VALUES('5','Жигули5','5');
insert into alexsf.car VALUES('6','Жигули6','6');
insert into alexsf.car VALUES('7','Жигули7','7');
insert into alexsf.car VALUES('8','Жигули8','8');
insert into alexsf.car VALUES('9','Жигули9','9');

insert into alexsf.telefon VALUES('0','Nokia0','MTS0','000');
insert into alexsf.telefon VALUES('1','Nokia1','MTS1','001');
insert into alexsf.telefon VALUES('2','Nokia2','MTS2','002');
insert into alexsf.telefon VALUES('3','Nokia3','MTS3','003');
insert into alexsf.telefon VALUES('4','Nokia4','MTS4','004');
insert into alexsf.telefon VALUES('5','Nokia5','MTS5','005');
insert into alexsf.telefon VALUES('6','Nokia6','MTS6','006');
insert into alexsf.telefon VALUES('7','Nokia7','MTS7','007');
insert into alexsf.telefon VALUES('8','Nokia8','MTS8','008');
insert into alexsf.telefon VALUES('9','Nokia9','MTS9','009');

После этого на многострадальном сервере bucardo делаю БД alexsf и схему alexsf, и создаю там аналогичные объекты. Не забываем дать alexsf права SUPERUSER. Затем настраиваем репликацию:

INSERT INTO bucardo.db (name, dbhost, dbname, dbuser, dbpass)
VALUES ('alexsf_slave_db','10.77.11.200','alexsf','alexsf','alexsf');
INSERT INTO bucardo.db (name, dbhost, dbname, dbuser, dbpass)
VALUES ('alexsf_master_db','10.77.11.71','alexsf_test','alexsf','alexsf');

INSERT INTO bucardo.dbgroup(name) VALUES('alexsf_slave_group');
INSERT INTO bucardo.dbgroup(name) VALUES('alexsf_master_group');

INSERT INTO bucardo.dbmap(db, dbgroup) VALUES('alexsf_slave_db','alexsf_slave_group');
INSERT INTO bucardo.dbmap(db, dbgroup) VALUES('alexsf_master_db','alexsf_master_group');

INSERT INTO bucardo.goat (db,schemaname,tablename,pkey,pkeytype,analyze_after_copy) VALUES ('alexsf_slave_db','alexsf','car','pk','bigint','false');
INSERT INTO bucardo.goat (db,schemaname,tablename,pkey,pkeytype,analyze_after_copy) VALUES ('alexsf_slave_db','alexsf','fio','pk','bigint','false');
INSERT INTO bucardo.goat (db,schemaname,tablename,pkey,pkeytype,analyze_after_copy) VALUES ('alexsf_slave_db','alexsf','telefon','pk','bigint','false');

INSERT INTO bucardo.herd(name) VALUES('alexsf_herd');

INSERT INTO bucardo.herdmap (herd, goat)
SELECT 'alexsf_herd', id
FROM goat WHERE db = 'alexsf_slave_db';

INSERT INTO bucardo.sync (name,source,targetdb,synctype,stayalive,checktime,analyze_after_copy)
VALUES('alexsf_sync','alexsf_herd','alexsf_master_db','pushdelta','false','1 minutes','false');

Вроде все. Перед тем, как запустить репликацию, ставлю inactive на не тестовых БД в таблице bucardo.db. Ну и запускаю... Делаю пинки по таблицам в slave БД, и вижу, что репликация работает.

Теперь дело за малым, разобраться в интересующем нас вопросе, а именно в изменении структуры БД...

Меняем структуру нашей подчиненной БД:

alter table car add column probeg numeric;
alter table telefon drop column "operator";
alter table telefon rename column nomer to n;

Перезапускаем bucardo и получаем:

May 28 09:39:46 bucardik Bucardo[16849]: MCP Validating source table "alexsf.car" on alexsf_slave_db
May 28 09:39:47 bucardik Bucardo[16849]: MCP Comparing tables and columns on alexsf_master_db
May 28 09:39:47 bucardik Bucardo[16849]: MCP FATAL: Source database "alexsf_sync", table alexsf.car has more columns than target "alexsf_master_db"
May 28 09:39:47 bucardik Bucardo[16849]: MCP Validation of sync FAILED

Обращаем внимание на то, что несмотря на изменение двух таблиц, bucardo ругается только на одну. То есть он не делает валидацию ВСЕХ таблиц, а обрывает этот процесс на первой, не прошедшей валидацию!!! Воблинзараза!!!

Итак, повторяем изменение структуры на master базе, и перезапускаем bucardo.

И ХРЕНЕЕМ!!! Bucardo не выдает никаких ошибок!!! Просто говорит, что реплицировать нечего!!! Возникает вопрос, то ли лыжи не едут, толи реально так и надо??? Делаем пинки по таблицам и наблюдаем за реакцией... Нормальная реакция, все работает... То есть так и надо, с репликацией ничего не случилось...

Будем считать то, что мы сделали первым вариантом. А именно:
-- изменили структуру подчиненной БД.
-- изменили структуру главной БД.
-- в промежутке между первыми двумя пунктами данные в подчиненной БД не менялись.
-- количество объектов участников репликации не изменилось.

Вывод по первому варианту: да просто офигенный вариант!!! малой кровью решаем проблемы с изменением структуры БД и не нарушаем при этом работу репликации!!! Имеет 100% право на жизнь, будем использовать.

Теперь опишем второй вариант:
-- меняем структуру подчиненной БД.
-- пинаем в подчиненной БД данные со всех сторон, особенно в местах, где произошли изменения структуры.
-- меняем структуру главной БД
-- количество объектов участников репликации не меняем.

Поехали делать второй вариант:

alter table fio add column vozr numeric;
alter table fio drop column o;
update fio set vozr=100;

Bucardo начинает орать всягую лабудень. Перезапустим его на всякий случай. И видим: таблица fio не прошла валидацию... Меняем структуру в главной БД, но НЕ ТРОГАЕМ там данные!!! Перезапускаем bucardo. И зашибись, репликация поехала. все пинки среплицировались!!!

Вывод по 2-му варианту: ну нормально!!! 100% рабочий. Крови больше не стало по сравнению с 1-м. Бум использовать.

Описываем 3-й вариант:
-- меняется поле первичного ключа в подчиненной БД.
-- тусуются данные в поле первичного ключа.
-- аналогично меняется поле первичного ключа в главной БД.
-- количество объектов репликации не меняем.

Поехали:

alter table telefon rename column pk to pkey;
update telefon set pkey=pkey+1000;

И вот о чудо, ничего не получилось. При выполнении update получили ошибку:

ERROR: record "old" has no field "pk"
SQL state: 42703
Контекст:PL/pgSQL function "bucardo_add_delta_u_pk" line 3 at SQL statement

,что вполне закономерно, ведь созданные объекты bucardo в подчиненной базе вроде как подвязаны на имена первичных ключей.

Анализируем...

-- Все объекты bucardo в подчиненной БД были созданы автоматически при внесении данных в таблицу bucardo.sync главной БД. То есть процесс выполнялся удаленно с главной БД.
-- Логично предположить, что в главной БД все эти объекты, а также таблицы и ключи на которые они ссылаются, зарегистрированы в конфигурационных таблицах bucardo.
-- Логично предположить, что изменение объектов в подчиненной БД также должно проводится с главной БД для обеспечения согласованности объектов подчиненной БД и конфигурационной информации в главной БД bucardo.
-- Объекты включались в репликацию фактически тремя действиями: вставка в таблицу goat, вставка в таблицу herdmap, вставка в таблицу sync.
-- Вставка в таблицы goat и herdmap была пообектной и создала, насколько я понял, набор конфигурационной информации объектов репликации. Вставка в таблицу sync ссылалась на весь набор объектов участников, была проведена одной строкой и привела к созданию набора объектов в подчиненной БД, с которыми у нас теперь проблемы.
-- На таблице sync есть триггер validate_sync AFTER INSERT OR UPDATE, который, как я предполагаю, и выполняет работы по удаленному созданию объектов в подчиненной БД. Логично предположить, что в случае изменения конфигурационной информации в goat и herdmap достаточно будет сделать UPDATE на sync для проведения удаленной корректировки объектов в подчиненной БД.

Вроде проанализировали и придумали, что делать дальше. А делать следующее:

Вариант 1:
-- Остановить репликацию
-- Из goat и herdmap выкосить telefon
-- Провести копию изменений структуры подчиненной БД в главной БД
-- В goat и herdmap занести telefon
-- Сделать UPDATE sync
-- Попробовать сделать неотработавший UPDATE в подчиненной БД
-- Запустить репликацию.

Вариант2:
-- Остановить репликацию
-- Порихтовать таблицы goat и herdmap с учетом изменения PK в telefon.
-- Провести копию изменений структуры подчиненной БД в главной БД
-- Сделать UPDATE sync
-- Проверить, изменились ли объекты bucardo в подчиненной БД на таблице telefon.
-- Запустить репликацию.

Пробуем вариант 1. Останавливаем bucardo и:

DELETE FROM bucardo.goat
WHERE db='alexsf_slave_db'
AND schemaname = 'alexsf'
AND tablename = 'telefon';

DELETE FROM bucardo.herdmap
WHERE goat = (SELECT id FROM bucardo.goat WHERE db='alexsf_slave_db'
AND schemaname = 'alexsf' AND tablename = 'telefon');

alter table telefon rename column pk to pkey;

INSERT INTO bucardo.goat (db,schemaname,tablename,pkey,pkeytype,analyze_after_copy) VALUES ('alexsf_slave_db','alexsf','telefon','pkey','bigint','false');

INSERT INTO bucardo.herdmap (herd, goat)
SELECT 'alexsf_herd', id
FROM goat
WHERE db = 'alexsf_slave_db'
AND schemaname = 'alexsf'
AND tablename = 'telefon';

UPDATE bucardo.sync SET status = 'inactive' WHERE name = 'alexsf_sync';
UPDATE bucardo.sync SET status = 'active' WHERE name = 'alexsf_sync';

В подчиненной БД выполняем:

update telefon set pkey=pkey+1000;

... и получаем ту же ошибку... БЛ....!!!

Попробуем грохнуть триггера на telefon и опять обновить sync:

-- В подчиненной БД
drop trigger bucardo_add_delta_d on alexsf.telefon;
drop trigger bucardo_add_delta_i on alexsf.telefon;
drop trigger bucardo_add_delta_u on alexsf.telefon;
drop trigger bucardo_triggerkick_alexsf_sync on alexsf.telefon;

-- В главной БД
UPDATE bucardo.sync SET status = 'inactive' WHERE name = 'alexsf_sync';
UPDATE bucardo.sync SET status = 'active' WHERE name = 'alexsf_sync';

Смотрим, и видим, что на таблице telefon в удаленной БД триггера не создались :( Я буду плакать... Ну почему bucardo не реплицирует DDL????

Перечитал сейчас всю доку, благо ее там не так много(или хреново, что немного), и ничего по работе с отдельными goat не нашел...

Попробую сделать еще одну таблицу и включить ее в репликацию...

-- В подчиненной БД:

CREATE TABLE komp(
pklu4 integer NOT NULL,
cpu character varying(100),
hdd character varying(100),
ozu character varying(100),
CONSTRAINT pk_komp PRIMARY KEY (pklu4)
)
WITH (OIDS=FALSE);
ALTER TABLE telefon OWNER TO alexsf;

-- В БД bucardo в главной БД:

INSERT INTO bucardo.goat (db,schemaname,tablename,pkey,pkeytype,analyze_after_copy) VALUES ('alexsf_slave_db','alexsf','komp','pklu4','bigint','false');

INSERT INTO bucardo.herdmap (herd, goat)
SELECT 'alexsf_herd', id
FROM goat
WHERE db = 'alexsf_slave_db'
AND schemaname = 'alexsf'
AND tablename = 'komp';

UPDATE bucardo.sync SET status = 'active' WHERE name = 'alexsf_sync';
UPDATE bucardo.sync SET synctype = 'fullcopy' WHERE name = 'alexsf_sync';
UPDATE bucardo.sync SET synctype = 'pushdelta' WHERE name = 'alexsf_sync';

И вот на synctype = 'fullcopy' я получил ошибку:

NOTICE: Issuing rollback() for database handle being DESTROY'd without explicit disconnect() at line 29.
КОНТЕКСТ: SQL-команда: "SELECT validate_sync('alexsf_sync')"
ERROR: error from Perl trigger function: error from Perl function: No such table found for database alexsf_master_db: "alexsf.komp" at line 259. at line 30.

О-о-о-о!!!!!!!!!!!!! Эврика!!!!!!!!!! Нашел!!!!!!!!!!
Триггер на таблице sync на UPDATE реагирует не при каждом UPDATE, а только каких-то конкретных полей!!!! Например очень хорошо реагирует на поле synctype. По крайней мере на таблице telefon создались триггера. Новую таблицу komp я пока выкосил...

Так, нужно теперь мысли в кучу собрать и расписать принципы работы 3-го варианта(переименования поля с PK)... Попробуем для порядка опять поменять имя ключа и проверить:

-- На подчиненной БД и на главной БД:
alter table telefon rename column pkey to pklu4;

-- На главной БД:

update bucardo.goat set pkey = 'pklu4'
where db = 'alexsf_slave_db'
and schemaname = 'alexsf'
and tablename = 'telefon';

UPDATE bucardo.sync SET synctype = 'fullcopy' WHERE name = 'alexsf_sync';
UPDATE bucardo.sync SET synctype = 'pushdelta' WHERE name = 'alexsf_sync';

И видим, что триггера на талбице telefon в подчиненной БД не поменялись, но при этом были созданы bucardo.bucardo_add_delta_d_pklu4, bucardo.bucardo_add_delta_i_pklu4 bucardo.bucardo_add_delta_u_pklu4 в подчиненной БД... То есть не до конца отработало, частично, так сказать, по кривому, если точнее :( Я зол.

Попробуем удалить триггера с таблицы telefon и заново пихнуть sync:

-- На пордчиненной БД alexsf:
drop trigger bucardo_add_delta_d on alexsf.telefon;
drop trigger bucardo_add_delta_i on alexsf.telefon;
drop trigger bucardo_add_delta_u on alexsf.telefon;
drop trigger bucardo_triggerkick_alexsf_sync on alexsf.telefon;

О, вот так получилось!!!

Теперь проверим, что не потеряются данные репликации при пинках по таблице sync:

Для этого мы:
- выключим репликацию
- напихаем новых данных в таблицу
- поменяем поле первичного ключа
- проведем изменения в структуре bucardo. Сделаем согласованной схему(sync)...
- опять понапихаем данных
- запустим репликацию
- проверим, что оба запихивания среплицировались

Поехали(напомню, что на данный момент у нас рабочая схема репликации):

#на хосте bucardo
/etc/init.d/bucardo_stop

-- на подчиненной БД под alexsf
insert into alexsf.telefon VALUES('20006','Nokia6','MTS6');
insert into alexsf.telefon VALUES('20007','Nokia7','MTS7');
insert into alexsf.telefon VALUES('20008','Nokia8','MTS8');
insert into alexsf.telefon VALUES('20009','Nokia9','MTS9');
alter table telefon rename column pklu4 to primokey;
drop trigger bucardo_add_delta_d on alexsf.telefon;
drop trigger bucardo_add_delta_i on alexsf.telefon;
drop trigger bucardo_add_delta_u on alexsf.telefon;

drop trigger bucardo_triggerkick_alexsf_sync on alexsf.telefon;

-- на главной БД alexsf
alter table telefon rename column pklu4 to primokey;

-- на главной БД bucardo
UPDATE bucardo.goat SET pkey = 'primokey'
WHERE db = 'alexsf_slave_db'
AND schemaname = 'alexsf'
AND tablename = 'telefon';
UPDATE bucardo.sync SET synctype = 'fullcopy' WHERE name = 'alexsf_sync';
UPDATE bucardo.sync SET synctype = 'pushdelta' WHERE name = 'alexsf_sync';

Ну и радость, все работает, ничего не потерялось... Вроде все варианты рассмотрели... Теперь подитожить, и сформировать набор правил...

2 комментария:

Александр комментирует...

Привет.
При перезапуске bucardo перечитывает конфигурацию колонок всех ему подчиненных таблиц. Только есть такой момент -- если хотя бы признак NULL\NOT NULL не совпадает в колонках -- он пропускает всю таблицу :(
А вообще, выходит, достаточно делать перезапуски, данных он не растеряет.

phas13 комментирует...

Данные он растеряет в том случае, если ты goat удалишь и создашь заново...