记录些Spring+题集(16)

动态链接库的实现原理

假设有这样两段代码,第一段代码定义了一个全量变量a以及函数foo,函数foo中引用了下一段代码中定义的全局变量b。

图片

第二段代码定义了全局变量b以及main函数,同时在main函数中调用了第一个模块中定义的函数foo。

接下来编译器出场,编译器会把这个两个源文件编译成对应的目标文件。目标文件中主要有两部分,代码段和数据段,这两部分里面分别包含什么内容呢?

我们定义的全局变量会被放到数据段,代码被编译生成的二进制指令会被放到代码段,第二个目标文件也一样。

图片

注意看第一段代码,这里引用了一个其它模块定义的全局变量b,这一信息记录在第一个目标文件,第二段代码引用了其它模块定义的函数foo,这一信息记录在第二个目标文件。

注意看第一段代码,这里定一个全局变量a和函数foo,我们记录下来,第二段代码定义了全局变量b和函数main,同样记录下来。

图片

第一个模块引用了变量b,变量b的定义可以在第二个模块找到。第二个模块引用了函数foo,foo的定义可以在第一个模块找到。

这个过程叫做符号解析。

图片

这里看到的引用以及定义的符号保存在所谓的符号表中。

而如果第二个模块引用了一个叫做bar的变量,链接器翻遍所有其它模块都没找到bar这个符号的定义,而只找到了一个叫做foo的定义,这时链接器就会报一个叫做符合未定义的错误,这个错误写c/c++的程序员一定不陌生。

图片

接下来链接器会把数据段合并到一起,代码段合并到一起并确定符号的内存地址,这个过程叫做重定位。

了解了这些就可以开始讲动态库的实现原理了,动态库又叫做共享库,我们的问题是,动态库是怎么实现可以被程序之间共享的呢?

假设现在有两个运行的程序和一个动态库liba. so,动态库中定了一个全局变量a,第一个程序把变量a修改为了10。

图片

然后第二个程序开始运行,第二个程序也使用该动态库,然后把全局变量a修改为了20。

图片

这是第一个程序运行一段时间后决定打印变量a,这时你会惊讶的发现变量a从10变成了20,但是为什么。原因就是这两个程序共享了同一个数据段,所以一个程序对数据的修改对另一人程序是可见的,因此动态库中的数据段不能共享,每个程序需要有自己的数据段。

现在数据的问题解决了,我们来看函数。

假设动态库liba.so需要引用外部定义的foo函数,由于程序1和程序2都使用了该动态库,因此必须定义出foo函数。我们知道函数调用最终会被编译器翻译成call机器指令后跟函数地址。

图片

接下来我们需要解析出foo函数的地址到底是什么,这就是刚才我们提到的重定位,只不过动态库将这一过程推迟到了运行时。

由于程序1的foo函数位于内存地址0x123这个位置,因此链接器将call指令后的地址修正为0x123。

这时CPU执行这条call指令就能正确的跳转到第一个程序的foo函数。

图片

而第二个程序的foo函数为内存地址0x456这个位置,接下来第二个程序开始运行,CPU开始执行foo函数,由于第二个程序的foo函数在0x456,因此我们希望CPU能跳转到这里,但由于动态库中call指令后跟的是0x123这个内存地址,因此CPU执行foo函数时依然会跳转到第一个程序的foo函数。

图片

这时系统就出现了错误。问题出在了哪里呢?

主要是call这条机器指令,这条指令后跟了一个绝对的内存地址,而不要忘了,这条指令或者说动态库是要被各个程序共享的,显然我们不能直接使用绝对地址。

该怎么办呢?计算机中所有问题都可以通过增加一个中间层来解决。

图片

这样我们就摒弃了直接调用,而采用间接调用。

而我们这里对函数的讨论对于全局变量的应用也是一样的道理,全局变量的使用也存在同样的问题,只不过是从函数调用变成了内存读写,解决问题的方法一样,我们从直接应用改为间接引用。

接下来我们依然以函数调用为例来讲解。那么这个中间层到底是什么呢?

答案就是got。

还记得刚才提到的每个程序都有自己的数据区吗,这个got段就属于数据区的一部分。

图片

got中有什么呢?got中记录了引用的全局变量或者函数的地址,在程序运行时链接器会找到foo的内存地址,然后填到got表中,这样通过查got表我们就能知道函数foo的内存地址了。

接下来的问题就是当CPU调用foo函数时怎么才能知道got表在哪里呢?

注意刚提到每个程序都有自己的数据区,实际上对于动态库来说也有自己的代码区。

我们现在只需要知道每个程序运行在自己的地址空间中,这些地址空间最终会被映射到真正的物理内存,动态库中的数据区会被映射到不同的内存区域,但代码段会被映射到同一段物理内存中,从而实现共享的目的。

图片

接下来我们重点看进程地址空间中的动态库布局。

注意看,动态库的数据区和代码区总是相邻的,也就是代码区和got段的相对位置总是不变的,而不管动态库被放到了哪个位置。多个程序也一样,也就是代码区和数据区的相对位置总是固定的,这个相对位置在编译时编译器就能确定。

图片

现在foo会被编译成call指令,而程序在加载时链接器会向got段中写入foo的内存地址,显然两个程序的foo地址是不一样的。

