产品模块原理系列 | OceanBase SQL 引擎的模块介绍和调优实践分享

2024年 5月 7日 32.3k 0

摘要我们将推出「产品模块原理系列」内容,其中包含了OceanBase最核心的SQL引擎、存储引擎、内存管理、分布式并行计算引擎等重要的技术原理,很多内容还是第一次对外发布哦~第二期我们为大家带来分布式数据库OceanBase的SQL引擎系统梳理,更多内容敬请期待!

产品模块原理系列 | OceanBase SQL 引擎的模块介绍和调优实践分享-1

前言    


在对OceanBase数据库SQL引擎整体介绍之前,我们先来回顾下OceanBase的架构:OceanBase本身是一个分布式的集群,至少包含三个子集群(以下均以三个子集群为例),每个子集群称作一个zone。一个zone内包含多台服务器,每台服务器称作OBServer。OceanBase支持分区表(非分区表也可以理解为只含单一分区的分区表),因此副本的最小单位是分区;对于每一个分区,在整个集群内它都会有三个副本(一主两从),每个zone内都存在一个副本。当前版本下每台OBServer都承载了计算和存储功能,在OS中表现为单进程程序,进程包括了SQL引擎和存储引擎功能。


产品模块原理系列 | OceanBase SQL 引擎的模块介绍和调优实践分享-2

图1: OceanBase集群基础架构图

OceanBase SQL引擎概述  

SQL引擎是RDBMS的灵魂,OceanBase作为NewSQL,在SQL引擎层面不仅高度兼容MySQL,同时在兼容Oracle方面也迈出了坚实的一步:目前已支持Oracle的常用语法功能。OceanBase的SQL引擎的工作流程,大致可以通过下图来了解:

产品模块原理系列 | OceanBase SQL 引擎的模块介绍和调优实践分享-3

图2: OceanBase SQL引擎的工作流程


当OceanBase收到一条SQL语句时,会历经语法解析器(Parser)、语义分析器(Resolver)、查询重写器(Rewriter)、查询优化器(Optimizer)和代码生成器(Code Generator)五个模块,最终生成物理执行计划并交由执行引擎去执行,而SQL语句的已分析版本也会保存到Plan Cache中用以加速下一次同SQL的再次访问。


如果是收到一条命令(如手工合并:ALTER SYSTEM MAJOR FREEZE、设置参数:ALTER SYSTEM SET enable_sql_audit = TRUE等),则只需经过头两步解析出语句中的参数之后,便会向总控服务(主)(Master Root Service)发起 RPC 请求,由 Master RS 修改内部表(元数据)、协调集群处理并达成一致。


OceanBase SQL引擎模块  


在语法解析器(Parser)阶段,OceanBase使用lex进行词法分析,使用yacc进行语法分析,将SQL语句生成语法分析树(见下图)。

产品模块原理系列 | OceanBase SQL 引擎的模块介绍和调优实践分享-4

图3: SQL语法分析树


在这里大家可能想到了,OceanBase既然可以同时兼容MySQL和Oracle,那语法解析阶段该如何实现?实际上OceanBase是以租户的方式提供MySQL和Oracle这两种兼容模式的(一个租户等同一个MySQL或Oracle实例,OceanBase可以混布这两种兼容模式的租户),在创建租户时指定兼容模式(不能修改),之后不同的租户走各自对应兼容模式的语法解析流程即可。


语义分析器(Resolver)相比上一阶段要复杂得多。针对不同类型的SQL语句(DML、DDL、DCL)或命令,会有不同的解析(在SQL引擎概述部分已做描述,这里不再赘述)。这一步主要用于生成SQL语句的数据结构,其中主要包含各子句(如 SELECT / FROM / WHERE)及表达式信息。


接着我们看下查询重写器(Rewriter)部分,查询重写的核心思想在于保证执行结果不变的情况下将SQL语句做等价转换,以获得更优的执行效率。因为用户认为的“好”SQL,不一定是内核认为的“好”SQL,我们不能希望所有程序开发人员都在成为SQL优化的专家之后再来写SQL、生成好的执行计划让数据库高效执行,这就是查询重写模块存在的意义。


