暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

如何从Oracle迁移到Greenplum 第二篇

Pivotal研发中心 2020-04-30
1433


在上周和大家分享的《如何从Oracle迁移到Greenplum 第一篇》中,我们介绍了Greenplum和Oracle的产品对比,迁移效果的几个关键因素。今天我们将在该系列的第二篇内容里,从迁移场景和迁移过程两个方面为大家继续介绍如何从Oracle迁移到Greenplum 。


迁移场景



如下图所示,大部分场景都可以直接迁移到Greenplum,但也有部分场景(如高并发事务型场景)不太适合迁移到目前的Greenplum 5.X版本。

Oracle中应用场景

Oracle中响应时间

迁移到Greenplum建议

分析型场景

1秒以上

此类应用完全可以迁移至Greenplum,迁移后性能会有较明显提升

并发小查询场景

1秒以内

并发小查询场景包括小表全表扫描和大表索引扫描场景,迁移至Greenplum性能在同一量级,但因为数据节点交互,延迟会略有增加  

并发数据加载场景

1秒以内

可以迁移至Greenplum,需要将逐笔插入操作改为微批量插入,由于Greenplum MPP架构优势,加载性能会有较明显提升

低并发事务型场景

1秒以内

可以迁移至Greenplum,需要做适当业务改造,将逐笔操作改为微批量操作

高并发事务型场景

1秒以内

不建议迁移到Greenplum,由于数据跨节点的网络交互和锁的问题,会导致性能有较大的损失,甚至无法满足业务要求。请关注Greenplum的研发进展和新版本特性,Greenplum社区正在不断增强高并发事务型特性。

Oracle到Greenplum迁移场景建议


用ELT替代ETL


ETL做为构建数据仓库的重要一环,承担了数据从OLTP系统到OLAP系统时重要的抽取、转换和加载工作。在银行等金融用户中,一般都是通过在ETL服务器上部署专门的ETL工具(如informatic 或 datastage),将数据抽取到数据仓库中进行复杂分析计算,再将算好的结果推回到OLTP系统以完成在线处理。一般的处理流程如下图所示:


数据以T-1的方式加载到Greenplum集群


但受限于ETL服务器的硬件性能,在大数据量的抽取转换时性能较差,随着分布式数据库技术的发展和硬件计算能力的增强,越来越多的用户采用ELT的方式,将ETL中最为耗时的工作放在OLAP系统中完成。这样做,一方面减少了ETL服务器的工作负载,提升了数据的转换效率;另一方面可以利用数据库本身提供的丰富转换函数减少开发的工作量,如下图所示,转换过程可以在数据入库过程中完成,或者在库内完成,从而利用MPP并行的优势,提升转换性能。



ETL到ELT


对于由Oracle到Greenplum的迁移,我们也推荐使用ELT的方式进行处理,如下图所示:


使用ELT流程代替ETL



迁移过程



数据平台迁移主要包括元数据迁移、数据迁移、数据校验、应用验证、性能优化等阶段,关于性能优化可以参考最佳实践的文档。接下来主要介绍元数据迁移数据迁移数据校验

一、元数据迁移


Oracle到Greenplum没有现成的迁移工具,可以借助部分自动化转换工具先将Oracle语法转换为PostgreSQL语法,再通过脚本替换,最终转换成Greenplum语法。Oracle到PostgreSQL常用的迁移工具有ora2pg以及 AWS Schema Conversion Tool。ora2pg为命令行工具,只能从Oracle转换到PostgreSQL;而AWS Schema Conversion Tool (简称AWS SCT)是为了方便用户数据上云,由AWS提供的图形化自动转换工具,可以在本地部署安装,安装部署过程简单,能生成详细的分析报告,并且支持多种数据平台的语法转换。根据我们在用户环境的验证,大概可以完成将近70%的语法自动转化工作。

关于AWS SCT安装和使用,可参考如下链接:

https://docs.aws.amazon.com/zh_cn/SchemaConversionTool/latest/userguide/Welcome.html


1) 配置好Oracle和PostgreSQL的URL连接串后,SCT会自动检索并进行分析,生成评估转换报告,如下图所示。从报告可以看出,100%的存储对象(如表、序列、约束)都可以直接进行转换,而存储过程还需要不少工作。

数据库存储对象转换评估报告


数据库代码转对象换评估报告


之后,我们点击应用就会在PostgreSQL数据库中创建对应的对象和函数。

 

SCT会自动作类型转换,如果你想了解Oracle和Greenplum中不同数据类型具体的转换方式,可以参考下表:


Oracle

Greenplum

Comment

VARCHAR2(n)

