CREATE TABLE bar (id integer, name text)
WITH(appendonly=true, orientation=column, COMPRESSTYPE=zstd, COMPRESSLEVEL=5)
DISTRIBUTED BY (id);
填写详细的Hadoop的服务信息后(涉及kerberos认证,非此文重点),PXF服务会启动,启动成功后如上图。
(2)创建PXF扩展
-- 管理员执行
create extension pxf_fdw;
(3)创建PXF外表
CREATE EXTERNAL TABLE pxf_hdfs_textsimple(location text, month text, num_orders int, total_sales float8)
LOCATION ('pxf://data/pxf_examples/pxf_hdfs_simple.txt?PROFILE=hdfs:text&SERVER=23')
FORMAT 'TEXT' (delimiter=E',');
说明:Location是hdfs源文件信息,/data/pxf_examples/pxf_hdfs_simple.txt,即业务访问的外部冷数据文件;SERVER=23指明了Hadoop外表的地址信息,其中23是集群地址信息的存放目录,在图8中可以根据PXF服务查到。
(4)访问外部表
访问外部表就和访问普通表没有区别
图6:外部表访问示例
ADB PG冷热数据分层存储方案
上面的pxf外表访问,有一个弊端,是如果冷数据(外表)要和热数据join,效率较差,原因是数据要从HDFS加载到ADB,再和ADB的表进行Join,徒增大量IO。因此,ADB PG在Greenplum的PXF外表的基础上,提供了冷热数据转换的功能,业务方可以在需要Join外表和普通表分析时,把外部表先转换为ADB的普通表数据,再做业务查询,整体方案称为冷热数据分层存储。由于都是利用PXF外表服务,3.4.1中的第1和第2步骤可以复用。额外的配置方法如下:
(1) 配置分层存储默认使用刚才的Foreign Server
用超级管理员执行
ALTER DATABASE postgres SET RDS_DEF_OPT_COLD_STORAGE TO 'server "23",resource "/cold_data", format "text",delimiter ","';
注意,这里需要将postgres替换为实际的数据库名,并将/cold_data替换为实际在HDFS上需要用来存储冷数据的路径。
(2) 重启数据库实例后执行检查
SHOW RDS_DEF_OPT_COLD_STORAGE;
验证是否配置成功。
(3) 创建测试表,并插入少量测试数据
create table t1(a serial) distributed by (a);
insert into t1 select nextval('t1_a_seq') from generate_series(1,100);
postgres=# select sum(a) from t1;
sum
------
5050
(1 row)
此时,t1表的数据是存在ADB的本地存储中的,属于热数据。
(4) 将表数据迁移到冷存HDFS
alter table t1 set (storagepolicy=cold);
图7:转换数据为冷数据
注意这个NOTICE在当前版本中是正常的,因为在冷存上是不存在所谓分布信息的,或者说分布信息是外部存储(HDFS)决定。
(5) 验证冷数据表的使用
首先,通过查看表的定义,验证表已经迁移到冷存
图8:冷存表的定义
然后正常查询表数据;
postgres=# select sum(a) from t1;
sum
------
5050
(1 row)
(6) 将数据迁回热存
alter table t1 set (storagepolicy=hot);
图9:数据迁回热存
注意:迁移回热存后,distributed信息丢失了,这是当前版本的限制。如果表有索引,则索引在迁移后会丢失,需要补建索引。以上两个方案,都能一定程度上把冷数据从ADB PG中迁移到外部存储,节约ADB PG的空间。
方案1,Join效率低,不支持冷热数据转换,但不再占用ADB的空间;
方案2,Join效率高,支持冷热数据转换,部分时间需要占用ADB的空间。
两个方案各有利弊,实际上项目中,根据业务应用来定。在该客户案例中,冷热数据分层存储方案,为整体ADB节约了数百T空间,这数百T空间中,大部分是设计阶段解决的,少部分是试运行期间进一步优化的。
3 垃圾数据vacuum
由于GP内核的MVCC管理机制,一个表的DML(t2时刻)提交后的数据元组,实际上并没有立即删除,而是一直与该表的正常元组存储在一起,被标记为dead tuples;这会导致表膨胀而占用额外空间。垃圾数据回收有两个方法:内核自动清理、SQL手动清理。自动清理的机制是:表的dead tuples累积到一定百分比,且所有查询该表的事务(t1时刻<t2时刻)都已经结束,内核会自动auto vacuum垃圾数据。这个机制,本身没有问题,但是在大库和大表场景下有一定问题,一个大表上T,数据变化10G才1%,多个大表一起变化,就会累计给整体空间带来问题,因此必须辅以手动回收。
手动回收方法
(1)统计出系统的top大表;
select *,pg_size_pretty(size) from
(select oid,relname,pg_relation_size(oid) as size from pg_class where relkind = 'r' order by 3 desc limit 100)t;
-- limit 100表示top100
(2)查询大表的dead tuple占比和空间;
-- 根据统计信息查询膨胀率大于20%的表
SELECT ((btdrelpages/btdexppages)-1)*100||'%', b.relname FROM gp_toolkit.gp_bloat_expected_pages a
join pg_class b on a.btdrelid=b.oid
where btdrelpages/btdexppages>1.2;
(3)使用pg_cron定时任务帮助业务回收垃圾数据
vacuum tablename;
或
vacuum analyze tablename;-- 先执行一个VACUUM 然后是给每个选定的表执行一个ANALYZE
或
vacuum full tablename;
这里需要与业务沟通清楚执行时间,具体vacuum时,虽然不影响读写,但还是有额外的IO消耗。vacuum full tablename要慎重使用,两者的区别要重点说明一下:简单的VACUUM(没有FULL)只是回收表的空间并且令原表可以再次使用。这种形式的命令和表的普通读写可以并发操作,因为没有请求排他锁。然而,额外的空间并不返回给操作系统;仅保持在相同的表中可用。VACUUM FULL将表的全部内容重写到一个没有任何垃圾数据的新文件中(占用新的磁盘空间,然后删除旧表的文件释放空间),相当于把未使用的空间返回到操作系统中。这种形式要慢许多并且在处理的时候需要在表上施加一个排它锁。因此影响业务使用该表。
(4)vacuum加入业务代码的恰当环节进行回收
如果某些表,更新频繁,每日都会膨胀,则可以加入到业务的代码中进行vacuum,在每次做完频繁DML变更后,立即回收垃圾数据。
系统表也需要回收
这是一个极其容易忽视的点。特别是在某些数据仓库需要频繁建表、改表(临时表也算)的场景下,很多存储元数据的系统表也存在膨胀的情况,而且膨胀率跟DDL频繁度正相关。某客户出现过pg_attribute膨胀到几百GB,pg_class膨胀到20倍的情况。以下表,是根据实际总结出来比较容易膨胀的pg系统表。
pg_attribute -- 存储表字段详情
pg_attribute_encoding -- 表字段的扩展信息
pg_class -- 存储pg的所有对象
pg_statistic -- 存储pg的数据库内容的统计数
图10:pg_class膨胀率示例
手动Vacuum的限制
手动做vacuum有一定的限制,也要注意。
(1)不要在IO使用率高的期间执行vacuum;
(2)vacuum full需要额外的磁盘空间才能完成。
如果磁盘水位高,剩余空间少,可能不够vacuum full大表;可以采取先删除一些历史表,腾出磁盘空间,再vacuum full目标table。
(3)必须先结束目标table上的大事务
有一次例行大表维护时,一个表做了一次vacuum,膨胀的空间并没有回收,仔细一查pg_stat_activity,发现这个表上有一个大事务(启动时间比手动vacuum启动更早)还没结束,这个时候,内核认为旧的数据还可能被使用,因此还不能回收,手动也不能。
4 冗余索引清理
索引本身也占用空间,尤其大表的索引。索引是数据库提高查询效率比较常用又基础的方式,用好索引不等于尽可能多的创建索引,尤其在大库的大表上。空间紧张,可以试着查一下是否有冗余索引可以清理。
排查思路
(1)是否有包含“异常多”字段的复合索引;
(2)是否有存在前缀字段相同的多个复合索引;
(3)是否存在优化器从来不走的索引。
排查方法与例子
首先,我们从第1个思路开始,查询索引包含字段大于等于4个列的表。SQL如下:
with t as (select indrelid, indkey,count(distinct unnest_idx) as unnest_idx_count
from pg_catalog.pg_index, unnest(indkey) as unnest_idx group by 1,2
having count(distinct unnest_idx)>=4 order by 3 desc)
select relname tablename,t.unnest_idx_count idx_cnt from pg_class c ,t where c.oid=t.indrelid;
某个客户,就建了很多10个字段以上的复合索引,如下图所示:
图11:按索引列数排序的复合索引
一般超过6个字段的复合索引,在生产上都很少见,因此我们初步判断是建表时,业务方创建了冗余的索引;接下来,可以按照索引的大小排序后输出冗余索引列表。SQL如下:
with t as (select indrelid,indexrelid, indkey,count(distinct unnest_idx) as unnest_idx_count
from pg_catalog.pg_index, unnest(indkey) as unnest_idx group by 1,2,3
having count(distinct unnest_idx)>=3 order by 3 desc
)
select relname tablename,(pg_relation_size(indexrelid))/1024/1024/1024 indexsize,
t.unnest_idx_count idx_cnt from pg_class c ,t where c.oid=t.indrelid order by 2 desc;
图12:按大小排序的复合索引
这里,我们很清楚发现,部分索引的大小都在500G以上,有10多个索引的size超过1TB,看到这些信息时,我们震惊又开心,开心的是应该可以回收很多空间。接下来,需要跟业务方去沟通,经过业务方确认不需要再删除。
在这个客户案例中,我们删除了200多个冗余索引,大小达24T,直接释放了7%的业务空间!非常可观的空间优化效果。这次优化也非常及时,我记得优化在11月底完成;接着正好12月初高峰来临,业务方又写入了20TB新数据,如果没有这次索引优化,毫不夸张:12月初该客户的ADB集群撑不住了!
第(2)个思路(是否有存在前缀字段相同的多个复合索引),排查SQL如下。最好把索引及包含的字段元数据导出到其他GP库去分析,因为涉及到索引数据的分析对比(涉及向量转字符数组,以及子集与超集的计算),比较消耗性能;
select idx1.indrelid::regclass,idx1.indexrelid::regclass, string_to_array(idx1.indkey::text, ' ') as multi_index1,string_to_array(idx2.indkey::text, ' ') as multi_index2,idx2.indexrelid::regclass
from pg_index idx1 , pg_index idx2 where idx1.indrelid= idx2.indrelid
and idx1.indexrelid!=idx2.indexrelid and idx1.indnatts > 1
and string_to_array(idx1.indkey::text, ' ') <@ string_to_array(idx2.indkey::text, ' ');
以下是排查例子user_t上复合第2个问题的索引,如下:
以下是查询结果
以上例子结果解释:multi_index1是multi_index2的子集,前者的索引列已经在后者中做了索引,因此,multi_index1属于冗余索引。
第(3)个思路:是否存在优化器从来不走的索引,排查的SQL如下:
SELECT
PSUI.indexrelid::regclass AS IndexName
,PSUI.relid::regclass AS TableName
FROM pg_stat_user_indexes AS PSUI
JOIN pg_index AS PI
ON PSUI.IndexRelid = PI.IndexRelid
WHERE PSUI.idx_scan = 0
AND PI.indisunique IS FALSE;
下面以一个测试表,讲述排查例子
执行SQL可以查到idx_scan=0的索引idx_b
另外,有一个很重要的知识点,Append-Only列存表上的索引扫描只支持bitmap scan方式,如果Greenplum关闭了bitmap scan的索引扫描方式,那么所有AO列存表的访问都会全表扫描,即理论上AO列存表上的所有非唯一索引都无法使用,可以全部drop掉。当然,这个操作风险很高,要求整个database里使用AO列存表的业务几乎都只做批处理,不存在点查或范围查找的业务。综上,删除冗余索引,可以帮助客户节约磁盘空间。
5 复制表修改为分布表
众所周知,ADB PG的表分布策略有DISTRIBUTED BY(哈希分布),DISTRIBUTED RANDOMLY(随机分布),或DISTRIBUTED REPLICATED(全分布或复制表)。前两种的表会根据指定的分布键,把数据按照hash算法,打散分布到各个Segment上;复制表,则会在每个Segment上存放完整的数据拷贝。复制表分布策略(DISTRIBUTED REPLICATED)应该在小表上使用。将大表数据复制到每个节点上无论在存储还是维护上都是有很高代价的。查询全分布表的SQL如下:
select n.nspname AS "schemaname",c.relname AS "tablename",case when p.policytype='p' then 'parted' when p.policytype='r' then 'replicated' else 'normal' end as "distrb_type", pg_size_pretty(pg_relation_size(c.oid))
from pg_class c
left join gp_distribution_policy p on c.oid=p.localoid
left join pg_namespace n on c.relnamespace=n.oid
where n.nspname='public'
and c.relkind='r'
and p.policytype='r'
order by 4 desc;
查询结果如下图,找到了大概10TB的全分布表,前3个表较大可以修改为哈希分布表,大概可以节约7T空间。
图13:业务库中的复制表
6 临时表空间独立存放
我们知道,Greenplum的默认表空间有两个
如果建表不指定表空间,默认会放到pg_default表空间,包含堆表、AO表、列存表、临时表等。具体到Segment的文件目录,则是每个Segment服务器上的~/data/Segment/${Segment_id}/base/${database_oid}目录下。同时,Greenplum在多种场景都会产生临时表,如:
(1)sql中order by、group by等操作;
(2)GP引擎由于数据读取或shuffle的需要,创建的临时表;
(3)业务方在ETL任务中创建的临时表。
这样存在一个问题,就是业务运行产生的临时表也会占用空间,但这部分不是业务表的数据占用,不方便精确管理大库的磁盘空间;因此我们把临时表的表空间独立出来,在服务器文件层面也独立出来,方便与业务数据进行分别精细化管理。好处还有:我们可以分别监控临时表空间、数据表空间、wal日志、错误日志,知道各个部分占用情况,如果磁盘空间告警,可以针对性采取措施。Greenplum创建临时表空间的方法,比较标准,如下:
postgres=
pg_relation_filepath
----------------------
base/13333/t_845345
psql -d postgres -c 'select distinct address from gp_Segment_configuration order by 1' -t > sheng_seg_hosts
gpssh -f sheng_seg_hosts -e "ls -l /home/adbpgadmin/tmptblspace"
gpssh -f sheng_seg_hosts -e "mkdir -p /home/adbpgadmin/tmptblspace"
~$ gpssh -f dg_seg_hosts -e "ls -l /home/adbpgadmin/tmptblspace"
postgres=
postgres=
spcname | spcowner | spcacl | spcoptions
--------------+----------+--------+------------
pg_default | 10 | |
pg_global | 10 | |
tmp_tblspace | 10 | |
(3 rows)
postgres=
create temp table tmp_jc2(id int);
insert into tmp_jc2 select generate_series(1,10000);
select * from pg_relation_filepath('tmp_jc2');
---------------------------------------------------
pg_tblspc/2014382/GPDB_6_301908232/13333/t_845369
表空间独立后,监控可以区分临时表空间、数据表空间、WAL日志、错误日志进行独立监控和告警,以下是监控采集输出的样例:
~$ sh check_disk_data_size.sh
usage: sh check_disk_data_size.sh param1 param2, param1 is file recording Segment hosts; param2 data, xlog, log or temp
监控输出的效果如下
图14:监控采集输出示意图
这样可以很清楚的了解业务数据或临时表数据在每个节点上的实际size,以及是否存在数据倾斜情况(超过平均值的10%)单独提醒,非常实用。
7 其他优化方案
除了上面详述的优化方案,一般来讲,Greenplum还有一些通用的处理方法:扩容Segment计算节点、业务数据裁剪、备份文件清理。计算节点扩容是最有效的。一般来讲,不管是阿里自己的业务,还是外部客户的业务,数据库的磁盘占用达到60%,考虑业务增量便会规划扩容,这些“基本实践”我们需要告诉客户。
业务数据裁剪,除了冷数据外,有一些中间表和历史表,我们也可以推动业务方做好数据生命周期管理,及时删除或转存归档。另外,对于临时运维操作,留下的备份文件,在操作完后需要及时进行清理,这个简单的习惯是非常容易忽略的,需要注意。在大库的磁盘管理中,任何小问题都会放大。
四 优化收益
1 为客户节约服务器成本
本案例,客户原DB2的数据量大于1PB,而我们通过上述方法综合优化,在ADB中只保存了300多T的数据,就让整体业务完整的运行起来。为客户节约了大概100台服务器及相关软件license费用,约合金额千万级别。
2 避免磁盘水位过高造成次生灾害
磁盘水位高会带来很多问题,通过磁盘空间优化方案,可以避免这些问题的发生。包括:
1.业务稍微增长,可能导致磁盘占满,发生“写锁定”,数据库临时罢工;
2.磁盘空间不足时,运维人员定位问题无法创建临时表;
3.ADB的大表维护,例如vacuum full,无空余磁盘空间使用。
以上磁盘空间优化方法不一定非常全面,希望对读者有所帮助。如果文中有疏漏或读者有补充,欢迎多多交流,一起探讨上云成本优化。
名词解释
业务方:指使用Greenplum做业务开发或数据分析的用户,通常是客户或客户的开发商。
OLAP:指联机分析处理型系统,是数据仓库系统最主要的应用,专门设计用于支持复杂的大数据量的分析查询处理,并快速返回直观易懂的结果。
DML:指增加、删除、修改、合并表数据的SQL,在数据库领域叫DML型SQL。
PB:1PB=1024TB=1024 * 1024 GB
Maven是一个项目管理工具,它包含了一个项目对象模型 (Project Object Model),一组标准集合,一个项目生命周期(Project Lifecycle),一个依赖管理系统(Dependency Management System),和用来运行定义在生命周期阶段(phase)中插件(plugin)目标(goal)的逻辑。当你使用Maven的时候,你用一个明确定义的项目对象模型来描述你的项目,然后Maven可以应用横切的逻辑,这些逻辑来自一组共享的(或者自定义的)插件。点击阅读原文查看详情。