在PostgreSQL中节省大量资金

继续上一章有关分区的文章中提出的记录大数据流的主题,在此,我们考虑减少 PostgreSQL中存储的“物理”大小的方法及其对服务器性能的影响。

这与TOAST设置和数据对齐有关“平均”而言,这些方法不会节省太多资源,但不会对应用程序代码进行任何修改。


但是,事实证明,在这方面我们的经验非常有用,因为几乎所有监视的存储库本质上都只能在记录数据方面追加而且,如果您对如何教数据库写磁盘而不是200MB / s感兴趣,那么我会要求削减。

大数据的小秘密


根据我们服务的概况,他定期从日志中接收文本包

而且,由于我们正在监控其数据库的VLSI复合体是具有复杂数据结构的多组件产品,因此通过具有复杂算法逻辑的“多卷”即可获得实现最高性能的查询因此,到达我们的日志中的每个请求实例或生成的执行计划的数量实际上是“平均”的。 让我们看一下其中写入“原始”数据的表的结构-即,这是日志条目中的原始文本:



CREATE TABLE rawdata_orig(
  pack -- PK
    uuid NOT NULL
, recno -- PK
    smallint NOT NULL
, dt --  
    date
, data --  
    text
, PRIMARY KEY(pack, recno)
);

这样一个典型的板块(当然已经分区了,因此它是一个节模板),其中最重要的是文本。有时相当庞大。

回想一下,PG中一条记录的“物理”大小不能占用一页以上的数据,但是“逻辑”大小是完全不同的事情。要将体积值(varchar /文本/ bytea)写入字段,使用TOAST技术
PostgreSQL使用固定的页面大小(通常为8 KB),并且不允许元组跨越多个页面。因此,不可能直接存储非常大的字段值。为了克服此限制,将大的字段值压缩和/或拆分为几条物理线。用户不会注意到这种情况,并且会稍微影响大多数服务器代码。这种方法被称为TOAST ...

实际上,对于具有“潜在大”字段的每个表,将自动创建一个配对表,并将每个“大”记录“切片”成2KB的段:

TOAST(
  chunk_id
    integer
, chunk_seq
    integer
, chunk_data
    bytea
, PRIMARY KEY(chunk_id, chunk_seq)
);

也就是说,如果我们必须写一个具有“大”值的行data,那么实际记录不仅会出现在主表及其PK中,还会出现在TOAST及其PK中

降低TOAST效果


但是这里的大多数记录仍然不是很大,它们应该适合8KB-您将如何保存呢?..

在此STORAGE,表列的属性对我们有帮助
  • EXTENDED允许压缩和单独存储。这是大多数与TOAST兼容的数据类型标准选项首先,尝试执行压缩,如果行仍然太大,则将其保存在表外。
  • MAIN允许压缩,但不允许单独存储。(但是,实际上,将为此类列执行单独的存储,但这只是最后的选择,当没有其他方法可以减少该行以使其适合页面时。)
实际上,这正是我们需要的文本- 尽可能地对其进行压缩,即使根本不合适,也可以将其放入TOAST中您可以使用以下命令直接“即时”执行此操作:

ALTER TABLE rawdata_orig ALTER COLUMN data SET STORAGE MAIN;

如何评估效果


由于数据流每天都在变化,因此我们无法比较绝对数字,但是相对而言,我们在TOAST中记录的比例越小越好。但是存在危险-每个记录的“物理”量越大,索引就越“宽”,因为必须覆盖更多的数据页。更改前的

部分
heap  = 37GB (39%)
TOAST = 54GB (57%)
PK    =  4GB ( 4%)

更改后的 部分
heap  = 37GB (67%)
TOAST = 16GB (29%)
PK    =  2GB ( 4%)

实际上,我们开始写TOAST的频率降低了2倍,这不仅卸载了磁盘,还卸载了CPU:



我注意到,我们还开始减少了对磁盘的“读取”,而不仅仅是“写入”了-因为在将记录插入某个表中时,还必须“减去”每个索引树的一部分,以确定其在索引中的未来位置。

谁在PostgreSQL 11上过得很好


升级到PG11后,我们决定继续“调整” TOAST,并注意到从该版本开始,该参数可用于配置toast_tuple_target
仅当要存储在表中的行值大于TOAST_TUPLE_THRESHOLD字节(通常为2 Kb)时,才会触发TOAST处理代码。TOAST代码将压缩和/或将字段值移出表,直到行值小于TOAST_TUPLE_TARGET字节(可变,通常也为2 KB)或无法减小大小为止。
我们认为我们通常拥有的数据是“非常短”或立即“非常长”,因此我们决定将自己限制为尽可能低的值:

