flink 1.14 异步 join 基础分析

@[TOC] flink 1.14 异步 join 思考

背景

flink 1.14 还没提供异步sql 版本jdbc join ,同时也没提供自定义传入SQL 查询结果集,然后再join的功能,但是他们提供了相关接口,恰好这两个功能对提升join 性能,以及SQL的灵活性上有需求,实现了一个版本。其中遇到一个问题,“异步join 的时候,如何保证顺序”?

你需要知道

 CustomSqlRowDataAsyncLookupFunction extends AsyncTableFunction<RowData> {
 
 public void eval(CompletableFuture<Collection<RowData>> future, Object... keys) {
 // 异步实现逻辑
 // 通过继承 AsyncTableFunction 实现自己的异步函数
 }
}

其次写过原生datastream 进行异步join的得知道有两个类:

// 这是异步以前用这个实现异步join 的方法,以及参数:AsyncDataStream
 public static <IN, OUT> SingleOutputStreamOperator<OUT> unorderedWait(
            DataStream<IN> in, 
            AsyncFunction<IN, OUT> func, 
            long timeout, 
            TimeUnit timeUnit) {
        return addOperator(/*参数略*/, OutputMode.UNORDERED);
    }


  public static <IN, OUT> SingleOutputStreamOperator<OUT> orderedWait(
            DataStream<IN> in,
            AsyncFunction<IN, OUT> func,
            long timeout,
            TimeUnit timeUnit,
            int capacity) {
        return addOperator(/*参数略*/, OutputMode.ORDERED);
    }
    
// 核心参数,也就是Operator 通过这个区分了是否有序
OutputMode.UNORDERED
OutputMode.ORDERED

其次

flink 不至于用多套逻辑实现异步,flink-sql 是翻译成算子,也是调用了datastream api 实现这个逻辑,因此我们来看看。
对于async 的 Operator 会有自己的工厂方法:

    /* 工厂创建 */
    public AsyncWaitOperatorFactory(
            AsyncFunction<IN, OUT> asyncFunction,
            long timeout,
            int capacity,
            AsyncDataStream.OutputMode outputMode) {
        // 定义的function,包括自定义的function。
        // 本质上我们的操作都是一个function进行处理的
        this.asyncFunction = asyncFunction;
        // 异步的情况一般是需要队列+异步完成时间实现的
        // 默认timeout:180000 capacity:100 
        this.timeout = timeout;
        this.capacity = capacity;
        // 这就是我们的是否顺序参数
        this.outputMode = outputMode;
        // 默认算子会合并在一起执行
        this.chainingStrategy = ChainingStrategy.ALWAYS;
    }
    
    /* 创建异步算子 */
    @Override
    public <T extends StreamOperator<OUT>> T createStreamOperator(
            StreamOperatorParameters<OUT> parameters) {
            // outputMode 是 Orderded
        AsyncWaitOperator asyncWaitOperator =
                new AsyncWaitOperator(
                        asyncFunction,
                        timeout,
                        capacity,
                        outputMode,
                        processingTimeService,
                        getMailboxExecutor());
        // 对asyncWaitOperator 内部进行初始化
        // 看看内部做了啥               
        asyncWaitOperator.setup(
                parameters.getContainingTask(),
                parameters.getStreamConfig(),
                parameters.getOutput());
        return (T) asyncWaitOperator;
    }

在这里,如果你用自定义的asyncLookupFunction 启动sql任务,本地进行debug到这里就会发现:
outputMode 参数值是:“ordered” 也就是保证顺序的。也就是说即使我们用asyncFunction,那么
内部还是给我们做了保证顺序。

异步如何保证顺序?

思考一下:
让你们实现一个异步处理数据的程序,但是要保证顺序输出,也就是进来的顺序和输出的顺序相同。
相信大家很容易想到用队列实现就行,为了简单,我简单画图说明一下:
请添加图片描述
1.数据流 1,2,3,4顺序到达
2.优先进入顺序队列queue ,同时异步线程(比如:CompletableFuture) 进行处理,返回future
3.程序优先从队列 queue 取出来,循环判断 future.isDone

这样必须等队列数据1 处理完成,才会去获取1的结果。但是 2 3 会异步进行处理,相当于提高了并发。当然需要控制队列长度,以及获取的超时时间。

看看flink怎么做

继续 进入AsyncWaitOperator#setup 看实现

 // Queue, into which to store the currently in-flight stream elements
 // 开始就定义了具体的队列存储
 private transient StreamElementQueue<OUT> queue;
 public void setup(/*略*/) {
        /*部分略*/
        switch (outputMode) {
            case ORDERED:
            	// 初始化元素队列(有序)
                queue = new OrderedStreamElementQueue<>(capacity);
                break;
            case UNORDERED:
                queue = new UnorderedStreamElementQueue<>(capacity);
                break;
            default:
                throw new IllegalStateException("Unknown async mode: " + outputMode + '.');
        }
        this.timestampedCollector = new TimestampedCollector<>(super.output);
    }

