流处理之基于 Key 的算子
实验介绍
在 SQL 中我们经常会用到分组(group by)操作,在 group 关键词之后指定要聚合的键,在 group 之前指定要聚合的逻辑(计数、求和、求最大值等),通过分区键将数据集分到不同的区块中,然后在各个区块中使用指定的聚合逻辑做操作。当然,在 Flink 的 API 也支持同样的操作,本节实验的内容就是通过学习基于 Key 的算子来实现 SQL 中的分组操作。
知识点
基于 Key 的算子可以分为三大类:
- KeyBy
- Rolling Aggregation
- sum
- min
- max
- minBy
- maxBy
- reduce
算子演示
在我们 FlinkLearning 工程的 com.vlab.operator
包下创建一个名为 KeyByOperator
的 Scala object,本节实验的示例代码都在此文件中执行。
紧接着我们拟造一下本节实验需要使用的数据。在 /home/vlab/
路径下执行 touch userlog.txt
命令创建一个名为 userlog.txt
的文件,使用 vim 编辑该文件,添加以下内容:
Jack Beijing 100
Bob Chengdu 300
William Chengdu 600
Lily Shanghai 200
Loius Beijing 400
Joker Shanghai 200
我们用以上文本中的每一行表示一条用户访问日志,每行中的三个字段分别表示访问用户名、访问者城市以及访问时长。例如第一行所表示的是 Jack 在北京访问某服务器 100 秒。如果你不嫌麻烦可以多添加一些数据。
KeyBy
KeyBy 算子的作用是将一个 DataStream 转换成 KeyedStream,输⼊必须是 Tuple 类型,逻辑地将⼀个流拆分成不相交的分区, 每个分区包含具有相同 key 的元素,在其内部以 hash 的形式实现的。
但是 keyBy
算子一般不会单独使用,会和我们后面介绍的 Rolling Aggregation
算子和 reduce
算子集合使用。
Rolling Aggregation
Rolling Aggregation 算子又被称作滚动聚合算子,是在经过 keyBy 之后的 KeyedStream 做聚合操作。sum、min、max 分别表示求和、求最小值和最大值。minBy 和 maxBy 稍微有点特殊,我们接下来用代码来解释。
package com.vlab.operator
import org.apache.flink.api.scala._
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment}
object KeyByOperator {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val data: DataStream[String] = env.readTextFile("/home/vlab/userlog.txt")
val userLogStream = data.map(log => {
val arr = log.split(" ")
// 将读取的数据集转为 UserLog
UserLog(arr(0), arr(1), arr(2).toInt)
})
userLogStream
.keyBy("city")
.sum("duration")
.print()
env.execute("KeyByOperator")
}
case class UserLog(name: String, city: String, duration: Int)
}
在上面的代码中,我们创建了一个 UserLog
样例类,然后读取我们前面准备的数据集,使用 map 函数将读取的日志转为 UserLog 对象,并使用 keyBy
算子按照 city
分组,最后对 duration
求和并打印结果。输出如下:
你可能会疑惑,输出的不应该是有两列,city 和 sum(duration)吗?请注意,我们这里的计算是流处理,而不是离线的批处理,我们创建的环境是 StreamExecutionEnvironment
。程序从上往下一行一行读取文本,然后按照 city
字段分组,当执行到第一行的时候,只有它自己,所以输出自己本身。当执行到第二行的时候,city
为 Chengdu
的也只有一行,所以也输出了它自己。当程序执行到第三行的时候,第二个 Chengdu
出现了,所以 sum 的结果是 900(300 + 600)。当程序执行到第四行的时候,Shanghai
第一次出现,所以也只有它自己。当程序执行到第 5 行的时候,第二个 Beijing
出现了,所以输出的是 500(100 + 400)。当程序执行到第 6 行,第二个 Shanghai
出现了,所以输出的是 400(200 + 200)。
min 和 max 的执行原理和 sum 类似,同样是按照从上往下的原则依次在同一个分组寻 找截止目前的最小值或者最大值,如果当前数据是该分组的第一条,则输出自己本身。接下来我们看一下 max 和 maxBy 的区别,其中 min 和 minBy 的区别也是类似。请看代码:
package com.vlab.operator
import org.apache.flink.api.scala._
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment}
object KeyByOperator {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val data: DataStream[String] = env.readTextFile("/home/vlab/userlog.txt")
val userLogStream = data.map(log => {
val arr = log.split(" ")
UserLog(arr(0), arr(1), arr(2).toInt)
})
userLogStream
.keyBy("city")
.max("duration")
.print()
userLogStream
.keyBy("city")
.maxBy("duration")
.print()
env.execute("KeyByOperator")
}
case class UserLog(name: String, city: String, duration: Int)
}
max
输出的结果应该是:
maxBy
输出的结果应该是:
乍眼一看好像并没有什么区别,但是请注意,这里要和原始数据进行对比,从第三行开始就不一致了。当程序执行到第三行的时候 Chengdu
分组中,duration
最大是 600,最大值对应的 name
是 William。maxBy 所对应的 name
正好是 William,而 max 对应的 name
依然是 Bob。同样在第五行第六行也有类似的情况出现。也就是说,max 和 maxBy 算子都会返回被聚合的最大值,但是 max 只会返回最大值,不会更新最大值所对应的其它字段,而 maxBy 会更新最大值对应的其它字段。
reduce
reduce 算子和 Spark 中的 reduceBy 的逻辑是一模一样的。它将 KeyedStream 转换成 DataStream,即⼀个有初始值的分组数据流的滚动折叠操作,合并当前元素和前⼀次折叠操作的结果,并产⽣⼀个新的值,返回的流中包含每⼀次折叠的结果,⽽不是只返回最后⼀次折叠的最终结果。
修改代码为如下:
package com.vlab.operator
import org.apache.flink.api.scala._
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment}
object KeyByOperator {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val data: DataStream[String] = env.readTextFile("/home/vlab/userlog.txt")
val userLogStream = data.map(log => {
val arr = log.split(" ")
UserLog(arr(0), arr(1), arr(2).toInt)
})
userLogStream
.keyBy("city")
.reduce((x, y) => UserLog(y.name, y.city, x.duration + y.duration))
.print()
env.execute("KeyByOperator")
}
case class UserLog(name: String, city: String, duration: Int)
}
在上面的代码中,我们使用 keyBy 算子对 UserLog 按照 city 做了分组,然后使用 reduce 算子对每个分组内的 UserLog 做了聚合。返回的 name
和 city
从后面一条日志中取,duration
是两条日志中的 duration
相加的结果。最终输出如下:
实验总结
在本节实验中我们介绍了 Flink 中基于 Key 的算子,keyBy 和 reduce 好理解,但是 Rolling Aggregation 算子的输出结果可能会和大家预想的不一样,特别是 min(minBy)和 max(maxBy),请一定要注意它们之间的区别。需要自己练习,确定效果。