作者回复: 非常好的建议,这块我是这么想的,后续我会整理一个github repo,把专栏中涉及的代码、数据、结果、以及常见问题等内容汇总到这个项目中去。当然内容不止这些,结合大家的需要,我们还会持续不断地向其中添加诸如笔试面试题、工作机会、职业发展等内容,把potatoes项目打造成我们共有的Spark私塾。看到你的这个问题,我觉得可以多加一类内容,就是常见报错,把这些报错分门别类,报错原因、应对办法,等等。 对于这个github repo,我是打算用开源的思路来做,比如就这个常见报错,我是期望大家群策群力,一起共享工作中遇到的各种报错,然后可以一起来分析背后的root cause和解决办法~ 然后不断地把它丰富、完善~ 等专栏写完,我就会着手做这件事,到时候一起弄哈~
作者回复: 理解得非常的对~ 分区键和Join Keys的选取,确实是个博弈,咱们课件中的例子,为了方便说明DPP的机制和原理,所以没有深究。但是你说的非常对,就是这个Join Keys,确实本身就是个限制。因为就像你说的,平时的数据关联,都是ID类的Join Keys比较多,而这样的数据列,往往是不适合做分区键的。 所以要想充分利用DPP,确实是得在数仓规划的最开始,就把以后要用的常用查询都考虑到,在做分区表设计的时候,尽量做到分区处理本身与查询执行性能之间的平衡。
作者回复: 好问题~ 先说前两个问题,其实这个问题是,Spark是先扫描事实表,还是先做DPP,答案肯定是后者。 说说DPP的意义,这里有个关键,就是Spark“一上来”、在最开始,是怎么知道谁是事实表、维度表的?其实,Spark SQL的静态优化,关于数据统计,比如针对Parquet、CSV等等不同的数据源,它其实不是什么都不做,它至少会统计“表大小”这些最基本的信息,根据这些size类型的信息,它其实可以判断哪个是事实表,哪个是维度表。 如此一来,它先去判断DPP的前提条件是否成立,成立的话,就走一波DPP。 再来说第3题,确实是好问题,不过这里其实本质上是和DPP的实现机制有关,也就是说,它用“一箭双雕”的方式,一方面利用广播来过滤,一方面顺水推舟,用ReuseExchange(内存中的reuse,跟之前的磁盘reuse不同)再次利用广播来完成Broadcast Hash Join,一石二鸟。 再者,DPP实际上是先于AQE Join策略调整的,因为这里还没有Shuffle呢,还记得AQE的触发机制是Shuffle吗?因此DPP相当于截了AQE的“胡儿”(打麻将的截胡儿),不过其实谁先谁后的,没那么重要,反正我们享受到了Broadcast Hash Join的性能提升。 再者,DPP“水到渠成”的BHJ,其实还有一个优势,就是相比AQE,它不需要Shuffle就能触发,因此不需要Shuffle Write阶段的计算,才能做优化,所以说DPP带来的BHJ,相比AQE的Join策略调整,其实是更优的优化~
作者回复: 问题1可以参考下面几个同学的思路~ 强制广播和依靠广播阈值(也就是spark.sql.autoBroadcastJoinThreshold )来广播,其实本质上区别不大。 DPP的核心机制,还是用维表的Key set来过滤事实表数据。这个Key set全集是必需的,Spark的默认实现,是采用了广播变量,这个广播的哈希表还能用于后续的BHJ,一箭双雕,但是也因此而受限于广播阈值和8G的限制。 问题2答得不错,就是用其他的方式获取Key set全集,分布式缓存、存储,其实都行,虽然都会引入数据分发,但去掉了广播的限制~ 在DPP的第二个环节,也就是两表过滤之后的Join,其实采用SMJ或者是Shuffle Hash Join,就可以再次去掉广播阈值的限制。 所以总结下来,要想去掉广播的限制,需要对两个环节进行改进,一个是Key set的全集存储、分发;一个是过滤之后两表的Join。
作者回复: 满分💯,答得好,问得也好~ 思考的很深入 先说第一题,没错,只广播Join Keys,广播阈值的限制会低很多,不过还是需要利用广播机制,来传递“过滤条件”,事实表拿到“过滤条件”来降低扫描量。因此就像你说的,虽然限制降低,但是还是有Join Keys超大撑爆广播阈值、甚至是Driver的风险。所以要想完全去掉广播,那就必须要换一种网络分发方式,举个例子,比如分布式文件系统、分布式缓存,也就是所有Executors都可以通过他们拿到“过滤条件”。当然,你可能会说,这样效率就低很多了,确实,不过其实这道题的目的,就是鼓励大家脑洞、多思考、发散思维,思维越发散,其实对于Spark为什么采用广播机制的理解就越深刻。 再说第二题,很不错的思路~ 相当于是Query rewrite,就是直接把维表Join Key上的过滤条件,在SQL查询优化阶段就传递给事实表,不错不错~ 不过,Spark并没有Query rewrite这个阶段,但是思路我很喜欢~ 其实传统的DBMS都是有Query rewrite这个环节的,Spark偷懒,这部分没做。 最后再说DPP的意义,就是你说的,Spark“一上来”、最开始怎么知道谁是事实表、维度表。其实它还真是知道的,Spark SQL的静态优化,关于数据统计,比如针对Parquet、CSV等等不同的数据源,它其实不是什么都不做,它至少会统计“表大小”这些最基本的信息,根据这些size类型的信息,它其实可以判断哪个是事实表,哪个是维度表。
作者回复: 那倒没有哈~ DPP并没有关联形式(inner、left、right)上的限制
作者回复: 好问题,这块确实是DPP的“硬伤”。 就像你说的,Join Keys往往是cardinality比较高的字段,比如userId;而分区键往往是要选择那些cardinality比较低的字段,否则数据的存储就会非常的分散。 一个cardinality高,一个cardinality低,两者相互矛盾。这块确实比较“蛋疼”,如果查询中的Join Keys多属于userId这种cardinality非常高的字段,坦白说咱们还真没什么好办法。 对于那些cardinality较高的Join Key,我们就需要做取舍,也就是在存储效率和DPP之间做权衡。如果查询效率是第一优先级,那么我们其实还是可以强行对cardinality较高的Join Key做分区键。但如果相反,存储效率最大的concern,那么也就只好放弃取Join Key做分区键,放弃DPP优化机制。
作者回复: 不错的思路~ 不过这里有个前提条件,就是对于维表上的过滤字段,事实表上也要有才行。比如,dim.c1 = "xxx",那么就要求fact上面也要有c1字段,否则仅在逻辑计划阶段,是做不到过滤条件传导的。 这个思路本质上其实是Query Re-writer,就是在逻辑计划阶段把查询进行重写。
作者回复: 好问题,确实,如果是left join,DPP就没有意义了,毕竟左表的数据不管能不能关联上,都需要扫描出来
作者回复: 确实,其实可以借鉴传统RDBMS中经典的优化方法,query rewrite就是其中一个。Spark SQL确实缺少query rewriter这个组件,采用逻辑规则加物理策略的方式,实际上缺少了一定的灵活性~ 不过,query rewrite这块,我理解和数据量本身应该关系不大