笔者上述感叹的背后是有故事的。那是本世纪最“2”的一天,笔者在排查Oracle到ADB(此处指ADB PG,全称AnalyticDB PostgreSQL版,是阿里的MPP架构的数据库)之间数据实时同步过程中,突然发现自己埋了个大雷——没区分空值和空字符串,导致同样的字段值在全量同步阶段标志为空字符串,增量同步阶段则标志为空值,这显然不是一种合乎逻辑的行为。
其实在发现这个问题前,笔者已经在这项目中苦苦挣扎了一个月,在此之前的两天,笔者自以为数据质量已经完全没问题,数据同步达到“又不是不能用”的境界了(笔者注:此处能用是真的能用,非为上线而上线那种能用)。Oracle DB本身确实不区分空值和空字符串,对于搞Oracle出身的工程师,没意识到这个似乎情有可原,然而笔者作为搬砖佬,已经前前后后搬了十几年,数据库异构迁移项目也认认真真地做过好几个,这件事对笔者而言,其实无异于“低级错误”。
回想起来,这个项目笔者做得甚是狼狈,这里面固然有进度紧以及新工具、新技术引入需要边学边用的客观因素,但这不足以平息笔者对自己的愤怒。问题来了,在开源技术使用日益广泛的今天,笔者也可能突然被要求用一个新工具同步数据到一个新数据库,时间还可能更紧迫。到时怎么办呢?再愤怒一次吗?不了不了,还是脚踏实地总结一下,记下这些坑,日后类似项目,哪怕被拿着枪指着头也好,下述问题都要在前期阶段予以考虑。
一、空值和空字符串
对于Oracle DB⽽⾔,空值和空字符串是同⼀回事,其中判断某个值是否为空值/空字符串统⼀使⽤"IS NULL"即可,⾄于=''是⽆效的。
这点对于其余数据库来说,可真不⼀定了。就源端为Oracle DB,⽬标端为其余数据库的项⽬⽽⾔,我们需要与开发商确认好,⽬标端究竟⽤哪种值表⽰源端的空值/空字符。⽽对于实时同步的项⽬来说,还需要在全量同步以及增量同步期间保持数据的⼀致。
除了空值和空字符串以外,其实还有⼀个隐藏的⼤boss——固定⻓度类型中的空格值。以Oracle DB为例,char类型⽤于存放固定⻓度的字符串,⽽⻓度不⾜的字符串则会被⾃动补充空格,对于这种值,⽆论是char_col=''还是char_col is null均⽆法识别,只能通过trim(char_col) is null识别,对此值使⽤length函数会返回本字段的⻓度,⼀个⾮0值。⽽这种数据同步到adb后则表现有⼀定差异了,char_col=''可以识别这种数据(哪怕ADB中也是⽤了固定⻓度的数据类型),然⽽对这个值使⽤char_length函数,结果会返回0,如果实在要把这种数据抓出来,我们还得加⼀个条件oct_length(char_col)>0。很明显,这⼜是⼀个坑!
二、数据类型转换问题
笔者的朋友阿强一直立志做一个温文儒雅的人,嗯,前提是不要让他“写材料”、“擦屁股”以及“估算工作量”!没错,“估算工作量”对他这种以txt狂魔自称的人来说是一种煎熬,光说起这个词,他已经脑补出把pm按在地上,一边揍一边说,“我想做个好人,为什么要逼我”?问题来了,如果让阿强估算这个数据类型转换的工作量,估计他连40米的刀都可以拿出来了。
跨数据库的表字段数据类型转换工作主要涉及“精度”、“效率”、以及“兼容性”等方面。部分数据同步软件确实具备自动转换的功能,然而这种情况,仅对于管理规范的数据库适用,不规范的库则需要耗费较大精力了,下面举几个典型的例子说明这个问题:
以从Oracle DB到PG的同步为例,整型的值固然可以通过number类型(不设定精度和小数位)存放到源端Oracle数据库中,问题来了,PG中应该用啥字段对应了,仅仅安全起见,免得精度丢失,那肯定是numeric类型,然而这毫无疑问存在性能损失。Bigint等类型自然是更好的选择,可是这又牵涉到与开发商沟通这样有没有可能导致应用报错等情况了,这又是一个工作量。
以从Oracle DB到DB2的同步为例,DB2有一个隐性要求,组成主键的字段值前后不能包含空格,否则会被过滤掉,这样会造成一些在源端Oracle数据库本来就不相同的两条记录的在目标端DB2库被误判为同一条记录进而引发数据冲突,影响数据同步。
不要问为什么要在id类字段加空格:
- 这是合法的;
- 这未尝不是一种有创意的备份数据方法。
这么干,真的,没毛病!
以从Oracle DB到ADB的同步为例,由于MPP架构需要,我们需要额外指定DISTRIBUTED列。对于ADB而言,这里还有一个附带的要求,这个列应当为主键的一部分。
以Oracle DB到HBase的同步为例,HBase是强制要求有主键的,否则不能同步。之前的项目中,笔者被迫无奈拿Oracle的转换后的ROWID作为HBase的rowkey,满足其同步前置条件。
为什么是转换后?这又是另一个故事了,这里就不展开了,只提示个关键词,“预分区”。
嗯嗯,这明显是个坑。那种由开发商定表结构的项目得心存感激,真的!
三、字符集转换问题
字符集转换深究起来其实并不是容易的事情,几年前笔者所参与的一个O2O同步(即Oracle到Oracle的同步,下同) 迁移项目中,涉及了BIG5到UTF8的转换,当时的同步工具为OGG。这个项目中各种乱七八糟的数据至今仍然对笔者历历在目,当然,这也让笔者能更有经验地处理异构数据库同步中的字符集转换问题。
跨字符集转换的工作其实陷阱不少,因此,条件允许的话,笔者建议尽量规避。当然,遇到PG这种服务器端不支持GBK,而源端Oracle DB偏偏是GBK的情况,那只好迎难而上了,下面为笔者的建议:
- 涉及中文的情况,所见非所得,判断一条中文记录是否正常的依据,应该为其十六进制编码是否正常。对于Oracle DB而言,我们可以用dump函数,其余DB需要找到类似的。
- 中文为多字节字符,在不同的字符集下占用的字节数并不一致,典型例子为GBK为2字节,UTF8为3字节。目标端环境可能需要相应调整字节宽度。
- 不同字符集所涵盖的汉字是不一样的,例如BIG5中就没有“邨”字。
- 字符集中有一个“自定义”区域,如不进行特殊处理,有可能导致数据丢失。
“乱码”会造成很大的困扰,对于Oracle DB而言,大致有如下几种情况:
- 无法通过Oracle自带convert函数转换为目标库编码,以ADB为例,这部分数据会导致GPFDIST导入失败,进而影响数据全量同步。
- 可以通过Oracle自带convert函数转换为目标库编码,但无法重新转换为原有数据,这部分数据会有潜在的数据丢失影响,对于迁移类项目需要重点关注是否涉及“自定义”字符区域。
- 含义不明的单字节字符,如chr(0)、chr(255),这些字符往往夹杂在正常的字符中,以ADB为例,涉及dts( Data Transmission Service, 数据传输服务,系阿里的数据同步工具/服务)增量同步的话,相应记录有数据一致性风险。
四、特殊字符处理
对于数据库异构同步而言,特殊的字符,诸如单引号、双引号、换行、斜杠、反斜杠等等也是一个困扰项,这一点在数据全量同步阶段尤其明显。
对于文本方式的全量数据同步来说,我们可以考虑下述几种方式:
- 使用CSV格式;
- 使用多字节分隔符;
- 进行数据清洗;
- 仅同步“正常”数据,“特殊”数据另行处理。
五、异常记录处理
这里的异常记录,指的是那种本身就违反数据库规范,不应该插入到数据库中的记录。
以Oracle DB为例,笔者遇到的记录有异常日期格式以及异常数值两类。
异常日期格式,典型例子有"0000-00-00 00:00:00"和"2022-02-30 00:00:00"。笔者在好几个客户环境都遇过这种数据,以至于笔者觉得这很“常见”,需要加到测试项目里面。笔者这段时间做的Oracle到ADB同步项目确实遇到这种数据了,后者还造成dts的增量同步中断,风险很高。所幸笔者的dts源端库是基于OGG的目标库部署的,Oracle自己的OGG工具也不能同步这种数据,这间接地挡了这部分异常的增量数据。在此基础上,笔者只需要修复已有的异常数据即可,修复方法也很简单,先+1再-1能修复大部分数据,至于不能修复的只能和业务协商重置一个值了。
异常数值类型,典型例子为NaN(即Not a Number),笔者仅在一个客户环境中遇到,当时的场景为O2O同步 ,比较可怕的是连基本“来者不拒”的数据泵都无法同步这种数据。考虑到这个环境没遇过这种数据,笔者这次偷懒了,没做相应的测试。
六、全量同步测试
通常情况下,各种数据同步软件都会自带全量数据同步的功能,至于这个功能的效率、资源消耗、空间占用等项目需要进行评估。如果其不能满足需求,则可能需要考虑替代的手段。
在选取测试表的时候,笔者考虑综合下面几个因素选择几个测试表:
- 需要包括大表,大表往往是个瓶颈项;
- 需要囊括本次同步涉及表的字段类型;
- 如果环境中存在中文等多字节数据,则建议包含这种表;
- 建议找静态的表或者准静态进行测试,以方便核对数据一致性。
七、增量同步测试
作为数据同步项目,同步效率是一个重要因素,笔者建议在搭建完整的同步链路之前,拿数据变更频繁的关键表进行测试,通过单表单进程的方式,剔除潜在的配置不当风险。
对于这方面,笔者建议如下:
- 尽量使用真实的数据;
- 笔者这次测试通过Ogg同步增量数据,比较切合生产实际变更情况,这种方式可以参考。
增量同步发起后,在目标数据库后台观察对应的SQL语句。以笔者本次项目为例,这个阶段发现了两个问题:
- 由于大小写敏感问题,dts目标侧未成功识别出主键,导致所有字段被加到where条件,影响效率,此问题后来通过修改同步配置解决。
- 笔者观察到dts侧虽然设置了高并发度,但实际运行中,仅个别进程工作,其余处于空闲状态,无法充分利用资源。此问题后来通过修改配置参数解决。
八、数据一致性测试
数据一致性又是一个可以另外写一篇文章的话题,对此,笔者建议如下:
- 对比静态或者准静态的数据。很显然,笔者这次使用的ogg中间库方案很切合这个主题,如果没这个,笔者只能通过停止同步进程后反查其停在哪个点,再用这个时间点做检验了。这个想法理论上可行,然而以笔者对dts的浅薄理解,这条路并不通,原因在于dts所停的时间点并不完全准确。
- 活用md5函数。大部分正经的数据库均包含内置的md5函数(PS:无意内涵DB2,真不是故意的),这可以将一个复杂的字符串简化,以便用于运算确认两端的数据一致性。
九、软件局限性
“越是漂亮的女人就越会骗人,记住啊!”
“不光是漂亮的女人不能相信,连貌似忠良的男人也不能相信。”
我觉得这段对话充分展示了一个产品的售前与售后的结局——殊途同归。对于售前来说,拼指标、造场景、讲故事等等手段都是为了证明我家产品很棒,快来买买买;就售后而言,找到产品的痛点,予以规避,以达到保证工作顺利开展,避免一口大锅从天而降的目的。大家都是靠博弈而生的,没什么两样,手里的牌均是对技术的了解。
扯远了,回到it项目中,异构数据库的同步往往是逻辑的同步方式,这种方式必然有各种瓶颈的。对售后来说,再怎么谩骂售前“管杀不管埋”也无济于事,最现实的做法莫过于:找到短板,通过改善流程、优化需求甚至协同开发商改造应用的方式保证软件的稳定运行。
这里先讲个故事。Timesten是Oracle的内存数据库,其Cachegroup功能可以实现从物理库(即Oracle DB)到内存库的实时数据同步,而这个同步延迟对业务稳定运行是非常关键的。在实际使用中,运维人员总结出的经验就是得规避大事务变更,最终他们与开发商达成相应的操作规范,无论是业务变更需求也好,数据库运维发起的清理作业也罢,如涉及Timestens同步的表,都得遵循变更量达10w万就得分批提交,每个批次2万条记录,每批次之间sleep 30秒的硬性规定。我觉得这个故事的结局很完满了,真的,要是换成非得揪着Timesten不放,意图纯粹靠软件解决问题的话,那才是妥妥的灾难现场呢,毕竟其基于trigger的同步机制从原理上就对大事务极不友好……
问题来了,如何找到软件的短板呢?
阅读官方文档自然是一个渠道,当然,阅读也是有“技巧” 的 :
我们支持xx指标以内的场景,这句话可以理解为超过这个您就得想想办法了,同时,这个值也许是要打个折的,毕竟环境不一样,存在差异也是很合理的。
我们支持功能a,也支持功能b,这都是实话,至于同时支持功能a和b是您自己认为的,我可没说。
这个嘛,春秋笔法是有的,这种玩法自古就有了。陈寿不也没在《三国志》里面明说司马昭弑君吗,后来大家不都知道了吗?
除了文档以外,我们还可以考虑结合自身经验考虑下述点。
大事务测试
分别对同步范围内外的对象做批量操作,加大数据库日志量,观察其对数据同步以及系统的影响,具体包括cpu、内存、io、空间等资源消耗以及同步延迟等。
以dts为例,源端oracle数据库产生的所有数据均会被拉到dts的库中分析,哪怕这数据与我们的同步策略无关。
目前有个黑名单功能可以绕过这问题。
长事务测试
包含启动增量同步前开启的事务能否正常同步、长时间未提交的事务是否影响同步进程重启等维度。
很明显, 这是被ogg吓到的结果。
频繁事务测试
笔者曾在O2O同步环境中遇到某应用使用了大量with as语法,后者隐式开启了大量的短事务,进而短时间内事务量暴涨,进而 导致同步软件Ogg抽取进程出现延迟。这个问题后来找开发商修改语句就解决了,然而其对笔者的心理阴影一直都在,以至于每遇到一个新场景,均会想想会不会遇到类似的问题。
事务顺序
这个探究的是,软件同步是否能保持事务的顺序,如不能保持,那就得多留个心眼了,这种情况轻则导致同步延迟误判,重则导致旧 数据覆盖新数据,影响数据一致性。
一个简单的测试样例为,创建一个周期性(如每分钟)更新的时间戳表,这个表的记录数与源端Oracle DB的节点数一致。定时脚本依次连接各个实例并以当前时间更新相应的字段,在目标端,我们可以通过查询时间点表观察会不会出现下面两种情况:
- 后更新的数据是否会先被查询出来;
- 同步出现延迟的话,时间戳表记录的时间会不会与同步进程的时间戳保持一致。
批量ddl测试
对于基于数据库日志的同步工具,大批量的DDL语句很可能会触发源端解析缓慢的情况,毕竟这涉及与数据字典的交互。
笔者曾遇过某个基于Ogg的O2O同步环境遇到这种瓶颈,而最终的解决方案为调整开发商版本上载的脚本,加大DDL语句之间等待时间间隔。
基于同步原理短板的探究
对于基于rowid的同步方案,没啥好说的,只能从规范上减少move、shrink等改变rowid操作,实在要操作的话,需要重新同步相应的数据。
对于基于主键的同步方案,则重点考虑如何处理无主键表如何处理。
进程重启测试
包含两种场景,正常重启以及异常重启。
异常重启即高可用方面的,具体不展开了;至于正常重启的话,需要观察进程的一些自定义参数会不会被重置。