在operator 内部有个核心处理数据的方法:

    // 当数据真正进入算子内部
    public void processElement(StreamRecord<IN> record) throws Exception {
        StreamRecord<IN> element;
        /* 部分代码略 */
        // 先加入队列
        final ResultFuture<OUT> entry = addToWorkQueue(element);
        // ResultHandler 实现了 ResultFuture,内部有回调结果的操作
        final ResultHandler resultHandler = new ResultHandler(element, entry);
		if (timeout > 0L) {
		    // 注册一个超时的东西
            resultHandler.registerTimeout(getProcessingTimeService(), timeout);
        }
        // 调用我们的函数进行处理
        userFunction.asyncInvoke(element.getValue(), resultHandler);
  }

而在addToWorkQueue 内部,简单看看如何操作

 private ResultFuture<OUT> addToWorkQueue(StreamElement streamElement)
            throws InterruptedException {
        Optional<ResultFuture<OUT>> queueEntry;
        // 调用tryPut进行存放元素
        while (!(queueEntry = queue.tryPut(streamElement)).isPresent()) {
            mailboxExecutor.yield();
        }
        return queueEntry.get();
    }

然后看看 OrderedStreamElementQueue 的结构和 tryPut 方法

  // 内部就是一个ArrayDeque,元素是StreamElementQueueEntry
  private final Queue<StreamElementQueueEntry<OUT>> queue;
  public OrderedStreamElementQueue(int capacity) {
        this.capacity = capacity;
        this.queue = new ArrayDeque<>(capacity);
    }
 // 创建元素   
 private StreamElementQueueEntry<OUT> createEntry(StreamElement streamElement) {}
 // 存储元素
 public Optional<ResultFuture<OUT>> tryPut(StreamElement streamElement) {
   //略
   StreamElementQueueEntry<OUT> queueEntry = createEntry(streamElement);
   // Queue<StreamElementQueueEntry<OUT>> queue 存放值得地方
   queue.add(queueEntry);
 }

这里看到,创建元素之后会返回一个ResultFuture 的异步对象,因为 StreamElementQueueEntry 是继承了 ResultFuture 的接口

interface StreamElementQueueEntry<OUT> extends ResultFuture<OUT> {
   boolean isDone();
   void emitResult(TimestampedCollector<OUT> output);
   StreamElement getInputElement();
   default void completeExceptionally(Throwable error) {}
}

数据进入队列后,我们是join完成?

当我们异步处理完成数据之后,肯定会调用:

public void eval(CompletableFuture<Collection<RowData>> future, Object... keys) throws InterruptedException {
   // 返回数据
   future.complete(rowData);
}

这里实际上是会回调的 ResultFuture 的 实现下到 AsyncWaitOperator#complete


        public void complete(Collection<OUT> results) {
            if (!completed.compareAndSet(false, true)) {
                return;
            }
            processInMailbox(results);
        }
       //  processInMailbox -> 直到这里
       private void processResults(Collection<OUT> results) {
            // 取消定时器超时控制
            if (timeoutTimer != null) {
                // canceling in mailbox thread avoids
                // https://issues.apache.org/jira/browse/FLINK-13635
                timeoutTimer.cancel(true);
            }
            // update the queue entry with the result
            resultFuture.complete(results);
            // 输出已经异步函数里面返回的元素
            outputCompletedElement();
        }

然后进入刚才的队列 OrderedStreamElementQueue

private void outputCompletedElement() {
        if (queue.hasCompletedElements()) {
            // emit only one element to not block the mailbox thread unnecessarily
            queue.emitCompletedElement(timestampedCollector);
            /*略*/ 
         }   
    }

    public void emitCompletedElement(TimestampedCollector<OUT> output) {
        if (hasCompletedElements()) {
            // 是先取头节点
            final StreamElementQueueEntry<OUT> head = queue.poll();
            head.emitResult(output);
        }
    }
     // 这里控制顺序,每次判断是否完成,都是取头部元素进行处理
    public boolean hasCompletedElements() {
        return !queue.isEmpty() && queue.peek().isDone();
    }
    

    public void emitResult(TimestampedCollector<OUT> output) {
        output.setTimestamp(inputRecord);
        for (OUT r : completedElements) {
            // 发送数据
            output.collect(r);
        }
    }

这里就数据发送完成了。

小结

  1. 我们用tableApi 做 async sql 函数的时候,实际内部用了异步,保顺。注意:这里是partition 保证顺序。
  2. 基本原理是把元素加入有序队列,每次complate取判断头部元素是否完成,再往下游发送做到保持顺序
  3. 内部有对接受数据是watermark 以及超时等处理,没仔细分析。有兴趣可以再看看
    4.有问题可以留言沟通、指正
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值