VARCHAR(n)

在Oracle中n代表的是字节数,在Greenplum中n代表的是字符数

CHAR(n)

CHAR(n)

同上

NUMBER(n,m)

NUMERIC(n,m)

number可以转换成numeric,但如果真实业务中数值类型可以用:smallint、 int或bigint等替代,性能会有较大提升

NUMBER(4)

SMALLINT


_

NUMBER(9)

INT


_

NUMBER(18)

BIGINT


_

NUMBER(n)

NUMERIC(n)

如果n>19,可以转换成numerice类型

DATE

TIMESTAMP(0)

Oracle和Greenplum都有日期类似,但Oracle的日期类型会同时保存日期和时间,而Greenplum只保包存日期

TIMESTAMP WITH LOCAL TIME ZONE

TIMESTAMPTZ

注意:PostgreSQL中TIMESTAMPTZ不等同于Oralce中的TIMESTAMP WITH TIME ZONE

CLOB

TEXT

PostgreSQL中TEXT类型不能超过1GB

BLOB
RAW(n)

BYTEA(1 GB limit)
Large object

在Oracle中BLOB用于存放非结构化的二进制数据类型,BLOB最大可存储128TB,而PostgreSQL中BYTEA类型最大可以存储1GB,如果有更大的存储要求,可以使用Large object类型

Oracle和Greenplum的字段类型映射表