接下来CPU开始执行第一个程序的call指令,此时CPU会做一个相对跳转,这个跳转距离是编译器确定的,CPU会跳转到got表,然后查找foo的地址发现是0x123,然后开始执行0x123这个位置的函数。

图片

而如果CPU执行第二个程序中的foo函数,那么CPU同样会进行相对跳转,这不过这次跳转到的是第二个程序的got表,然后发现foo的地址是0x456,然后开始执行第二个程序中的foo函数。

图片

这样我们就实现了执行同一个指令但却会跳转到不同地址的目的,从而在不改动动态库代码的前提先实现共享。

而如果一个动态库中引用了很多外部函数会怎么样呢?

这样程序在启动时链接器不得不对所有函数进行重定位,因此会拖慢程序启动速度。

而我们知道一个程序中不是所有的函数都会被调用到,经常调用的都是少数几个函数,为了利用这一点编译链接系统使用procedure linkage table, plt来推迟重定位这个过程,也就是程序在启动时不进行函数重定位,而是推迟到真正调用函数时,没用调用过的函数根本就不进行重定位,从而加快程序启动速度。

从这个一过程我们可以看到动态库的这种间接调用实际上会对程序性能有一定影响,但相对于动态库带来的好处与便捷,这点影响可以忽略不计。

这样,不管动态库被加载到内存的哪个位置都能正确被各个程序共享。

动态库的这个特性被称之为位置无关代码,简称position-independent code, pic,这就是为什么你在编译生成动态库时要加上pic编译选项的原因。

图片

金融行业如何有效评测分布式数据库事务一致性

分布式事务评测缘起

曾经参与多个国内金融行业的分布式数据库选型测试工作,围绕银行内部的实际业务场景进行验证。遇到了一个比较有意思的案例。

前期在联机业务场景测试中,各大数据库厂商在事务测试上都比较顺利,在功能和性能角度都很好的满足了业务要求。 但在跑批业务场景的测试中,个别数据库厂商就遇到了分布式事务的一致性问题(会读到分布式事务中间状态的数据),但该厂商通过了行业的各项事务测评认证。因此展开了如何有效评测国产数据库事务一致性的话题。

金融行业通用测评

近年来国内自主可控诉求越来越强烈,国内数据库行业蓬勃发展,诞生了很多创业型公司,国家层面也出台了一系列的数据库评测标准,大体分为集中式和分布式数据库的测评。 中国人民银行在2020年发布了一个行业标准,《分布式数据库技术金融应用规范 技术架构(JR/T 0203-2020)》 关于分布式事务测试的描述:

分布式数据库金融标准,是分布式数据库行业中最完整的标准之一,给出了对于分布式数据库的事务ACID测试的指导性意见。

机构测试设计的测试用例,普遍会采用模拟数据库故障方式来验证分布式事务,细分一下场景:

  1. 模拟数据库多副本的重启,可以验证数据库事务的持久性 (ACID中的D)

  2. 模拟主机的异常故障(断网、IO Hang等),可以验证数据库事务的原子性 (ACID中的A)

  3. 采用SQL交互,设置不同的数据隔离级别,开多个链接,手工验证事务的隔离级别 (ACID中的I)

  4. 比如运行TPC-C 或者 特定的转账测试,在数据导入完成、以及程序运行完成后,运行几条一致性校验的SQL来验证数据的一致性 (ACID中的C)

整个测评方案更多是围绕事务ACID,进行了比较全面的覆盖型验证,但从数据库内核研发的视角视角、以及金融行业实际业务场景的实测来看,还是有一定的局限性,否则也不会出现通过分布式数据库金融行业认证,但无法通过金融行业实际业务场景的测试验收。

PolarDB-X的设计和思考

事务是一切的基础

先抛开分布式数据库,我们首先思考一个通用数据库的话题: 数据库事务在哪些地方影响了业务使用? 我们拿一个银行的转账场景做一下例子:A账户余额有100元,B账户余额0元,在这个基础上A向B转账40元 复盘常见的业务场景:

  • 在线联机业务,通过事务ACID机制,需要保证A向B转账过程中的数据一致性,满足任意时刻A和B上的账户总余额为100

  • 数据库备份和恢复,按时间点的恢复(point-in-time recovery)目前也是行业的共识需求,我们需要确保恢复出来的备份集数据,满足A和B上的账户总余额为100

