作者:吴学强
我们或许都玩过 Join the Dots 这个游戏,一张纸上随机的画了很多点,每个点旁边标了一个数字,沿着这些数字从小到大将这些点连接起来,带着期待,我们发现了这些点构成的神奇图案。对了,这个游戏还有另外一个名字:Connecting the Dots ,或许你对它更熟悉^^。
过去的两年,我们团队花了巨大的精力对 PolarDB-X 进行了全新的设计,像这个游戏的设计者一样,精心思考每个“点”应该放在什么位置。
2020年 5 月份,基于 PolarDB-X 1.0 近五年的打磨经验和用户反馈,我们重新设计了系统架构。新架构采用存储计算分离方式,通过将计算节点与存储节点进行一比一配比,简化系统拓扑结构,存储层采用基于 Paxos 协议的 X-DB,提供数据的强一致保障。计算节点与存储节点通过私有协议迈出了突破性的一步,使得我们的分布式事务、优化器等能力取得极大突破,产品上推出了更友好的一体化形态的 PolarDB-X 2.0。
9月份的云栖大会上,我们推出了具备存储资源强隔离、数据强一致的在线分析能力,开始往 HTAP 方向迈出第一步。同时支持了GSI(全局二级索引),使得 PolarDB-X 具备了单机数据库一样的索引能力。
接下来我们一起看下 PolarDB-X 2.0 又新增了哪些“点”,看看连接上这些“点”后能否更接近它的全貌。
HTAP
计算资源隔离
云栖发布的 HTAP 能力中,我们利用 Paxos 副本提供了存储层资源的强隔离,对计算层资源仅做了几个关键部分的隔离。本次发布的版本中,我们对计算层的资源进行了系统化的梳理和设计,从而实现了计算层资源的强隔离。
CPU 隔离
PolarDB-X 计算层开发语言为 Java,所以在 CPU 隔离上我们使用了JVM 相关的隔离技术。阿里JDK提供了基于cgroup、wisp/wisp2等 CPU 相关隔离方式,其中 cgroup 是以租户形式提供强隔离,可以严格限制住 CPU quota。wisp/wisp2是基于协程时间片统计的软隔离。通过cgroup attach线程有一定开销,所以适合常驻线程池,wisp方式 attach 代价比较小,但wisp2 要求所有线程池都采用 wisp2。综合考虑后,我们采用了cgroup + wisp结合的方式进行 TP 和 AP 的 CPU 资源隔离。
另外,对于 Parser、Optimizer 等混用的线程,我们引入令牌桶的方式更精确的区分 TP 和 AP 的 CPU 使用量。同时设计了 TP 退化成 AP 的策略,当一条 Query 执行时间超过 TP 阈值时,会退化成 AP 来执行,避免阻塞 TP 队列,同时处理完毕后,会将该执行计划反馈给 SPM,以便下次优化器以 AP 方式来执行相同请求。
Memory 隔离
PolarDB-X 2.0 通过 Memory Pool 机制来管理内存,Memory Pool 的目的是通过限制请求在各个阶段临时表的大小,保护计算节点的稳定运行。
Memory Pool 自顶向下分为多个层次:
- Global Pool:整个计算节点可以使用的临时表内存,默认限制 = JVM_Heap_Size - 4GB
- Schema Pool:单个 Schema 可以使用的临时表内存,默认不限制
- TP/AP Pool: 对 Schema Pool 做的一个逻辑隔离,其中 TP 与 AP 的 Memory 比例可以动态调整
- Request Pool:单个 Query 可以使用的临时表内存,以 TraceID 命名,默认限制 = Global_Pool_Limit / 4
- Operator Pool:单个算子的内存,仅仅用于显示、方便监控,默认不限制
计算节点在处理 Aggragate、Join、Sort 等算子时,会采用“临时表”进行排序等计算,临时表会通过 Memory Pool 进行管理。考虑到对象的序列化/反序列化的开销比较大,当前内存占用大小通过计算对象占用空间实现,由于临时表中的对象都是 Integer、String、Timestamp 等简单对象,这种估算方式还是比较准确的。
当一个请求中用到临时表时,会从 Memory Pool 中扣除其空间占用,临时表释放后,该空间会还给 Memory Pool。
若 Memory Pool 超过其大小限制,空间扣除请求会失败,并返回异常。
另外,我们还设计了 Memory Pool Reset 机制,用来应对“泄露”等内存管理异常情况。
对于 Schema Pool 里的内存资源,在 HTAP 场景下,首先需要保证 TP 请求的高优先级使用,其次应该最大化 AP 请求的资源可用量。所以我们对 Memory Pool 做了弹性划分,分别为 TP 和 AP 设置了高低水位。默认情况下,TP 内存水位比例为 20%-80%,AP 内存水位比例为 30% - 60% 。当内存使用冲突时,优先保障 TP 请求。比如当前 TP 水位 50 %,AP 水位 45%,当一个 TP 请求内存使用量超过 5% 时,会触发系统 kill 机制,该机制会终止 AP 正在处理的内存使用量排名第一的请求,释放内存供 TP 请求使用。
Memory Pool 的保护与隔离机制,即保证了计算节点的稳定运行,又使 TP 和 AP 内存资源使用上保持了足够的弹性。
MPP Cluster 资源调度
CPU 资源隔离与 Memory Pool 机制都是单个计算节点上的资源隔离方式,HTAP 中的 MPP 能力会把所有计算节点当成一个整体进行任务调度,所以每个计算节点负载的相对均衡,也是系统总体稳定的一个关键因素。
基于这个考虑,MPP 在进行任务调度时,会遵循以下几个原则:
- 主实例可以向主实例和只读实例所有计算节点派发任务,只读实例只能向所有只读节点派发任务
- 在派发任务时,若某个计算节点负载超过阈值,则不向该节点派发任务
- 若可用节点数少于所需节点数,退化为 Local 模式在本节点完成计算
- 若派发任务时,实例正在版本升级,首先评估兼容性后,再决定是否派发
下图是我们进行的一次 TPC-C 和 TPC-H 混合负载测试。第一阶段为 TPC-C 测试,可以看出此时系统吞吐保持在一个稳定的水位。第二阶段加入 TPC-H 负载,但不做 TP 与 AP 隔离,此时系统进入一个剧烈波动状态,虽然TPC-C 和 TPC-H 可以继续执行,但相互干扰非常严重。第三阶段动态开启隔离后,系统恢复到一个稳定的状态,此时 TPC-C 正常进行,TPC-H 也能在短时间内跑完。
RuntimeFilter
RuntimeFilter 一般是 MPP 数据库对于 Join 的一种优化手段,可以减少数据读取,减少 Shuffle 数据量,对于可以命中的查询,性能可以有非常大的提升。比如对于下图中的 SQL,我们在 Join 构建 Build 表过程中,基于items.id 构建一个 BloomFilter,然后在运行过程中将 BloomFilter 动态推给 Probe Scan,这样在 Shuffle 之前经过 BloomFilter 提前过滤,可以大幅减少 Shuffle 数据量,提高性能。
RuntimeFilter 业界有多种实现方案,Impala 通过 Coordinator 在 Shuffle 阶段均可支持 RuntimeFilter,Presto 的方案相对有局限性,仅支持右表可以 broadcsat 的场景,Flink 的方案与 Impala 类似,都是通过中央化的 Coordinator 做 BloomFilter 的 merge 后再广播,不同点是 Flink 会更多的考虑 BloomFilter 的下推。
PolarDB-X 采用类似 Flink 的方案,即通过 Coordinator 完成 BloomFilter 的 merge 和广播,并尽量考虑 BloomFilter 的下推,在此基础上,我们会利用优化器的统计信息进行代价评估,进一步判断 RuntimeFilter 是否值得去做。另外,我们的存储层是 MySQL,所以 BloomFilter 可以直接下推到 MySQL 进行计算,这样可以大大减少 Scan 返回的数据量,进而降低网络开销。
我们对 RuntimeFilter 特性开启前后进行了 TPC-H 性能测试,结果如下图。
从测试结果来看,RuntimeFilter 对 Q5、Q8、Q9、Q17、Q21 等多条 Query 有非常明显的加速作用,对大部分 Query 都有提升,总体性能提升在 20% 以上。
另外测试结果同时可以看出,有部分 Query 出现了少量的性能回退,分析原因后我们认为,这主要是 CBO 采用的统计信息不够准确导致的,目前我们在估算过滤率时采用的算法是,这是个很粗糙的方式,其假设数据是均匀分布的,所以目前我们正在尝试跟 CBO 有更好的结合以便做进一步的优化。另一个优化点方向是,对于星型模型和雪花模型的查询( PKFK )的查询,在 RuntimeFilter 生成阶段仍然可以进行 Join Reorder,其复杂度为线性,我们评估后认为会对 TPC-DS 等更复杂查询有很大帮助。
除了计算层资源隔离、RuntimeFilter 等优化或特性,2.0 中还增加了LookUpJoin 流式执行、Window Function等,这里不再展开。
透明分布式
兼容性
PolarDB-X 作为一个新进者,保持与现有数据库生态的兼容性是我们长期追求的一个目标。本次发布的版本在 SQL 语法体验上有了质的改变。
如上图所示,在建表时只需通过有无 'PARTITION' 关键字区分单表和拆分表,无需通过其他语句额外指定拆分键、拆分算法、分片数量等,极大简化了分布式上手门槛,熟悉 PolarDB-X 1.0(DRDS) 的用户应该有更加强烈的体感。另外,用户可以像使用单机索引一样,使用 GSI(全局二级索引)对查询进行加速,其语法跟单机完全一致。
这些简单的语法改动的背后,是对系统重新设计的结果。
在拆分策略上,系统会默认选择主键进行拆分,对于没有主键的表,系统会增加隐藏主键。拆分算法默认选择一致性Hash 算法,减少分片数变化时需要搬迁的数据规模,提高拆分效率。
索引创建上,默认会选择 GSI,GSI 与主表数据通过分布式事务保持强一致。在处理请求时,优化器会根据优化规则和统计信息选择合适的 GSI,以提高查询效率。
同时,对于高级用户,我们提供了设定拆分算法等入口,同时也提供了GSI 与 Local Index 的设定入口。
SQL 限流
实际业务中我们经常遇到数量不多但非常耗资源的 SQL 把整个体统拖慢的情况,或出于业务或管理要求,需要对某类 SQL 进行临时限制,我们提供了 SQL 限流功能以满足这部分诉求。
对于限流或限制资源使用,目前业界有多种实现方案,Oracle 通过设置用户级别的资源(CPU、Memory、IO)使用量进行限制。SQL Server 通过将 Workload 按照指定规则分类的方式进行资源(CPU、Memory、IO)隔离,并且可以实现类似多租户的隔离效果。阿里云 RDS for MySQL 通过存储过程实现对限流规则的管理。
PolarDB-X 中提供基于规则匹配的 SQL 限流方式,语法如下。
CREATE CCL_RULE IF NOT EXISTS ccl_name ON database_name.table_name TO ‘username’@’host’
FOR { UPDATE | SELECT | INSERT | DELETE }
[filter_options]
with_options
user:
‘username’ @ ’host’
filter_options:
[ FILTER BY KEYWORD(‘KEYWORD1’, ’KEYWORD2’,…) ]
[ FILTER BY TEMPLATE(‘template_id’) ]
with_options:
WITH MAX_CONCURRENCY = value1 [, WAIT_QUEUE_SIZE = value2, WAIT_TIMEOUT = value3]
DROP CCL_RULE id1;
CLEAR CLL_RULES;
SHOW CLL_RULES;
注:左右滑动阅览
该限流方案可以通过库名、账号、并发数、关键字、等待数量等多个维度设置限流规则,限流规则实时生效。
在高并发场景下,限流规则带来的 overhead 是需要重点考虑的因素。经过测试,我们发现限流的 overhead 主要在 SQL 解析上,所以通过将 SQL 转换成模板并缓存,我们将限流的 overhead 降低到可以忽略不计的程度。
下图是 SQL 限流功能的测试示例。测试中,我们用 TPC-C 模拟业务流量,中间用 JMeter 跑全表扫描这样的耗资源 SQL,之后通过动态添加规则对其进行限制。
限流的半自动甚至自动化是我们追求的下一个目标,该能力会通过收集 Query RT、CPU quota、Memory 使用量等多维度信息,并配合用户设置的限流策略实现。
SQL Advisor
实际业务中如果发现慢 SQL 并尝试通过添加索引来加速查询,添加索引时,DBA 或内核研发同学会依赖 SQL 所涉及的表的数据量(统计信息),过滤条件,Sort、Join、Aggragate 方式等等来考虑建议添加什么索引。之所以知道添加某些索引有效是因为这些同学对数据库的优化执行行为有比较深刻的认识。
PolarDB-X 2.0 中,我们参考微软在 VLDB 发表的一篇论文中的方法实现了 SQL Advisor 能力。
具体来说,SQL 中存在一些所谓的 Indexable Column,比如 Where 条件中的列。这些列通过单个或多个组合构成 Candicate Index,而多个 Candicate Index 可以组成一个 Configuration。我们的目标就是通过优化器对这些 Configuration 进行代价评估,选出最优的结果并推荐给用户。
实现中,我们用过将 SQL 解析并通过 RBO 优化的逻辑计划做 Visitor + MetaQuery 遍历找到 Indexable Column。之后采用启发式规则构造 Candicate Index。根据经验,一般一个索引的前两列提供了最大贡献,并考虑到 DML 负担和死锁风险,所以我们选择两个列组成索引。Configuration 构造上则直接通过枚举所有的 2 列组合索引完成,即总数为 C(n,2)。
在 Configuration 对比阶段,考虑两个 Configuration A 和 B,我们基于以下原则进行选择:
- 当使用 A 后,SQL 的代价比使用 B 更低,超过一定阈值则认为 A 更好
- 当 A 与 B 的代价差别不大,如 1% 范围。则索引越少的越优,索引数相同情况下,索引总长度越少越优
下面是我们的一次测试的截图。启用 SQL Advisor 做索引推荐前,一条 SQL 的查询时间为 28.76s。
之后查看 SQL Advisor 给出的索引推荐并添加相应的索引。
添加索引后执行相同的 SQL,时间减少到 1.41s。
PITR
备份恢复是数据库的必备能力,分布式数据库的备份恢复在实现上相对于单机有更大的挑战,PolarDB-X 2.0 中提供了一致性备份和任意时间点恢复能力。
备份阶段,我们通过 TSO 获取全局版本号,并用该版本后从存储节点副本中获取全量数据的一致性快照,同时会将 Binlog 日志进行归档,Binlog 中带有 TSO 版本号信息。
恢复阶段,首先根据指定的时间点选择最近的全量备份集进行恢复,之后根据该全量快照所对应的 TSO 版本号定位到 Binlog 中的起始位置,重放 Binlog 直到指定的时间点,这样便完成了数据的任意时间点恢复。
一致性与高可用
智能并行发包
上图展示的是一个部署了三个节点的存储集群,设计上引入了多分组 X-Paxos 技术替换传统的复制协议,基于多分组技术可支持多点写入+多点可读的能力,解决了传统 MySQL 数据库下的单点写瓶颈的问题。
多分组 Paxos 相比于单分组 Paxos 会带来额外的资源开销,所以我们通过节点上共享网络通信模块和并包的方式解决了连接风暴和消息风暴问题,通过持久化 X-Paxos 分组和数据分片的映射关系,保持了 Consensus 层的独立性和通用性,降低了系统耦合度。
异步化事务提交
传统的 MySQL 都是 One Thread per Connection 的工作模式, 在引入线程池后是以一个线程池孵化一定数量的工作线程, 每个线程负责处理一次 Query 的解析、优化、修改数据、提交、回网络包等等。在集群跨地域部署场景下,一次事务提交由于需要在集群之间同步日志,网络通信带来的延迟会达到数十毫秒级别。如果将整个事务的提交异步化,将工作线程从等待 X-Paxos 日志同步中解放出来,在大负载下可以拥有更高的处理能力。
异步化提交核心思想是将每个事务的请求分成两个阶段,提交之前一个阶段,提交和回包一个阶段。两个阶段都可以由不同的工作线程来完成。为了完成异步化的改造,我们增加了等待同步队列和等待提交队列,用于存储不同阶段的事务。这样一来,系统中只有少数的线程在等待日志同步操作, 其余的线程可以充分利用 CPU 处理客户端的请求,异步化提交结合 MySQL 的 Group Commit 逻辑,让 Worker 线程可以去执行新的请求响应。经测试,同城部署场景中异步化相比同步提交有 10% 的吞吐量提升,跨区域场景下有 500% 的吞吐量提升。
热点更新
热点更新原本就是数据库的一个难题,PolarDB-X 在 AliSQL 的热点功能之上优化了复制,使得在保证数据强一致的情况下,热点更新性能提升非常明显。
如上图所示,针对热点行更新的基本思路是合并多个事务对同一行的更新。为了让批量的更新事务能够同时进行提交,PolarDB-X 在存储引擎中增加了一种新的行锁类型——热点行锁。热点行锁下,热点更新的事务之间是相容的,为了保证数据的一致性,对同一批的热点更新事务日志打上特殊 tag, X-Paxos 会根据 tag 将这一整批事务的日志组成一个单独的网络包进行集群间的数据同步,保证这些事务是原子的提交/回滚。除此之外为了提升日志回放的效率,PolarDB-X 将每个批次事务中对于热点行的更新日志也做了合并。
总结与展望
本次发布的版本中,我们通过计算资源强隔离、RuntimeFilter、Window Function、LookupJoin流式执行等,提供了更稳定、更易用、更高效的 HTAP 能力。通过接近单机数据库的建表语法以及系统性的设计,极大的降低了分布式的上手门槛,同时提供拆分算法、局部索引等的设定入口,满足高阶用户的需求。通过提供维度丰富、overhead 极小的 SQL 限流能力,满足用户对耗资源 SQL 实时限制或 SQL 分类管理诉求。通过设计索引组合和评估框架,SQL Advisor 将 CBO 的优化能力赋予用户,让索引选择变得更加简单。通过 TSO 版本号,将分布式数据库备份恢复这项有挑战的功能做到了极致。存储层,通过 Paxos 智能并行发包、事务异步提交优化、热点写入场景优化等,进一步提高了存储的稳定性和效率。
这些功能“点”逐渐勾勒出了 PolarDB-X 2.0 的轮廓,我们依然在快速迭代,依然在精心打造接下来的每一个“点”:更强的优化器、更高效的分布式事务、更优的 HTAP、更极致的弹性、更好的兼容性、更完备的容灾能力、更丰富的企业特性。Join the dots,让我们一起期待~