来源:PostgreSQL学徒
前言
这两天关于 DTS 场景又遇到了一个关于 TOAST 的小坑。闲话少叙,进入主题。
复现
首先新建一个逻辑复制槽,同时用到 toast-able 的字段
postgres=# create table test(id int primary key,info text);
CREATE TABLE
postgres=# select pg_create_logical_replication_slot('myslot','decoder_raw');
pg_create_logical_replication_slot
------------------------------------
(myslot,0/163F0E0)
(1 row)
postgres=# SELECT oid::regclass,
reltoastrelid::regclass,
pg_relation_size(reltoastrelid) AS toast_size
FROM pg_class
WHERE relkind = 'r'
AND reltoastrelid <> 0 and relname = 'test';
oid | reltoastrelid | toast_size
------+-------------------------+------------
test | pg_toast.pg_toast_16479 | 0
(1 row)
postgres=# insert into test values(1,'test');
INSERT 0 1
postgres=# update test set id = 99 where id = 1;
UPDATE 1
使用自带的消费端 pg_recvlogical 进行观察
[postgres@xiongcc ~]$ pg_recvlogical -d postgres -S myslot -P decoder_raw --start -f -
INSERT INTO public.test (id, info) VALUES (1, 'test');
UPDATE public.test SET id = 99, info = 'test' WHERE id = 1;
正常的行为没有什么疑问。现在让我们观察一下 TOAST
postgres=# insert into test values(4,repeat('-',200555));;
INSERT 0 1
postgres=# SELECT oid::regclass,
reltoastrelid::regclass,
pg_relation_size(reltoastrelid) AS toast_size
FROM pg_class
WHERE relkind = 'r'
AND reltoastrelid <> 0 and relname = 'test';
oid | reltoastrelid | toast_size
------+-------------------------+------------
test | pg_toast.pg_toast_16479 | 8192
(1 row)
可以看到,数据已经切成了 chunk。由于数据过大,此处就不贴出来了,数据也成功解析出来了。但是让我们观察一下 update 的行为!
postgres=# update test set id = 100 where id = 99;
UPDATE 1
postgres=# update test set id = 101 where id = 4; ---toast数据
UPDATE 1
观察一下解码出来的数据
[postgres@xiongcc ~]$ pg_recvlogical -d postgres -S myslot -P decoder_raw --start -f -
...
...
UPDATE public.test SET id = 100, info = 'test' WHERE id = 99;
UPDATE public.test SET id = 101 WHERE id = 4; ---??注意观察
看仔细,第二行 toast 数据,没有将 info 解析出来!? 这也正是导致我们 DTS 产品出问题的原因,如果没有操作 TOAST 数据,解析出来的数据是不包括 TOAST 字段的,导致前后解析出来的表结构不一致,导致异常。
其实,论原理来说,这个也是符合逻辑的行为:为什么要用 TOAST?因为数据过大,无法跨页存储,所以切片。回想一下我之前在烤面包里提到的读取 TOAST 数据的算法逻辑:
如果数据存在 TOAST 表中,则先调用函数 toast_fetch_datum 从 TOAST 表中获取该数据的片段来重组数据。如果是经过压缩的还需要先解压再返回数据 如果数据没有行外存储但是经过压缩的,则解压后返回数据 如果需要的数据不需要访问 TOAST,则直接返回
所以当 SELECT 时没有查询被 TOAST 的列数据时,不需要把这些 TOAST 的 PAGE 加载到内存,从而加快了检索速度并且节约了使用空间。假如一股脑将超长的 TOAST 解析出来,势必会增加大量的资源消耗!PostgreSQL 这样的行为是合理的。
另外我们知道 TOAST 的存储策略还会尝试压缩,那让我们再观察一下压缩后的数据会如何处理:
postgres=# insert into test values(2,repeat('-',2005));
INSERT 0 1
postgres=# select lp, t_data from heap_page_items(get_raw_page('test', 0)) where lp = 6;
lp | t_data
----+----------------------------------------------------------------------------------
6 | \x020000008e000000d5070000fe2d0f01ff0f01ff0f01ff0f01ff0f01ff0f01ff0f01ff010f014b
(1 row)
此处的 \x8e00,表示压缩过的数据,压缩后的长度是0000 00001000 11。具体原理之前文章写过,此处就不再赘述。插入的解析没问题,此处就不贴出来了,数据太长。让我们观察下更新
postgres=# update test set id = 3 where id = 2;
UPDATE 1
查看解析结果
UPDATE public.test SET id = 3, info = '-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------' WHERE id = 2;
可以看到,压缩的数据也会解析出来。这也符合 TOAST 的逻辑,如果数据没有行外存储但是经过压缩的,则解压后返回数据。
小结
这个 TOAST + logical decoding 的特殊场景也是我之前没有注意到的,如果没有操作 TOAST 数据,解析出来的数据是不包括 TOAST 字段的,对于某些 DTS 产品需要注意这个潜在的坑。