查询重写本质上是一个模式匹配的过程,先基于规则对SQL语句进行重写(这些规则如:恒真恒假条件简化、视图合并、内连接消除、子查询提升、外连接消除等等),之后进入基于代价的重写判定。相比总能带来性能提升的基于规则的重写,基于代价的重写多了代价评估这一步(需要查询优化器参与):基于访问对象的统计信息以及是否有索引,在进行了重写之后会对重写前后的执行计划进行比较,如果代价降低则接受,代价不减反增则拒绝。在迭代了基于代价的重写之后,如果接受了重写的SQL,内部会再迭代一次基于规则的重写,生成终态的内核认为的“好”SQL并给到查询优化器模块。查询重写的流程见下图:


产品模块原理系列 | OceanBase SQL 引擎的模块介绍和调优实践分享-5

图4: 查询重写流程


查询优化器是数据库管理系统的“大脑”,它会枚举传入语句的执行计划,基于代价模型和统计信息对每一个执行计划算出代价,并最终选取一条代价最低的执行计划。OceanBase的查询优化器基于System-R框架,是一个bottom-up的过程,通过选择基表访问路径、连接算法和连接顺序、最后综合一些其他算子来计算代价,从而生成最终的执行计划。


直到这里我们会发现,似乎与普通的单机RDBMS区别不大。诚然,作为一个已存在40多年的行业,很多理论的工程实践大同小异,但从这里开始,OceanBase作为NewSQL的魅力才正要显现。上面的查询优化部分生成的是串行执行计划,为了充分利用OceanBase的分布式架构和多核计算资源的优势,OceanBase的查询优化器随即会进入并行优化阶段:根据计划树上各个节点的数据分布,对串行执行计划进行自底向上的分析,把串行的逻辑执行计划改造成一个可以并行执行的逻辑计划。并行优化最重要的参考信息是数据的分布信息(Location),即查询所需访问的每个分区的各个副本在集群中的存储位置,这一信息由总控服务(主)(Master Root Service)维护,为了提升访问效率该分布信息还有缓存机制。


代码生成器是SQL编译器的最后一个步骤,其作用是把逻辑执行计划翻译成物理执行计划。查询优化器生成逻辑执行计划是对执行路径的逻辑表示,理论上已经具备可执行能力,但是这种逻辑表达带有过多的语义信息和优化器所需的冗余数据结构,对于执行引擎来说还是相对“偏重”。为了提高计划的执行效率,OceanBase通过代码生成器把逻辑计划树翻译成更适合查询执行引擎运行的树形结构,最终得到一个可重入的物理执行计划。


物理执行计划的生成意味着SQL编译器的工作完成,之后交给执行引擎进行处理。需要注意的是,由于OceanBase是一个分布式数据库,分区副本遍布每一台OBServer上,我们执行的SQL语句要根据是访问本机数据、还是其他机器上的数据抑或是多台服务器上的数据来区别对待,这里就引入了调度器的概念。调度器把执行计划分为本地、远程、分布式三种作业类型,在对外提供统一接口且不侵入SQL执行引擎的同时,根据三种作业类型的特点,充分利用存储层和事务层的特性,实现了各自情况下最合适的调度策略。以下分别介绍下三种作业类型:


产品模块原理系列 | OceanBase SQL 引擎的模块介绍和调优实践分享-6

