当逻辑解码遇上TOAST会有什么坑?

来源: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 数据的算法逻辑:

  1. 如果数据存在 TOAST 表中,则先调用函数 toast_fetch_datum 从 TOAST 表中获取该数据的片段来重组数据。如果是经过压缩的还需要先解压再返回数据
  2. 如果数据没有行外存储但是经过压缩的,则解压后返回数据
  3. 如果需要的数据不需要访问 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 产品需要注意这个潜在的坑。


请使用浏览器的分享功能分享到微信等