文章目录
Flink学习笔记
Flink实操篇
Flink 并行度 & Slot & Task
Flink 中的 TaskManager 是执行任务的节点,Flink 的每个 TaskManager 为集群提供 slot(插槽),这些 slot 可以同时执行多个任务。
slot 个数代表的是每一个 TaskManager 的并发执行能力,每个 task slot 代表了 TaskManager 的一个固定大小的资源子集。
slot的数量通常与每个 TaskManager 节点的可用 CPU 内核数成比例。一般情况下你的 slot 数是你每个节点的 cpu 的核数。
1. 并行度
- 一个 Flink 程序由多个任务组成(source、transformation和 sink)。 一个任务由多个并行的实例(线程)来执行, 一个任务的并行实例 (线程) 数目就被称为该任务的并行度。
2. 并行度的设置
- 一个任务的并行度设置可以从多个级别指定
- Operator Level(算子级别)
- Execution Environment Level(执行环境级别)
- Client Level(客户端级别)
- System Level(系统级别)
- 这些并行度的优先级为
- Operator Level > Execution Environment Level > Client Level > System Level
算子级别
DataStream<Tuple2<String, Integer>> resultStream = streamSource
.flatMap((FlatMapFunction<String, Tuple2<String, Integer>>) (line, collector) -> {
Arrays.stream(line.split(" ")).filter(Objects::nonNull)
.forEach(word -> collector.collect(new Tuple2<>(word, 1)));
}).returns(Types.TUPLE(Types.STRING, Types.INT))
.keyBy((KeySelector<Tuple2<String, Integer>, String>) tuple2 -> tuple2.f0)
.sum(1)
// 设置并行度
.setParallelism(4);
执行环境级别
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment()
// 设置并行度
.setParallelism(2);
客户端级别
- 并行度可以在客户端将 job 提交到 Flink 时设定,对于 CLI 客户端,可以通过 -p参数指定并行度
bin/flink run -p 10 examples/batch/WordCount.jar
系统级别
- 在系统级可以通过设置 flink-conf.yaml 文件中的 parallelism.default 属性来指定所有执行环境的默认并行度
parallelism.default: 1
3. 并行度操作
- 为了方便在本地测试观察任务并行度信息,可以在本地工程添加以下依赖
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-runtime-web_2.11</artifactId>
<version>${flink.version}</version>
</dependency>
- 代码演示
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment}
import org.apache.flink.api.scala._
/**
* 本地调试并行度
*/
object ParallelismTest {
def main(args: Array[String]): Unit = {
// 1. 使用 createLocalEnvironmentWithWebUI 方法,构建本地流处理环境
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI()
// // 设置环境级别 并行度
// env.setParallelism(4)
// 2. 接收socket数据
val sourceStream: DataStream[String] = env
.socketTextStream("node01", 9999) // source task
// 3. 数据处理
sourceStream.flatMap(x => x.split(" ")) // flatMap task
.map(x => (x, 1)) // map task
.keyBy(0)
.sum(1) // sum task
.print() // print task
// 4. 启动
env.execute()
}
}
- 本地启动程序,并访问地址:http://localhost:8081,查看并行度和Task数量
为什么 Task 数量是 25?不设置并行度时,默认 tasks 数量是 CPU 核心线程数
再比如:
DataStream 编程模型
DataStream 编程模型
- DataStream 的编程模型包括四个部分:Environment、DataSource、Transformation、Sink
Flink 的 DataSource 数据源
1. 基于文件
- 使用 readTextFile(path) 读取文本文件,文件遵循 TextInputFormat 读取规则,逐行读取并返回。
2. 基于 Socket
- 使用 socketTextStream 从socker中读取数据,元素可以通过一个分隔符切开。
3. 基于集合
- 使用 fromCollection(Collection) 集合创建一个数据流,集合中的所有元素必须是相同类型的。
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment}
import org.apache.flink.api.scala._
/**
* 基于数组或者集合构建DataStream
*/
object StreamSourceFromCollection {
def main(args: Array[String]): Unit = {
// 1. 获取流处理环境
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
// 2. 准备数据源--数组
val array = Array("hello world","world spark","flink test","spark hive","test")
val fromArray: DataStream[String] = env.fromCollection(array)
// 3. 数据处理
val resultDataStream: DataStream[(String, Int)] = fromArray
.flatMap(x => x.split(" "))
.map(x =>(x,1))
.keyBy(0)
.sum(1)
// 4. 打印
resultDataStream.print()
// 5. 启动
env.execute()
}
}
4. 自定义输入
- 使用 addSource 可以实现读取第三方数据源的数据
案例一:自定义单并行度数据源
- source task 只有一个,通过继承 SourceFunction 来自定义单并行度 Source,代码实现如下:
import org.apache.flink.streaming.api.functions.source.SourceFunction
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment}
import org.apache.flink.api.scala._
object CustomSourceTest {
def main(args: Array[String]): Unit = {
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
val getSource: DataStream[Long] = env.addSource(new SingleParallelSource)
val resultStream: DataStream[Long] = getSource.filter(x => x % 2 == 0)
resultStream.setParallelism(1).print()
env.execute()
}
}
/**
* 自定义单并行度source
*/
class SingleParallelSource extends SourceFunction[Long] {
private var number = 1L
private var isRunning = true
override def run(sourceContext: SourceFunction.SourceContext[Long]): Unit = {
while (isRunning) {
number += 1
sourceContext.collect(number)
Thread.sleep(1000)
}
}
override def cancel(): Unit = {
isRunning = false
}
}
// 结果输出
11> 2
12> 4
1> 6
2> 8
3> 10
4> 12
案例二:自定义多并行度数据源
- source task 可以有多个,通过继承 ParallelSourceFunction 来自定义多并行度的 Source,代码实现如下:
- 此外系统内置提供了一批connectors,连接器会提供对应的source支持
- Apache Kafka (source/sink)
- Apache Cassandra (sink)
- Amazon Kinesis Streams (source/sink)
- Elasticsearch (sink)
- FileSystem (Hadoop included) - Streaming only (sink)
- FileSystem (Hadoop included) - Streaming and Batch (sink)
- RabbitMQ (source/sink)
- Apache ActiveMQ (source/sink)
- Apache NiFi (source/sink)
- Twitter Streaming API (source)
- Google PubSub (source/sink)
- JDBC(source/sink)
Flink 的 Sink 数据目标
- writeAsText():将元素以字符串形式逐行写入,这些字符串通过调用每个元素的 toString() 方法来获取。
- print()/printToErr:打印每个元素的 toString() 方法的值到标准输出或者标准错误输出流中。
- 使用 addSink() 添加自定义输出,我们可以通过 sink 算子,将我们的数据发送到指定的地方去,例如 kafka、redis、hbase 等,下面我们来实现自定义 sink 将数据发送到 redis 里面。
1. Flink 写数据到 redis 中
- 导入 flink 整合 redis 的 jar 包
<dependency>
<groupId>org.apache.bahir</groupId>
<artifactId>flink-connector-redis_2.11</artifactId>
<version>1.0</version>
</dependency>
- 代码开发
import org.apache.flink.api.scala._
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment}
import org.apache.flink.streaming.connectors.redis.RedisSink
import org.apache.flink.streaming.connectors.redis.common.config.FlinkJedisPoolConfig
import org.apache.flink.streaming.connectors.redis.common.mapper.{RedisCommand, RedisCommandDescription, RedisMapper}
/**
* Flink实时程序处理保存结果到redis中
*/
object Stream2Redis {
def main(args: Array[String]): Unit = {
// 1. 构建流处理环境
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
// 2. 数组数据
val streamSource: DataStream[String] = env.fromElements("1 hadoop", "2 spark", "3 flink")
// 3. 数据处理
val tupleValue: DataStream[(String, String)] = streamSource.map(x => (x.split(" ")(0), x.split(" ")(1)))
// 4. 构建RedisSink
val builder = new FlinkJedisPoolConfig.Builder
// 设置 redis 客户端参数
builder.setHost("node01")
builder.setPort(6379)
builder.setTimeout(5000)
builder.setMaxTotal(50)
builder.setMaxIdle(10)
builder.setMinIdle(5)
val config: FlinkJedisPoolConfig = builder.build()
// 获取redis sink
val redisSink = new RedisSink[Tuple2[String, String]](config, new MyRedisMapper)
// 5. 使用自定义Sink,实现数据写入到 redis 中
tupleValue.addSink(redisSink)
// 6. 执行程序
env.execute("redisSink")
}
}
/**
* 定义一个RedisMapper类
*/
class MyRedisMapper extends RedisMapper[Tuple2[String, String]] {
override def getCommandDescription: RedisCommandDescription = {
// 设置插入数据到 redis 的命令
new RedisCommandDescription(RedisCommand.SET)
}
override def getKeyFromData(data: (String, String)): String = {
// 指定 key
data._1
}
override def getValueFromData(data: (String, String)): String = {
// 指定 value
data._2
}
}
DataStream 转换算子
- 通过从一个或多个 DataStream 生成新的 DataStream 的过程被称为 Transformation 操作。在转换过程中,每种操作类型被定义为不同的 Operator,Flink 程序能够将多个 Transformation 组成一个 DataFlow 的拓扑。
- DataStream 官网转换算子操作:https://ci.apache.org/projects/flink/flink-docs-release-1.13/docs/dev/datastream/operators/overview/
1. map、filter
/**
* 测试 map、filter 算子
*/
object MapFilterTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val sourceStream = env.fromElements(1, 2, 3, 4, 5, 6)
val mapStream = sourceStream.map(x => x * 10)
val resultStream = mapStream.filter(x => x % 2 == 0)
resultStream.print()
env.execute()
}
}
2. flatMap、keyBy、sum
/**
* 测试 flatMap、keyBy、sum 算子
*/
object FlatMapSumTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val sourceStream = env.socketTextStream("node01", 9999)
val resultStream = sourceStream
.flatMap(x => x.split(" "))
.map(x => (x, 1))
.keyBy(0)
.sum(1)
resultStream.print()
env.execute()
}
}
3. reduce
- 将输入的 KeyedStream 流通过传入的用户自定义的 ReduceFunction 滚动地进行数据聚合处理
/**
* 测试 reduce 算子
*/
object ReduceTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val sourceStream = env.fromElements(("a", 1), ("a", 2), ("b", 2), ("b", 3), ("c", 2))
val keyByStream = sourceStream.keyBy(0)
// 聚合
val resultStream = keyByStream.reduce((t1, t2) => (t1._1, t1._2 + t2._2))
resultStream.print()
env.execute()
}
}
// 输出结果
3> (b,2)
8> (a,1)
6> (c,2)
8> (a,3)
3> (b,5)
4. union
- 把两个流的数据进行合并,两个流的数据类型必须保持一致
/**
* 测试 union 算子
*/
object UnionTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val firstStream = env.fromCollection(Array("hello spark", "hello flink"))
val secondStream = env.fromCollection(Array("hadoop spark", "hive flink"))
// 合并两个流
val resultStream = firstStream.union(secondStream)
resultStream.print()
env.execute()
}
}
// 输出结果
8> hello spark
9> hello flink
4> hadoop spark
5> hive flink
5. connect
- 和 union 类似,但是只能连接两个流,两个流的数据类型可以不同
/**
* 测试 connect
*/
object ConnectTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val firstStream = env.fromCollection(Array("hello world", "spark flink"))
val secondStream = env.fromCollection(Array(1, 2, 3, 4))
// 连接两个流
val connectStream = firstStream.connect(secondStream)
val resultStream = connectStream.flatMap(new CoFlatMapFunction[String, Int, String] {
override def flatMap1(in: String, out: Collector[String]): Unit = {
out.collect(in.toUpperCase())
}
override def flatMap2(in: Int, out: Collector[String]): Unit = {
out.collect(in * 2 + "")
}
})
resultStream.print()
env.execute()
}
}
// 输出结果
8> 8
7> 6
6> 4
5> 2
12> SPARK FLINK
11> HELLO WORLD
6. 重分区算子
- 重算子允许我们对数据进行重新分区,或者解决数据倾斜等问题。
- Random Partitioning:随机分区
- 根据随机的分配元素给下游 task(类似于 random.nextInt(5),0-5 在概率上随机)
- dataStream.shuffle()
- Rebalancing:均匀分区
- 分区元素循环,每个分区创建相等的负载。数据发生倾斜的时候可以用于性能优化。
- 对数据集进行再平衡,重分区,消除数据倾斜
- dataStream.rebalance
- Rescaling:
- 跟rebalance有点类似,但不是全局的,这种方式仅发生在一个单一的节点,因此没有跨网络的数据传输。
- dataStream.rescale()
- Custom partitioning:自定义分区
- 需要实现 Partitioner 接口
- dataStream.partitionCustom(partitioner, “someKey”)
- 或者dataStream.partitionCustom(partitioner, 0);
- Broadcasting:广播变量
/**
* 测试重新分区:对filter之后的数据进行重新分区
*/
object PartitionTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val dataStream = env.fromCollection(1 to 100)
val filterStream = dataStream.filter(x => x > 10)
// .shuffle // 随机的重新分发数据:上游的数据,随机的发送到下游的分区里面去
.rebalance // 对数据重新进行分区,涉及到shuffle的过程
// .rescale // 跟rebalance有点类似,但不是全局的,这种方式仅发生在一个单一的节点,因此没有跨网络的数据传输。
// 带有Rich的类,表示富函数类,它的功能比较强大,在内部是可以获取state、分布式缓存、广播变量、运行时的上下文对象等
val resultStream = filterStream.map(new RichMapFunction[Int, (Int, Int)] {
override def map(value: Int): (Int, Int) = {
// 获取任务id,以及value
(getRuntimeContext.getIndexOfThisSubtask, value)
}
})
resultStream.print()
env.execute()
}
}
- 自定义分区策略:实现不同分区的数据发送到不同分区里面去进行处理,将包含hello的字符串发送到一个分区里面去,其他的发送到另外一个分区里面去
object CustomPartitionTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val sourceStream = env.fromElements("hello hadoop", "spark flink", "hello flink", "hive hadoop")
val rePartition = sourceStream.partitionCustom(new MyPartitioner, x => x + "")
rePartition.map(x => {
println("数据的key为" + x + ", 线程为" + Thread.currentThread().getId)
x
})
rePartition.print()
env.execute()
}
}
/**
* 定义分区类
*/
class MyPartitioner extends Partitioner[String] {
override def partition(line: String, num: Int): Int = {
println("分区数目:" + num)
if (line.contains("hello")) 0
else 1
}
}
// 输出结果
分区数目:12
分区数目:12
分区数目:12
分区数目:12
分区数目:12
分区数目:12
分区数目:12
分区数目:12
2> spark flink
数据的key为spark flink, 线程为74
数据的key为hive hadoop, 线程为74
2> hive hadoop
1> hello hadoop
数据的key为hello hadoop, 线程为73
1> hello flink
数据的key为hello flink, 线程为73
DataSet 转换算子
- DataSet官网转换算子操作:https://ci.apache.org/projects/flink/flink-docs-release-1.13/docs/dev/dataset/overview/#dataset-transformations
- map:输入一个元素,返回一个元素,中间可以做一些清洗转换等操作。
- flatMap:输入一个元素,可以返回零个、一个或多个元素。
- mapPartition:类似 map,一次处理一个分区的数据,如果在进行 map 处理的时候需要获取第三方资源链接,建议使用mapPartition。
- filter:过滤函数,对传入的数据进行判断,符合条件的数据会被留下。
- reduce:对数据进行聚合操作,结合当前元素和上一次 reduce 返回的值进行聚合操作,然后返回一个新的值。
- aggregate:sum、max、min等。
- distinct:返回一个数据集中去重之后的元素,data.distinct()。
- join:内连接。
- outerJoin:外连接。
- cross:获取两个数据集的笛卡尔积。
- union:返回两个数据集的总和,数据类型需要一致。
- first-N:获取集合中的前N个元素。
- sortPartition:在本地对数据集的所有分区进行排序,通过 sortPartition() 的链接调用来完成对多个字段的排序。
1. mapPartition
/**
* 测试 mapPartition
*/
object MapPartitionTest {
def main(args: Array[String]): Unit = {
val env = ExecutionEnvironment.getExecutionEnvironment
val buffer = ArrayBuffer[String]()
buffer.+=("Hello World1")
buffer.+=("Hello World2")
buffer.+=("Hello World3")
buffer.+=("Hello World4")
val dataSet = env.fromCollection(buffer)
val result = dataSet.mapPartition(p => p.map(l => l + "!!!"))
result.print()
}
}
2. distinct
/**
* 测试 distinct
*/
object DistinctTest {
def main(args: Array[String]): Unit = {
val env = ExecutionEnvironment.getExecutionEnvironment
val buffer = ArrayBuffer[String]()
buffer.+=("Hello World")
buffer.+=("Hello World")
buffer.+=("Hello World")
buffer.+=("Hello World")
val dataSet = env.fromCollection(buffer)
val result = dataSet.flatMap(x => x.split(" ")).distinct()
result.print()
}
}
3. join
/**
* 测试 join
*/
object JoinTest {
def main(args: Array[String]): Unit = {
val env = ExecutionEnvironment.getExecutionEnvironment
val buffer1 = ArrayBuffer((1, "zhangsan"), (2, "lisi"), (3, "wangwu"))
val buffer2 = ArrayBuffer((1, 23), (2, 14), (3, 35))
val dataSet1 = env.fromCollection(buffer1)
val dataSet2 = env.fromCollection(buffer2)
val result = dataSet1.join(dataSet2).where(0)
.equalTo(0).map(x => (x._1._1, x._1._2, x._2._2))
result.print()
}
}
// 输出结果
(3,wangwu,35)
(1,zhangsan,23)
(2,lisi,14)
4. leftOutJoin、rightOuterJoin
/**
* 测试 leftOuterJoin、rightOuterJoin
*/
object OuterJoinTest {
Logger.getLogger("org").setLevel(Level.ERROR)
def main(args: Array[String]): Unit = {
val env = ExecutionEnvironment.getExecutionEnvironment
val buffer1 = ArrayBuffer((1, "zhangsan"), (2, "lisi"), (3, "wangwu"), (4, "Tom"))
val buffer2 = ArrayBuffer((1, 23), (2, 14), (3, 35), (5, 50))
val dataSet1 = env.fromCollection(buffer1)
val dataSet2 = env.fromCollection(buffer2)
// 左外连接
val leftOuterJoin = dataSet1.leftOuterJoin(dataSet2).where(0).equalTo(0)
val leftResult = leftOuterJoin.apply(new JoinFunction[(Int, String), (Int, Int), (Int, String, Int)] {
override def join(left: (Int, String), right: (Int, Int)): (Int, String, Int) = {
val result = if (right == null) {
Tuple3[Int, String, Int](left._1, left._2, -1)
} else {
Tuple3[Int, String, Int](left._1, left._2, right._2)
}
result
}
})
leftResult.print()
// 右外连接
val rightOuterJoin = dataSet1.rightOuterJoin(dataSet2).where(0).equalTo(0)
val rightResult = rightOuterJoin.apply(new JoinFunction[(Int, String), (Int, Int), (Int, Int, String)] {
override def join(left: (Int, String), right: (Int, Int)): (Int, Int, String) = {
val result = if (left == null) {
Tuple3[Int, Int, String](right._1, right._2, "null")
} else {
Tuple3[Int, Int, String](right._1, right._2, left._2)
}
result
}
})
rightResult.print()
}
}
5. cross
/**
* 测试 cross
*/
object CrossTest {
def main(args: Array[String]): Unit = {
val env = ExecutionEnvironment.getExecutionEnvironment
val buffer1 = ArrayBuffer((1, "zhangsan"), (2, "lisi"), (3, "wangwu"), (4, "Tom"))
val buffer2 = ArrayBuffer((1, 23), (2, 14), (3, 35), (5, 50))
val dataSet1 = env.fromCollection(buffer1)
val dataSet2 = env.fromCollection(buffer2)
// cross笛卡尔积
val result = dataSet1.cross(dataSet2)
result.print()
}
}
6. first-n和sortPartition
/**
* 测试 first、sortPartition
*/
object FirstAndSortPartitionTest {
def main(args: Array[String]): Unit = {
val env = ExecutionEnvironment.getExecutionEnvironment
val buffer1 = ArrayBuffer((1, "zhangsan", 13), (2, "lisi", 24), (3, "wangwu", 5), (4, "Tom", 66))
val dataSet = env.fromCollection(buffer1)
// 获取前三个元素
dataSet.first(3).print()
println("------------------")
dataSet.groupBy(0) // 按照第一个字段进行分组
.sortGroup(2, Order.DESCENDING) // 按照第三个字段进行排序
.first(1) // 获取每组的前一个元素
.print()
println("------------------")
dataSet.sortPartition(0, Order.DESCENDING).sortPartition(2, Order.ASCENDING).print()
}
}
// 输出结果
(1,zhangsan,13)
(2,lisi,24)
(3,wangwu,5)
------------------
(3,wangwu,5)
(1,zhangsan,13)
(4,Tom,66)
(2,lisi,24)
------------------
(4,Tom,66)
(3,wangwu,5)
(2,lisi,24)
(1,zhangsan,13)
- github 代码地址:https://github.com/shouwangyw/bigdata/tree/master/flink-demo