
今年618期间,申通快递通过引入阿里云PolarDB云原生数据库替代Oracle数据库,完美扛过618业务高峰,IT成本降幅超过50%申通快递是国内较早经营快递业务的民营快递品牌,以往使用传统IOE架构来支撑业务的运行,但是随着公司业务的快速发展,原有IT 系统遇到了瓶颈。以应对电商大促为例,以往申通要提前几个月进行相关Oracle硬件和数据库的扩容,但是大促之后,IT资源的使用率则非常低,投入产出比较低。 2019年底,申通决定全面迁移至阿里云,也因此成为业内首个全面上云的快递企业,并率先实现了订单平台、巴枪系统等核心系统的上云,日处理订单量近3000万。在今年618大促前,申通加快了散单、集包等业务系统上云步伐,使用阿里云云原生数据库PolarDB替代Oracle数据库。 PolarDB使用了存储计算分离架构,可以实现分钟级的弹性,高峰期可以快速扩容支撑业务发展,同时最大程度降低成本。在今年618大促期间,PolarDB在申通取得了完美表现,IT成本实现了超过50%的降幅。 申通散单平台技术经理杨恒连表示:“IT基础设施的成本是申通一贯重视的,PolarDB单位资源承担的访问量比上云前有倍数的提升,这高度依赖云原生架构和高性能内核,而且对于散单后台的多维查询,PolarDB的并行查询功能可以轻松完成。综上两点,PolarDB是云上物流用户正确的选择。” 阿里云数据库负责人李飞飞日前表示,经过多年技术演进,“去O”已进入大规模攻坚阶段,阿里云今年将帮1000家企业“去O”,完成10000套传统数据仓库上云。 据了解,PolarDB是阿里云自主研发的云原生数据库,可以100%兼容MySQL语法,深度兼容Oracle的语法和数据类型,可通过DTS服务实现一键迁移上云。目前,阿里云已经稳居亚太云数据库市场份额第一,客户包含政务、零售、金融、电信、制造、物流等多个领域的龙头企业。 点击文字了解PolarDB更多精彩详情新用户还有免费试用福利哦!
谈到事务隔离级别,开发同学都能说个八九不离十。脏读、不可重复读、RC、RR...这些常见术语也大概知道是什么意思。但是做技术,严谨和细致很重要。如果对事务隔离级别的认识,仅仅停留在大概知道的程度,数据库内核研发者可能开发出令用户费解的隔离级别表现,业务研发者可能从数据库中查出与预期不符的结果。 那么如何判断自己是不是对事务隔离级别有了较为深入的理解了呢?开发同学可以问自己这样两个问题:(1)事务隔离级别分为几类?分别能解决什么问题?是否有明确定义?这样的定义是否准确?(2)当前主流数据库(Oracle/MySQL...)的隔离级别表现和实现是怎样的?是否与“官方”定义一致? 如果能清楚明白的回答这两个问题,恭喜,你对事务隔离级别认识已经非常深刻了。如果不能,也没有关系,读完本文你就有答案了。 1.事务隔离级别 事务隔离级别,主要保障关系数据库ACID特性的I(Isolation),既针对存在冲突的并发事务,提供一定程度的安全保证。ANSI(American National Standards Institute) SQL 92标准(http://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt) 首先定义了3种并发事务可能导致的不一致异象: Dirty read: SQL-transaction T1 modifies a row. SQL- transaction T2 then reads that row before T1 performs a COMMIT. If T1 then performs a ROLLBACK, T2 will have read a row that was never committed and that may thus be considered to have never existed.Non-repeatable read: SQL-transaction T1 reads a row. SQL- transaction T2 then modifies or deletes that row and performs a COMMIT. If T1 then attempts to reread the row, it may receive the modified value or discover that the row has been deleted.Phantom: SQL-transaction T1 reads the set of rows N that satisfy some . SQL-transaction T2 then executes SQL-statements that generate one or more rows that satisfy the used by SQL-transaction T1. If SQL-transaction T1 then repeats the initial read with the same , it obtains a different collection of rows. 嫌弃以上定义冗长,可以直接看以下形式化描述: A1 Dirty Read:w1[x] ... r2[x] ... (a1 and c2 in any order)A2 Fuzzy Read:r1[x] ... w2[x] ... c2 ... r1[x] ... c1A3 Phantom Read:r1[P] ... w2[y in P] ... c2 ... r1[P] ... c1 其中w1[x]表示事务1写入记录x,r1表示事务1读取记录x,c1表示事务1提交,a1表示事务1回滚,r1[P]表示事务1按照谓词P的条件读取若干条记录,w1[y in P]表示事务1写入记录y满足谓词P的条件。 据此,ANSI定义了四种隔离级别,分别解决以上三种异常:根据上述几种异常现象定义隔离级别,可谓十分不严谨,Jim Gray大名鼎鼎的论文A Critique of ANSI SQL Isolation Levels(后文简称Critique)就对此做了批判。 不严谨之一:禁止了P1/P2/P3的事务,即满足了Serializable级别。但是在ANSI标准中又明确描述Serializable级别为“多个并发事务执行的效果与某种串行化执行的效果等价”。显然这两者是矛盾的,禁止P1/P2/P3的事务,不一定能满足“等价于某种串行执行”。所以Critique将ANSI定义的禁止了P1/P2/P3的隔离级别称为Anomaly Serializable。 不严谨之二:异常现象定义不准确,如下例并未被A1囊括,却仍然出现了Dirty Read(Txn2读到x+y!=100)。同样,A2/A3也能举出这样的例子,感兴趣的同学可以自己尝试列举,这里不再详述。 究其原因,ANSI对异象的定义太为严格,如果除去对事务提交、回滚和数据查询范围的要求,仅保留关键的并发事务之间读写操作的顺序,更为宽松且准确的异象定义如下: P1 Dirty Read: w1[x]...r2[x]...(c1 or a1)P2 Fuzzy Read: r1[x]...w2[x]...(c1 or a1)P3 Phantom: r1[P]...w2[y in P]...(c1 or a1) 不严谨之三:三种异象仅针对S(ingle) V(alue)系统,不足以定义M(ulti)V(ersion)系统的隔离性。很多商业数据库所实现的SI,未违反P1、P2和P3,但又可能出现Constraint violation,不可串行化。除了P1/P2/P3,还可能出现哪些异常呢? P4 Lost Update:r1[x]...w2[x]...w1[x]...c1 A5A Read Skew:r1[x]…w2[x]... w2[y]…c2…r1[y] …(c1 or a1) A5B Write Skew:r1[x]…r2[y]…w1[y]…w2[x]…(c1 and c2 occur) A5B2 Write Skew2:r1[P]... r2[P]…w1[y in P]…w2[x in P]...(c1 and c2 occur) 对这四种情况,分别举一个例子: r1[x=50] r2[x=50] w2[x=60] c2 w1[x=70] c1 Lost Update:事务1和事务2同时向同一个账户x分别充20和10块,事务1后提交,将70块写入数据库,事务2提交结果60块被覆盖。正确的情况下,事务1和2提交成功,账户里应该有80块。 (x+y=100) r1[x=50] w2[x=10] w2[y=90] c2 r1[y=90] c1 Read Skew: x和y账户分别有50块钱,加起来共100块。事务1读x(50块)后,事务2将x账户的40块转到y账户,事务2提交后,事务1读y(90块)。在事务1看来,x+y=140,出现了不一致。 (x+y>=60) r1[x=50] r2[y=50] w1[y=10] c1 w2[x=10] c2 Write Skew:x和y账户分别有50块钱,加起来共100块。假设存在某种约束,x和y账户的钱加起来不得少于60块。事务1和事务2在自认为不破坏约束的情况下(分别读了x账户和y账户),再分别从y账户和x账户取走40。但事实上,这两个事务完成后,x+y=20,约束条件被破坏。 (count(P)<=4):r1[count(P)=3],r2[count(P)=3],insert1[x in P],insert2[y in P],c1,c2, Write Skew2:将Write Skew的条件改为范围。 2.隔离级别实现 上一节介绍了ANSI定义的3种异象,及根据禁止异象的个数而定义的事务隔离级别。因为不存在严格、严谨的“官方”定义,各主流数据库隔离级别的表现也略有不同,一些现象甚至让用户感到困惑。我认为相较于纠结隔离级别的准确定义,认识各数据库隔离级别的表现和实现,在生产环境中正确的使用它们才是更应该关注的事情。本节将以大篇幅具体的例子为切入点,介绍几种主流数据库隔离级别的表现,及内部对应的实现。 2.1 Lock-based 隔离级别实现 在展示Lock-based隔离级别实现前,先介绍几个与锁相关的概念: Item Lock:对访问行加锁,可以防止dirty/fuzzy read。Predicate Lock(gap lock):对search的范围加锁,全表扫描直接对整张表加锁,可防止phantom read。Short duration:语句结束后释放锁。Long duration:事务提交或回滚后释放锁。 上述锁操作组合,便可实现不同级别的事务隔离标准,如下表所示。其中S lock代表共享锁,X lock代表排它锁。 首先所有写操作加X locks时,都会选择Long duration,否则short duration锁被释放后,在事务提交前该条更改可能被其它事务写操作覆盖,造成脏写(dirty write)。 其次对于读操作: Short duration Item S lock 禁止了 P1发生,读操作如果遇到正在修改的行(写事务加了X Lock),阻塞在S Lock,直到写事务提交。 Long duration Item S lock 禁止了P2发生,写操作遇到读事务(S Lock),阻塞在X Lock上直到读事务提交或回滚。 Long duration Predicate/Table S Lock 禁止了P3发生,(范围)写操作遇到范围读操作(加Predicate S Lock),会被阻塞,直到读事务提交或回滚。 基于锁实现的三种隔离级别分别能禁止的异象如下表所示: 然而当今数据库基于性能等多方面考虑,很少有完全基于锁实现隔离级别的,MVCC+Lock的方式,可以满足读请求不加锁,是主流的实现方式。 2.2 Oracle隔离级别的实现 Oracle仅支持两种隔离级别:Read Committed与Serializable。尽管官方这样描述,Oracle的Serializable实际是基于MVCC+Lock based的SI(Snapshot Isolation)隔离级别。 为实现快照读,内部维护了全局变量SCN(System Commit/Change Number),在事务提交时递增。读请求获取Snapshot便是获取当前最新的SCN。Oracle实现MVCC的方式是将block分为两类:(1)Current blocks为当前最新的页面,与持久化态数据保持一致。(2)Consistent Read blocks,根据snapshot SCN生成相应的一致性版本页面。 以下两个具体的例子展示了:不同隔离级别下,读写语句在数据库内部发生了什么。 Oracle在read committed隔离级别下,每条语句都会获取最新的snapshot,读请求全部是snapshot读。写请求在更新行之前,需要加行锁。由于写操作不会因为有其它事务更新了同一行,而停止更新(除非不满足更新的谓词条件了),因此Lost Update有可能发生。 Oracle在serializable隔离级别下,事务开始便获取snapshot。读请求全部是snapshot读,而写请求在更新行之前,需要加行锁。写操作在加锁后,首先检查该行,如果发现:最近修改过这行的事务的SCN大于本事务的SCN,说明它已经被修改且无法被本事务看到,会做报错处理,避免了Lost Update。这种写冲突的实现,显然是first committer wins。 下表展示了Oracle的两种隔离级别,分别能够避免哪些异象: 2.3 MySQL(InnoDB)隔离级别实现 InnoDB同样以MVCC+Lock的方式实现隔离级别。其中普通select语句均是snapshot read。而delete/update/select for update等语句是加锁实现的current read,如下表所示(注:该表为Pecona 5.6版本的代码实现)。 InnoDB的RC隔离级别的表现与Oracle相似。而相较于Oracle的SI,InnoDB RR隔离级别依旧不能避免Lost Update(例如下例)。究其原因,InnoDB在RR隔离级别下,不会在事务提交时判断是否有其它事务修改过该行。这避免了了SI更新冲突带来的回滚代价,带来了可能发生Lost Update的风险。 由于update等操作均是加锁的当前读,因此Phantom Read的现象也是存在的(如下表所示)。但是如果将Txn1的update语句替换为select语句,Phantom Read现象则可以禁止,因为整个事务select语句使用的是同一个snapshot。 Innodb RR的实现方式虽然并非并未严格排除Lost Update和Repeatable Read,但其充分利用MVCC读不加锁的并发能力,同时current read避免了SI在更新冲突剧增时过多的回滚代价。 InnoDB还实现了Lock Based Serializable(详见2.1),禁止了所有异象。 3.MySQL (X-Engine) 隔离级别实现 X-Engine 隔离级别实现同样采用MVCC+Lock的方式,支持RC和SI,表现与Oracle的RC,Serializable一致。具体实现层面,X-Engine 实现了行级MVCC,每条记录的key都附有一个 Sequence 代表自己的版本。所有的读操作均是快照读(包括加锁读),读请求所需要的snapshot也是一个Sequence 。写写冲突处理依靠两阶段锁,并遵循First committer wins。 按照惯例,以下面两个例子分析,说明我们的实现原理:与Oracle类似,X-Engine SI隔离级别,可以避免Lost Update: 4.总结 前文介绍了多种数据库隔离级别的表现,对比如上表所示。其种MySQL比较特殊,如前文所述,其RR级别可以禁止部分幻读现象。开发人员在使用数据库时,需要注意:尽管不同数据库隔离级别名称相同,但是表现却可能存在差异。
背景 虽然同为LSM-tree架构,X-Engine的设计哲学与传统基于LSM-tree架构的Rocksdb等引擎并不完全一致,如下图所示: 设计关键点1:X-Engine磁盘上的数据,在常态下只有两层(L1/L2),L0层是MemTable在compaction来不及的情况下暂存到磁盘上缓解内存压力时才启用的,正常情况下被冻结的MemTable可以直接和磁盘上的L1合并。 设计关键点2:在L1/L2之间的compaction合并过程中,X-Engine的冷热合并算法倾向于将热点数据保留在L1层(基于访问频度),将访问较少的数据下刷到L2层并进行压缩存储。这是一个对数据在物理上进行冷热分离的过程, 其结果是L1存储的都是热点数据,L2存储的都是冷数据。对L1进行缓存时会有更高的内存利用率。 按照设计初衷,X-Engine正常运行时,Memtable中缓存了最近写入还未刷盘的数据,L1中保存了磁盘访问频度最高的数据,也大部分被内存缓存,分层之后X-Engine的读性能优化被分解为两个独立子问题: 内存MemTable部分和L1层数据读操作是CPU bound的,手段主要是优化CPU指令的执行效率和访问主存的速度。 L2层的读性能依赖于磁盘的随机读能力,对此部分的优化手段是更精准的冷热识别,其目标是最大化IOPS利用率。 考虑到Memtable部分数据量较少,在冷热识别算法精准并且内存足够缓存热点数据的前提下,X-Engine的性能整体上取决于对L1部分数据的内存查找效率,这也是今天这篇文章探讨的主题: 如何最大化命中内存时的读取性能。 读取路径存在的问题 在数据集小于可用内存时,X-Engine的读性能受限于CPU资源。分析一个CPU bound程序的性能问题时,我们需要看CPU在哪些地方繁忙。按照CPU使用率的定义,CPU在执行指令时或者在等待数据时(stall) 都会处于busy状态,存储引擎的读性能优化最后都会落到两个点上: 提升CPU Cache命中率,主要靠执行线程在时间上和空间上访存的局部性。 提升CPU指令的执行效率,这一方面需要精简实现代码,减少不必要的执行路径,另一方面需要减少不同线程之间的状态同步,保证指令流水线顺畅执行。 L1层数据组织和读取过程:X-Engine将数据划分成2MB大小的Extent,Extent内部会记录编码成16KB的Block,每个Extent内部包含一个IndexBlock以辅助定位DataBlock。整体看X-Engine中L1/L2层的数据组织是一个类似B+树的索引结构。如果所有操作都能命中内存,在Extent中读取一条key=X的记录,操作会按如下四个步骤执行: ExtentMeta数组是这棵B+树的根节点,在其中二分查找定位出X所属的Extent. Extent可以理解为一棵子树,首先需要通过一次Hash查找(查询缓存)获取到该Extent的IndexBlock。 从IndexBlock中定位出X所属的DataBlock的Key, 并通过一次Hash查找定位到X所属的DataBlock。 在内存中的DataBlock中查找到该记录的实际内容,并返回对应的Value. 结合上述读路径的实现逻辑,同时对X-Engine全内存命中读过程进行Perf分析之后。我们在如下三个方面进行改进尝试(1)数据页的编码及查找指令优化 (2) 降低BufferPool的管理开销(3)优化多核上的多线程运行的Cache冲刷问题,最终获得了整体124%的读性能提升。 接下来我们将详述这三个问题的根源以及我们的优化方法,最后通过实验对每一步优化手段的收益进行了评估。考虑到这三个问题在数据库存储引擎中的普遍性,这些优化方法对于InnoDB, Rocksdb等引擎也是适用的。 Block编码及SIMD 数据页编码及其问题 与Rockdb/InnoDB数据页格式类似,X-Engine索引页和数据页具有相同的格式。每个Block顺序存储KV pairs,并对key进行分段前缀压缩(每个段称为restart interval),KV pairs之后有一个restart array,存储每个restart interval在Block中的offset。搜索时首先会在restart array中进行二分查找,定位到可能包含target key的最小的restart interval(下图1、2、3、4),再从这个restart interval开始顺序比较(下图5),如下图所示:查找过程本质上就是指针数组中的二分查找,restart array中存储的restart interval的offset可以理解为指向restart interval的指针,perf分析显示,该过程存在如下两点问题: 1.内存访问在前面的KVs和restart array之间跳跃。restart array中仅存储restart interval的offset,在查找时需要根据offset访问BlockContent的相应位置获得key,这样跳跃的内存访问没有规律(每次restart array和实际key的距离随机)且间隔多个cache line,无法充分利用CPU cache,带来一定的访存开销。 2.前后两次比较无法利用CPU cache。除了最后一次比较之外,每次比较的index key和前一次比较的index key间隔都可能超过一个cache line,因此无法利用前一次的CPU cache,导致访存开销很大。 Cache友好的Block编码 针对指针数组中二分查找CPU cache不友好的问题,尝试将频繁访问的数据页转换成CPU cache友好的两层B+树结构,将访问时序上前后相邻的记录在物理空间上聚簇在一起,其逻辑存储形式如下:物理存储形式如下:构建时按物理存储形式顺序构建每个node,查找时使用类似B+树的查找方式。CPU cache友好的两层B+树从三个角度提升CPU cache利用效率: 使内存组织的空间局部性和访问顺序的时间局部性一致。使用B+树作为索引结构,直接在B+树中存储kv的内容,并在节点内采用顺序查找。B+树中访问顺序相邻的节点存储在同一个node中,kv内容直接存储在node中避免了访存地址跳跃的问题,顺序查找进一步保证了时间局部性和空间局部性的一致,并且访问顺序和prefetch的顺序一致,尽可能地保证了prefetch在恰当的时机进行。计算开销方面,B+树查找算法的时间复杂度和二分查找相同,因为node内key的数量较少,顺序查找和二分查找计算开销接近。 提升cache size与数据量的比值:压缩存储元信息。例如使用2Byte存储offset而不是存储leaf node的指针。 选择合适的B+树层数:B+树层数为2,fanout为N开根号。每层节点至少会产生一次cache miss,过多的层数会带来更多的cache miss,而1层的B+树node过大,计算开销和CPU cache效率实际上和restart array差不多,当层数为2时,CPU cache对in-flight的load请求和prefetch的支持可以让每层node访问仅产生1-2次cache miss,是CPU cache效率最佳的选择。 这种编码格式对range查询也能完好的支持,对range查询的第一次seek操作,能起到和点查类似的加速效果,而对于后续的scan迭代,也无负面影响。此部分优化更详细的新可以参见公众号前一篇文章:数据库存储引擎如何利用好CPU缓存 SIMD指令加速长Key比较 解决了数据页中记录查找的cache访问问题之后,继续往下perf分析,会发现查找路径中的compare比较函数消耗了较多的CPU cycle数,在查找长key场景时,计算开销的问题会更加明显。考虑到我们新的数据页编码结构中,每次读取会将多个需要先后比较的index key同时load进CPU cache, 引入SIMD指令集可以实现在一个CPU cycle内并行检索完在cache中的多个index key。 为了使用SIMD指令集,我们需要对X-Engine的数据页编码继续进行调整,如下图所示:对key进行lossy compression:由于SIMD寄存器的处理单位segment最长为64bit(AVX-512指令集),简单地分段并行比较长key会因为额外逻辑抹去并行计算带来的性能提升。因此这里将key拆分为common prefix+significant Nbits+suffix,避免了分段并行比较在prefix上的多余操作,而significant Nbits(N取决于SIMD寄存器的segment大小)逻辑上具有很高的区分度,在大多数情况下都仅需要一次SIMD比较就能得到结果,如果出现相等的情况,也只需要顺序比较一部分的index key的suffix。 选择固定的fanout并使用padding补齐:SIMD指令的处理带宽(即SIMD寄存器中segment的数量)均为2的指数,如果fanout仍然是N开根号,prepare过程会增加很多额外的操作。同时考虑对CPU cache友好的node长度和对SIMD指令处理带宽的适配,以及DataBlock中restart key的数量的范围,leaf node的fanout固定为8,root node的fanout固定为16,实际key的数量不足node的fanout,则用node中最大的key padding补齐,这样可以直接在代码中将SIMD指令写死,减少分支预测开销。 对索引页和数据页进行重新编码再配合SIMD指令加速,在不同行长下测试最大能提升20%左右的点查性能。在TP系统中,单个请求读写的数据量非常少,在我们的测试场景一次只操作一条记录,因此数据页编码和SIMD指令优化一起对端到端的性能提升达到20%,已经非常可观. BP的代价及PointSwizzling BufferPool管理成本 面向全内存的数据库和面向磁盘的数据库具有不同的设计原则。一个典型的差异是:当访问一个数据页时,全内存数据库倾向于使用指针直接访问,而面向磁盘的数据库一般则通过PageID在一个数据结构中间接地获得该数据页的内存地址(当该数据页没有被缓存时需要先读盘), 然后才能访问到该数据页。这样的间接访问会带来额外的查找开销,已有研究显示在数据集能够完全装载进内存时,BufferPool管理结构会消耗超过30%的CPU资源。 由于OS的虚拟内存地址空间相对于DB的数据集大小来讲是足够的,一个可能的解决办法是对面向磁盘的数据库引擎也使用指针直接访问,当内存空间不足时,则依靠OS的Swap机制来进行内存页的汰换,这样在数据集小于可用内存时,由于消除了PageID映射查找的开销,存储引擎会具有与In-Memory数据库一样的性能。 但这种方法有一个缺陷,在数据集大于系统可用物理内存时,由于OS的的Swap机制不能准确的感知DB中的数据访问特征,它有可能将一个非常关键的内存对象Swap到到磁盘上,导致性能波动。 X-Engine是一个通用存储引擎,其装载的数据集大部分时候都超出了可用内存的大小,因此也必须引入一个BufferPool来管理对数据页的访问。但是在我们的设计中,冷热分离精准且用户负载存在明显的局部性特征时,大部分访问都是命中内存的,这个时候BP的30%的CPU资源消耗显得非常浪费,需要找到办法消除。 X-Engine中的Extent信息/索引页/数据页都是通过BufferPool的Cache管理。其中的Cache使用哈希表和双向链表管理数据,通过哈希查找获取数据,命中后还要维护LRU链表的顺序信息。开销主要来源于三个方面: 哈希查找算法本身的计算开销:包括计算hash值和在hashtable中的某个slot进行链表的顺序查找,链表的顺序查找具有CPU cache不友好的问题。 维护LRU信息的计算开销:命中后需要调整entry在双向链表中的位置,还可能需要维护old pool和high priority pool的指针。 锁竞争的开销:Cache除了用于查找之外,还需要Insert和Evict,因此查找和维护LRU信息时需要对分片加锁,当并发度很高、访问流量很大时,锁的竞争非常激烈。 PointSwizzling 优化BufferPool的间接引用导致的CPU资源开销有一个方法,叫做PointSwizzling,即使用直接指针代替使用PageID+HashMap查找的方法,针对X-Engine中Cache中哈希查找计算开销大的问题,我们引入直连指针进行数据访问,Cache仅用于数据管理,具体方案如下:目标索引页/数据页/ExtentMeta在第一次装载进内存之后,我们增加一个直连指针指向目标对象(PointSwizzling), 这样可在下一次访问使用直连指针,而不必通过一次HashMap的计算。 目标内存对象并发访问和回收:多线程访问时,可能出现一个线程正在使用一个内存对象,而另一个线程触发了Cache淘汰,需要淘汰这个内存对象并回收这个内存对象的空间。这个问题有两种解决方案:基于Epoch的内存回收或者基于引用计数的内存回收,我们对两种方案都进行了测试,结论是基于Epoch的方式优于基于引用计数的方式(基于Epoch的具体实现见附录)。原因是引用计数虽然是一个原子变量,但是多核环境下计数器的高频更新和读取依然存在因cache一致性导致的cache失效问题,导致性能不佳。 通过PointSwzzling,X-Engine在内存部分的数据访问形态就和全内存数据库一样具有非常高的CPU使用效率。而当存在数据汰换时,可以继续使用BufferPool保持内存数据的命中率。PointSwizzling引入之后,X-Engine在KV接口上全内存命中场景的点查性能提升了近60%。 多核CPU上的Cache访问 数据页编码以及PointSwizzling都是着眼于单线程运行过程中的优化方法,其中数据页的新编码提升了CPU 的Cache命中率,而SIMD和PointSwizzling都是提升了CPU指令的执行效率,即提升了程序的IPC。 分析X-Engine的整体执行框架,我们发现另一个导致CPU Cache利用率低问题: 多线程并发问题 X-Engine作为RDS MySQL以及PolarDB MySQL中的一个存储引擎,其执行框架与MySQL类似,使用One-thread-per-client或者采用线程池模式。特点就是任何线程均可以访问所有表中的数据,在现今动辄32/64/128core的服务器上,执行线程的高度并发导致如下一些问题: CPU cache的低效率。例如在128core的服务器上,我们并发访问1000张表,则同一内存数据会同时在多个CPU core的L1/L2 cache中被加载,导致cache利用率较低。同时如果存在更新,在多核上的cache一致性也会带来较大的开销。线程在不同的core上调度发生context switch,同样会造成CPU cache频繁大量的失效。 线程同步的开销较大。内存数据都是共享的,访问和修改(例如上面提到的Cache查找、插入和淘汰)需要锁的保护,锁竞争的开销很大。对于使用原子操作进行的线程同步,原子变量在多个CPU core的L1/L2中同步状态需要进行数据传输和跨核通信,开销也很大。 针对此类问题,业界先驱如H-Store,VoltDB给出了一条可供参考的优化方法 多核Shared-nothing架构 我们对X-Engine的执行框架做如下改造: 限制读数据的执行线程数为可用CPU core数目,并进行绑core处理,特定线程只能在特定的core上执行,避免频繁的线程调度。 对X-Engine中的所有Subtable进行分表,特定的分表只能在被特定Core上执行的线程所访问到,避免Cache中出现同一数据的多个副本以及同一数据在不同core的cache间传输。这样Per-core-level Shared-nothing的架构有如下优势: CPU cache效率高:同一份数据只可能缓存在一个CPU core的L1/L2 cache中,也避免了实现cache一致性的开销。线程不会发生context switch而导致Cache失效。 大大减少线程同步的开销:数据在前台线程间不共享,不需要锁或原子操作等进行线程间同步,例如Cache的锁竞争和pointer swizzling的swizzle/unswizzle以及内存回收都同时可以解决。 目前该方法对只读性能的提升达到30%左右。此项优化工作还未完全完成,进一步的我们需要优化对热点Subtable的访问问题, 另一方面在事务引擎中,跨表访问是非常常见的,如果一个线程执行一个需要访问到多张表的事务,则如何调度该事务是一个很有挑战性的问题。 实验验证 测试环境 64threads,2Socket.L1-Cache:32KB Data Cache / 32KB Instruction CacheL2 Cache:256KBL3 Cache:32MBRAM:512GB,使用numactl --interleave=all关闭NUMA 测试场景 key 16Byte,value 8Byte,32张表,每张表2000W条记录,写入数据之后下压到L1层,执行一轮预读预热到缓存中,然后执行readrandom测试。 测试结果 我们以X-Engine原始架构的随机读性能做baseline,然后依次叠加上前述的优化手段。 第一步增加了新的数据页编码方法以及数据页上的SIMD指令查找算法,获得了13.8%的性能提升。 第二步,我们在代码中增加了PointSwizzling优化,性能提升相比BaseLine达到了90%。最后我们重构了X-Engine的执行框架,引入多核无共享执行架构。 最终整体性能相对比BaseLine的提升达到了123%. 测试中也分析了这测试过程中CPU L1 Data Cache miss率的变化以及程序运行的IPC的变化。为了确定我们的优化手段在哪些路径上产生了最关键的作用,我们对每一步优化手段产生的性能提升进行了breakdown测试并制作了如下的表格. 从这里可以看出对性能提升最大的一步是对DataBlock的间接应用改Direct引用,考虑到DataBlock的数目较多,其原始实现中的Hash查找表比较大,Cache效率较低,产生这样的效果在预期之中。而Index和ExentMeta对象相对数目较少(大概为DataBlock的1/128), 本身具有较好的查找效率,因此对性能的影响也较小。
X-Engine是阿里巴巴自研的存储引擎,作为阿里云 RDS MySQL 的一个可选引擎,除了主打高性能和低成本,还增加了不少惠及用户的新功能。本文将详细介绍 MySQL(X-Engine) 如何近乎瞬时完成传统数据库需要数小时完成的DDL操作。 1.数据库DDL操作面临的问题 互联网业务发展迅速,应用模式频繁更改是常态。相应地,数据库访问模式和schema也随之变化。DDL(Data Definition Language)是SQL的一类,主要作用是创建和更改数据的schema信息,最常见的操作包括:加减列、更改列类型、加减索引等。熟悉MySQL的同学都知道,在8.0以前,虽然Online DDL不阻塞其它DML(Insert/Update/Delete)操作,但许多重要的DDL操作,如加列、减列等,仍旧需要等待数小时、甚至好几天时间(依据数据量的大小)才会生效。更改列类型等操作甚至仍需要锁表执行,阻塞DML操作。 DDL操作运行时间长,占用系统资源,需要额外的磁盘空间(建立临时表),影响系统吞吐,并且一旦DDL过程中实例crash,恢复时间也会很久。以加列DDL为例,MySQL经历如下过程: 1.以新schema建立空表。 2.拷贝数据到新表,并且将新加列的值赋为默认值,同时更新索引表。数据库接受到的DML操作被记录在临时文件。 3.加exclusive lock,阻塞写操作,将临时文件记录的DML操作apply到新表。如果DML很多,这一阶段将花费较多时间。 4.删除旧表,将新表命名为旧表的名字。 显然,这个过程加锁时间长,拷贝数据操作会占用系统资源和临时空间,并需要大量I/O。为了适应变化频繁的业务,不立即更改存储层数据、可以快速完成的DDL(我们称之为Fast DDL)成为了一个必要feature。MySQL 8.0 增加了instant add column功能,可以在短时间内只修改table元信息,完成加列操作。遗憾的是,它还不支持其它类型的DDL。得益于阿里自研的存储引擎X-Engine存储了多版本Table Schema,每一行记录在引擎层就完成了解析,并且可以依据更新版本的schema实现格式转换,X-Engine因此可支持多种类型的Fast DDL。 2.业界Fast ddl实现方案 MySQL 8.0 record记录了列个数, instant add column操作只修改系统表。 写操作:新格式的记录。 读操作:根据存储在系统表中default value补齐新加列。 支持类型: • Change index optionRename table• Set/drop default• Modify column when the table is empty• Add/drop virtual columnsAdd columns MariaDB10.3 整体实现方案与MySQL8.0类似,record记录了列个数,在leftmost leaf page中记录所有列的default值.支持类型: • Add column• Drop column• Extend VARCHAR maximum (Only if the physical format allows; not VARCHAR(255) to VARCHAR(256)) Aurora 发生ddl后,更新系统表,新、旧版本的schema均要记录下来。然后广播该修改。之后接受DML请求,首先转换相关leaf page的所有记录,然后执行DML。 select请求会将旧版本的记录拼接成新版本记录。 支持类型 • only supports adding nullable columns, without default values 3.X-Engine多版本schema 顾名思义,Fast DDL指数据库能够在极短的时间内完成用户发出的DDL指令并返回。之所以这么快,是因为只修系统表里的元数据,不变更引擎层存储的数据。其实现的关键在于:元信息变更之后,内存、磁盘中的物理记录该如何解析。 Engine的架构采用了LSM-Tree的思想,将新写入的数据以追加方式写入内存memtable,memtable到一定大小后switch为immutable memtable,不再修改。然后逐渐以固定大小extent的形式,flush到持久化存储中。当extent到一定数量后,通过合并(Compaction)操作,将相同Key的多个版本合并。为了让每行记录可解析,最直观简单的方案便是将元信息附着在记录上面。为了能够不依赖系统表解析记录,X-Engine存储了较为详细的元数据,如果为每一行都附着一份,会占用大量的空间。为了大大减少存储成本,我们保证每个memtable和extent内部的数据schema一致,并将schema信息存储在memtable和extent之上。schema信息包含了诸如列个数、列类型、列长度、默认值等关键信息。利用这些信息,X-Engine可以在返回结果之前,完成列解析,并只需返回查询目标列的对应结果。下面给出了一个具体的例子,同一张表存在不同schema版本的extent时,如何返回结果。 4.X-Engine fast ddl实现 当 MySQL 接收到一条fast ddl语句时,更新相关系统表及元数据,新版本的表结构随之生效,这时这条DDL语句就执行成功啦!到现在为止X-Engine存储的信息没有发生任何变化。 读请求 当系统接收到Select请求时,MySQL 会将请求本身,连同当前最新版本schema信息(称之为target schema)传递到X-Engine。X-Engine首先定位到记录的位置(某个memtable或extent),并取相应数据schema解析记录得到初步结果。接着,对比数据schema和target schema,对初步结果做适当填充、删减或修改得到最终结果返回。 X-Engine schema更新 Fast DDL命令执行成功,新版本的schema生效,X-Engine还对此无感知。当接收到第一条针对该表的DML(Insert/Update/ Delete)请后,如果发现X-Engine的活跃memtable的schema版本落后于最新版本,会触发switch memtable行为:冻结当前活跃memtable,产生新活跃memtable,将新schema赋予新活跃memtable。为了保证数据的正确性,该操作会等待所有正在进行的写事务完成后再执行。 写请求 每个写事务可能涉及到n(n>=1)个表。事务在提交时,需要在写入活跃memtable之前判断:事务写入数据的schema版本是否与活跃memtable的schema版本一致,如果不一致则应该报错退出,提醒用户重试。 Flush/Compaction 内存中memtable数量到一定个数时会触发Flush操作,被选中memtable的数据以extent的形式写入磁盘,schema也随之由memtable传递到extent。Compaction操作会合并多个extent,如果参与同一任务的extent schema版本不一致,X-Engine会以其中最新版本为准,生成新extent。 总结 Fast DDL可以解决很多应用的痛点,加列、扩展列的常用的操作不用再需要漫长的等待。技术上,X-Engine通过存储详细的多版本schema信息,不仅无需借助系统表解析记录,而且可以轻易地实现不同版本schema之间的数据转换,进而可以支持丰富的Fast DDL类型。
Redis 6.0更多精彩详情 Redis 6.0是Redis发展史上极为重要的版本,包含多项重大功能更新和大幅度的性能提升。为了让用户可以第一时间体验到最新版Redis的各项功能,阿里云紧随其后发布了最新版 Redis 6.0云数据库。在昨天的发布会中,云数据库Redis 6.0版本正式与大家见面。 今天小编就为大家揭秘一下云数据库Redis极速上云背后的故事。 2020年5月2日,Redis 6.0 GA版正式发布,这个版本被作者誉为社区版史上最大的一次发布,包含诸如 RESP3新协议支持、ACL 管理、多 IO 线程、SSL 加密、客户端缓存与集群代理等多项重大关键功能更新和内核架构提升。 2020年5月7日,阿里云全球首发(除原厂)最新版 Redis 6.0云数据库,全面涵盖开源Redis 6.0各项功能。 短短不到一周的时间,阿里云Redis是如何做到极速上云的呢?总结起来,阿里云Redis团队对内核深度的掌控能力和云原生管控强大的助力是两大主因。 内核的掌控能力 这次社区版6.0中阿里云Redis团队的贡献排在第二位,仅次于作者和其所在的商业公司,高居原厂外代码贡献第一名,充分说明了阿里云Redis对内核的掌控能力,在6.0正式GA之前阿里云团队便开始不断对这些新feature进行测试和优化,将云上积累的东西反馈给社区,引导社区的发展。 另一方面,阿里云Redis企业版(Tair)此前就已具备了Redis 6.0版本中的很多新功能,例如:多IO线程、权限管理、SSL加密以及集群proxy。在线下,阿里云Redis团队成员也和作者进行了多次沟通,就Tair上的实现方案与他进行深入探讨。 比如多IO线程,从RC1到GA,阿里云Redis团队帮社区在多线程的基础上又提升了30%的性能。而在权限管理上,阿里云Redis云服务有很多运维的经验把它结合到ACL中,让ACL更加方便的使用和管理。 除此以外,我们在主备数据一致性和对于过期时间的判断又做了进一步加强,这亦是阿里云Redis和许多客户共同成长的成果。另外针对云上用户实现的一些通用需求也合并到了6.0主干,比如新添加了bitfiled_ro、set keepttl等等命令,让更多用户享受到阿里的技术红利。 云原生管控 Redis 6.0大版本快速输出得益于管控基于云原生的新架构,新管控平台屏蔽了管控上复杂的机器操作和实例交互,通过使用非常驻临时pod的方式来完成所有的管控动作,同时新增通用模块,提供了数据库领域级别的模型抽象,业务只需要根据相应的业务模型去描述service_spec.yaml文件即可。 通过通用和高度可定制化的云原生管控平台,新增业务功能无需过多关心平台接口,更多聚焦于业务本身,大大提升了新业务接入开发效率,Redis 6.0在极短的时间内完成管控适配并上线,助力全球首发。随着阿里云Redis团队对内核的掌控能力进一步提升以及云原生管控的助力,包括内核优化、功能增强与弹性伸缩能力等多方面均得到大幅度提升, Redis 6.0之后的阿里云Redis服务后续会有更为丰富的功能和更佳的用户体验。 而由同一团队所出品的兼容Redis的高性能KV存储Tair本年度也将推出数据闪回(按时间点恢复数据)、全球分布式缓存、存储级内存KV等多项重磅功能和新产品,为更多行业客户与场景赋能。 另外,我们还为用户提供了Redis云数据库专属集群(https://www.aliyun.com/product/apsaradb/cddc)这是阿里云专为大中型企业用户定制优化的解决方案,填补了市场空白,具有资源独享、自主可运维、多数据库混合部署等特点,让用户既享受到云数据库的灵活性,又满足了企业对数据库合规性、高性能和安全性要求。 相关阅读: 全球首发 | 阿里云正式推出云数据库Redis 6.0版本 倒计时7天 | 全球第一的KV内存数据库即将再度升级! 错过直播的同学可扫描下方二维码观看回放哦 公众号菜单栏回复“redis 6.0”即可获取讲师直播PPT资料啦! 点击文字立即解锁Redis 6.0更多精彩详情新用户还可享5折优惠哦!
2020年06月