一句话概括flink:
flink核心是一个流式的数据流执行引擎,其针对数据流的分布式计算提供了数据分布,数据通信以及容错机制等功能。
WordCount源码
放源码之前,先介绍一下一些预备知识:
首先介绍一下map与flatMap区别
- map,就是把一个函数传入map中,然后利用传入的函数,把集合中每个元素做处理,然后把处理后的结果返回。
- flatMap与其区别仅仅是返回的是一个列表
然后把一些基本算子过一遍
- map
支持用lambda表达式来表达map函数
说白了就是把一个数据流转换成另外一个数据流
下面这段代码就是用kafka作为数据源,把字符串类型的流转换为Student类型的流。
public class kafkatoSink {
public static void main(String[] args) throws Exception {
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9093");
props.put("zookeeper.connect", "localhost:2181");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
SingleOutputStreamOperator<Student> student = env.addSource(new FlinkKafkaConsumer010<>(
"student", //这个 kafka topic 需要和上面的工具类的 topic 一致
new SimpleStringSchema(),
props)).setParallelism(1)
.map(string -> JSON.parseObject(string, Student.class)); //Fastjson 解析字符串成 student 对象
student.addSink(new SinkFunction<Student>() {
@Override
public void invoke(Student value) throws Exception {
System.out.println(value.name+" "+value.age+""+value.id);
}
}); //数据 sink 到 CONSOLE
env.execute("Flink add sink");
}
}
- flatMap
flatMap方法返回的是一个collector
public static final class LineSplitter implements FlatMapFunction<String, Tuple2<String, Integer>> {
@Override
public void flatMap(String value, Collector<Tuple2<String, Integer>> out) {
// normalize and split the line
String[] tokens = value.toLowerCase().split("\\W+");
// emit the pairs
for (String token : tokens) {
if (token.length() > 0) {
out.collect(new Tuple2<String, Integer>(token, 1));
}
}
}
}
- filter
实例:
lambda返回true or false
// 根据domain字段,过滤数据,只保留BAIDU的domain
SingleOutputStreamOperator<UrlInfo> filterSource = flatSource.filter(urlInfo -> {
if(StringUtils.equals(UrlInfo.BAIDU,urlInfo.getDomain())){
return true;
}
return false;
});
filterSource.addSink(new PrintSinkFunction<>());
- keyBy
DataStream -> KeyedStream
把相同key的所有记录分配为同一个分区。在内部,keyBy()是使用散列分区实现的。相当于SQL中的groupBy()函数
dataStream.keyBy(0) //由数组第一个元素作为key
public static void main(String [] args)throws Exception{
//wordCount();
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.fromElements(Tuple2.of(2L,3L),Tuple2.of(1L,5L),Tuple2.of(1L, 7L), Tuple2.of(2L, 4L), Tuple2.of(1L, 2L))
.keyBy(0)
//这里的mapFunction 把Tuple转化为String
.map((MapFunction<Tuple2<Long,Long>,String>) tuple->
"Key:"+tuple.f0+",value:"+tuple.f1)
.print();
env.execute("execute");
}
result:
6> Key:1,value:5
6> Key:1,value:7
6> Key:1,value:2
8> Key:2,value:3
8> Key:2,value:4
6>,8>表示分组,这里是按照tuple第一个元素作为key来分组的
reduce
reduce表示将数据合并成一个新的数据,返回单个的结果值,并且 reduce 操作每处理一个元素总是创建一个新值。
所以reduce需要针对分组或者一个window(窗口)来执行,也就是分别对应于keyBy、window/timeWindow 处理后的数据,根据ReduceFunction将元素与上一个reduce后的结果合并,产出合并之后的结果。
public static void main(String [] args)throws Exception{
//wordCount();
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.fromElements(Tuple2.of(2L,3L),Tuple2.of(1L,5L),Tuple2.of(1L, 7L), Tuple2.of(2L, 4L), Tuple2.of(1L, 2L))
.keyBy(0)
//这里的mapFunction 把Tuple转化为String
.reduce((ReduceFunction<Tuple2<Long,Long>>)(t2,t1) -> new Tuple2<>(t1.f0,t1.f1+t2.f1))
.print();
env.execute("execute");
}
8> (2,3)
8> (2,7)
6> (1,5)
6> (1,12)
6> (1,14)
- Aggregate
KeyedStream -> dataStream
public static void main(String [] args)throws Exception{
//wordCount();
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
KeyedStream keyedStream = env.fromElements(Tuple2.of(2L,3L),Tuple2.of(1L,5L),Tuple2.of(1L, 7L), Tuple2.of(2L, 4L), Tuple2.of(1L, 2L))
.keyBy(0);
SingleOutputStreamOperator<Tuple2> sumStream = keyedStream.sum(1);
sumStream.addSink(new PrintSinkFunction<>());
env.execute("execute");
至于min,minBy什么的查文档吧
现在上wordCount源码
package wordCount;
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
public class wordCount {
public static void main(String [] args) throws Exception{
if(args.length!=2){
System.err.println();
return;
}
String hostname = args[0];
Integer port = Integer.parseInt(args[1]);
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStreamSource<String> text = env.socketTextStream(hostname,port);
text.flatMap(new LineSplitter()).setParallelism(1)
.keyBy(0)
.sum(1).setParallelism(1)
.print();
env.execute("wordCount SocketTextStream");
}
public static final class LineSplitter implements FlatMapFunction<String, Tuple2<String,Integer>>{
@Override
public void flatMap(String value, Collector<Tuple2<String,Integer>> out){
String [] tokens = value.toLowerCase().split("\\W+");
for(String token:tokens){
if(token.length()>0){
out.collect(new Tuple2<>(token,1));
}
}
}
}
在终端用
nc -l 9000
然后运行程序
Flink的图结构
StreamGraph 程序的拓扑结构,从代码直接生成的图
JobGraph 交给flink去生成task的图
数据从operator到另外一个operator的时候,上游作为生产者提供了intermediateDataset,下游作为消费者需要jobEdge.jobEdge是一个通信管道,连接了上游生产的dataset和下游的jobVertex。
ExecutionGraph 真正执行的一层
jobGraph转换到ExecutionGraph过程中
- 加入了并行度概念,变为真正可以调度的图结构。
- 生成了与jobVertex对应的ExecutionJobVertex,ExecutionVertex,与IntermediateDataset对应的IntermediateResult和IntermediateResultPartition等
- executionGraph可以用于调度任务,flink根据该图生成了一一对应的task,每一个task对应一个execution
总结而言,
streamGraph是对用户逻辑的映射。
jobGraph在此基础上进行了一些优化。例如把一部分操作串成chain以提高效率。
executionGraph是为了调度存在的,并且加入并行处理的概念。
streamGraph的生成
源码路径:
flink把每一个算子transform成一个对流的转换,并且注册到执行环境中。
public StreamGraphGenerator(List<Transformation<?>> transformations,
ExecutionConfig executionConfig, CheckpointConfig checkpointConfig) {
/org/apache/flink/streaming/api/graph/StreamGraphGenerator.java
StreamTransform类代表了流的转换
本质上是一个或者多个dataStream生成新的dataStream的操作。
- 首先,用户代码里定义的UDF会被当作其基类对待,然后交给StreamMap这个operator做进一步包装。事实上,每一个Transformation都对应了一个StreamOperator。
- 由于map这个操作只接受一个输入,所以再被进一步包装为OneInputTransformation。
- 最后,将该transformation注册到执行环境中,当执行上文提到的generate方法时,生成StreamGraph图结构。
JobGraph的生成
flink会根据上一步生成的streamGraph生成JobGraph,然后把JobGraph发送到server端进行executionGraph的解析。
jobGraph生成的源码
/home/alex/.m2/repository/org/apache/flink/flink-streaming-java_2.11/1.6.0/flink-streaming-java_2.11-1.6.0-sources.jar!/org/apache/flink/streaming/api/graph/StreamingJobGraphGenerator.java
private JobGraph createJobGraph() {
// make sure that all vertices start immediately
jobGraph.setScheduleMode(ScheduleMode.EAGER);
// Generate deterministic hashes for the nodes in order to identify them across
// submission iff they didn't change.
Map<Integer, byte[]> hashes = defaultStreamGraphHasher.traverseStreamGraphAndGenerateHashes(streamGraph);
// Generate legacy version hashes for backwards compatibility
List<Map<Integer, byte[]>> legacyHashes = new ArrayList<>(legacyStreamGraphHashers.size());
for (StreamGraphHasher hasher : legacyStreamGraphHashers) {
legacyHashes.add(hasher.traverseStreamGraphAndGenerateHashes(streamGraph));
}
Map<Integer, List<Tuple2<byte[], byte[]>>> chainedOperatorHashes = new HashMap<>();
//生成job节点,然后串成chain
setChaining(hashes, legacyHashes, chainedOperatorHashes);
setPhysicalEdges();
setSlotSharingAndCoLocation();
configureCheckpointing();
JobGraphGenerator.addUserArtifactEntries(streamGraph.getEnvironment().getCachedFiles(), jobGraph);
// set the ExecutionConfig last when it has been finalized
try {
jobGraph.setExecutionConfig(streamGraph.getExecutionConfig());
}
catch (IOException e) {
throw new IllegalConfigurationException("Could not serialize the ExecutionConfig." +
"This indicates that non-serializable types (like custom serializers) were registered");
}
return jobGraph;
}
这里我们看一下setChaining具体是怎样实现的
private void setChaining(Map<Integer, byte[]> hashes, List<Map<Integer, byte[]>> legacyHashes, Map<Integer, List<Tuple2<byte[], byte[]>>> chainedOperatorHashes) {
for (Integer sourceNodeId : streamGraph.getSourceIDs()) {
createChain(sourceNodeId, sourceNodeId, hashes, legacyHashes, 0, chainedOperatorHashes);
}
}
大体思想:
遍历节点,如果节点是chain的头结点,就可以生成一个jobVertex
如果不是头结点,就要把自身配置并入头结点,然后把头结点和自己的出边相连。
对于不能chain的节点,当做只有头结点处理
private List<StreamEdge> createChain(
Integer startNodeId,
Integer currentNodeId,
Map<Integer, byte[]> hashes,
List<Map<Integer, byte[]>> legacyHashes,
int chainIndex,
Map<Integer, List<Tuple2<byte[], byte[]>>> chainedOperatorHashes) {
只对没有build的节点做处理
if (!builtVertices.contains(startNodeId)) {
List<StreamEdge> transitiveOutEdges = new ArrayList<StreamEdge>();
List<StreamEdge> chainableOutputs = new ArrayList<StreamEdge>();
List<StreamEdge> nonChainableOutputs = new ArrayList<StreamEdge>();
//先把所有的出边扫一遍,判断其能否连成一个chain
for (StreamEdge outEdge : streamGraph.getStreamNode(currentNodeId).getOutEdges()) {
if (isChainable(outEdge, streamGraph)) {
chainableOutputs.add(outEdge);
} else {
nonChainableOutputs.add(outEdge);
}
}
如果不是头结点,把头结点和自己的出边相连,对于不能chain的节点,当做只有头结点处理
for (StreamEdge chainable : chainableOutputs) {
transitiveOutEdges.addAll(
createChain(startNodeId, chainable.getTargetId(), hashes, legacyHashes, chainIndex + 1, chainedOperatorHashes));
}
for (StreamEdge nonChainable : nonChainableOutputs) {
transitiveOutEdges.add(nonChainable);
createChain(nonChainable.getTargetId(), nonChainable.getTargetId(), hashes, legacyHashes, 0, chainedOperatorHashes);
}
List<Tuple2<byte[], byte[]>> operatorHashes =
chainedOperatorHashes.computeIfAbsent(startNodeId, k -> new ArrayList<>());
byte[] primaryHashBytes = hashes.get(currentNodeId);
for (Map<Integer, byte[]> legacyHash : legacyHashes) {
operatorHashes.add(new Tuple2<>(primaryHashBytes, legacyHash.get(currentNodeId)));
}
chainedNames.put(currentNodeId, createChainedName(currentNodeId, chainableOutputs));
chainedMinResources.put(currentNodeId, createChainedMinResources(currentNodeId, chainableOutputs));
chainedPreferredResources.put(currentNodeId, createChainedPreferredResources(currentNodeId, chainableOutputs));
StreamConfig config = currentNodeId.equals(startNodeId)
? createJobVertex(startNodeId, hashes, legacyHashes, chainedOperatorHashes)
: new StreamConfig(new Configuration());
setVertexConfig(currentNodeId, config, chainableOutputs, nonChainableOutputs);
if (currentNodeId.equals(startNodeId)) {
config.setChainStart();
config.setChainIndex(0);
config.setOperatorName(streamGraph.getStreamNode(currentNodeId).getOperatorName());
config.setOutEdgesInOrder(transitiveOutEdges);
config.setOutEdges(streamGraph.getStreamNode(currentNodeId).getOutEdges());
for (StreamEdge edge : transitiveOutEdges) {
connect(startNodeId, edge);
}
config.setTransitiveChainedTaskConfigs(chainedConfigs.get(startNodeId));
} else {
Map<Integer, StreamConfig> chainedConfs = chainedConfigs.get(startNodeId);
if (chainedConfs == null) {
chainedConfigs.put(startNodeId, new HashMap<Integer, StreamConfig>());
}
config.setChainIndex(chainIndex);
StreamNode node = streamGraph.getStreamNode(currentNodeId);
config.setOperatorName(node.getOperatorName());
chainedConfigs.get(startNodeId).put(currentNodeId, config);
}
config.setOperatorID(new OperatorID(primaryHashBytes));
if (chainableOutputs.isEmpty()) {
config.setChainEnd();
}
return transitiveOutEdges;
} else {
return new ArrayList<>();
}
}
学习一个新的框架,我们得思考其与同类产品的优势是什么:flink与spark这些计算框架相比有什么优势呢?
原来,flink为了更高效地分布式执行,会尽可能将operator的subtask链接在一起形成一个大的task
而一个task在一个线程中运行,这样的好处是:
- 减少了线程之间的切换
- 减少消息的序列化与反序列化
- 减少数据在缓冲区的交换
- 减少延迟的同时,提高整体的吞吐量
key agg与sink两个operator进行合并。合并之后不能改变整体的拓扑结构。
总结一下,把opeator串成一个task,flink避免了数据序列化后通过网络发送给其他节点的开销,能够大大增强效率。
JobGraph的提交依赖于jobClient和jobManager之间的异步通信
executionGraph的生成:
其不是在客户端程序生成的,而是在服务端生成的。
一个flink只维护一个jobManager
flink集群启动后,会启动一个jobManager和多个taskManager。
用户代码会提交给jobManager,jobManager再把来自不同用户的任务发送给不同的taskManager去执行。
每个TaskManager管理着多个task,task是执行计算的最小结构。其负责把心跳和统计信息汇报给jobMaanager。
taskManager之间以流的形式进行数据传输。
以上manager都是独立的jvm进程。
taskManager与job并非一一对应的关系。flink调度的最小单元还是task而非taskManager
也就是说,来自不同job的不同task可能运行在同一个taskManager不同线程上。
计算资源的调度
Task slot是一个TaskManager内资源分配的最小载体,代表一个固定大小的资源子集,每个taskManager会把其所占的所有资源平分给它的slot
如果每个taskManager有一个slot,意味着每个task运行在独立的jvm中。
每个taskManager如果有多个slot,就是说多个task运行在同一个jvm中。
而在同一个jvm进程中的task,可以共享tcp链接和心跳信息,可以减少数据网络传输,也能够共享一些数据结构。能够减少每个task的消耗。
每个slot可以接受单个task,也可以接受多个连续task组成的pipeline。flatMap函数占用一个taskslot,而key agg函数和sink函数共用一个taskslot