最近分布式数据库领域可谓非常之火(也可能是非常之卷),特别的,很多人会关注TPCC的测试结果,也有不少产品会投入很多精力在TPCC的优化上。
我们首先需要搞明白的是,我们从TPCC的测试结果,究竟能得出对这个分布式数据库什么样的评价。
TPCC代表了什么
TPCC的业务场景其实很简单,本文中我们关注其中几个重要的概念即可:
- 仓库(Warehouse),有很多的仓库,每个仓库有很多的商品(Item),每个商品在每个仓库有自己的库存数目。仓的数目很多,比如一般测试都是1000仓起步(这个与一些电商平台是不一样的,例如淘宝,核心单元可能就个位数)。
- 买家(Customer),绝大多数情况下,买家只会从绑定的一个仓库买东西;少数情况下,买家才会从其他仓库买东西
基于这样的业务特征,所以在数据库的设计上,很容易想到将数据库按照仓库的ID(wid)进行分片。这种分片可以确保大多数的事务可以在单个分片内完成。
例如假设我们有这样一份TPCC的测试报告:
- TPMC:100,000,000(1亿)
- 数据库使用机器:222台(Intel Xeon Platinum 8163,42C84T712G)
Transaction Mix Percent Number
New-Order 45%
- Percentage of Home order-lines 99.000%
- Percentage of Remote order-lines 1.000%,每个订单有10个商品,所以有10%的下单涉及到分布式事务
Payment 43%
- Percentage of Home Transactions 85%
- Percentage of Remote Transactions 15%
- Order-Status 4%
- Delivery 4%
- Stock-Level 4%
我们可以做如下的解读:
- TPMC就是指每分钟下单(New-Order)的数量。是一般情况大家最关注的指标,也是TPCC官方进行性能排名使用的指标。
- 下单和支付(Payment)两种事务的数量,占了总事务量的45%+43%=88%。
- 90%的下单操作是单机事务,85%的支付操作是单机事务,分布式事务合计占比45%*10%+43%*15%=11%。整个集群分布式事务的TPS是:100,000,000*11%/60=18万TPS
- 按机器数做下平均,每台机器贡献的单机TPMC是1亿/222*89%=40万,分布式TPMC是1亿/222*11%=5万,加起来是45W
可以看出,TPCC中,TPMC指标中最大头是单机事务贡献的。但是,这种单机事务其实是无法体现分布式特有的一些问题的,例如,节点之间无需互相传递数据,也无需对事务的状态进行协调等。
这里的第一个结论是,TPCC可以很好的体现一个数据库的单机性能,但是它无法很好的体现一个分布式数据库的分布式事务能力(性能、扩展性,甚至在一些不严格的测试中,连一致性也无法验证)
如果真的像TPCC一样,严格控制分布式事务的使用,对业务来说代表了什么?
代表业务永远都要去考虑分区键这个东西,只有业务能划出很统一的分区键,才能尽可能的去减少分布式事务。
无论一个分布式数据库号称多么“先进”,只要它需要你去设计分区键,那其实它就是个“伪”分布式数据库。使用这样的数据库,和使用分库分表中间件+单机数据库没有太明显的区别。
事实上,分库分表中间件+单机数据库除了这11%的分布式事务无法实现,TPCC的其他指标诸如性能、扩展性都能做的很好。只要加足够多的机器,一样能跑到很高的TPMC,这个过程会是非常线性的。只是那11%的分布式事务,阻止了分库分表+单机数据库的方案。
这里的第二个结论是,如果你的业务愿意像使用中间件“谨慎并良好的设计每个表的分区键,避免分布式事务”一样去使用分布式数据库,那么每台机器承载的TPMC(上例中的45万)是很好的参考依据,集群的总TPMC(上例中的1亿)并没有太大的参考价值
如果要看单机TPMC,我们可以与一些传统单机数据库做下对比,其单机能达到的TPMC是什么样的水平呢,我们找几份数据看一下。
- 数据1:
这一份对官方的PG11做的测试(https://developer.aliyun.com/article/698138),在类似的机器配置中,可以跑到103W的TPMC。
- 数据2:
TPCC官方榜单上有很多单机数据库的数据,其TPMC普遍在百万级以上(甚至是十多年前的古董级CPU)。
这样看起来非常巨大的数字(1亿TPMC),比较下来,除了机器规模着实吓人之外,和中间件+单机数据库比似乎并没有什么优势。更别说,为了提升TPMC,通常还会使用存储过程来封装TPCC的事务,而这种用法对一般业务来说侵入性太强,很少场景会使用。
也许你会说,还有11%的分布式事务呢,这是中间件+单机的方案解决不了的。
这里的问题是,11%的分布式事务一定代表着强依赖分区键,我想,大多数业务应该不想总是要想着分区键的概念吧,因为这个东西实在太难选了。
诚然,TPCC的模型对分区键的选择是非常非常友好的,每个主要(ITEM\WAREHOUSE\CUSTOMER\ORDER)的表都能按照WAREHOUSE ID进行分区。但在实际的业务中,你会发现情况远比这个复杂的多,比如:
- 很多电商系统的订单表会有两个查询维度,卖家和买家,选哪个当分区键?
- 做社保系统,缴费表按照人的id和按照企业的id去查都很常见,选哪个当分区键?
- 有一堆的业务代码需要去改,在SQL里带上分区键,这怎么改?
...
自然,我们想知道,什么样的技术,才能让你“忘掉”分区键这个东西呢。
二级索引与分区键
广义的“分区键”的概念,其实并不是分布式数据库特有的。
我们在单机数据库中,例如MySQL中,数据存储成了一棵一棵B树。如果一个表只有主键,那它只有一棵B树,例如:
CREATE TABLE orders(
id int,
seller_id int,
buyer_id int,
primary key (id)
);
这个表唯一的B树是按照主键(id)进行排序的。如果我们的查询条件中带了id的等值条件例如where id=1,那么就可以在这棵树上快速的定位到这个id对应的记录在哪里;反之,则要进行全表扫描。
B树用于排序的Key,通过二分查找可以定位到一个叶子节点;分区键通过哈希或者Range上的二分查找,可以定位到一个分片。可以看出它们都是为了能快速的定位到数据。
如果我们要对上面的表做where seller_id=1来查询,在MySQL中,我们并不需要将seller_id设为主键。更加自然的方式是,在seller_id上创建一个二级索引:
CREATE INDEX idx_seller_id ON orders (seller_id);
每个二级索引在MySQL中都是一棵独立的B Tree,其用于排序的Key就是二级索引的列。
也就是说,当前t1这张表,有两颗B Tree了,主键一棵,idx_seller_id一棵,分别是:
id->seller_id,buyer_id
seller_id->id
当使用where seller_id=1进行查询是,会先访问idx_seller_id这颗B树,根据seller_id=1定位到叶子节点,获取到id的值,再使用id的值到主键那颗B树上,找到完整的记录。
二级索引实际上是通过冗余数据,使用空间与提升写入的成本,换取了查询的性能。
同时,二级索引的维护代价并不是非常的高,一般情况下可以放心的在一个表上创建若干个二级索引。
同理,在分布式数据库中,想让你“忘掉”分区键这个东西,唯一的方法就是使用分布式二级索引,也称为全局索引(Global Index)。并且这个全局索引需要做到高效、廉价、与传统二级索引的兼容度高。
全局二级索引
我们在数据分布解读(一)中介绍过全局索引:
Global Index,也称为全局索引,由一组同构的Partition构成。
Global Index包含两种类型的字段,Key字段与Covering字段,Key字段为索引字段,其余字段为Covering字段。
Global Index有Partition Key与分区算法两个属性,其Partition Key是Global Index的Key字段。
......
对于Key字段的查询,CN可以直接定位到对应的Partition。
全局二级索引同样也是一种数据冗余。例如,当执行一条SQL:
INSERT INTO orders (id,seller_id,buyer_id) VALUES (1,99,1000);
如果orders表上有seller_id这个全局二级索引,可以简单理解为,我们会分别往主键与seller_id两个全局索引中执行这个insert,一共写入两条记录:
INSERT INTO orders (id,seller_id,buyer_id) VALUES (1,99,1000);
INSERT INTO idx_seller_id (id,seller_id) VALUES (1,99);
其中orders主键索引的分区键是id,idx_seller_id的分区键是seller_id。
同时,由于这两条记录大概率不会在一个DN上,为了保证这两条记录的一致性,我们需要把这两次写入封装到一个分布式事务内(这与单机数据库中,二级索引通过单机事务来写入是类似的)。
当我们所有的DML操作都通过分布式事务来对全局索引进行维护,我们的二级索引和主键索引就能够一直保持一致的状态了。
好像全局索引听起来也很简单?
实则不然,一个可以广泛使用的全局索引,以下条件缺一不可,不然只能是个玩具。
索引的强一致性、高性能与分布式事务
索引一定要是强一致的,例如:
- 不能写索引表失败了,但是写主表成功了,导致索引表中缺数据
- 同一时刻去读索引表与主表,看到的记录应该是一样的,不能读到一边已提交,一边未提交的结果
...
这里对索引的一致性要求,实际上就是对分布式事务的要求。
由于全局索引的引入,100%的事务都会是分布式事务,对分布式事务的要求和“强依赖分区键类型的分布式数据库”完全不同了,要求变得更高:
- 至少需要做到SNAPSHOT ISOLATION以上的隔离级别,不然行为和单机MySQL差异会很大,会有非常大的数据一致性风险。目前常见的方案是HLC、TrueTime、TSO、GTM方案,如果某数据库没有使用这些技术,则需要仔细甄别。
- 100%的分布式事务相比TPCC模型10%的分布式事务,对性能的要求更高,HLC、TSO、TrueTime方案都能做到比较大的事务容量,相对而言,GTM由于更重,其上限要远远低于同为单点方案的TSO(TSO虽然是单点,但由于有Grouping的优化,容量可以做的很大)。
- 即使用了TSO/HLC等方案,优化也要到位,例如典型的1PC、Async Commit等优化。不然维护索引增加的响应时间会很难接受。
与单机索引的兼容性
单机数据库中,索引有一些看起来非常自然的行为,也是需要去兼容的。
例如:
- 能通过DDL语句直接创建索引,而不是需要各种各样的周边工具来完成。
- 前缀查询,单机数据库中,索引是可以很好的支持前缀查询的,全局索引应该如何去解这类问题?
- 热点问题(Big Key问题),单机数据库中,如果一个索引选择度不高(例如在性别上创建了索引),除了稍微有些浪费资源外,不会有什么太严重的问题;但是对于分布式数据库,这个选择度低的索引会变成一个热点,导致整个集群的部分热点节点成为整个系统的瓶颈。全局索引需要有相应的方法去解决此类问题。
面向索引选择的查询优化器
我们知道,数据库的优化器核心工作机制在于:
- 枚举可能的执行计划
- 找到这些执行计划中代价最低的
例如一个SQL中涉及三张表,在只考虑左深树的情况下:
- 在没有全局索引的时候,可以简单理解为,执行计划的空间主要体现在这三张表的JOIN的顺序,其空间大小大致为3x2x1=6。执行计划的空间相对是较小的,优化器判断这6个执行计划的代价也会容易很多。(当然优化器还有很多工作,例如分区裁剪等等,这些优化有没有索引都要做,就不多说了)。
- 在有全局索引的时候,情况就复杂多了。假设每个表都有3个全局索引,那执行计划空间的大小大致会变成(3x3)x(2x3)x(1x3)=162,这个复杂度会急剧的上升。相应的,对优化器的要求就会高的多。优化器需要考虑更多的统计信息,才能选择出更优的执行计划;需要做更多的剪枝,才能在更短的时间内完成查询优化。
所以我们可以看到,在没有全局索引的“分布式数据库”或者一些中间件产品中,其优化器是很羸弱的,大多是RBO的,它们根本就不需要一个强大的优化器,更多的优化内容实际上被单机优化器给替代了。
其他
索引的创建速度,索引回表的性能,索引的功能上的限制,聚簇索引,索引的存储成本等等,其实也都极大的影响了全局索引的使用体验,这里鉴于篇幅原因,就不继续展开了。
索引的数量
对全局索引的这些要求,本质来源于全局索引的数量。
透明性做的好的数据库,所有索引都会是全局索引,其全局索引的数量会非常的多(正如单机数据库中一个表一个库的二级索引数量一样)。数量多了,要求才会变高。
而,这些没有全做好的分布式数据库,即使有全局索引,你会发现它们给出的用法依然会是强依赖分区键的用法。
它们会让创建全局索引这件事,变成一个可选的、特别的事情。这样业务在使用全局索引的时候会变的非常慎重。自然,全局索引的数量会变成的非常有限。
当全局索引的数量与使用场景被严格限制之后,上述做的不好的缺点也就变得没那么重要了。
PolarDB-X的透明分布式
PolarDB-X实现了非常优秀的分布式事务与全局索引,满足上文提到了对全局索引的要求,做到了透明分布式。
在透明分布式模式下(CREATE DATABASE中指定mode='auto'),所有的索引都是全局索引,应用无需关心分区键的概念。
例如,我们的建表语句,与单机MySQL完全一致,并不需要指定分区键:
create table orders (
id bigint,
buyer_id varchar(128) comment '买家',
seller_id varchar(128) comment '卖家',
primary key(id),
index sdx(seller_id),
index bdx(buyer_id)
)
创建全局索引也与单机MySQL创建二级索引的体验一致,全程是Online的:
CREATE INDEX idx_seller_id ON orders (seller_id);
PolarDB-X的全局索引是强一致的,其数据一致性体验与单机MySQL没有明显差异,提供了符合MySQL语义的RC与RR的隔离级别。
同时,PolarDB-X在索引的维护、优化器上也做了大量的工作,确保索引能高效的创建、维护,优化器能正确的生成使用索引的执行计划。
PolarDB-X的分区算法,也能很好的处理索引中产生的热点、数据倾斜等问题,参考:https://yuque.antfin.com/coronadb/kaswnf/ixsvfi
结语
我们的结论是:
- TPCC的业务逻辑非常契合分库分表的架构,90%的事务是单机事务。对于TPCC的测试结果,建议更多的关注其平均到每台机器的TPMC。
- 使用全局索引的数据库,才有可能做到真正的透明;没有全局索引的数据库,和分库分表没有本质区别,都要应用自己去设计分区键。
- 全局索引的写入必然是分布式事务,对分布式事务的性能、一致性有很高的要求。对于一个透明的分布式数据库,100%的事务都是分布式事务。
- 全局索引的数量很关键,透明的分布式数据库,所有索引都是全局索引,数量会非常多。
需要指定分区键的分布式数据库在五六年前还是比较流行的,但在现代,透明分布式是一个合格的分布式数据库必须提供的能力。
它能够极大的减轻应用的迁移成本,例如不需要修改应用中的SQL带上分区键,不需要为每个表都设计一个分区键等;同时,也能极大的减轻开发人员的学习成本,让开发人员能将精力集中在满足业务需求上,例如,可以沿用使用单机数据库时SQL优化的经验,通过加索引的手段去改善SQL的执行性能。