ALTER TABLE rawplan_orig SET (toast_tuple_target = 128);

让我们看看新设置如何影响迁移后的磁盘加载:


不错!磁盘的平均队列减少了约1.5倍,磁盘“占用率”降低了20%!但是,也许这以某种方式影响了CPU?


至少,它绝对没有恶化。虽然,即使这样的卷仍然无法将平均CPU负载提高到5%以上,却很难判断

从位置改变,总和...改变!


如您所知,一分钱可以节省卢布,而且由于我们的存储量约为10TB /月,即使是很小的优化也可以带来可观的利润。因此,我们提请注意数据的物理结构- “字段”在各表的记录中的布局方式

因为由于数据对齐,这直接影响最终的体积
许多架构都提供跨机器字边界的数据对齐方式。例如,在x86 32位系统上,整数(整数类型,占4个字节)将在4字节字的边界以及双精度浮点数(双精度类型,8字节)的边界对齐。并且在64位系统上,双精度值将在8字节字的边界上对齐。这是不兼容的另一个原因。

由于对齐,表行的大小取决于字段的顺序。通常,这种影响不是很明显,但是在某些情况下,可能会导致尺寸显着增加。例如,如果将类型为char(1)和整数混合的字段放在它们之间,通常,将浪费3个字节而没有任何浪费。

让我们从综合模型开始:

SELECT pg_column_size(ROW(
  '0000-0000-0000-0000-0000-0000-0000-0000'::uuid
, 0::smallint
, '2019-01-01'::date
));
-- 48 

SELECT pg_column_size(ROW(
  '2019-01-01'::date
, '0000-0000-0000-0000-0000-0000-0000-0000'::uuid
, 0::smallint
));
-- 46 

在第一种情况下,多余的一对字节来自哪里?一切都很简单-2字节smallint在下一个字段之前在4字节边界上对齐,当它是最后一个字段时,没有任何内容,也不需要对齐它。

从理论上讲,一切都很好,您可以根据需要重新排列字段。让我们在其中一个表的示例上检查真实数据,该表的每日部分占用10-15GB。

源结构:

CREATE TABLE public.plan_20190220
(
--  from table plan:  pack uuid NOT NULL,
--  from table plan:  recno smallint NOT NULL,
--  from table plan:  host uuid,
--  from table plan:  ts timestamp with time zone,
--  from table plan:  exectime numeric(32,3),
--  from table plan:  duration numeric(32,3),
--  from table plan:  bufint bigint,
--  from table plan:  bufmem bigint,
--  from table plan:  bufdsk bigint,
--  from table plan:  apn uuid,
--  from table plan:  ptr uuid,
--  from table plan:  dt date,
  CONSTRAINT plan_20190220_pkey PRIMARY KEY (pack, recno),
  CONSTRAINT chck_ptr CHECK (ptr IS NOT NULL),
  CONSTRAINT plan_20190220_dt_check CHECK (dt = '2019-02-20'::date)
)
INHERITS (public.plan)

更改列顺序之后的部分是完全相同的字段,只是顺序不同

CREATE TABLE public.plan_20190221
(
--  from table plan:  dt date NOT NULL,
--  from table plan:  ts timestamp with time zone,
--  from table plan:  pack uuid NOT NULL,
--  from table plan:  recno smallint NOT NULL,
--  from table plan:  host uuid,
--  from table plan:  apn uuid,
--  from table plan:  ptr uuid,
--  from table plan:  bufint bigint,
--  from table plan:  bufmem bigint,
--  from table plan:  bufdsk bigint,
--  from table plan:  exectime numeric(32,3),
--  from table plan:  duration numeric(32,3),
  CONSTRAINT plan_20190221_pkey PRIMARY KEY (pack, recno),
  CONSTRAINT chck_ptr CHECK (ptr IS NOT NULL),
  CONSTRAINT plan_20190221_dt_check CHECK (dt = '2019-02-21'::date)
)
INHERITS (public.plan)

该部分的总容量由“事实”的数量决定,并且仅取决于外部进程,因此我们将heap(pg_relation_size的大小除以其中的记录数,即得到实际存储记录平均大小


减去6%的音量,非常好!

但是,所有事情当然都不是那么乐观-因为在索引中我们无法更改字段的顺序,因此“一般”(pg_total_relation_size)...


...毕竟,他们在这里节省了1.5%,而无需更改任何代码行。是的是的!



我注意到上述领域的安排并不是最佳的事实。因为某些字段块出于美学原因不希望被“撕开”,例如,一对(pack, recno),该表为PK。

通常,“最小”字段排列的定义是相当简单的“穷举”任务。因此,您可以获得比我们的数据更好的结果-试试吧!

All Articles