flink前身--对作业调度系统的思考

628 阅读4分钟

一开始看到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 可以根据该位置重启输入。

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");
    }
}