这也是目前行业测试过程中常见的关注场景,但结合业务场景思考一下,还远不止,比如:

  • 跑批业务,典型的ETL机制,通过数据的全量读取和批量写入,为满足业务处理效率,很多数据库厂商会提供旁路导入和导出的机制,同样需要满足事务的总余额为100。旁路导出例子:select * from user partition(p1) order by id desc limit 10,10,每个分片数据单独做分页排序,减少分布式的分页排序代价。

  • flashback query,典型的数据快速恢复场景,Oracle基于MVCC多版本提供了AS OF Timestamp语法,可以快速读取一个历史的事务数据版本,结合insert into xxx select xx as of timestamp '10分钟前'可以快速恢复出业务误操作的数据。

  • 读写分离 or follower read,典型的技术架构场景,比如Paxos/Raft的三副本,很多数据库厂商会提供follower read的能力,本质上就是一种读写分离的架构,同样需要满足事务的总余额为100。需要注意:事务的一致性读 和 多副本下的数据复制延迟带来的数据不一致读,这是两个不同的概念。比如;数据延迟只是让我们读到1秒钟之前的事务数据,但1秒前的数据读取总余额时也要为100,不能读到40或者140的中间状态。

  • 容灾架构(两地三中心、同城3AZ),典型的容灾架构场景,比如考虑两地三中心的极端场景,中心地域挂了,切换到异地机房,异地机房的数据可以有延迟(RPO>0),但需要事务粒度的一致性,满足A和B上的账户总余额为100。

  • 主备复制 or CDC(MySQL Binlog订阅),典型的数据增量复制的场景,常见于数据库的Binlog增量日志,部署异地多活复制,同样需要需要保证事务的一致性,在外置复制增量数据的情况下,满足A和B上的账户总余额为100(查询数据仓库 或者 异构的备库)。

  • HTAP架构,常见的数据库实现为采用多份数据副本的方式,通过行转列构建异步的列存副本,虽然数据行转列会有延迟,但查询到列存副本时同样需要考虑事务的一致性,即使读取1秒前的数据也要满足总余额时为100。

大家可以辩证地思考一下,这部分的业务场景在传统的单机数据库事务中是一个默认能力,但在分布式事务中绝不是一个简单的ACID机制,还需要有更多的顶层设计,一句话总结:事务是一切的基础,影响重大。

图片

举一个反例子来看,常见于传统的分库分表的事务架构,可以通过开源MySQL或者PG的主备强复制、两阶段的事务提交,可以一定程度的满足ACID的定义,但在遇到其他业务场景会事务一致的局限性:

  1. 指定时间的备份恢复,因缺少任意时间点的一致性视图,导致无法满足一致性恢复。变种的方法:定时做高频的一致性快照,比如每各30秒备份一次全局活跃事务链表,可以达到恢复到30秒的粒度

  2. 联机和跑批混合场景,跑批场景读取全量数据过程中,因缺少一致性的视图,会有机会读取到事务提交阶段的状态。变种的方法:跑批场景不能做旁路,全量数据拉取都得经过全局活跃事务链表来判断

分布式事务方案

目前分布式事务常见方案:

图片

简单做一个解读:

  1. XA协议,全名为 X/Open XA 协议,是一项通用的事务接口标准,最早在90年代开始提出,可参考:《Distributed Transaction Processing: The XA Specification》。注意:XA主要基于2PC两阶段提交实现事务的原子性,而分布式下的一致性则需要额外的设计,会出现读偏斜的问题

  2. GTM,最早起源于PG-XC开源数据库,主要是通过GTM分配一个事务ID,通过活跃事务链表来解决事务的可见性问题。目前常见于PG生态,比如GaussDB,活跃事务链表在单机数据库中也比较常见。

  3. TSO/HLC,主要是基于时间戳技术,参考Oracle的MVCC多版本设计,每个事务都有start_ts / end_ts的时间戳,通过时间戳的先后顺序来判断事务的可见性,相比于活跃事务链表会更轻量。TSO和HLC主要还是对于时间戳分配算法上的一些差异,目前来看海外数据库重点关注GEO Partition带来的多活架构。

PolarDB-X 事务设计上的总结:

一、事务的主链路(在线联机业务的ACID)

a. 持久性:Paxos多数派,通过共识协议确保主机故障时RPO=0,不丢中间数据

b. 原子性:2PC + XA,通过两阶段提交,确保事务的原子性

c. 隔离性 & 一致性:TSO + MVCC,通过时间戳版本号 + MVCC多版本,实现跨分片的数据一致性读

二、数据库的备份恢复 / flashback query

按时间点的恢复(point-in-time recovery),通常诉求是通过指定一个物理时间,PolarDB-X在分布式事务设计上,允许将物理时间转化为事务的TSO时间戳,通过分布式事务的TSO来做数据库的精确恢复边界。

flashback query,也算一种特殊的数据恢复场景,通过AS OF TIMESTAMP指定一个物理时间,PolarDB-X会将物理时间转化为事务的TSO时间戳,基于分布式事务的MVCC多版本来实现对历史版本的数据读取。

比如:分布式事务的TSO,是有42位的物理实践 + 16位的逻辑时间戳组成,可以通过位移快速实现物理时间戳和TSO时间戳的转化。

图片

参考文档:PolarDB-X 是如何拯救误删数据的你?

三、联机&跑批 混合场景

联机场景主要是简单的查询和写入为主的高并发业务,非常容易基于分布式多节点来达到线性扩展。而跑批场景更多是围绕数据的批量处理,需要最大化的利用分布式多节点资源的并行性,期望跑批也能达到线性扩展的性能。PolarDB-X跑批场景最佳实践设计上,我们引入了数据旁路hint能力,业务可以通过该hint进行快速的旁路数据导入和导出,随着DN节点数量的扩容,整体跑批容量和吞吐能力可以达到线性增加。PolarDB-X在面向跑批场景的分布式事务设计上,引入业界oracle通用的2PC + MVCC事务机制

  1. 支持undo/redo,事务中间数据通过undo提前落盘,可以支持GB级别的超大事务

  2. 计算存储彻底分离,MVCC的事务上下文(undo/buffer、可见性判断等)全部落在存储节点DN上,计算节点CN只承担分布式事务协调者的状态,可以确保在旁路导入导出时数据存储DN节点的数据一致性。区别于Spanner/Percolator等事务模型、以及基于GTM的事务机制。

