1 设置并行度
Flink应用程序在一个像集群这样的分布式环境中并行执行。当一个数据流程序提交到作业管理器执行时,系统将会创建一个数据流图,然后准备执行需要的操作符。每一个操作符将会并行化到一个或者多个任务中去。每个算子的并行任务都会处理这个算子的输入流中的一份子集。一个算子并行任务的个数叫做算子的并行度。它决定了算子执行的并行化程度,以及这个算子能处理多少数据量。
算子的并行度可以在执行环境这个层级来控制,也可以针对每个不同的算子设置不同的并行度。默认情况下,应用程序中所有算子的并行度都将设置为执行环境的并行度。执行环境的并行度(也就是所有算子的默认并行度)将在程序开始运行时自动初始化。如果应用程序在本地执行环境中运行,并行度将被设置为CPU的核数。当我们把应用程序提交到一个处于运行中的Flink集群时,执行环境的并行度将被设置为集群默认的并行度,除非我们在客户端提交应用程序时显式的设置好并行度。
通常情况下,将算子的并行度定义为和执行环境并行度相关的数值会是个好主意。这允许我们通过在客户端调整应用程序的并行度就可以将程序水平扩展了。我们可以使用以下代码来访问执行环境的默认并行度。
我们还可以重写执行环境的默认并行度,但这样的话我们将再也不能通过客户端来控制应用程序的并行度了。
算子默认的并行度也可以通过重写来明确指定。在下面的例子里面,数据源的操作符将会按照环境默认的并行度来并行执行,map操作符的并行度将会是默认并行度的2倍,sink操作符的并行度为2。
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment; int defaultP = env.getParallelism; env .addSource(new CustomSource) .map(new MyMapper) .setParallelism(defaultP * 2) .print() .setParallelism(2);
当我们通过客户端将应用程序的并行度设置为16并提交执行时,source操作符的并行度为16,mapper并行度为32,sink并行度为2。如果我们在本地环境运行应用程序的话,例如在IDE中运行,机器是8核,那么source任务将会并行执行在8个任务上面,mapper运行在16个任务上面,sink运行在2个任务上面。
并行度是动态概念,任务槽数量是静态概念。并行度<=任务槽数量。一个任务槽最多运行一个并行度。
2 类型
Flink程序所处理的流中的事件一般是对象类型。操作符接收对象输出对象。所以Flink的内部机制需要能够处理事件的类型。在网络中传输数据,或者将数据写入到状态后端、检查点和保存点中,都需要我们对数据进行序列化和反序列化。为了高效的进行此类操作,Flink需要流中事件类型的详细信息。Flink使用了Type Information
的概念来表达数据类型,这样就能针对不同的数据类型产生特定的序列化器,反序列化器和比较操作符。
Flink也能够通过分析输入数据和输出数据来自动获取数据的类型信息以及序列化器和反序列化器。尽管如此,在一些特定的情况下,例如匿名函数或者使用泛型的情况下,我们需要明确的提供数据的类型信息,来提高我们程序的性能。
在这一节中,我们将讨论Flink支持的类型,以及如何为数据类型创建相应的类型信息,还有就是在Flink无法推断函数返回类型的情况下,如何帮助Flink的类型系统去做类型推断。
2.1 支持的数据类型
Flink支持Java和Scala提供的所有普通数据类型。最常用的数据类型可以做以下分类:
- Primitives(原始数据类型)
- Java和Scala的Tuples(元组)
- Scala的样例类
- POJO类型
- 一些特殊的类型
接下来让我们一探究竟。
Primitives
Java和Scala提供的所有原始数据类型都支持,例如Int
(Java的Integer
),String,Double等等。下面举一个例子:
DataStream[Long] numbers = env.fromElements(1L, 2L, 3L, 4L);
numbers.map(n -> n + 1);
Tuples
元组是一种组合数据类型,由固定数量的元素组成。
Flink为Java的Tuple提供了高效的实现。Flink实现的Java Tuple最多可以有25个元素,根据元素数量的不同,Tuple都被实现成了不同的类:Tuple1,Tuple2,一直到Tuple25。Tuple类是强类型。
DataStream<Tuple2<String, Integer>> persons = env .fromElements( Tuple2.of("Adam", 17), Tuple2.of("Sarah", 23) ); persons.filter(p -> p.f1 > 18);
Tuple的元素可以通过它们的public属性访问——f0,f1,f2等等。或者使用getField(int pos)
方法来访问,元素下标从0开始:
import org.apache.flink.api.java.tuple.Tuple2 Tuple2<String, Integer> personTuple = Tuple2.of("Alex", 42); Integer age = personTuple.getField(1); // age = 42
不同于Scala的Tuple,Java的Tuple是可变数据结构,所以Tuple中的元素可以重新进行赋值。重复利用Java的Tuple可以减轻垃圾收集的压力。举个例子:
personTuple.f1 = 42; // set the 2nd field to 42 personTuple.setField(43, 1); // set the 2nd field to 43
POJO
POJO类的定义:
- 公有类
- 无参数的公有构造器
- 所有的字段都是公有的,可以通过getters和setters访问。
- 所有字段的数据类型都必须是Flink支持的数据类型。
举个例子:
public class Person { public String name; public int age; public Person() {} public Person(String name, int age) { this.name = name; this.age = age; } } DataStream<Person> persons = env.fromElements( new Person("Alex", 42), new Person("Wendy", 23) );
其他数据类型
- Array, ArrayList, HashMap, Enum
- Hadoop Writable types
2.2 为数据类型创建类型信息
Flink类型系统的核心类是TypeInformation
。它为系统在产生序列化器和比较操作符时,提供了必要的类型信息。例如,如果我们想使用某个key来做联结查询或者分组操作,TypeInformation
可以让Flink做更严格的类型检查。
Flink针对Java和Scala分别提供了类来产生类型信息。在Java中,类是
org.apache.flink.api.common.typeinfo.Types
举个例子:
TypeInformation<Integer> intType = Types.INT; TypeInformation<Tuple2<Long, String>> tupleType = Types .TUPLE(Types.LONG, Types.STRING); TypeInformation<Person> personType = Types .POJO(Person.class);
3 定义Key以及引用字段
在Flink中,我们必须明确指定输入流中的元素中的哪一个字段是key。
3.1 使用字段位置进行keyBy
DataStream<Tuple3<Int, String, Long>> input = ...
KeyedStream<Tuple3<Int, String, Long>, String> keyed = input.keyBy(1);
如果我们想要用元组的第2个字段和第3个字段做keyBy,可以看下面的例子。
input.keyBy(1, 2);
3.2 使用字段表达式来进行keyBy
对于样例类:
DataStream<SensorReading> sensorStream = ...
sensorStream.keyBy("id");
对于元组:
DataStream<Tuple3<Integer, String, Long>> javaInput = ... javaInput.keyBy("f2") // key Java tuple by 3rd field
3.3 Key选择器
方法类型
KeySelector[IN, KEY]
> getKey(IN): KEY
两个例子
scala version
val sensorData = ...
val byId = sensorData.keyBy(r => r.id)
val input = ...
input.keyBy(value => math.max(value._1, value._2))
java version
DataStream<SensorReading> sensorData = ...
KeyedStream<SensorReading, String> byId = sensorData.keyBy(r -> r.id);
DataStream<Tuple2<Int, Int>> input = ...
input.keyBy(value -> Math.max(value.f0, value.f1));
4 实现UDF函数,更细粒度的控制流
4.1 函数类
Flink暴露了所有udf函数的接口(实现方式为接口或者抽象类)。例如MapFunction, FilterFunction, ProcessFunction等等。
例子实现了FilterFunction接口
class FilterFilter extends FilterFunction<String> { @Override public Boolean filter(String value) { return value.contains("flink"); } } DataStream<String> flinkTweets = tweets.filter(new FlinkFilter);
还可以将函数实现成匿名类
DataStream<String> flinkTweets = tweets.filter( new RichFilterFunction<String> { @Override public Boolean filter(String value) { return value.contains("flink"); } } )
我们filter的字符串"flink"还可以当作参数传进去。
DataStream<String> tweets = ... DataStream<String> flinkTweets = tweets.filter(new KeywordFilter("flink")); class KeywordFilter(keyWord: String) extends FilterFunction<String> { @Override public Boolean filter(String value) = { return value.contains(keyWord); } }
4.2 匿名函数
匿名函数可以实现一些简单的逻辑,但无法实现一些高级功能,例如访问状态等等。
DataStream<String> tweets = ...
DataStream<String> flinkTweets = tweets.filter(r -> r.contains("flink"));
4.3 富函数
我们经常会有这样的需求:在函数处理数据之前,需要做一些初始化的工作;或者需要在处理数据时可以获得函数执行上下文的一些信息;以及在处理完数据时做一些清理工作。而DataStream API就提供了这样的机制。
DataStream API提供的所有转换操作函数,都拥有它们的“富”版本,并且我们在使用常规函数或者匿名函数的地方来使用富函数。例如下面就是富函数的一些例子,可以看出,只需要在常规函数的前面加上Rich
前缀就是富函数了。
- RichMapFunction
- RichFlatMapFunction
- RichFilterFunction
- ...
当我们使用富函数时,我们可以实现两个额外的方法:
- open()方法是rich function的初始化方法,当一个算子例如map或者filter被调用之前open()会被调用。open()函数通常用来做一些只需要做一次即可的初始化工作。
- close()方法是生命周期中的最后一个调用的方法,通常用来做一些清理工作。
另外,getRuntimeContext()方法提供了函数的RuntimeContext的一些信息,例如函数执行的并行度,当前子任务的索引,当前子任务的名字。同时还它还包含了访问分区状态的方法。下面看一个例子:
public static class MyFlatMap extends RichFlatMapFunction<Integer, Tuple2<Integer, Integer>> { private int subTaskIndex = 0; @Override public void open(Configuration configuration) { int subTaskIndex = getRuntimeContext.getIndexOfThisSubtask; // 做一些初始化工作 // 例如建立一个和HDFS的连接 } @Override public void flatMap(Integer in, Collector<Tuple2<Integer, Integer>> out) { if (in % 2 == subTaskIndex) { out.collect((subTaskIndex, in)); } } @Override public void close() { // 清理工作,断开和HDFS的连接。 } }
5 Sink
Flink没有类似于spark中foreach方法,让用户进行迭代的操作。所有对外的输出操作都要利用Sink完成。最后通过类似如下方式完成整个任务最终输出操作。
stream.addSink(new MySink(xxxx));
官方提供了一部分的框架的sink。除此以外,需要用户自定义实现sink。
5.1 Kafka
Kafka版本为0.11
<dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-connector-kafka-0.11_2.11</artifactId> <version>${flink.version}</version> </dependency>
Kafka版本为2.0以上
<dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-connector-kafka_2.11</artifactId> <version>${flink.version}</version> </dependency>
主函数中添加sink:
DataStream<String> union = high .union(low) .map(r -> r.temperature.toString); union.addSink( new FlinkKafkaProducer011<String>( "localhost:9092", "test", new SimpleStringSchema() ) );
5.2 Redis
<dependency> <groupId>org.apache.bahir</groupId> <artifactId>flink-connector-redis_2.11</artifactId> <version>1.0</version> </dependency>
定义一个redis的mapper类,用于定义保存到redis时调用的命令:
scala version
object SinkToRedisExample { def main(args: Array[String]): Unit = { val env = StreamExecutionEnvironment.getExecutionEnvironment env.setParallelism(1) val stream = env.addSource(new SensorSource) val conf = new FlinkJedisPoolConfig.Builder().setHost("localhost").build() stream.addSink(new RedisSink[SensorReading](conf, new MyRedisSink)) env.execute() } class MyRedisSink extends RedisMapper[SensorReading] { override def getKeyFromData(t: SensorReading): String = t.id override def getValueFromData(t: SensorReading): String = t.temperature.toString override def getCommandDescription: RedisCommandDescription = new RedisCommandDescription(RedisCommand.HSET, "sensor") } }
java version
public class WriteToRedisExample { public static void main(String[] args) throws Exception { StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(1); DataStream<SensorReading> stream = env.addSource(new SensorSource()); FlinkJedisPoolConfig conf = new FlinkJedisPoolConfig.Builder().setHost("localhost").build(); stream.addSink(new RedisSink<SensorReading>(conf, new MyRedisSink())); env.execute(); } public static class MyRedisSink implements RedisMapper<SensorReading> { @Override public String getKeyFromData(SensorReading sensorReading) { return sensorReading.id; } @Override public String getValueFromData(SensorReading sensorReading) { return sensorReading.temperature + ""; } @Override public RedisCommandDescription getCommandDescription() { return new RedisCommandDescription(RedisCommand.HSET, "sensor"); } } }
5.3 ElasticSearch
在主函数中调用:
<dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-connector-elasticsearch6_2.11</artifactId> <version>${flink.version}</version> </dependency>
可选依赖:
<dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>7.9.1</version> </dependency> <dependency> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> <version>7.9.1</version> </dependency>
示例代码:
scala version
object SinkToES { def main(args: Array[String]): Unit = { val env = StreamExecutionEnvironment.getExecutionEnvironment env.setParallelism(1) val httpHosts = new util.ArrayList[HttpHost]() httpHosts.add(new HttpHost("127.0.0.1", 9200, "http")) val esSinkBuilder = new ElasticsearchSink.Builder[SensorReading]( httpHosts, new ElasticsearchSinkFunction[SensorReading] { override def process(t: SensorReading, runtimeContext: RuntimeContext, requestIndexer: RequestIndexer): Unit = { val hashMap = new util.HashMap[String, String]() hashMap.put("data", t.toString) val indexRequest = Requests .indexRequest() .index("sensor") // 索引是sensor,相当于数据库 .source(hashMap) requestIndexer.add(indexRequest) } } ) // 设置每一批写入es多少数据 esSinkBuilder.setBulkFlushMaxActions(1) val stream = env.addSource(new SensorSource) stream.addSink(esSinkBuilder.build()) env.execute() } }
java version
public class SinkToES { public static void main(String[] args) throws Exception { StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(1); ArrayList<HttpHost> httpHosts = new ArrayList<>(); httpHosts.add(new HttpHost("127.0.0.1", 9200, "http")); ElasticsearchSink.Builder<SensorReading> sensorReadingBuilder = new ElasticsearchSink.Builder<>( httpHosts, new ElasticsearchSinkFunction<SensorReading>() { @Override public void process(SensorReading sensorReading, RuntimeContext runtimeContext, RequestIndexer requestIndexer) { HashMap<String, String> map = new HashMap<>(); map.put("data", sensorReading.toString()); IndexRequest indexRequest = Requests .indexRequest() .index("sensor") // 索引是sensor,相当于数据库 .source(map); requestIndexer.add(indexRequest); } } ); sensorReadingBuilder.setBulkFlushMaxActions(1); DataStream<SensorReading> stream = env.addSource(new SensorSource()); stream.addSink(sensorReadingBuilder.build()); env.execute(); } }
5.4 JDBC自定义sink
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.21</version> </dependency>
示例代码:
scala version
object SinkToMySQL { def main(args: Array[String]): Unit = { val env = StreamExecutionEnvironment.getExecutionEnvironment env.setParallelism(1) val stream = env.addSource(new SensorSource) stream.addSink(new MyJDBCSink) env.execute() } class MyJDBCSink extends RichSinkFunction[SensorReading] { var conn: Connection = _ var insertStmt: PreparedStatement = _ var updateStmt: PreparedStatement = _ override def open(parameters: Configuration): Unit = { conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/sensor", "zuoyuan", "zuoyuan" ) insertStmt = conn.prepareStatement("INSERT INTO temps (id, temp) VALUES (?, ?)") updateStmt = conn.prepareStatement("UPDATE temps SET temp = ? WHERE id = ?") } 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() } } }
java version
public class SinkToMySQL { public static void main(String[] args) throws Exception { StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(1); DataStream<SensorReading> stream = env.addSource(new SensorSource()); stream.addSink(new MyJDBCSink()); env.execute(); } public static class MyJDBCSink extends RichSinkFunction<SensorReading> { private Connection conn; private PreparedStatement insertStmt; private PreparedStatement updateStmt; @Override public void open(Configuration parameters) throws Exception { super.open(parameters); conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/sensor", "zuoyuan", "zuoyuan" ); insertStmt = conn.prepareStatement("INSERT INTO temps (id, temp) VALUES (?, ?)"); updateStmt = conn.prepareStatement("UPDATE temps SET temp = ? WHERE id = ?"); } @Override public void invoke(SensorReading value, Context context) throws Exception { 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 public void close() throws Exception { super.close(); insertStmt.close(); updateStmt.close(); conn.close(); } } }