Образование spill-файлов при логической репликации. Влияние репликационных идентификаторов (REPLICA IDENTITY) на логическую репликацию
При работе логической репликации в случаях, когда транзакция содержит множество изменений, в директории pg_replslot для каждого слота логической репликации появляются файлы типа spill. В этом разделе описаны причины такого поведения PostgreSQL и влияние репликационных идентификаторов (REPLICA IDENTITY) на появление файлов типа spill и на логическую репликацию в целом.
PostgreSQL должен переупорядочивать изменения, чтобы они применялись в правильном порядке. Для этого процессы отправителя журнала транзакций (WAL sender) приступают к логическому декодированию и переупорядочивают изменения в памяти. Однако, если транзакция содержит множество изменений, это займет много памяти. Поэтому PostgreSQL записывает эти изменения на диск.
В более ранних версиях PostgreSQL данное поведение регламентировалось константой max_changes_in_memory (src/backend/replication/logical/reorderbuffer.c):
/*
* Maximum number of changes kept in memory, per transaction. After that, changes are spooled to disk.
*
* The current value should be sufficient to decode the entire transaction without hitting disk in OLTP workloads, while starting to spool to disk in other workloads reasonably fast.
*
* At some point in the future it probably makes sense to have a more elaborate resource management here, but it's not entirely clear what that would look like.
*/
static const Size max_changes_in_memory = 4096;
max_changes_in_memory = 4096 означает, что при добавлении/изменении/удалении в одной транзакции более, чем 4095 строк, PostgreSQL записывает эти изменения на диск в виде snap-файлов, что можно наблюдать в логах СУБД, выставив параметр log_min_messages = DEBUG2:
DEBUG: spill 4096 changes in XID 1084 to disk
В 12 версии ядра PostgreSQL расширение «snap» сменило название на «spill».
max_changes_in_memory = 4096 включает в себя именно 4096 изменений строк, а не 4096 инструкций SQL. Такое ограничение не практично, так как структура таблиц может очень сильно варьироваться и нагрузка при операциях над таблицами, которые содержат в себе TOAST, будет сильно отличаться от нагрузки при работе с таблицами, которые имеют более простую структуру.
В 13 версии PostgreSQL поведение в части записи изменений на диск при логической репликации было изменено. Появился параметр logical_decoding_work_mem, который задает объем памяти, выделяемой для логического декодирования, после превышения которого данные вытесняются на диск (src/backend/replication/logical/reorderbuffer.c):
/*
* Maximum number of changes kept in memory, per transaction. After that, changes are spooled to disk.
*
* The current value should be sufficient to decode the entire transaction without hitting disk in OLTP workloads, while starting to spool to disk in other workloads reasonably fast.
*
* At some point in the future it probably makes sense to have a more elaborate resource management here, but it's not entirely clear what that would look like.
*/
int logical_decoding_work_mem;
static const Size max_changes_in_memory = 4096; /* XXX for restore only */
Пример вытеснения данных на диск при работе логической репликации
Настройте логическую репликацию и выполните вставку данных (параметр logical_decoding_work_mem для тестовых целей выставлен в значение 5MB):
-- Публикующий сервер:
create table test(id int primary key);
create publication test_pub for table test;
-- Сервер подписчик:
create table test(id int primary key);
create subscription test_sub connection 'host=0.0.0.0 port=5433 user=postgres' publication test_pub;
-- Вставка данных:
insert into test select x from generate_series(1, 50000) as x;
В логах СУБД можно увидеть следующие сообщения (log_min_messages = DEBUG2):
DEBUG: spill 39719 changes in XID 1559 to disk
DEBUG: spill 10281 changes in XID 1559 to disk
В то же время в каталоге pg_replslot/test_sub появился файл:
xid-1559-lsn-2-22000000.spill
Репликационные идентификаторы (REPLICA IDENTITY) при логической репликации
Процесс логической репликации строится на идее репликационных идентификаторов. Они нужны при репликации команд UPDATE и DELETE для однозначной идентификации изменяемых или удаляемых строк.
По умолчанию в роли репликационных идентификаторов выступают Primary Key или уникальные NOT NULL индексы, в случае их отсутствия репликационные идентификаторы придется настраивать с нуля, в противном случае команды UPDATE или DELETE будут приводить к отмене транзакций на мастере.
REPLICA IDENTITY – эта форма изменяет информацию, которая записывается в журнал для идентификации обновленных или удаленных строк. В большинстве случаев старое значение каждого столбца записывается в журнал только в том случае, если оно отличается от нового значения, однако если старое значение хранится во внешнем журнале, оно записывается всегда, независимо от того, изменилось ли оно. Эта опция не имеет эффекта, за исключением случаев, когда используется логическая репликация.
Узнать текущее значение REPLICA IDENTITY для таблицы можно при помощи запроса:
SELECT CASE relreplident
WHEN 'd' THEN 'default'
WHEN 'n' THEN 'nothing'
WHEN 'f' THEN 'full'
WHEN 'i' THEN 'index'
END AS replica_identity
FROM pg_class
WHERE oid = 'test'::regclass;
REPLICA IDENTITY принимает следующие значения:
DEFAULT– записываются старые значения столбцов первичного ключа, если он есть. Это режим по умолчанию для несистемных таблиц.USING INDEX имя_индекса– записываются старые значения столбцов, составляющих заданный индекс, который должен быть уникальным, не частичным, не отложенным и включать только столбцы, помеченныеNOT NULL. Если этот индекс удален, поведение будет таким же, как в режимеNOTHING.FULL– записываются старые значения всех столбцов в строке.NOTHING– информация о старой строке не записывается. Это режим по умолчанию для системных таблиц.
Пример использования различных репликационных идентификаторов
Для демонстрации отличий репликационных идентификаторов в работе необходимо создать слот логической репликации с встроенным плагином test_decoding.
Встроенный плагин test_decoding в PostgreSQL предназначен для демонстрационных целей и тестирования механизмов логического декодирования.
Этот плагин позволяет увидеть, какие изменения записываются в журнал транзакций (WAL), и как они могут быть декодированы и прочитаны.
Однако test_decoding является лишь примером и не предоставляет функциональности для применения изменений на реплике.
Настройка логической репликации для демонстрации отличий REPLICA IDENTITY:
create table test (id int primary key, name text, a bigint, b int);
create publication test_pub for table test;
select pg_create_logical_replication_slot('test_sub', 'test_decoding');
-- Добавление в таблицу 5 строк, поле text попадет в toast таблицу
with gener as (select x, (select string_agg(chr(trunc(65 + random() * 26)::integer), '') from generate_series(1, 2000)), x + 1, x + 2 from generate_series(1, 5) as x) insert into test select * from gener;
При помощи функций pg_logical_slot_get_changes() и pg_current_wal_insert_lsn() посмотрите изменения в слоте после выполнения команды UPDATE при различных значениях REPLICA IDENTITY и на объем генерируемых WAL-файлов в байтах:
REPLICA IDENTITY = DEFAULT:
SELECT CASE relreplident
WHEN 'd' THEN 'default'
WHEN 'n' THEN 'nothing'
WHEN 'f' THEN 'full'
WHEN 'i' THEN 'index'
END AS replica_identity
FROM pg_class
WHERE oid = 'test'::regclass;
replica_identity
------------------
default
(1 row)
select pg_current_wal_insert_lsn();
pg_current_wal_insert_lsn
---------------------------
2/2C000130
(1 row)
update test set b = b + 10 where id = 1;
select pg_current_wal_insert_lsn();
pg_current_wal_insert_lsn
---------------------------
2/2C000200
(1 row)
------ Изменения в слоте ------
select pg_logical_slot_get_changes('test_sub', NULL, NULL);
pg_logical_slot_get_changes
-------------------------------------------------------------------------------------------------------------------------
(2/2C000130,1610,"BEGIN 1610")
(2/2C000168,1610,"table public.test: UPDATE: id[integer]:1 name[text]:unchanged-toast-datum a[bigint]:2 b[integer]:13")
(2/2C000200,1610,"COMMIT 1610")
(3 rows)
------ Размер журнальных записей ------
select '2/2C000200'::pg_lsn - '2/2C000130'::pg_lsn as bytes;
bytes
-------
208
(1 row)
------ Изменения в слоте при изменения PK ------
update test set id = id + 10 where id = 1;
select pg_logical_slot_get_changes('test_sub', NULL, NULL);
pg_logical_slot_get_changes
------------------------------------------------------------------------------------------------------------------------------------------------------------
(2/2D0003E0,1611,"BEGIN 1611")
(2/2D000418,1611,"table public.test: UPDATE: old-key: id[integer]:1 new-tuple: id[integer]:11 name[text]:unchanged-toast-datum a[bigint]:2 b[integer]:13")
(2/2D000590,1611,"COMMIT 1611")
(3 rows)
------ К изменениям добавилось старое значение PK
old-key: id[integer]:1
При REPLICA IDENTITY = DEFAULT записываются только старые значения столбцов первичного ключа, если он есть, для остальных полей старые значения не записываются.
REPLICA IDENTITY = FULL:
alter table test replica identity full;
select pg_current_wal_insert_lsn();
pg_current_wal_insert_lsn
---------------------------
2/2F000028
(1 row)
update test set b = b + 10 where id = 3;
select pg_current_wal_insert_lsn();
pg_current_wal_insert_lsn
---------------------------
2/2F0008E8
(1 row)
------ Изменения в слоте ------
select pg_logical_slot_get_changes('test_sub', NULL, NULL);
pg_logical_slot_get_changes
------------------------------------------------------------------------------------------------------------------------------------------------------------
(2/2F000028,1614,"BEGIN 1614")
(2/2F000060,1614,"table public.test: UPDATE: old-key: id[integer]:3 name[text]:'QNBMGJPDYFFIQBWSHGFLFRXZQPAOJCDNGTLRPFEPEGBYNOVTCMGELZTKBNGPOFDOMPNGYCJBLSGCHQUSFNNVOSWWJGPAATGGZCRKNVJPSNLZKCFVQFWHVFJDNKKDFRYZDOKRHQVVMRVMC
YHEDHPFMSXVSSESWWZOMHFZTPYBXOOMCJSLKXROKMAQLDCDHBYJVQBGHPVXIVCKTDAZEVDFVILFBQRZBHOBCXIORFIHGIVWKRRRGSSHZTZTBEAIXDSDFMYGYNLBZLMUQOSDTUWKJUGFZQZKUZPILYJGUZHRDCVKPMEOHVOSIPCHZJGRDRPTPMLGEXLRCWKZBUSBEQQCOJZYGFWNKHCXJYLIGBVMPJD
IXUNSQQESZNRTRDFIWNEJVMULQAFIUNIUAXDYFWFWJMGVPVCFORCLJZSDFOCEQOKLQHOQAKIOLLKKZMCAKTWPVPHIARAMXSBMGYNPDZVVRPUEIFRYTEKJZMRITXSUHMAWYIQWABUEORWIDUBPZCVTFLATFEGOIZJGGPAEYCOPQVXAHBSQGVNNPVUSUTIMHJNZJGRMKFFPBNDZSGDSTRZEEKJAJYGHW
SPSYJKMULJEONHGESYGWVWDNIKTOVJQXZPRHFJETRBMKBJADAEDUVMXSIFMKVADZZVVDRKMVQYJRETYANCNIZMFHRYABDAVXAHUVUTAESPZBHGCHIBSSKGOZNACNDNEDIFCUJPWZNDCOPTXAZBTXTLYDFEEUOOKBYMYWICCLGSDPCZFIFEGACYIYQOLIQADFLKUVOFBHYPKPGOQIMQAYCDOSVWVYHX
XMPHMMEXUOPFXOGDBWUDAFHXOIJJBXXQOCJJPAMIJKDLGQZDIPBISCYKCHATDPFFJPSHTJIGRZBBDCQJYIVUBXUUHUTCQFZMMRKLOIYKOFZLAAXCHFOJANOXAZBQDGSFABCNMVKHTTGKHHVLPOIMIDEWCDJOYUBOECISCCOSIWZHUTMVLFZFAHQEIFNRVSKDCIIVPSPAOLEDQIWUJSTIHACFHIGFTS
VICBJKMBTPLZTFIEBBJDEAVJCGRLBEYCSNGWUUIXEMVQDNOFHVXLXHSUCOECMCYRMHIRZSHZOIVEWTYMKLBOKPXJMEDMLJSDXUOHTIERBAGIBDKRJZRDSVURLUWDQSBORWAGQYKGAWAQPVWVSQSEXKXDOJLKVNYQJLQFUCRTTRQYFDECQTASVFINIPCRRGZXAQEPSQDXOEXHHERNQNXYUBLXDLSKDL
HMVDCSMVXGIFFKVPTBTXIJSGKJKGEZHEXBKORJXILUTNPVRREOTNTXYUMKACAIBYYLJLGTHWQAYCBJIHJXLFRRPOZFGTQHCZLDIWPVVBSXMJRHSLIHGMWJWHLPLNLWUZNQGLWBUHOIISDWPRAMVJHEJAHKOGSBIFJQIUUSPRMIXYRTJUPKUNROOYGDOSIUPLWABTNGCASOVBCIQWRLWSTRXPVUENFG
SRWYYSONCZRCFXQHYKKHSHEHDCXUFHUKAWTFHXUAIDEGKKXIELFLHABMJCAMOBUAQWZIVRVVEHXGFHDIOFWIHJNAIQXHCNMNVWDNCVDUXQFSYNDHZTVWNTGGGUATLABSOAMFZYHDCGCNKOOUSXZUPELZGYREBONHBBGEBQTOIIJFBWZUCFUPXBZMXJTCUQNXOTLKNXBQPWHVDHJEHHSUGPJURXZHNW
XNBGLAPNNUVYBDVCEQJGEFIVNCIEITBUGXHOZXHGHNRSYLIOXHVIQKQOBFGEGDMBWKITUFJGTZBRGWABJTIVFFBADKAUIENABHCHCUCJNHXSBZVCIQWYPIGFMCGCJQKURZZCKHYNQYVSMEIQUTHFLWOEJDJQWBXMZYKWCJAAPSEPXAWIUSUSWKMTZRHFIHQCSMWWLLUVFXJFZUBFWGOVPTYKWVAXOM
OLSEKGTQCDLJGGZTYZWVPYIXHMNYKNYHYUPJYTEDQKLIWLLJDDMADMJENLHMQATFKTSBWUGYYBWFBTZVLMH' a[bigint]:4 b[integer]:5 new-tuple: id[integer]:3 name[text]:unchanged-toast-datum a[bigint]:4 b[integer]:15")
(2/2F0008E8,1614,"COMMIT 1614")
------ Размер журнальных записей ------
select '2/2F0008E8'::pg_lsn - '2/2F000028'::pg_lsn as bytes;
bytes
-------
2240
(1 row)
При REPLICA IDENTITY = FULL записываются старые значения всех столбцов в строке, что существенно увеличивает нагрузку.
Вытеснение данных на диск при работе логической репликации в зависимости от репликационного идентификатора:
Из предыдущего примера видно, на сколько разрастается объем хранимых и передаваемых данных при REPLICA IDENTITY = FULL:
------ Размер журнальных записей при REPLICA IDENTITY = DEFAULT ------
select '2/2C000200'::pg_lsn - '2/2C000130'::pg_lsn as bytes;
bytes
-------
208
(1 row)
------ Размер журнальных записей при REPLICA IDENTITY = FULL ------
select '2/2F0008E8'::pg_lsn - '2/2F000028'::pg_lsn as bytes;
bytes
-------
2240
(1 row)
Пример вытеснения данных на диск при различных значениях REPLICA IDENTITY
Для демонстрации вытеснения данных на диск при различных значениях REPLICA IDENTITY настройте логическую репликацию:
-- Публикующий сервер:
create table test (id int primary key, name text, a bigint, b int);
create publication test_pub for table test;
-- Сервер подписчик:
create table test (id int primary key, name text, a bigint, b int);
create subscription test_sub connection 'host=0.0.0.0 port=5433 user=postgres' publication test_pub;
-- Вставка данных:
with gener as (select x, (select string_agg(chr(trunc(65 + random() * 26)::integer), '') from generate_series(1, 2000)), x + 1, x + 2 from generate_series(1, 5000) as x) insert into test select * from gener;
Выполните UPDATE всех 5000 строк при разных REPLICA IDENTITY.
REPLICA IDENTITY = DEFAULT:
update test set b = b + 10;
При значении REPLICA IDENTITY = DEFAULT в логах СУБД не появилось сообщений о вытеснении данных на диск.
REPLICA IDENTITY = FULL:
alter table test replica identity full;
update test set b = b + 10;
При значении REPLICA IDENTITY = FULL в логах СУБД наблюдаются вытеснения на диск:
DEBUG: spill 2349 changes in XID 1641 to disk
DEBUG: spill 2349 changes in XID 1641 to disk
DEBUG: spill 302 changes in XID 1641 to disk
Данный пример демонстрирует на сколько сильно значение REPLICA IDENTITY = FULL нагружает СУБД. Рекомендуется максимально воздержаться от данного значения.