25

漫谈分布式系统 (20):基于规则的优化

 3 years ago
source link: https://mp.weixin.qq.com/s/eapyuP-oQMvYd79Psuu0bQ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

这是《漫谈分布式系统》系列的第 20 篇,预计会写 30 篇左右。每篇文末有为懒人准备的 TL;DR,还有给勤奋者的关联阅读。扫描文末二维码,关注公众号,听我娓娓道来。也欢迎转发朋友圈分享给更多人。

细节是负担,甩掉有代价

前面几篇文章,我们讲到,MapReduce 和 Spark 在引入 SQL 之后,诞生了 Hive 和 Spark SQL,大大提升了开发性能。

然后又讲到,受 Shuffle 性能优化的启发,对 SQL Join 的性能做了一些优化。

在讲 Join 性能优化的时候,虽然我没明说,但也能隐约感觉到一个不好的趋势。

那就是, SQL 这种声明式的语言,通过隐藏实现细节,减轻了开发成本。但也同样因为隐藏实现细节,失去了主动优化性能的可能

一条 SQL 性能的好坏,很大程度上,已经不受开发者的控制了。

这样也没错,毕竟开发者的水平参差不齐,都去手写 MR 和操作 RDD,肯定有人能写的性能很好,但大部分人恐怕都需要花非常多的时间去积累经验。

所以, 把性能优化的主动权交给 SQL 引擎,再集思广益,把常见的优化经验都统一实现掉,也不失为好的选择

细节是负担,甩掉有代价。但我们可以用经验来弥补这个代价

在 SQL 里,这些经验被总结为规则,这一类性能优化操作在查询优化器(Query Optimizator)里完成,也就是我们今天要讲的 基于规则的优化(Rule Based Optimization) ,简称 RBO

向旧技术取经

一个很现实又略显沮丧的事实是,计算机界尤其是互联网界的很多所谓新东西,都只是旧技术和新场景的组合,再套上个炫酷的名字而已,所谓新瓶装旧酒

虽然要解决很多我们前面文章讲过的问题,但分布式领域的 SQL,本质上也只是传统关系数据库的 SQL 在多机扩展下的实现而已。

但幸运的是,这也意味着,在传统关系数据库库里已经积累了很多年的经验,也可以继续在分布式 SQL 里用到。包括 RBO

nu6Jf2I.png!web

上图是 Oracle 支持一些的 RBO Path。Oracle 会对查询语句按照支持的规则去解析和匹配,然后选择优先级最高的执行,编号越小,优先级越大。

以 Path 1 和 Path 15为例。当我们通过 Rowid 查询一条数据时,由于主键有索引,显然走索引会比全表扫描要快的多,因此会自动选择 Path 1 而不是 Paht 15。

只要把这些久经考验的规则依葫芦画瓢实现一遍,分布式下的 SQL 就有了基础了 RBO 了。

下面看下我们前面重点关注的 Hive 和 Spark SQL 的情况。

既然 RBO 是个非常套路固定的东西,而 Hive 作为 Apache 开源项目的代表之一,就没有选择造轮子,而是使用了著名的 Apache Calcite 作为优化器(包括下一篇要讲的 CBO)。关于 Calcite,我之前写过几篇文章,链接会贴在文末,这里就不重复了。

下面这张图,我截取了部分目前 Calcite 支持的 rules,感兴趣的可以自己去看实现,具体对应的是 Calcite 的 HepPlanner 和 org.apache.calcite.plan.hep 包。

zeUJ3a7.png!web

而对于 Spark,我前面的文章也说过,Spark 是个非常有野心的项目。所以并没有直接用现成的 Calcite,而是造了个叫 Catalyst 的轮子。

在 Spark SQL 的 org.apache.spark.sql.catalyst.optimizer.Optimizer 类中,可以看到也定义了很多规则:

rI7nyin.png!web

有了这些规则,Hive 和 Spark SQL 就能实现非常多的常见优化了,最基本的性能保证就有了,再差的新手写出的 SQl,性能也有底线了。

规则是怎么生效的

从编译原理中我们知道,一个 parser 对一段程序处理后,会输出一个抽象语法树(AST)。SQL 也是一门语言,最终要编译成底层执行引擎能处理的样子。