四、同构多副本(读写分离、容灾架构场景)

分布式下的多副本,一般会按照multi-raft/paxos的进行分组,同一个分组会复用一个多数派日志流,而不同的分组会有独立的多个日志流,每个日志流会因为各自的复制进度产生一些差异。

PolarDB-X在多副本架构下的事务设计上,引入了强一致读写分离的机制:

  1. 如下图所示,不同副本会因为日志流的复制进度不同而产生差异,需要在读写分离访问副本时进行识别和处理

  2. 分布式事务的上下文,需要在多副本架构下进行数据事务状态的复制,确保在多副本中事务数据的一致性

五、事务增量日志 (CDC增量复制、HTAP行转列)

传统数据库会提供CDC机制(Change Data Capture),在mysql生态里主要就是binlog,下游可以通过定于binlog来实现增量数据同步或者消费。

PolarDB-X在事务增量日志设计上,参考了mysql binlog的协议和格式:

  1. 记录分布式事务的上下文,比如事务中DML的提交顺序、事务TSO时间戳等,将这些状态数据持久化到单个DN的局部增量日志中

  2. 引入CDC全局日志组件,基于分布式事务的TSO时间戳进行多个DN局部日志的汇总、合并和排序,生成最终的binlog增量日志流

图片

增强版转账测试

PolarDB-X 结合众多业务的实际使用场景,我们在典型银行转账测试的场景基础上,增强了场景正交组合测试,可以帮助大家更快、更有效的评测分布式事务。 说明:本测试重点针对ACID中的一致性C场景的测试,事务持久化、原子性的测试,可以复用行业通用测试中的模拟故障来进行验证。

银行转账测试的技术模型抽象

不断随机选取两个账户 A/B,然后模拟 A 账户给 B 账户转账。每一笔转账在数据库中如下图所示:

其中,balance 表示余额,version 表示该账户完成了多少笔转账。在备库一致性读的验证中,我们除了会验证总余额必须一致外,还会先开启事务查询主库所有账户,再开启另一个事务查询备库的所有账户,并确保备库上看到的每个账户 version 值,必须不小于主库中该账户的 version 值,即备库不能产生延迟。

内置数据库测试场景

整体测试设计围绕金融行业的典型场景,进行组合验证分布式事务的一致性。 比如:模拟联机和跑批场景的组合、跑批场景中直连备库(满足联机和跑批的资源强隔离)、以及跑批场景指定访问某时刻的数据(比如,访问历史某时刻的数据,进行数据ETL处理后恢复误操作的数据)

测试场景

场景描述

备注说明

联机流量

N个线程并发的模拟转账

背景流量,通过随机两个分片之间的转账才产生分布式事务

常规一致性场景

M个线程通过select * from xx,读取所有数据后再内存计算总余额,并验证数据的一致性

模拟事务在主副本上并发写和读,确保事务一致性

跑批场景

M个线程通过旁路hint导出(指定hint访问select * from xx),读取所有分片的所有数据后在内存计算总余额,并验证数据的一致性

模拟事务在主副本上并发写,模拟跑批的旁路hint操作,确保事务一致性

flashback query

M个线程通过select * from xx AS OF TIMESTAMP ’xx' 访问历史某一刻的所有数据后在内存计算总余额,并验证数据的一致性

模拟事务在主副本上并发写,模拟数据恢复场景的读操作,确保事务一致性

备副本一致性读 (又名:读写分离)

高级特性:备库强一致读 (replica_strong_consistency=true)

M个线程访问分布式的备副本,通过select * from xx,读取所有数据后再内存计算总余额,并验证数据的一致性如果开启备库强一致读,会检查写后读的可见性,通过行记录里的业务version进行判断

模拟事务在主副本上并发写,模拟读写分离访问备副本,确保事务一致性

跑批场景 + 备副本一致性

M个线程访问分布式的备副本,通过旁路hint导出(指定hint访问select * from xx),读取所有数据后在内存计算总余额,并验证数据的一致性

模拟跑批场景,通过备副本来实现资源隔离,同时需要确保事务一致性

flashback query + 备副本一致性

M个线程访问分布式的备副本,通过select * from xx AS OF TIMESTAMP ’XX' 访问历史某一刻的所有数据后在内存计算总余额,并验证数据的一致性

模拟数据恢复,通过备副本来实现资源隔离,同时需要确保事务一致性

跑批场景 + flashback query

