数据库高可用是企业级用户在关键业务系统中对数据库的基本要求,在高可用方面,怎么提高能力都不为过。也正是因为高可用的问题,很多金融企业在选择核心系统数据库的时候,都被迫选择分布式数据库。因为目前国产集中式数据库在高可用切换方面与分布式数据库相比还存在较大的差距。前段时间在一个会议上,有一家券商就对某个国产集中式数据库厂商提出了高可用的需求,问他们有没有能力在证券交易系统上实现对Oracle的替代。
实现自动切换,RPO为零,RTO越低越好是关键业务系统给国产数据库厂商出的一道考题。在这方面Oracle用TAC给出了一个近乎完美的答案。在Oracle RAC集群中,如果某个节点故障,应用系统的连接可以快速的以秒级的速度切换到存活的节点,SELECT查询以及绝大多数DML/DDL也可以直接切过去继续执行。这意味着一个建表操作或者一个数百万数据写入的事务可以在用户无感知的情况下在一个实例故障时完成透明切换。顶多最终用户会感觉某笔交易的延时长了一点点。在23C中,TAC不仅仅支持RAC,还支持ADG,当ADG SWITCHOVER的时候,或者是ADG是采用同步模式的时候,主库故障或者切换的的时候,应用可以通过TAC切换到ADG备库。
看起来好像这个功能也不算太难实现,不过为了这个功能,Oracle从98那年的TAF推出到实现TAC FOR JAVA整整花了20年时间,而推出比较全面的TAC功能,要到明年了。Oracle花了25年完成的革命性的功能提升,其背后到底有什么秘密呢?
一个外行来看数据库产品的时候,往往觉得某些功能似乎很简单,似乎只要开发人员代码写得好一点,实现起来很容易。其实有些看似很简单的功能的背后有着十分复杂的逻辑。就像大家一直吐槽的XID64,十多年过去了,在PG 16里依然跳票了,虽然这个功能对于PG来说很必要,但是要想真正拥有这个功能没那么简单。TAC可能不一定有XID 64复杂,但是已经足够复杂了。当RAC某个节点故障的时候,应用的客户端感知RAC节点故障,自动重连到备用实例,如果是只读事务,则根据PGA中的CURSOR数据以及SELECT发起时的SCN在新实例上继续完成没有完成的SQL PLAN余下的操作。如果是写操作事务,事务守护模块在新的连接中重放当前没有完成的事务,实现无感知的自动切换。我们看上面的描述似乎实现起来并不难,实际上要想实现TAC功能,有很多基础能力需要具备。
首先是数据库产品必须拥有一个RPO为0的备机方案,这是确保TAC切换能顺利完成绕不过去的技术点。业务系统高可用全自动快速切换中很重要的一点是数据0丢失。共享存储多读多写,共享存储强一致性读写分离、同步备机等都是可以实现RPO为0的备节点解决方案。如果是同步备机,一般而言可以选择FAR-SYNC方案来避免备机对主库产生的性能影响。
其次是高可用切换的备节点选择问题,在多节点RAC上,如果某个节点故障时,无计划,无策略的随意切换会带来很多不可知的问题,放大集群自研动态REMASTER的开销,严重时甚至会引发集群性能问题。Oracle在这方面设计了SERVICE HA,需要做TAC的应用必须连接到某个设定好的SERVICE NAME,而每个需要TAC切换的SERVICE实现都已经配置好了HA切换策略。通过合理的SERVICE HA策略,可以确保切换的有序性。在这里,就需要数据库产品有SERVICE和SERVICE HA的能力。
数据库核心具备了上述能力后,还需要客户端能够快速切换的能力。在TAF里,是客户端感知到服务器端的异常后,主动采取的切换动作,因为网络超时、数据库访问超时等的判断有一定的TIMEOUT限制,以及RETRY的需求,因此这样的切换往往是分钟级的,存在较大的 延时。为了更快的切换,必须采取其他的方案。Oracle为了实现快速切换,在服务端引入了一个服务—Oracle Notify Service(ONS),当某个数据库实例故障时,ONS会很快发现,并通过Fast Application Notification(FAN)快速通知到每个连接到这个数据库实例的会话。而每个BackEnd也都必须有接受FAN消息,并且根据FAN消息自动决策的能力。
当会话自动切换到新的数据库实例上的时候,对于存在写操作的会话,还需要能够正在进行中的写操作的能力。Oracle是通过Transaction Guard(TG)来实现的 ,TG通过当前会话正在进行的事务的完成情况自动完成未完成的事务。为了实现这个功能,Oracle引入了LTXID。LTXID的全称是Logical Transaction ID,即逻辑事务ID。LTXID由以下四个部分组成:分支限定符(branch qualifier),用于标识事务的分支;全局事务ID(global transaction ID),用于标识事务的全局范围;事务分支号(transaction branch number),用于标识事务的分支序号;事务序列号(transaction sequence number),用于标识事务的序列号。在TG中,把一个本地事务模拟成一个分布式事务,并将这个逻辑分布式事务的 ID与一个真实的本地事务做关联。本地事务的变化也会同步到TG中的逻辑映射中,当发生故障切换时,TG可以根据当前操作的状态(Prepared、committed、rolled back、complete、unknown等)采取不同的 自动化动作,完成写操作的自动切换。如果事务的状态是未知(unknown),事务协调器会重新查询各个分支的状态,并根据多数派原则(majority rule)决定事务的最终状态,并选择回滚还是提交。
似乎到这里TAC所需要的功能基本上都罗列了,不过还没完,还有很多很重要的小地方的改造。比如判断当前数据库切换时发生的哪些错误是可以恢复的,哪些错误是无法恢复的。比如当前事务中存在一个当前数据库无法恢复的错误,那么再去尝试无损切换是不合理的,此时最好的办法是向客户端报个措,让应用程序去处理这个故障。Oracle的处理方式是对每个故障增加了一个OracleException.IsRecoverable属性,来标志故障是否可恢复,对于可恢复的故障才采取TAC无损切换,否则立马向客户端报错。
前几天我写了一篇关于Oracle TAC的文章,当时就有好几个国产数据库厂商的朋友和我沟通,说他们想在自己的数据库产品中实现TAC,从而更好的改善用户体验。我当时就和他们说,TAC对用户来说是个十分棒的功能,但是要实现TAC其实并不容易。当时我承诺有空了再写篇详细一点的文章来介绍一些TAC的实现细节,这个周末正好想起了了,于是开始构思,今早约好了10点去拜访一家国产数据库厂家,因此早上的时间比较充裕,就写了上面的内容。
实际上对于实现TAC来说,还有最后一个难关,那就是Oracle在TAC上的专利壁垒。作为参与全球化竞争的中国数据库企业,遵守国际规则,尊重知识产权是必须要做的事情。在TAC实现路径上,Oracle已经拥有了大量的专利,因此我们的国产厂商直接照着Oracle一顿猛抄肯定是不行的。首先我们要去研究明白在这方面哪些是Oracle的专利,必须在实现中对这些进行规避。