图5: 三种作业类型示意图


  • 地作业:所有要访问的数据都位于本机的查询,就是一个本地作业。调度器对于这样的执行计划,无需多余的调度动作,直接在当前线程运行执行计划。事务也在本地开启。如果是单语句事务,则事务的开启和提交都在本地执行,也不会发生分布式事务。这样的执行路径和传统单机数据库类似。


  • 远程作业:如果查询只涉及到一个分区组,但是这个分区组的数据位于其他服务器上,这样的执行计划就是远程作业(多半是由于切主等原因导致缓存还未来得及更新数据的分布信息)。调度器把整个执行计划发送到数据所在机器上执行,查询结果流式地返回给调度器,同时流式地返回给客户端。这样的流式转发能够提供较优的响应时间。不仅如此,远程作业对于事务层的意义更大。对于一个远程作业,如果是单语句事务,事务的开启提交等也都在数据所在服务器上执行,这样可以避免事务层的RPC,也不会发生分布式事务。


  • 分布式作业:当查询涉及的数据位于多台不同的服务器时,需要作为分布式作业来处理,这种调度模式同时具有并行计算的能力。对于分布式计划,执行时间相对较长,消耗资源也较多。对于这样的查询,我们希望能够在任务这个小粒度上提供容灾能力。每个任务的执行结果并不会立即发送给下游,而是缓存到本机,由调度器驱动下游的任务去拉取自己的输入。这样,当任务需要重试时,上游的数据是可以直接获取到的。同时,对于分布式的计划,需要在调度器所在服务器上开启事务,事务层需要协调多个分区,必要时会产生分布式事务。

SQL Tunning 实践  

OceanBase的SQL引擎还在不断进化的过程中,在日常使用中难免会遇到语句执行不符合预期的情况。这里我想分两个方面来看:


一方面查询优化器生成语句执行计划主要基于代价模型和统计信息,在这里正确的统计信息至关重要。OceanBase默认是在每天凌晨02:00(配置项:major_freeze_duty_time,可调)自动合并时收集统计信息的,分表和列两个纬度,分别保存在sys租户下的all_meta_table、all_column_statistic两张内部表中的。一天更新一次统计信息的频率并不低,对于绝大多数场景来说完全够用,但如果应用中存在buffer表(即在每日合并完成之后存在大量且重复的插入、删除动作的表),那么访问此类表的SQL语句的执行计划确实会出现不稳定的情况。比如合并时该表记录数为0,白天业务时间段该表记录数很快涨到几千万,而优化器根据的还是合并时的统计信息,结果可想而知。


针对这样的情况,我们举例来说明怎样应对:


CREATE TABLE `test` (
  `id` bigint(20) NOT NULL,
  `name` varchar(128) DEFAULT NULL,
  `status` bigint(2) NOT NULL,
  PRIMARY KEY (`id`),
  KEY idx_test_status (`status`)
);

mysql> select * from test;
+----+------+--------+
| id | name | status |
+----+------+--------+
|  1 | aa   |      1 |
|  2 | bb   |      1 |
|  3 | cc   |      1 |
|  4 | dd   |      1 |
|  5 | ee   |      1 |
|  6 | ff   |      1 |
|  7 | gg   |      0 |
+----+------+--------+
7 rows in set (0.00 sec)

1. 使用 explain 查看预估的执行计划:


EXPLAIN SELECT * FROM test WHERE status = 0\G
Query Plan:
===================================
|ID|OPERATOR  |NAME|EST. ROWS|COST|
-----------------------------------
|0 |TABLE SCAN|test|1        |37  |
===================================

可以看到由于记录数不多,默认是全表扫描。


2. 如果该表是buffer表,白天插入了大量数据,统计信息还未更新,再全表扫描的话多半最终就查询超时了(查询超时系统变量:ob_query_timeout,默认10秒),针对这样的情况我们要用force index或者index HINT来稳定执行计划(这两个效果是一样的:force index是兼容mysql的语法,index HINT是OceanBase中引入的):


  • 使用force index

EXPLAIN SELECT * FROM test FORCE INDEX(idx_test_status) WHERE status = 0\G
Query Plan:
====================================================
|ID|OPERATOR  |NAME                 |EST. ROWS|COST|
----------------------------------------------------
|0 |TABLE SCAN|test(idx_test_status)|1        |87  |
====================================================

  • 使用index HINT


