首先假如我们有两个Dataset,一个Dataset中的数据为用户信息,另一个Dataset中的数据是站点访问记录。
case class PageVisit(url: String, ip: String, userId: Long)
case class User(id: Long, name: String, email: String, country: String)
如果想通过这两个Dataset获取来自中国用户的访问记录应该怎么做?很显然,把两个Dataset join一下然后根据country进行过滤即可,join的key选择userId和id。
val visits: DataSet[PageVisit] = ...
val users: DataSet[User] = ...
// filter the users data set
val chinaUsers = users.filter((u) => u.country.equals("ch"))
// join data sets
val chinaVisits: DataSet[(PageVisit, User)] =
// equi-join condition (PageVisit.userId = User.id)
visits.join(chinaUsers).where("userId").equalTo("id")
很美好对不对,不过join背后的实现可就不像用起来这么简单了。
在Flink中,join的实现分为两个阶段,第一个阶段被称为Ship阶段,而第二个阶段被称为Local阶段,这个Ship阶段很像mapreduce中的shuffle,就是将具有相同join key的element shuffle到同一个节点,不然没法在task节点进行本地join。ship阶段有两种不同的策略,一种是根据join key把element重新进行partition。

第二种策略是将一个完整的Dataset shuffle到每个task节点。比如R S两个Dataset,图中示例就是将R shuffle到每个节点。

可以看到ship阶段需要将大量的数据缓存在内存中,数据量如果很大的时候会造成频繁的内存溢出,所以为了应对这种问题,前面也提到过Flink应用一套更高效的内存管理机制,而且Flink中特有的序列化方式能将java 对象大大压缩从而节约内存空间。
Ship阶段完成之后,每个节点都要开始进行本地join,也就是上面所说的Local阶段。
在这个阶段,Flink借用了数据库中常见的两种join方式,一种是Sort-Merge-Join,另一种是Hybrid-Hash-Join。
所谓Sort-Merge-Join其实就是先将两个Dataset中的元素进行排序,然后利用两个游标不断的从Dataset中取具有相同join key的元素出来。而Hybrid-Hash-Join相对来说更复杂一些,先来看简单的Hash-Join,就是将一个较小的Dataset装进哈希表,然后遍历另一个Dataset,根据join key进行配对。但是如果较小的那个Dataset中的数据量也很大根本没法装进内存中呢?这时候就要将Dataset再进行partition,然后在各个partition上进行简单的Hash-Join。Hybrid-Hash-Join在此基础上有个小的优化,就是在内存足够的情况下,将一些数据一直保存在内存中。
因为不同阶段有不同的策略,所以可以构建出多种的join策略,具体选择哪种join策略,在Flink中会根据初始Dataset的数据量对各个join策略进行预估,然后选择预估时间最小的那个策略。
1525

被折叠的 条评论
为什么被折叠?