2) 通过pg_dump导出元数据,命令如下:


    pg_dump  -s  -n pivotal >pivotal.sql


    3) 在Greenplum中导入元数据,命令如下:


      psql –af pivotal.sql >import.log 2>&1


      注意:如果是正式的项目迁移,需要根据之前的介绍,确定每张表的分布键和存储方式。如果只是做POC验证,可以先通过如下命令修改数据库参数,保证数据库采用建议的存储方式充分打散,然后再用上面语句进行恢复:


        ALTER DATABASE pivotal SET gp_create_table_random_default_distribution=on;
        ALTER DATABASE pivotal  SET gp_default_storage_options='appendonly=true,blocksize=32768,compresstype=zlib,compresslevel=5,checksum=true,orientation=row';


        此外,还应注意以下事项:


        1)在Greenplum中,所有的用户表都需要指定分布键。如果Oracle中有明确的业务主键,可以设其为分布键。如果没有,可以选择区分度高的字段作为分布键或者将分布方式设为randomly。

         

        2)在Greenplum中,如果因为业务需要指定主键字段,那么主键一定要包括分布键的所有字段,并与分布键定义的顺序一致,即如果主键是(a,b,c),那分布键可以是(a),(a,b),(a,b,c)。一般情况下,主键与分布键相同。

         

        如下图所示,假如没有主键必须包括分布键的约束,c1作主键、c2作分布键。在插入第一条记录(1,2,3)时,根据c2字段计算哈希值后会把对应的记录存放在p1实例,在插入第二条记录(1,1,6)时,根据c2字段计算哈希值后会把对应的记录存放在p2实例。虽然在各个实例中,数据不会违反唯一性约束,但却违反了全局唯一性约束。


        主键不包括分布键带来的问题


        目前,Greenplum版本要求分布键一定要是主键的左子集,否则会报如下错误:

          pivotal=# CREATE TABLE test2(a int,b int,primary key (b,a)) DISTRIBUTED BY (a);
          ERROR: PRIMARY KEY and DISTRIBUTED BY definitions incompatible
          HINT:  When there is both a PRIMARY KEY, and a DISTRIBUTED BY clause, the DISTRIBUTED BY clause must be equal to or a left-subset of the PRIMARY KEY

          在即将发布的Greenplum6.0版本中,我们将该约束进一步放宽,只要分布键是主键的子集即可,在社区版本测试的结果如下图所示。


          主键必须包括分布键


          注意:对于Greenplum等分布式数据库,并不存在全局唯一的主键索引。要实现主键逻辑,都要借助分布键,要求主键必须包括分布键

          3)Greenplum中不支持外键约束,如果需要在Greenplum中做相关一致性数据约束,只能通过业务手段实现。
           
          4)Greenplum的每张表中最多只能有一个主键或者唯一约束。
           
          5)Oracle存储过程迁移到Greenplum时必须转换成函数。另外,在函数里面不能使用begin和commit做部分提交。在Greenplum中,默认函数作为一个完整的事务,如果函数在执行时报错,则整个事务会回滚。可以使用下图所示的方案进行改写。

          在函数中实现部分提交


          执行后输出结果如下图所示。


          部分提交的结果输出


          注意:函数本质上还是一个事务,通过exception捕获,只是屏蔽掉了异常。让函数继续执行,如果在之后又遇见异常,前面做的操作仍然会被回滚,如下例所示。如果不将函数最外层的SQL封装在单独的begin……exception……end 块中,一旦出现异常,前面的操作仍然会被回滚。


          外层SQL异常,导致前面操作回滚


          解决办法就是拆成多个业务处理块,每个块中都做异常捕获,如下图所示。


          拆成多个语句块,避免操作回滚


          如果要在函数中进行事务的控制,可以通过dblink的方式实现。Greenplum 5.x版本之后,已经集成了dblink模块,首先通过如下命令安装dblink模块:

            psql -af usr/local/greenplum-db/share/postgresql/contrib/dblink.sql

            之后,通过dblink的方式实现函数中的事务控制,测试样例如下图所示:

            注意:从PostgreSQL 11开始支持存储过程,可以实现Oracle同样的部分事务提交,随着Greenplum与PostgreSQL的不断融合,相信从Oracle迁移到Greenplum改写的工作量会越来越小。

            在函数中实现事务控制


            函数没有结束,其他会话也能看到已经提交的事务


            二、数据迁移


            数据迁移包括全量和增量数据迁移。进行全量迁移时,可以用sqluldr2工具先把数据以csv的格式导出,然后再通过gpfdist加载到Greenplum中。当然,也可以使用命名管道或者可执行外部表实现数据的不落地加载。但在真实业务场景中,通过管道会造成数据从Oracle导出到Greenplum紧耦合,一旦网络出现问题,会出现pipe broken的错误。另外,真实场景中不同表的加载作业一般不会有依赖,在数据导出的同时也会有数据导入,实测之后使用管道和文件落地后再加载性能并无太大差异,所以真实业务场景中大都采用文件落地的方式再导入到Greenplum中。


            增量迁移一般借助golden gate等cdc软件尽量做到数据的实时捕获,再通过gpfdist加载到Greenplum中。曾经有用户以250ms的间隔通过gpfdist实时加载数据到Greenplum中,在8个计算节点的集群上速度可达到200万/s。


            具体步骤如下:

            1)在ETL服务器上部署sqludr和Oracle用户端,命令如下:


              wget http://www.onexsoft.com/software/sqluldr2linux64.zip
              vi $ORACLE_HOME/network/admin/tnsnames.ora


              2)将数据导成CSV文件,命令如下:


                export NLS_LANG="SIMPLIFIED CHINESE"_CHINA.ZHS16GBK
                sqluldr2 user='username'/'password'@tnsname query="select /* parallel(2) */ * from PICCPROD.T_CONTRACT_MASTER" text=CSV file="/data/ods/test.dat" log=/tmp/sqluldr2.log


                3)创建外部表进行数据导入,命令如下:


                  CREATEcreate EXTERNALexternal TABLEtable ext.t_contract_master_ext (like t_contract_master) 
                  LOCATION('gpfdist://10.111.224.1:9999/T_CONTRACT_MASTER.dat') FORMAT 'CSV' (HEADER delimiter as ',') ENCODING 'GB18030' LOG ERRORS SEGMENT REJECT LIMIT 2 rows;


                  数据导入过程中常见问题总结如下:


                  问题一:"invalid byte sequence for encoding "GB18030": 0x00"


                  原因:PostgreSQL不支持在文本字段中存储NULL(\ 0x00)字符


                  解决办法:删除掉NULL字符,即

                    for i in *.dat;do tr < $i -d '\000' > nonull/$i & <&-;done


                    问题二:ERROR:  Segment reject limit reached. Aborting operation. Last error was: character 0x809f of encoding "GB18030" has no equivalent in "UTF8"  (seg21 slice1 10.111.224.3:40001 pid=11849)
                    DETAIL:  External table t_claim_case_ext, line 10862893 of file gpfdist://10.111.224.1:9999/T_CLAIM_CASE.dat

                    原因:Oracle原本数据库中有乱码,转换成UTF8的过程中报错

                    解决办法:将原始文件中非GB18030编译的字符替换掉,即
                      /usr/local/bin/iconv -f GB18030 -t GB18030 --byte-subst="<0x%x>" T_CLAIM_CASE.dat

                      具体字符集的介绍和转换过程可以参考如下链接:
                      http://dewoods.com/blog/character-encodings-demystified
                       

                       三、数据校验

                       

                      完成新数据平台的搭建后,一般会和原有的数据平台并行运行一段时间,一方面是为了和原有平台进行业务和数据的比对,确保业务的正确性和连续性;另一方面,应用改造迁移是一个循序渐进的过程,在所有应用迁移完成前,原有数据平台还是要承担正常的业务访问。一般的做法是通过类似灰度发布的过程,开始的时候同时往两个平台写入数据,但只有原有数据平台对外提供业务访问,每天通过数据校验作业,比较两个平台的数据一致性。经过一段时间,确认数据没有问题后,再把对外访问的流量切换到新的数据平台,再经过一段时间撤除原有平台上的作业。

                      接下来以Oracle迁移到Greenplum为例介绍一下数据比对的方法。


                      首先,分别往Oracle和Greenplum中插入Demo数据:

                       

                      --oracle

                        CREATE TABLE hello(id int,name varchar2(32),price number);
                        INSERT INTO hello SELECT rownum,rownum,rownum*0.1 FROM dual Connect By Rownum <= 100;


                        --greenplum

                          CREATE TABLE hello(id int,name varchar(32),price numeric);
                          INSERT INTO hello select i,i,i*0.1 FROM generate_series(1,100) i;

                          方案1,数据条数比对

                          这是最简单和常用的方法,就是在Greenplum和Oracle中比分别统计出数据表的条数,然后进行比对。如果条数匹配,就认为两边数据是一致的。这种方法的优点是效率很高,缺点是不能完全保证数据的一致性。

                          以测试数据为例,分别在Oracle和Greenplum中执行以下命令:


                            select count(*) from hello;

                            输出结果分别如下图所示。

                            Oracle中输出count(*)结果


                            Greenplum中输出count(*)结果

                             

                            方案2,数据条数比对+关键字段校验


                            在条数比对的基础上,再加上关键字段的校验,通过这种方法基本上可以确保数据的一致性。这种方法的缺点是需要业务方梳理相应的表,找出关键字段,对不同表写不同的比对语句。另外,也不能保证非比对字段的数据一致性。
                             
                            以测试数据为例,分别在Oracle和Greenplum中执行以下命令:
                              select count(*),sum(price),avg(price) from hello;

                              执行结果分别如下图所示。

                               

                              Oracle输出特征值结果


                              Greenplum中输出特征值结果


                              方案3,通过md5算法计算整表的checksum

                              md5算法有着非常悠久的历史,通常用来作为加密和验证数据文件是否被篡改的手段,并且具有跨平台和数据库的特点。很多数据库,如PostgreSQL、MySQL、Oracle,都有对应的函数实现。

                              以测试数据为例,分别在Oracle和Greenplum中执行以下命令:

                              --Oracle

                                WITH foo AS (SELECT stragg(lower(standard_hash(id||name||to_char(price,'fm999999990.999999999'), 'MD5')) order by lower(standard_hash(id||name||to_char(price,'fm999999990.999999999'), 'MD5')) asc) AS total_md5 from hello ) SELECT lower(standard_hash(total_md5, 'MD5')) AS md5 FROM foo;

                                --Greenplum

                                  WITH foo AS (select string_agg(md5(id||name||to_char(price,'fm999999990.999999999')) order by md5(id||name||to_char(price,'fm999999990.999999999')) asc) AS total_md5  FROM hello) SELECT md5(total_md5) AS md5 FROM foo;


                                  执行结果分别如下图所示。


                                  Oracle中全字段算md5结果


                                  Greenplum中全字段算md5结果


                                  通过查询数据字典,将上述过程封装成pl/pgsql和pl/sql过程语言,就可以实现表的md5值计算。其优点是对应用入侵较小,具有通用性,可以做全字段比对;缺点是需要对数据进行排序,比对过程较慢,即使使用Greenplum,最终md5值的计算也要汇总到master节点进行全局排序。而且,当表非常大时,string_agg会报内存不足的错误。另外,如果有中文等特殊字符,要求Oracle和Greenplum有相同的数据库端编码,否则显示出的字符看起来一致,但由于底层字节码不同,也会导致md5值不一样。

                                   

                                  方案4,将每行计算出的md5值转换为4组数值类型,最终将sum后的结果作为整表的checksum值


                                  由于md5是128位的十六进制编码,需要转换为4组32位编码的数值类型。通过这种方式,不需要进行全局排序,性能大大提升,另外也克服了string_agg和md5函数对内存使用的限制,但中文等特殊字符的编码限制仍然存在。

                                   

                                  以测试数据为例,在Oracle下运行以下命令:


                                  --Oracle

                                    WITH foo AS (SELECT standard_hash(''||id||name||to_char(price,'fm999999990.999999999'),'MD5') AS hash FROM hello),
                                    foo1 as (SELECT sum(to_number(substr(hash, 1, 8),'xxxxxxxx')) AS hash_p1,sum(to_number(substr(hash, 9, 8),'xxxxxxxx')) AS hash_p2,sum(to_number(substr(hash, 17, 8),'xxxxxxxx')) AS hash_p3,sum(to_number(substr(hash, 25, 8),'xxxxxxxx')) AS hash_p4 FROM foo)
                                    SELECT lower(standard_hash(sum(hash_p1)||sum(hash_p2)||sum(hash_p3)||sum(hash_p4),'MD5')) AS md5 FROM foo1;


                                    运行结果如下图所示。


                                    Oracle中经过性能优化后算md5结果


                                    在Greenplum下运行以下命令:


                                    --Greenplum

                                      WITHwith foo ASas (SELECTselect md5(''||id||name||to_char(price,'fm999999990.999999999')) ASas hash FROMfrom hello),
                                      foo1 ASas (SELECTselect sum(('x' || substring(hash, 18))::bit(32)::bigint) ASas hash_p1,sum(('x' || substring(hash, 98))::bit(32)::bigint) as hash_p2,sum(('x' || substring(hash, 178))::bit(32)::bigint) ASas hash_p3,sum(('x' || substring(hash, 258))::bit(32)::bigint) ASas hash_p4 FROMfrom foo)
                                      SELECTselect md5(''||sum(hash_p1)||sum(hash_p2)||sum(hash_p3)||sum(hash_p4)) ASas md5 FROMfrom foo1;

                                      运行结果如下图所示。


                                      Greenplum中经过性能优化后算md5结果


                                      如果发现表的数据不一致,接下来要定位到具体的记录。当数据量非常大时,通过人工或外部程序进行比对都不太现实,这时候可以利用Greenplum强大的并行计算能力,通过两表关联来比对每行的md5值,找出具体的差异。

                                       

                                      以测试数据为例,命令如下:


                                        WITH oracle_checksum AS (SELECT id,md5(textin(record_out(foo2))) AS row_md5 FROM ora_hello AS foo2),
                                        gp_checksum AS (SELECT id,md5(textin(record_out(foo3))) AS row_md5 FROM gp_hello AS foo3),
                                        compare_result AS (SELECT oracle_checksum.id AS ora_id ,oracle_checksum.row_md5 AS ora_md5,gp_checksum.id AS gp_id,gp_checksum.row_md5 AS gp_md5 FROM oracle_checksum full outer join gp_checksum on oracle_checksum.row_md5=gp_checksum.row_md5)
                                        SELECT * FROM compare_result WHERE ora_md5 is null or gp_md5 is null;


                                        总之,用户可以根据具体的业务要求和数据平台选择不同的数据比对方案。一般来说,选择方案2的用户较多,因为该方案不依赖底层数据平台,也不依赖具体数据库端的编码,数据一致性和性能都能得到较好的保证。但缺点是,在一些特殊情况下并不能保证数据完全一致,在Greenplum数据库中可以将方案4封装成如下的checksum函数来判断表中数据是否发生变化,从而扩展出更多的应用场景。 在8个计算节点的Greenplum真实用户环境下,计算1.5亿记录的checksum值只需要10秒。


                                          CREATE OR REPLACE FUNCTION checksum(tablename character varying)
                                          RETURNS text AS
                                          $BODY$
                                          DECLARE
                                          v_tablename varchar := $1;
                                          v_md5 varchar;
                                          v_check_sql varchar;
                                          BEGIN
                                          v_check_sql := $$with foo as (select md5(textin(record_out(foo2))) as hash FROM $$||v_tablename ||$$ as foo2), foo1 as (select sum(('x' || substring(hash, 1, 8))::bit(32)::bigint) as hash_p1,sum(('x' || substring(hash, 9, 8))::bit(32)::bigint) as hash_p2,sum(('x' || substring(hash, 17, 8))::bit(32)::bigint) as hash_p3,sum(('x' || substring(hash, 25, 8))::bit(32)::bigint) as hash_p4 from foo) select md5(''||sum(hash_p1)||sum(hash_p2)||sum(hash_p3)||sum(hash_p4)) from foo1;$$;
                                          EXECUTE v_check_sql INTO v_md5;
                                          RETURN v_md5;
                                          END;
                                          $BODY$
                                          LANGUAGE 'plpgsql' immutable ;


                                          下周,我们将在第三篇中,为大家分析Oracle迁移到Greenplum的特殊场景


                                          本文摘录自Greenplum官方教材《Greenplum:从大数据战略到实现》,点击阅读原文获取购买链接。




                                          最后修改时间:2020-05-14 15:06:25
                                          文章转载自Pivotal研发中心,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                                          评论