EXPLAIN SELECT /*+ INDEX(test idx_test_status) */ * FROM test WHERE status = 0\G
Query Plan:
====================================================
|ID|OPERATOR  |NAME                 |EST. ROWS|COST|
----------------------------------------------------
|0 |TABLE SCAN|test(idx_test_status)|1        |87  |
====================================================

另外一方面,执行计划选错时的应急应对。线上业务出现抖动时争风夺秒,如果确定了是因为某条SQL语句引起,此时可能没有时间再去细究此时到底是统计信息不正确,还是SQL引擎哪里还有改进,甚至让业务改SQL加HINT都是来不及的。在这种情况下需要有紧急应对手段,让SQL RT尽快回复正常。


针对这样的情况,我们可以绑定outline应对:


1. 定位问题SQL:


root@sys登录集群,查看最近10分钟的TOP10 SQL(【租户ID】替换成真正的租户ID;ORDER BY时间排序的纬度可以按需换成SELECT LIST中的elapsed_time【总的执行时间】、execute_time【真正执行时间】、queue_time【排队时间】):


select /*+READ_CONSISTENCY(WEAK), QUERY_TIMEOUT(100000000), PARALLEL(4)*/ sql_id, substr(query_sql, 1, 50) query_sql, t1.svr_ip, count(*)/600 as QPS,
  AVG(elapsed_time),
  AVG(execute_time),
  AVG(queue_time)
from oceanbase.gv$sql_audit t1, __all_server t2  
where t1.svr_ip = t2.svr_ip and IS_EXECUTOR_RPC = 0    
      and request_time > (time_to_usec(now()) - 600000000)    
      and request_time < time_to_usec(now())
      and tenant_id=【租户ID】
GROUP BY
  sql_id
ORDER BY
  AVG(execute_time) DESC
LIMIT 10;


下图中红框中的sql单次执行超过7s,假使它就是本次定位到的问题SQL:


产品模块原理系列 | OceanBase SQL 引擎的模块介绍和调优实践分享-7

2. root用户登录业务租户,绑定outline:


注:这里的sql_id填入上一步中定位到的问题sql的sql_id,index(【table_name】 【index_name】) 中替换成表名、索引名,如表名有别名必须用别名。

use 【dbname】;     --dbname替换成对应的数据库名,切记一定要进入到数据库中,否则outline无法绑定成功
create outline 【outline_name】 on '【sql_id】' using hint /*+ index(【table_name】 【index_name】) */

3. 验证outline是否创建成功,以及语句的执行是否走对了执行计划:


  • 查看outline是否创建成功:


select * from __all_outline where tenant_id=【tenant_id】\G

  • 从与执行计划相关的性能视图中找到sql_id对应的plan_id:


select * from gv$plan_cache_plan_stat where sql_id='【sql_id】'\G

  • 将上一步获得的plan_id带入以下查询,确认绑定生效,走了正确的执行计划:

select plan_id, operator, name, rows, cost from gv$plan_cache_plan_explain where tenant_id=【tenant_id】 and ip='xxx.xxx.xxx.xxx' and port=2882 and plan_id=【plan_id】;


总结 


本文以较为精短的篇幅为大家介绍了组成OceanBase SQL引擎的各个模块,由于篇幅有限,每一个模块无法讲得很细,旨在让大家对OceanBase SQL引擎有个通篇的感性认识。最后一部分的实践篇内容管中窥豹,写了一些上手SQL tuning的常规思路,但也可借此对“OceanBase有关SQL tunning是如何做的”有一个初步的了解。

相关文章

Oracle如何使用授予和撤销权限的语法和示例
Awesome Project: 探索 MatrixOrigin 云原生分布式数据库
下载丨66页PDF,云和恩墨技术通讯(2024年7月刊)
社区版oceanbase安装
Oracle 导出CSV工具-sqluldr2
ETL数据集成丨快速将MySQL数据迁移至Doris数据库

发布评论