一开始看到flink打印的日志是有点懵逼的。checkPoint?taskManager?jobManager? 这些究竟是用来做什么的?
经过阅读文献,在meiduim上找到了这么一篇描述分布式计算的文章(对于批处理计算) a-thorough-introduction-to-distributed-systems
一个最原始的分布式计算系统是什么样子的呢?
但是如果我们要对实时数据进行处理,这样的架构据没办法持久化。而且也不满足cpu资源在计算时得到充分的分配。也就是一种无状态的计算。为了让其cpu资源在计算时得到充分的分配。flink的作业调度如下:
上面的架构解决了在任务分配的时候会有空闲的现场资源进行处理。但仍存在问题。也就是一致性问题
在流处理中,一致性分为 3 个级别。
-
at-most-once:这其实是没有正确性保障的委婉说法——故障发生之后,计数结果可能丢失。
-
at-least-once:这表示计数结果可能大于正确值,但绝不会小于正确值。也就是说,计数程序在发生故障后可能多算,但是绝不会少算。
-
exactly-once:这指的是系统保证在发生故障后得到的计数结果与正确值一致。
曾经,at-least-once 非常流行。第一代流处理器(如 Storm 和 Samza)刚问世时只保证 at-least-once。最先保证 exactly-once 的系统(Storm Trident 和 Spark Streaming)在性能和表现力这两个方面付出了很大的代价。
检查点机制:在出现故障时将系统重置回正确状态。
作为Flink默认的exactly-once触发检查机制如下:
当 Flink 数据源遇到检查点屏障时,它会将其在输入流中的位置保存到稳定存储中。这让 Flink 可以根据该位置重启输入。
上述的检查点状态其实就存放在stateBackend当中,至于状态的底层原理,请看这篇Flink 状态管理与checkPoint数据容错机制深入剖析
参考 在 Flink 算子中使用多线程如何保证不丢数据? 虽然社区作者代码没写完整,但难能可贵的是有个大致的思路,所以实现如下
其实之所以会丢失数据,是因为在程序执行算子链的过程中,是遵循反应式设计的。即工作线程在提交一批任务之后,线程切换进行checkpoint存储计算状态(主流提交任务,开始存储计算状态)可能会丢失。所以不如把执行算子链的线程当做一个主流,而算子内多线程都作为这个主流提交任务之后,分流的线程执行任务。那么必然要等分流的任务全部处理玩成之后,才能继续让主流提交任务。 而我们只需要判断程序是否进入snapshotState事件,则可以反映完成分流的状态存储任务,继续在主流接受对应算子的task线程的数据
代码如下
MultiThreadConsumerClient.java
public class MultiThreadConsumerClient implements Runnable {
private Logger LOG = LoggerFactory.getLogger(MultiThreadConsumerClient.class);
private LinkedBlockingQueue<String> bufferQueue;
private CyclicBarrier barrier;
private int batchSize = 5;
private int timeout = 2000;
private List<String> entities = new ArrayList<>();
public MultiThreadConsumerClient(
LinkedBlockingQueue<String> bufferQueue, CyclicBarrier barrier) {
this.bufferQueue = bufferQueue;
this.barrier = barrier;
// 超时定时器
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
executor.scheduleAtFixedRate(
new Runnable() {
@Override
public void run() {
if (!entities.isEmpty()) {
System.out.println("timeout -> doSomething ...");
doSomething(entities);
entities = new ArrayList<>();
}
}
},
0,
timeout,
TimeUnit.MILLISECONDS);
}
@Override
public void run() {
String entity;
while (true){
try {
// 从 bufferQueue 的队首消费数据,并设置 timeout
entity = bufferQueue.poll(50, TimeUnit.MILLISECONDS);
// entity != null 表示 bufferQueue 有数据
if(entity != null) {
if (entities.size() >= batchSize) {
// 执行 client 消费数据的逻辑
System.out.println("normal -> doSomething ...");
doSomething(entities);
entities = new ArrayList<>();
}
entities.add(entity);
} else {
// entity == null 表示 bufferQueue 中已经没有数据了,
// 且 barrier wait 大于 0 表示当前正在执行 Checkpoint,
// client 需要执行 flush,保证 Checkpoint 之前的数据都消费完成
if ( barrier.getNumberWaiting() > 0 ) {
LOG.info("MultiThreadConsumerClient 执行 flush, " +
"当前 wait 的线程数:" + barrier.getNumberWaiting());
if (!entities.isEmpty()) {
flush();
entities = new ArrayList<>();
}
barrier.await();
}
}
} catch (InterruptedException| BrokenBarrierException e) {
e.printStackTrace();
}
}
}
// client 消费数据的逻辑
private void doSomething(List<String> entities) {
System.out.println("client doSomething ..." + entities.size());
}
// client 执行 flush 操作,防止丢数据
// 如果client有攒批操作的话,内存中是会存在未处理的数据,需要flush处理
private void flush() {
List<String> queue = entities;
//TODO 处理未消费完的数据
System.out.println("client flushing ..." + entities.size());
}
private static String convertDataString(List<String> list) {
String commaSeparated = list.stream().collect(Collectors.joining(","));
return "[" + commaSeparated + "]";
}
}
MultiThreadConsumerSink.java
public class MultiThreadConsumerSink extends RichSinkFunction<String> implements CheckpointedFunction {
private Logger LOG = LoggerFactory.getLogger(MultiThreadConsumerSink.class);
// Client 线程的默认数量
private final int DEFAULT_CLIENT_THREAD_NUM = 2;
// 数据缓冲队列的默认容量
private final int DEFAULT_QUEUE_CAPACITY = 5000;
private LinkedBlockingQueue<String> bufferQueue;
private CyclicBarrier clientBarrier;
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
// new 一个容量为 DEFAULT_CLIENT_THREAD_NUM 的线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(DEFAULT_CLIENT_THREAD_NUM, DEFAULT_CLIENT_THREAD_NUM,
0L,TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
// new 一个容量为 DEFAULT_QUEUE_CAPACITY 的数据缓冲队列
this.bufferQueue = Queues.newLinkedBlockingQueue(DEFAULT_QUEUE_CAPACITY);
// barrier 需要拦截 (DEFAULT_CLIENT_THREAD_NUM + 1) 个线程
this.clientBarrier = new CyclicBarrier(DEFAULT_CLIENT_THREAD_NUM + 1);
// 创建并开启消费者线程
MultiThreadConsumerClient consumerClient = new MultiThreadConsumerClient(bufferQueue, clientBarrier);
for (int i=0; i < DEFAULT_CLIENT_THREAD_NUM; i++) {
threadPoolExecutor.execute(consumerClient);
}
}
@Override
public void invoke(String value, Context context) throws Exception {
// 往 bufferQueue 的队尾添加数据
bufferQueue.put(value);
}
@Override
public void snapshotState(FunctionSnapshotContext functionSnapshotContext) throws Exception {
LOG.info("snapshotState : 所有的 client 准备 flush !!!");
// barrier 开始等待
clientBarrier.await();
}
@Override
public void initializeState(FunctionInitializationContext functionInitializationContext) throws Exception {
}
}
启动入口:MultiThreadSinkJob.java
public class MultiThreadSinkJob {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.enableCheckpointing(5000);
env.setParallelism(1);
env.addSource(new SensorSource()).map((MapFunction<SensorReading, String>) value -> {
String[] strs = new String[]{value.getId(), String.valueOf(value.getTemperature()), String.valueOf(value.getTimestamp())};
return String.join(",", strs);
}).addSink(new MultiThreadConsumerSink());
// env.socketTextStream("localhost", 9999).addSink(new MultiThreadConsumerSink());
env.execute("MultiThreadSinkJob");
}
}