所以 RBO 的大致过程可以理解为, 通过应用不同规则,对 AST 做等价转换

上篇刚讲过 join,那就一起看一个 join 的例子,搭配一个最常规的规则 -- 谓词下推(Predict Pushdown)。

首先,有这么两张表:

create table user(id int, name string);

create table order(id int, uid int, item_id int);

查询语句是这样:

select

u.id as user_id, u.name as user_name, o.id as order_id

from

user u

join

order o

on

u.id = o.uid

where

u.id > 100;

初始解析完的执行计划,是先扫描两张表,然后 join 匹配数据,最后过滤出 u.id > 100 的数据。

而谓词下推,顾名思义,就是把判断条件下推到存储引擎,提前过滤掉不需要的数据,这样就能大幅提升性能。

画成示意图是这样:

eeEnYrU.png!web

我们用 explain 语句,来看下实际的执行计划。

在 Hive 里是这样,可以看到,两张表都先做了 Filter 操作,然后才 Join。

Mz2IJz6.png!web

下面这个 Spark SQL 的图看的更清楚。

7v6rame.png!web

在 Analyzed Logical Plan 里,是在 Join 之后才 Filter。而经过优化后的 Optimized Logical Plan 里,就变成了分别先对两表做 Filter,然后再 Join。

类似 Predicate Pushdown 这样简单又实用的规则还有很多,常见的比如 Projection Pushdown,会在扫描表的时候就把不相干的列去掉;Filter Combine 会把多个 Filter 合并之后再去过滤,等等,限于篇幅,就不再举更多的例子了。

这样,通过对一系列规则的应用,不断地对 AST 做等价转换,直至找出最优的逻辑计划。然后就可以转换成物理执行计划,交给执行引擎去处理了。

RBO 的局限性

RBO 能发挥非常不错的优化效果,是因为汇集了大量经验,并具象化为一条条可执行的规则

但也正是这些规则,导致了 RBO 的局限性

因为规则是死的:

  • 规则无法穷举,虽然可以逐步完善(人是活的),但总有覆盖不到的点,

  • 规则可能矛盾,各自独立的规则,不会考虑和其他规则的关系,一旦矛盾,就很难取舍,

  • 规则很难确定优先级,先应用哪个后应用哪个,可能产生不一样的优化效果,

  • 规则对不同的数据可能产生差距巨大的效果,

  • ......

举一个例子,我们都知道一个很简单的规则,如果一个列建了索引,当以这个列为查询条件时,应该走索引,而不是全表扫描。

但是,看这样一种情况,有一张公司人员表,其中有个建有索引的字段 gender。现在要查询男性员工的地区分布,但是这个公司是个做苦力的工厂,超过 90% 的员工都是男性。那这个时候先查索引找到所有男性员工,再去查地区分布,性能就远不如直接全表扫描了。

很显然,RBO 并不能解决所有性能优化问题,甚至可能帮倒忙。

究其根本原因, 任何规则 -- 不仅是 SQL 优化里的规则 -- 作为经验的积累,都必然不能解决所有问题。因为经验是人对事物共性的归纳,在归纳的过程中必然存在样本不足、信息损耗和过度拟合的情况。这是经验的局限性,在表象上的体现,就是不灵活或者有时候失灵了

TL;DR

这篇文章,我们粗略了解了基于代价的优化是怎么一回事,简单提炼要点如下:

  • SQL 通过隐藏实现细节,提高了开发效率,但也失去了主动优化性能的可能,

  • 好在有些通用的性能优化手段,可以统一实现,即所谓基于规则的优化(RBO),

  • Oracle 等传统关系数据库已经积累了非常丰富的 RBO 经验,分布式 SQL 只需要照做即可,

  • RBO 本质上就是通过应用规则,对 AST 做等价变换,直至找出最优计划,

  • 但 RBO 是基于经验的,而任何经验都是有缺陷的,如对不同的数据内容会产生悬殊的结果。

所以,我们需要另外的思路,来继续 SQL 性能优化。 另一种不像 RBO 这样间接归纳的思路,另一种更加直接却又灵活的思路

下一篇,我们一起了解下基于代价的优化(CBO)。

原创不易

关注/分享/赞赏

给我坚持的动力

yEfMje3.jpg!web

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK