Flink要点总结

1. 什么是Flink?

Flink 是一个框架和分布式处理引擎,用于对无界和有界数据流进行有状态计算。并且 Flink 提供了数据分布、容错机制以及资源管理等核心功能。Flink 提供了诸多高抽象层的 API 以便用户编写分布式任务:

  • DataSet API, 对静态数据进行批处理操作,将静态数据抽象成分布式的数据集,用户 可以方便地使用Flink提供的各种操作符对分布式数据集进行处理,支持Java、Scala和Python。
  • DataStream API,对数据流进行流处理操作,将流式的数据抽象成分布式的数据流,用 户可以方便地对分布式数据流进行各种操作,支持 Java 和 Scala。
  • Table API,对结构化数据进行查询操作,将结构化数据抽象成关系表,并通过类 SQL 的 DSL 对关系表进行各种查询操作,支持 Java 和 Scala。

2. Flink的组件栈

Flink组件
自下而上,每一层分别代表:

  • Deploy 层:该层主要涉及了 Flink 的部署模式,在上图 中我们可以看出,Flink 支持包括 local、Standalone、Cluster、Cloud 等多种部署模式。
  • Runtime层:Runtime 层提供了支持 Flink 计算的核心实现,比如:支持分布式 Stream 处理、JobGraph 到 ExecutionGraph 的映射、调度等等,为上层 API 层提供基础服务。
  • API 层:API 层主要 实现了面向流(Stream)处理和批(Batch)处理 API,其中面向流处理对应 DataStream API, 面向批处理对应 DataSet API,后续版本,Flink 有计划将 DataStream 和 DataSet API 进行统 一。
  • Libraries 层:该层称为 Flink 应用框架层,根据 API 层的划分,在 API 层之上构建的满 足特定应用的实现计算框架,也分别对应于面向流处理和面向批处理两类。面向流处理支持: CEP(复杂事件处理)、基于 SQL-like 的操作(基于 Table 的关系操作);面向批处理支持: FlinkML(机器学习库)、Gelly(图处理)。

3. Flink集群运行时的角色及其作用

Flink角色
Flink 程序在运行时主要有 TaskManager,JobManager,Client 三种角色。

  1. JobManager 扮演着集群中的管理者 Master 的角色,它是整个集群的协调者,负责接收 Flink Job,协调检查点,Failover 故障恢复等,同时管理 Flink 集群中从节点 TaskManager。
  2. TaskManager 是实际负责执行计算的 Worker,在其上执行 Flink Job 的一组 Task,每个 TaskManager 负责管理其所在节点上的资源信息,如内存、磁盘、网络,在启动的时候将资 源的状态向 JobManager 汇报。
  3. Client 是 Flink 程序提交的客户端,当用户提交一个 Flink 程 序时,会首先创建一个 Client,该 Client 首先会对用户提交的 Flink 程序进行预处理,并提 交到 Flink 集群中处理,所以 Client 需要从用户提交的 Flink 程序配置中获取 JobManager 的 地址,并建立到 JobManager 的连接,将 Flink Job 提交给 JobManager。

4. Flink分区策略

整个 Flink 实现的8种分区策略继承图:
分区策略

  • ChannelSelector: 接口,决定将记录写入哪个Channel。有3个方法:
    • void setup(int numberOfChannels): 初始化输出Channel的数量。
    • int selectChannel(T record): 根据当前记录以及Channel总数,决定应将记录写入下游哪个Channel。八大分区策略的区别主要在这个方法的实现上。
    • boolean isBroadcast(): 是否是广播模式。决定了是否将记录写入下游所有Channel。
  1. GlobalPartitioner数据会被分发到下游算子的第一个实例中进行处理。

selectChannel实现


public int selectChannel(SerializationDelegate<StreamRecord<T>> record) {
        //对每条记录,只选择下游operator的第一个Channel
		return 0;
}

API


dataStream
    .setParallelism(2)
    // 采用GLOBAL分区策略重分区
    .global()
    .print()
    .setParallelism(1);
  1. ShufflePartitioner数据会被随机分发到下游算子的每一个实例中进行处理。
private Random random = new Random();

@Override
public int selectChannel(SerializationDelegate<StreamRecord<T>> record) {
    //对每条记录,随机选择下游operator的某个Channel
	return random.nextInt(numberOfChannels);
}

API

dataStream
    .setParallelism(2)
    // 采用SHUFFLE分区策略重分区
    .shuffle()
    .print()
    .setParallelism(4);

  1. RebalancePartitioner数据会被循环发送到下游的每一个实例中进行处理。
public int selectChannel(SerializationDelegate<StreamRecord<T>> record) {
    //第一条记录,输出到下游的第一个Channel;第二条记录,输出到下游的第二个Channel...如此循环
	nextChannelToSendTo = (nextChannelToSendTo + 1) % numberOfChannels;
	return nextChannelToSendTo;
}

API

dataStream
        .setParallelism(2)
        // 采用REBALANCE分区策略重分区
        .rebalance()
        .print()
        .setParallelism(4);
  1. RescalePartitioner这种分区器会根据上下游算子的并行度,循环的方式输出到下游算子的每个实例。这里有点难以理解,假设上游并行度为2,编号为A和 B。下游并行度为4,编号为 1,2,3,4。那么A则把数据循环发送给1和2,B 则把数据循环发送给3和4。假设上游并行度为4,编号为A,B,C,D。下游并行度为 2,编号为1,2。那么A和B则把数据发送给1,C和D则把数据发送给2。
private int nextChannelToSendTo = -1;

@Override
public int selectChannel(SerializationDelegate<StreamRecord<T>> record) {
	if (++nextChannelToSendTo >= numberOfChannels) {
		nextChannelToSendTo = 0;
	}
	return nextChannelToSendTo;
}

API

dataStream
    .setParallelism(2)
    // 采用RESCALE分区策略重分区
    .rescale()
    .print()
    .setParallelism(4)
  1. BroadcastPartitioner 广播分区会将上游数据输出到下游算子的每个实例中。 适合于大数据集和小数据集做Jion的场景。
@Override
public int selectChannel(SerializationDelegate<StreamRecord<T>> record) {
    //广播分区不支持选择Channel,因为会输出到下游每个Channel中
	throw new UnsupportedOperationException("Broadcast partitioner does not support select channels.");
}

@Override
public boolean isBroadcast() {
    //启用广播模式,此时Channel选择器会选择下游所有Channel
	return true;
}

API

dataStream
    .setParallelism(2)
    // 采用BROADCAST分区策略重分区
    .broadcast()
    .print()
    .setParallelism(4)
  1. ForwardPartitioner用于将记录输出到下游本地的算子实例。它要求上下游算子 并行度一样。简单的说,ForwardPartitioner用来做数据的控制台打印。
@Override
public int selectChannel(SerializationDelegate<StreamRecord<T>> record) {
	return 0;
}

API

dataStream
    .setParallelism(2)
    // 采用FORWARD分区策略重分区
    .forward()
    .print()
    .setParallelism(2);
  1. KeyGroupStreamPartitioner Hash分区器。会将数据按Key的Hash 值输出到下游算子实例中。
@Override
public int selectChannel(SerializationDelegate<StreamRecord<T>> record) {
	K key;
	try {
		key = keySelector.getKey(record.getInstance().getValue());
	} catch (Exception e) {
		throw new RuntimeException("Could not extract key from " + record.getInstance().getValue(), e);
	}
	return KeyGroupRangeAssignment.assignKeyToParallelOperator(key, maxParallelism, numberOfChannels);
}

// KeyGroupRangeAssignment中的方法
public static int assignKeyToParallelOperator(Object key, int maxParallelism, int parallelism) {
	return computeOperatorIndexForKeyGroup(maxParallelism, parallelism, assignToKeyGroup(key, maxParallelism));
}

// KeyGroupRangeAssignment中的方法
public static int assignToKeyGroup(Object key, int maxParallelism) {
	return computeKeyGroupForKeyHash(key.hashCode(), maxParallelism);
}

// KeyGroupRangeAssignment中的方法
public static int computeKeyGroupForKeyHash(int keyHash, int maxParallelism) {
	return MathUtils.murmurHash(keyHash) % maxParallelism;
}

API

dataStream
    .setParallelism(2)
    // 采用HASH分区策略重分区
    .keyBy((KeySelector<Tuple3<String, Integer, String>, String>) value -> value.f0)
    .print()
    .setParallelism(4);
  1. CustomPartitionerWrapper 用户自定义分区器。需要用户自己实现 Partitioner中的partition方法(自定义),来定义自己的分区逻辑。
Partitioner<K> partitioner;
KeySelector<T, K> keySelector;
public CustomPartitionerWrapper(Partitioner<K> partitioner, KeySelector<T, K> keySelector) {
	this.partitioner = partitioner;
	this.keySelector = keySelector;
}
@Override
public int selectChannel(SerializationDelegate<StreamRecord<T>> record) {
	K key;
	try {
		key = keySelector.getKey(record.getInstance().getValue());
	} catch (Exception e) {
		throw new RuntimeException("Could not extract key from " + record.getInstance(), e);
	}
	return partitioner.partition(key, numberOfChannels);
}

自定义partition,将指定的Key分到指定的分区

// 自定义分区器,将不同的Key(用户ID)分到指定的分区
// key: 根据key的值来分区
// numPartitions: 下游算子并行度
static class CustomPartitioner implements Partitioner<String> {
      @Override
      public int partition(String key, int numPartitions) {
          switch (key){
              case "user_1":
                  return 0;
              case "user_2":
                  return 1;
              case "user_3":
                  return 2;
              default:
                  return 3;
          }
      }
  }

API

dataStream
    .setParallelism(2)
    // 采用CUSTOM分区策略重分区
    .partitionCustom(new CustomPartitioner(),0)
    .print()
    .setParallelism(4);

5. Flink容错机制

Flink 实现容错主要靠强大的 CheckPoint 机制和 State 机制。Checkpoint 负责定时制作分布式快照、对程序中的状态进行备份;State 用来存储计算过程中的中间状态。

  • Flink 的分布式快照是根据 Chandy-Lamport 算法量身定做的。简单来说就是持续创建分布式数据流及其状态的一致快照。核心思想是在 input source 端插入barrier,控制 barrier 的同步来实现 snapshot 的备份和 exactly-once 语义。
    barrier
  • Flink 通过实现两阶段提交和状态保存来实现端到端的一致性语义,分为以下几个步骤:
    1. 开始事务(beginTransaction):创建一个临时文件夹,来写把数据写入到这个文件夹里面。
    2. 预提交(preCommit):将内存中缓存的数据写入文件并关闭 。
    3. 正式提交(commit):将之前写完的临时文件放入目标目录下。这代表着最终的数据会有一些延迟 。
    4. 丢弃(abort):丢弃临时文件 。
    5. 若失败发生在预提交成功后,正式提交前。可以根据状态来提交预提交的数据,也可删除预提交的数据。

6. Flink计算资源的调度是如何实现的?

Flink 调度
TaskManager中最细粒度的资源是Task slot,代表了一个固定大小的资源子集,每个TaskManager会将其所占有的资源平分给它的slot。通过调整task slot的数量,用户可以定义task之间是如何相互隔离的。每个TaskManager有一个slot,也就意味着每个task运行在独立的JVM中。每个TaskManager有多个slot的话,也就是说多个task运行在同一个JVM中。而在同一个JVM进程中的task,可以共享 TCP 连接(基于多路复用)和心跳消息,可以减少数据的网络传输,也能共享一些数据结构,一定程度上减少了每个task的消耗。每个slot可以接受单个task,也可以接受多个连续task组成的pipeline,如上图所示,FlatMap 函数占用一个 taskslot,而key Agg函数和sink函数共用一个taskslot。

猜你喜欢

转载自blog.csdn.net/weixin_42526352/article/details/106247558