1. Flink流处理API
1.1 Environment
1.1.1 getExecutionEnvironment
创建一个执行环境,表示当前执行程序的上下文。如果程序是独立调用的,则此方法返回本地执行环境;如果从命令行客户端调用程序以提交到集群,则此方法返回此集群的执行环境,也就是说,getExecutionEnvironment会根据查询运行的方式决定返回什么样的运行环境,是最常用的一种创建执行环境的方式。
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
如果没有设置并行度,会以flink-conf.yaml中的配置为准,默认是1。
parallelism.default: 1
1.1.2 createLocalEnvironment
返回本地执行环境,需要在调用时指定默认的并行度。
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.createLocalEnvironment(1)
1.1.3 createRemoteEnvironment
返回集群执行环境,将Jar提交到远程服务器。需要在调用时指定JobManager
的IP和端口号,并指定要在集群中运行的Jar包。
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.createRemoteEnvironment("jobManagerHost",6123,"jarPath")
1.2 Source
1.2.1 从集合读取数据
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.streaming.api.scala._
case class SensorReading(id:String, timestamp: Long, temperature: Double)
object Sensor {
def main(args: Array[String]): Unit = {
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
val strem: DataStream[SensorReading] = env.fromCollection(List(
SensorReading("sensor_1", 1547718199, 35.80018327300259),
SensorReading("sensor_6", 1547718201, 15.402984393403084),
SensorReading("sensor_7", 1547718202, 6.720945201171228),
SensorReading("sensor_10", 1547718205, 38.101067604893444)
))
strem.print("从集合中读取数据")
env.execute("Sensor")
}
}
1.2.2 从文件中读取数据
env.readTextFile("/data.txt")
1.2.3 以kafka消息队列的数据作为来源
需要添加 kafka 依赖
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka-0.11_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
</dependency>
具体实现:
kafka消费者配置
def getPro():Properties = {
val props = new Properties
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, broker_list)
props.put(ConsumerConfig.GROUP_ID_CONFIG, "flink_consumer_group")
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer")
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer")
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"true")
//如果单条消息超过1 MB,建议设置为1。
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, "1000")
//设置比单条消息的大小略大一点。
props.put(ConsumerConfig.FETCH_MAX_BYTES_CONFIG, "1024")
props.put(ConsumerConfig.MAX_PARTITION_FETCH_BYTES_CONFIG, "1024")
props
}
// 初始化环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 获取kafka配置
val properties = getPro()
// 创建kafka消费者
val consumer = new FlinkKafkaConsumer011(topic,new SimpleStringSchema,properties)
// 指定kafka消费位置
consumer.setStartFromEarliest()
// 添加数据源
val dataStream: DataStream[String] = env.addSource(consumer)
dataStream.print()
env.execute("flink_source_kafka")
1.2.4 自定义 Source
除了以上的source数据来源,我们还可以自定义source。需要做的,只是传入一个SourceFunction就可以。具体调用如下:
val stream = env.addSource(new MySensorSource() )
package com.flink.scala
import org.apache.flink.streaming.api.functions.source.SourceFunction
import scala.collection.immutable
import scala.util.Random
class MySensorSource extends SourceFunction[SensorReading]{
var running: Boolean = true
override def run(ctx: SourceFunction.SourceContext[SensorReading]): Unit = {
// 初始化一个随机数发生器
val rand = new Random()
var curTemp: immutable.IndexedSeq[(String, Double)] = 1.to(10).map(
i => ("sensor_" + i, 65 + rand.nextGaussian() * 20)
)
while (running) {
// 更新温度值
curTemp = curTemp.map(
t => (t._1,t._2 + rand.nextGaussian())
)
//获取当前时间戳
val curTime = System.currentTimeMillis()
curTemp.foreach(
t => ctx.collect(SensorReading(t._1, curTime, t._2))
)
Thread.sleep(100)
}
}
override def cancel(): Unit = {
running = false
}
}
1.3 Transform 转换算子
1.3.1 map
val streamMap = steam.map(x => (x,1))
1.3.2 flatMap
flatMap的函数签名:def flatMap[A,B](as: List[A])(f: A⇒List[B]): List[B]
例如:flatMap(List(1,2,3))(i⇒List(i,i))
结果是List(1,1,2,2,3,3),
而List(“a b”, “c d”).flatMap(line⇒line.split(" "))
结果是List(a, b, c, d)。
val streamFlatMap = stream.flatMap (
x => x.split(",")
)
1.3.3 Filter
val streamFilter = stream.filter(
x => x == 1
)
1.3.4 keyBy
DataStream→KeyedStream:逻辑地将一个流拆分成不相交的分区,每个分
区包含具有相同key的元素,在内部以hash的形式实现的。
1.3.5 滚动聚合算子 (Rolling Aggregation)
这些算子可以针对KeyedStream的每一个支流做聚合。
- sum()
- min()
- max()
- minBy()
- maxBy()
注意:max 和 maxBy之间的区别在于 max 返回流中最大的值,但 maxBy 返回具有最大值的元素,min 和 minBy 同理。
1.3.6 Reduce
KeyedStream→DataStream:一个分组数据流的聚合操作,合并当前的元素和上次聚合的结果,产生一个新的值,返回的流中包含每一次聚合的结果,而不是只返回最后一次聚合的最终结果。
val stream = env.readTextFile("/data.txt").map( data => {
valdataArray = data.split(",")
SensorReading(dataArray(0).trim, dataArray(1).trim.toLong,
dataArray(2).trim.toDouble)
})
.keyBy("id")
.reduce( (x, y) =>SensorReading(x.id, x.timestamp +1, y.temperature) )
1.3.7 split 和 select
1. split
DataStream→SplitStream:根据某些特征把一个DataStream拆分成两个或者多个DataStream。
2. select
SplitStream→DataStream:从一个SplitStream中获取一个或者多个DataStream。需求:传感器数据按照温度高低(以30度为界),拆分成两个流。
val splitStream = stream.split( sensorData => {
if(sensorData.temperature >30)Seq("high")elseSeq("low")
})
val high = splitStream.select("high")
val low = splitStream.select("low")
val all = splitStream.select("high","low")
1.3.8 connect 和 coMap
图Connect算子
**DataStream,DataStream→ConnectedStreams**:连接两个保持他们类型的数据 流,两个数据流被Connect之后,只是被放在了一个同一个流中,内部依然保持各自的数据和形式不发生任何变化,两个流相互独立。
图CoMap/CoFlatMap
**ConnectedStreams → DataStream**:作用于ConnectedStreams上,功能与map 和flatMap一样,对ConnectedStreams中的每一个Stream分别进行map和flatMap 处理。
val warning = high.map(
sensorData => (sensorData.id,sensorData.temperature)
)
val connected = warning.connect(low)
val coMap = connected.map(
warningData => (warningData._1, warningData._2,"warning"),
lowData => (lowData.id,"healthy")
)
1.3.9 union
图Union
**DataStream→DataStream**:对两个或者两个以上的DataStream进行union操作,产生一个包含所有DataStream元素的新DataStream。
// 合并以后打印
val unionStream: DataStream[StartUpLog] = appStoreStream.union(otherStream)unionStream.print("union:::")
connect 与 union 的区别:
- union之前两个流的类型必须是一样,connect可以不一样,在之后的coMap
中再去调整成为一样的。 - connect只能操作两个流,union可以操作多个。
1.4 支持的数据类型
Flink流应用程序处理的是以数据对象表示的事件流。所以在Flink内部,我们需要能够处理这些对象。它们需要被序列化和反序列化,以便通过网络传送它们;或者从状态后端、检查点和保存点读取它们。为了有效地做到这一点,Flink需要明确知道应用程序所处理的数据类型。Flink使用类型信息的概念来表示数据类型,并为每个数据类型生成特定的序列化器、反序列化器和比较器。
Flink还具有一个类型提取系统,该系统分析函数的输入和返回类型,以自动获取类型信息,从而获得序列化器和反序列化器。但是,在某些情况下,例如lambda
函数或泛型类型,需要显式地提供类型信息,才能使应用程序正常工作或提高其性能。
Flink支持Java和Scala中所有常见数据类型。使用最广泛的类型有以下几种。
1.4.1 基础数据类型
Flink支持所有的Java和Scala基础数据类型,Int, Double, Long, String, …
val numbers:DataStream[Long]=env.fromElements(1L,2L,3L,4L)
numbers.map(n=>n+1)
1.4.2 Java 和 Scala 元组 (Tuples)
val persons:DataStream[(String,Integer)]=env.fromElements(
("Adam",17),
("Sarah",23)
)
persons.filter(p => p._2 > 18)
1.4.3 Scala 样例类 (case classes)
case class Person(name:String,age:Int)
valpersons:DataStream[Person]=env.fromElements(
Person("Adam",17),
Person("Sarah",23)
)
persons.filter(p => p.age > 18)
1.4.4 Java 简单对象
public class Person {
public String name;
public int age;
public Person () {}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
1.4.5 其他 (Arrays, List, Map, Enum 等等)
Flink对Java和Scala中的一些特殊目的的类型也都是支持的,比如Java的 ArrayList,HashMap,Enum等等。
1.5 实现 UDF 函数 —— 更细粒度的控制流
1.5.1 函数类 (Function Classes)
Flink暴露了所有 UDF 函数的接口(实现方式为接口或者抽象类)。例如 MapFunction, FilterFunction, ProcessFunction等等。
下面例子实现了FilterFunction接口:
class FilterFilter extends FilterFunction[String]{
overridedeffilter(value:String):Boolean={
value.contains("flink")
}
}
val filterStream=stream.filter(new FlinkFilter)
还可以将函数实现成匿名类
val filterStream = stream.filter(
new RichFilterFuntion[String] {
override def filter(value:String):Boolean={
value.contains("flink")
}
}
)
我们 filter 的字符串 “flink” 还可以当做参数传进去。
val filterStream = stream.filter(new KeywordFilter("flink"))
class KeywordFilter(keyWord:String) extends FilterFunction[String]{
override def filter(value:String):Boolean={
value.contains(keyWord)
}
}
1.5.2 匿名函数 (Lambda Funtions)
val filterStream = stream.filter(_.contains("flink"))
1.5.3 富函数 (Rich Functions)
富函数”是DataStream API提供的一个函数类的接口,所有Flink函数类都有其Rich版本。它与常规函数的不同在于,可以获取运行环境的上下文,并拥有一些生命周期方法,所以可以实现更复杂的功能。
- RichMapFunction
- RichFlatMapFunction
- RichFilterFunction
- …
Rich Function有一个生命周期的概念。典型的生命周期方法有:
-
open()
方法是 Rich Function 的初始化方法,当一个算子例如map或者filter被调用之前open()会被调用。 -
close()
方法是生命周期中的最后一个调用的方法,做一些清理工作。 -
getRuntimeContext()
方法提供了函数的RuntimeContext
的一些信息,例如函数执行的并行度,任务的名字,以及state状态。
package com.flink.scala
import org.apache.flink.api.common.functions.RichFlatMapFunction
import org.apache.flink.configuration.Configuration
import org.apache.flink.util.Collector
class MyFlatMap extends RichFlatMapFunction[Int ,(Int,Int)] {
var subTaskIndex = 0;
override def open(config: Configuration): Unit = {
subTaskIndex = getRuntimeContext.getIndexOfThisSubtask
// todo 这里可以做一些初始化工作,例如建立一个 HDFS 的连接
}
override def flatMap(in: Int, collector: Collector[(Int, Int)]): Unit = {
if (in % 2 == subTaskIndex) {
collector.collect((subTaskIndex,in))
}
}
override def close(): Unit = {
// todo 这里可以做一些清理工作,比如关闭 HDFS 的连接
}
}
1.6 Sink
Flink 没有类似于 spark 中 foreach 方法,让用户进行迭代的操作。虽有对外的输出操作都要利用 Sink完成。最后通过类似如下方式完成整个任务最终输出操作。
stream.addSink(new MySink())
官方提供了一部分的框架的sink。除此以外,需要用户自定义实现sink。
1.6.1 Sink Kafka
pom 添加依赖:
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka-0.11_2.11</artifactId>
<version>1.10.0</version>
</dependency>
主函数添加 sink:
stream.addSink(new FlinkKafkaProducer011[String]("localhost:9092","sinkTopic",new SimpleStringSchema()))
或
def getProp: Properties = {
val props = new Properties
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092")
props.put(ProducerConfig.CLIENT_ID_CONFIG, "car_kafka")
props.put(ProducerConfig.ACKS_CONFIG, "-1")
props.put(ProducerConfig.RETRIES_CONFIG, "3")
// 发到每个分区的消息缓存量,提高吞吐量
props.put(ProducerConfig.BATCH_SIZE_CONFIG, 4096)
// 每条消息待在缓存中的最长时间,控制延迟
props.put(ProducerConfig.LINGER_MS_CONFIG, 10000)
// 所有缓存消息的总体大小超过这个数值后,就会触发把消息发往服务器
// 此时会忽略batch.size和linger.ms的限制。
// 默认数值是32 MB
props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 40960)
props
}
val producerConfig = getProp
stream.addSink(new FlinkKafkaProducer011[String]("sinkTopic",new SimpleStringSchema(),producerConfig))
1.6.2 Sink Redis
pom添加依赖:
<dependency>
<groupId>org.apache.bahir</groupId>
<artifactId>flink-connector-redis_2.11</artifactId>
<version>1.0</version>
</dependency>
定义一个 redis 的 Mapper 类,用于定义保存到 redis 时调用的命令:
class MyRedisMapper() extends RedisMapper[SensorReading] {
/**
* 定义保存到数据的 redis 命令
* @return
*/
override def getCommandDescription: RedisCommandDescription = {
new RedisCommandDescription(RedisCommand.HSET,"aaaa")
}
/**
* 定义保存到redis的 key
* @param t
* @return
*/
override def getKeyFromData(t: SensorReading): String = t.id
/**
* 定义保存到redis 的 value
* @param t
* @return
*/
override def getValueFromData(t: SensorReading): String = t.temperature.toString
}
主函数中调用:
val conf = new FlinkJedisPoolConfig.Builder()
.setHost("localhost")
.setPort(6379)
.setPassword("123456")
.build()
stream.addSink(new RedisSink[SensorReading](conf,new MyRedisMapper))
1.6.4 Sink Elasticsearch
pom添加依赖:
<!-- flink-connector-elasticsearch6 -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-elasticsearch6_2.11</artifactId>
<version>1.10.0</version>
</dependency>
在主函数中调用:
package com.flink.scala
import java.util
import org.apache.flink.api.common.functions.RuntimeContext
import org.apache.flink.api.scala._
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.streaming.connectors.elasticsearch.{ElasticsearchSinkFunction, RequestIndexer}
import org.apache.flink.streaming.connectors.elasticsearch6.ElasticsearchSink
import org.apache.http.HttpHost
import org.elasticsearch.client.Requests
object ElasticSearchSinkTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(10)
val stream = env.fromCollection(List(
new SensorReading("sensor_1", 1547718199, 35.80018327300259),
new SensorReading("sensor_6", 1547718201, 15.402984393403084),
new SensorReading("sensor_7", 1547718202, 6.720945201171228),
new SensorReading("sensor_10", 1547718205, 38.101067604893444)
))
val httpHosts = new util.ArrayList[HttpHost]()
httpHosts.add(new HttpHost("localhost",9200))
val builder = new ElasticsearchSink.Builder[SensorReading](httpHosts, new ElasticsearchSinkFunction[SensorReading]() {
override def process(t: SensorReading, runtimeContext: RuntimeContext, requestIndexer: RequestIndexer): Unit = {
println("saving data: " + t)
val json = new util.HashMap[String, String]()
json.put("data", t.toString)
val indexRequest =
Requests.indexRequest().index("sensor").`type`("info").source(json)
requestIndexer.add(indexRequest)
println("saved successfully")
}
}).build()
stream.addSink(builder)
env.execute("es sink")
}
}
1.6.4 JDBC 自定义 sink
pom 添加依赖:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
自定义mysql sink:
package com.flink.scala
import java.sql.{Connection, DriverManager, PreparedStatement}
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.functions.sink.{RichSinkFunction, SinkFunction}
class MysqlSink extends RichSinkFunction[SensorReading] {
var conn : Connection = _
var insertStmt : PreparedStatement = _
var updateStmt: PreparedStatement = _
override def open(parameters: Configuration): Unit = {
super.open(parameters)
val url = "jdbc:mysql://localhost:3306/db"
val username = "root"
val password = "123456"
conn = DriverManager.getConnection(url,username,password)
val insertSql = "insert into temperatures (sensor, temp) values (?,?)"
insertStmt = conn.prepareStatement(insertSql)
val updateSql = "update temperatures set temp = ? where sensor = ?"
updateStmt = conn.prepareStatement(updateSql)
}
override def invoke(value: SensorReading, context: SinkFunction.Context[_]): Unit = {
updateStmt.setDouble(1,value.temperature)
updateStmt.setString(2,value.id)
updateStmt.execute()
if (updateStmt.getUpdateCount == 0) {
insertStmt.setString(1,value.id)
insertStmt.setDouble(2,value.temperature)
insertStmt.execute()
}
}
override def close(): Unit = {
insertStmt.close()
updateStmt.close()
conn.close()
}
}
在 main 方法中添加:
dataStream.addSink(new MysqlSink())