M个线程通过通过旁路hint导出(指定hint访问,select * from xx AS OF TIMESTAMP ’XX'),访问历史某一刻所有分片的所有数据后在内存计算总余额,并验证数据的一致性

模拟跑批场景,通过flashback query来实现日切卸数等类似场景,同时需要确保事务一致性

跑批场景 + flashback query + 备副本一致性

M个线程访问分布式的备副本,通过旁路hint导出(指定hint访问,select * from xx AS OF TIMESTAMP ’XX' ),访问历史某一刻所有分片的所有数据后在内存计算总余额,并验证数据的一致性

模拟跑批场景,通过flashback query来实现日切卸数等类似场景,通过备副本来实现资源隔离,同时需要确保事务一致性

外置组合测试场景

整体测试设计围绕金融行业的测试标准,通过组合一些额外的人肉操作来验证PolarDB-X数据库实例以外的事务一致性。

测试场景

场景描述

备注说明

指定时间点的备份恢复PITR(point-in-time recovery)

1.启动转账程序15分钟

2.备份集指定时间点恢复,时间点T选择为转账程序运行过程中的某一个时间

3.待备份集恢复完成后,人工检查数据一致性

通过模拟联机场景,验证备份恢复后数据的一致性。

1.检查最新一条记录(order by time desc limit 1)的时间戳,与恢复的预期时间T值,满足<=1秒。

2.统计总余额,select sum(balance),满足数据一致性

事务增量复制(CDC日志)

1.启动转账程序,并保持持续运行

2.配置数据库增量复制,比如将PolarDB-X数据增量同步到MySQL

3.增量复制过程中,人工检查数据一致性

通过模拟联机场景,验证下游事务增量日志的数据一致性

1. MySQL的主备复制,会基于PolarDB-X binlog进行回访,通过验证select sum(balance)的总余额,来验证是否满足数据一致性

如何运行测试

PolarDB-X提供了转账测试工具,通过docker一键启动即可快速完成验证,快速配置可以适配特定数据库的语法,从而满足对数据库厂商的分布式事务验证。 比较典型的组合场景:联机场景 + 跑批场景 + 备库一致性(将跑批场景的ETL调度到备库上,在满足事务一致性下,实现联机和跑批业务的互相隔离)

测试场景设计:

图片

安装数据库

1.PolarDB-X安装

参考:PolarDB-X快速开始

PolarDB-X快速开始

https://doc.polardbx.com/quickstart/topics/quickstart.html

# 使用virtual environmentpython3 -m venv venvsource venv/bin/activate
# 安装pxdpip install -i https://mirrors.aliyun.com/pypi/simple/ pxd

创建polardbx.yaml文件

version: v1type: polardbxcluster:  name: pxc-tryout-new  gms:    image: polardbx/polardbx-engine:latest    engine: galaxy    engine_version: "8.0"    host_group: [127.0.0.1]    resources:      mem_limit: 4G  cn:    image: polardbx/polardbx-sql:latest    replica: 1    nodes:      - host: 127.0.0.1    resources:      mem_limit: 8G      cpu_limit: 4  dn:    image: polardbx/polardbx-engine:latest    engine: galaxy    engine_version: "8.0"    replica: 1    nodes:      - host_group: [127.0.0.1, 127.0.0.1, 127.0.0.1]    resources:      mem_limit: 8G  cdc:    image: polardbx/polardbx-cdc:latest    replica: 1    nodes:      - host: 127.0.0.1    resources:      mem_limit: 4G

通过PXD(PolarDB-X部署工具) 创建实例,建议主机规格>=8C32G (压测工具会产生一定的并发压力)

pxd create -f polardbx.yaml

部署完成后,PXD 会输出 PolarDB-X 集群的连接方式,通过 MySQL 命令行即可登录 PolarDB-X 数据库进行测试。

2.部署其他数据库(比如:开源的单机MySQL),作为PolarDB-X CDC同步到的下游数据库

# 部署mysql 8.0.32docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -v /etc/localtime:/etc/localtime -d mysql:8.0.32 --default-authentication-plugin=mysql_native_password --gtid_mode=ON_PERMISSIVE --enforce_gtid_consistency=OFF
# 配置PolarDB-X CDC和MySQL的复制关系,指定PolarDB-X的账号和密码docker exec -it some-mysql  bashmysql -h127.0.0.1 -uroot -p123456
> CHANGE MASTER TO    MASTER_HOST='10.0.0.102',     MASTER_USER='polardbx_root',    MASTER_PASSWORD='',    MASTER_PORT=xxx,    MASTER_LOG_FILE='binlog.000001',    MASTER_LOG_POS=4;# 启动主从复制> start slave ;> show slave status;

3.登录PolarDB-X数据库,创建测试库 transfer_test:

基于步骤1返回的连接串,快速登录PolarDB-X:

mysql -h127.0.0.1 -uroot -pxxxx
# 创建PolarDB-X数据库> create database transfer_test mode = auto
# 开启PolarDB-X的follower read(副本读)> set global ENABLE_FOLLOWER_READ=true;

创建测试库后,可以登录单机MySQL实例看下是否正常同步了transfer_test库,如果看到了库说明整体复制链路就正常了

测试工具使用

1.拉取镜像

docker pull polardbx/transfer-test:latest

2.准备配置文件 config.toml,假设存放于 /tmp/transfer-config/config.toml ,以下是一个示例:​​​​​​​

# 主库的数据源, 格式: '${user_name}:${password}@tcp(${ip}:${port})/${db_name}'dsn = 'polardbx_root:123456@tcp(127.0.0.1:8527)/transfer_test'
# 账户数量row_count = 100
# 每个账户的初始余额initial_balance = 1000
# 建表语句后缀,适配不同的分布式数据库create_table_suffix = 'PARTITION BY HASH(id) PARTITIONS 4'
# 数据库加强特性:备库强一致读 (查询follower副本或者备库场景,是否强制校验写后读的数据一致性)# 比如:主库写了一条测试记录并更新version=100,查询路由到备库后,对应的读操作需要确保一定读到业务测试的写,保证读到的version>=100replica_strong_consistency = true
# 模拟转账,构造分布式事务[transfer_simple]enabled = truethreads = 10
# 场景1. 正常在线查询,校验主库的数据一致性[check_balance]enabled = truethreads = 2
# 场景2.使用hint模拟跑批任务,校验主库的数据一致性[session_hint]enabled = truethreads = 2
# 场景3:使用flashback query模拟数据恢复,校验主库一致性[flashback_query]enabled = truethreads = 2# flashback 到 (min_seconds, max_seconds) 秒之前min_seconds = 10max_seconds = 20
# 场景4. 查询follower副本或者备库,校验副本的数据一致性[replica_read]enabled = truethreads = 2# 加在 SQL 语句前的 hint,对于 PolarDB-X,该 hint 可以强制查询路由到备库replica_read_hint = '/*+TDDL:SLAVE()*/'# 备库数据源,对于PolarDB-X和主库dsn保持一致即可,如果测试其余数据库的备库,可以通过指定备库的连接串replica_dsn = 'polardbx_root:123456@tcp(127.0.0.1:8527)/transfer_test'
# 场景2和4的组合,使用hint查询follower副本或者备库,校验组合场景的数据一致性[replica_session_hint]enabled = truethreads = 2# 加在 SQL 语句前的 hint,对于 PolarDB-X,该 hint 可以强制查询路由到备库replica_read_hint = '/*TDDL:SLAVE()*/'# 备库数据源,对于PolarDB-X和主库dsn保持一致即可,如果测试其余数据库的备库,可以通过指定备库的连接串replica_dsn = 'polardbx_root:123456@tcp(127.0.0.1:8527)/transfer_test'
# 场景3和4的组合,使用 flashback query查询follower副本或者备库,校验组合场景的数据一致性[replica_flashback_query]enabled = truethreads = 2# 加在 SQL 语句前的 hint,对于 PolarDB-X,该 hint 可以强制查询路由到备库replica_read_hint = '/*+TDDL:SLAVE()*/'# 备库数据源,对于PolarDB-X和主库dsn保持一致即可,如果测试其余数据库的备库,可以通过指定备库的连接串replica_dsn = 'polardbx_root:123456@tcp(127.0.0.1:8527)/transfer_test'# flashback 到 (min_seconds, max_seconds) 秒之前min_seconds = 10max_seconds = 20
# 场景5,校验数据库通过CDC同步到下游数据库,通过链接下游数据库,检查数据一致性[check_cdc]enabled = truethreads = 2# 下游 MySQL(或其他 DB)数据源downstream_dsn = 'root:123456@tcp(127.0.0.1:3306)/transfer_test'

[session_hint] 和 [flashback_query] 都开启,还会额外测试场景 2 和 3 的组合,使用 flashback query + session hint 进行测试,校验组合场景下主库的数据一致性。 [replica_session_hint] 和 [replica_flashback_query] 都开启,还会额外测试场景 2、3、4 的组合,使用 flashback query + session hint + 备库读进行测试,校验组合场景下备库的数据一致性。

3.导入数据,注意替换配置文件存放目录

docker run --rm -v /etc/localtime:/etc/localtime \-v #配置文件存放目录#:/tmp \polardbx/transfer-test:latest prepare -config=/tmp/config.toml
示例:docker run --rm -v /etc/localtime:/etc/localtime \-v /tmp/transfer-config:/tmp \polardbx/transfer-test:latest prepare -config=/tmp/config.toml

如果上一步配置了 127.0.0.1 作为数据库 ip,容器运行需要加上 --network host 参数。

4.观察测试结果 (如果满足事务一致性会持续运行,不满足一致性会立即终止)

2023/04/13 08:45:27 all partitions: p1, p2, p3, p42023-04-13T08:45:27.364Z    INFO    transfer/app.go:30  App start2023-04-13T08:45:37.364Z    INFO    transfer/main.go:349    TPS check_balance: 30.902023-04-13T08:45:37.365Z    INFO    transfer/main.go:349    TPS flashback_query: 0.002023-04-13T08:45:37.365Z    INFO    transfer/main.go:349    TPS flashback_session_hint: 0.002023-04-13T08:45:37.365Z    INFO    transfer/main.go:349    TPS session_hint: 16.502023-04-13T08:45:37.365Z    INFO    transfer/main.go:349    TPS transfer_simple: 180.402023-04-13T08:45:47.364Z    INFO    transfer/main.go:349    TPS check_balance: 0.002023-04-13T08:45:47.364Z    INFO    transfer/main.go:349    TPS flashback_query: 0.002023-04-13T08:45:47.364Z    INFO    transfer/main.go:349    TPS flashback_session_hint: 0.002023-04-13T08:45:47.364Z    INFO    transfer/main.go:349    TPS session_hint: 0.002023-04-13T08:45:47.364Z    INFO    transfer/main.go:349    TPS transfer_simple: 0.002023-04-13T08:45:57.364Z    INFO    transfer/main.go:349    TPS check_balance: 58.202023-04-13T08:45:57.364Z    INFO    transfer/main.go:349    TPS flashback_query: 157.90

运行测试过程中,数据库show full processlist的截图:

通用数据库场景的验证

测试场景

MySQL

PolarDB-X

某分布式数据库

联机场景

正常

正常

正常

跑批场景

正常

正常

正常(不支持hint导出单个分片数据)

flashback query

/

正常

正常

备副本一致性读

(读写分离)

有限正常(不支持备库强一致读)

正常

正常

跑批场景 + 备副本一致性

有限正常(不支持备库强一致读)

正常

正常

(不支持hint导出单个分片数据)

flashback query + 备副本一致性

/

正常

正常

CDC同步到其他数据库

正常

正常

正常

指定时间点恢复

正常

正常

正常

最后

分布式事务的评测模型,目前行业的通用评测重点聚焦于模拟数据库故障(验证了事务的原子性和持久性),数据库事务的隔离性和一致性测试,主要也以SQL功能隔离级别的功能测试为主,缺少比较健壮且有效的测试方法。 同时在面向实际业务场景测试中,前期大家关注焦点也是常见的联机事务ACID(很多数据库厂商的事务方案也是针对性的设计),最后在关联了上下游业务后才发现国产数据库在事务机制上的缺陷性。

最后,从数据库内核研发的角度来看,分布式事务的一致性,会作为非常重要的基础能力,除了数据库内核的ACID机制外,同时也会涉及:读写分离多副本、备份恢复一致性、两地三中心容灾切换、CDC事务增量日志等多个场景的一致性问题,不要迷恋任何国产数据库的介绍,可以通过更有技巧性的事务一致性测试来验证(可以辅助常见的主机故障模拟),基于联机和跑批的组合场景,实现国产数据库事务的有效评测。

设计高性能、高可用、高并发的技术架构

一、模块化与解耦

模块化与解耦是构建高性能、高可用、高并发技术架构的基础。模块化意味着将整个系统拆分成多个独立、可复用的组件,每个组件都有明确的接口和职责。解耦则是降低组件之间的耦合度,使得各个组件能够独立地运行、升级和维护。

1. 组件化设计

组件化设计是模块化的核心。通过将系统拆分成多个组件,我们可以降低系统的复杂度,提高可维护性和可扩展性。每个组件都应该具有明确的功能边界和清晰的接口定义,以便于其他组件的调用和集成。

图片

例如,在一个电商系统中,我们可以将用户管理、商品管理、订单管理等核心功能拆分成独立的组件,每个组件都负责一个特定的业务领域。这些组件之间通过标准的接口进行通信,实现数据的交换和共享。

2. 依赖管理

依赖管理是解耦的重要手段。通过合理地管理组件之间的依赖关系,我们可以降低系统的耦合度,提高系统的灵活性和可维护性。在设计系统时,应尽量避免组件之间的直接依赖,而是通过接口或中间件进行间接的调用和通信。

图片

3. 组件隔离

组件隔离是保障系统稳定性的关键措施。通过将各个组件部署在不同的进程或服务器上,我们可以实现组件之间的物理隔离,避免一个组件的故障影响到整个系统的运行。同时,组件隔离还可以提高系统的并发处理能力和资源利用率。

二、可扩展性与伸缩性

可扩展性和伸缩性是构建高性能、高可用、高并发技术架构的关键要素。可扩展性指的是系统能够方便地添加新的功能和组件,而不需要对现有的系统进行大规模的修改和重构。伸缩性则是指系统能够根据业务需求和资源状况动态地调整处理能力,以满足不同场景下的性能要求。

1. 分布式架构

分布式架构是实现可扩展性和伸缩性的重要手段。通过将系统拆分成多个分布式的节点或服务,我们可以将计算资源和数据资源分散到多个物理节点上,提高系统的并发处理能力和容错能力。同时,分布式架构还可以方便地添加新的节点或服务,实现系统的水平扩展。

图片

2. 微服务架构

微服务架构是分布式架构的一种重要实现方式。它将整个系统拆分成多个独立的服务,每个服务都负责一个特定的业务功能。这些服务之间通过轻量级的通信协议进行通信,实现数据的交换和共享。微服务架构可以提高系统的可维护性和可扩展性,降低系统的复杂度和耦合度。

图片

3. 负载均衡

负载均衡是实现系统伸缩性的关键技术之一。它通过将请求分发到多个节点或服务上,实现计算资源的充分利用和性能的均衡分配。负载均衡可以有效地提高系统的并发处理能力和响应速度,降低单点故障的风险。

负载均衡技术:支撑高并发应用的基石

三、弹性设计

弹性设计是构建高性能、高可用、高并发技术架构的重要补充。它关注系统在面对异常情况和突发流量时的自适应能力和恢复能力,确保系统能够持续稳定地运行。

1. 监控与告警

监控与告警是弹性设计的基础。通过对系统的运行状态、性能指标和资源使用情况进行实时监控和告警,我们可以及时发现系统的异常情况,并采取相应的措施进行处理。这有助于降低系统故障的风险,提高系统的稳定性和可用性。

2. 容错与恢复

容错与恢复是弹性设计的重要组成部分。通过设计合理的容错机制,我们可以确保系统在面对单点故障或网络分区等异常情况时仍然能够正常运行。同时,通过设计自动恢复机制,我们可以在系统出现故障时自动进行修复和恢复,减少人工干预的成本和风险。

图片

3. 流量控制

流量控制是应对突发流量的重要手段。通过对系统的流量进行限制和调度,我们可以避免系统因过载而崩溃或性能下降。流量控制可以基于令牌桶算法、漏桶算法等机制实现,确保系统的稳定性和可用性。

微服务架构的“安全阀”:熔断与限流技术深探

四、总结

构建高性能、高可用、高并发的技术架构需要综合考虑模块化与解耦、可扩展性与伸缩性、弹性设计等多个方面。通过合理地设计系统的架构和组件,优化系统的性能和稳定性,我们可以确保系统在面对复杂业务场景和大规模用户访问时仍然能够保持高效、稳定和可靠的运行。在实际应用中,我们还需要结合具体的业务需求和资源状况进行灵活调整和优化,以实现最佳的性能和效果。

深入理解一致性Hash和虚拟节点

在分布式系统中架构中我们经常提到一致性哈希算法,那么什么是一致性哈希算法,为什么需要一致性哈希算法呢?

1、为什么需要一致性哈希算法

假设现在有三台缓存服务器(缓存服务器A、缓存服务器B、缓存服务器C),现在将数据预热到这三台服务器,我们可以使用负载均衡的方法将数据缓存到服务器上,如下图所示:

图片

通过负载均衡的方式可以把数据均匀的分发到三台缓存服务器上,在读取缓存的热点数据就存在一定的困难(因为不清楚数据被缓存在那台服务器上),读取数据的过程如下所示:

图片

 通过轮询缓存服务器的方式读取缓存的热点数据,此时效率就非常的低了,接口的响应时间也会变长,从而导致用户的体验非常差。

负载均衡的方案致命的缺点是无法快速的定位数据在哪台服务器上,导致需要轮询服务器来获取数据,为了解决这个痛点便提出使用Hash算法。Hash算法的预热数据的流程如下图:

图片

将数据的key计算一个hash值,然后将这个hash值和服务器的台数取模,取模之后的结果就决定当前的数据存放在哪台服务器上。获取数据的流程如下:

图片

读取数据的时候,将数据key同样方式获取hash值,然后将hash值与服务器的台数取模来定位数据在哪台服务器上。但是hash法也存在一个严重的缺陷,假设现在增加/减少服务器数据量,如下图所示:

图片

我们继续使用:hash(key)% 服务器数量,来定位数据在哪台服务器就存在问题了,因为服务器数量变化导致原先数据定位不准,如下所示:

图片

假设现在有大量的请求打进来,由于命中缓存服务上没有数据,请求都落到了资源服务器上,由于资源服务器瞬间压力过大可能会导致服务崩溃。

hash随着服务器的数量变化(增加或减少),定位服务上的缓存的数据位置也会变动,就会导致无法获取数据的问题。为了解决这个问题便提出了一致性hash算法。

2、一致性hash和虚拟节点

图片

    一致性hash算法是对2^32方取模,从0-2^32方计数形成一个圆环,我们称这个圆环为hash环。

    通过hash(服务器的ip) % 2^32 = X;通过这个X值可以定位服务器在圆环上的位置。

    如何确定数据存放在哪个服务器上呢?如下图所示:

图片

如上的数据A,我们可以使用hash(数据A) % 2^32 = LA;通过LA可以定位数据A在圆环上的位置,然后顺时针方便找距离数据A最近的服务器,发现是服务器A,那么我们将数据A存放到服务器A上。同理数据B也是存放在服务器上A上。

读取数据也是同样按照hash算法取模的方式来定位服务器,通过这样的方式可以很快地定位数据在哪台服务器上。如下所示:

图片

假设现在服务器C下线了,如下所示:

图片

此时数据A定位是没有问题,数据C从原先的服务器C上定位到服务器A上,数据C是无法获取到的。换句话讲,虽然服务器C下线了,但是只是部分数据异常,不会使得整个服务集群数据错乱,数据异常的部分如下所示:

图片

假设现在增加了一台机器D,那么也只会导致部分数据出现错乱,如下图所示:

图片

此时我们只需要将错乱的这一部分数据迁移到服务器D上可以实现数据的同步了。理想状态下,一致性hash是很完美的,但是在极端的情况下由于离散型差的问题导致服务器都集中分布在一起,如下图所示:

图片

此时数据又刚好落在服务器C和服务器A之间的区域上,如下图所示:

图片

这样就导致所有的数据压力都到了服务器A上,服务器B和服务器C就是一个摆设了作用了。如果服务器A挂了,那么整个缓存就失效了,这个就是hash环的倾斜问题。为了解决hash环倾斜问题,于是便引入了虚拟节点,也就是把真实的服务器通过虚拟化的方式复制一些节点出来成为虚拟虚拟节点。如下图所示:

图片

通过虚拟节点的加入就不会导致所有的数据都到一台机器中,同时虚拟节点越多,缓存数据越均匀。

总结:

(1)一致性hash常用于负载均衡、分布式缓存分区、数据库分库分表等场景。

(2)为防止服务器上的数据倾斜问题,通常增加虚拟节点的方式来让数据更加均匀的分布在机器上。

  • 11
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值