之前使用Flink查询Redis数据的过程中,由于对数据一致性的要求并不是很高,当时是用MapFunction +  State 的方案。先缓存一大堆数据到State中,达到一定数量之后,将批量Key提交到Redis中进行查询。

        由于Redis性能极高,所以并没有出现什么问题,后来了解到了Flink异步IO机制,感觉使用异步IO机制实现会更加优雅一点。本文就是记录下自己对Flink异步IO的一个初步认识。

异步算子主要应用于和外部系统交互,提高吞吐量,减少等待延迟。用户只需关注业务逻辑即可,消息顺序性和一致性由Flink框架来处理:

图来自官网:

       

flinkcdc 偏移量同步 flink异步_ci

异步IO支持输出无序和有序,也支持watermark以及ExactlyOnce语义:

异步IO的核心代码都在AsyncWaitOperator里面:

switch (outputMode) {
   case ORDERED:
      queue = new OrderedStreamElementQueue<>(capacity);
      break;
   case UNORDERED:
      queue = new UnorderedStreamElementQueue<>(capacity);
      break;
   default:
      throw new IllegalStateException("Unknown async mode: " + outputMode + '.');
}

        orderedWait(有序):消息的发送顺序与接收到的顺序完全相同(包括 watermark )。

        unorderWait(无序):在ProcessingTime中是完全无序的,即哪个先完成先发送(最低延迟和消耗);在EventTime中,以watermark为边界,介于两个watermark之间的消息是乱序的,但是多个watermark之间的消息是有序的。

        异步IO处理内部会执行 userFunction.asyncInvoke(element.getValue(), resultHandler) 调用用户自己编写的方法来处理数据。userFunction就是用户自己编写的自定义方法。resultHandler就是用户在完成异步调用自己,如何把结果传入到异步IO算子中:

(ps: userFunction是基于CompletableFuture来完成开发的。CompletableFuture是 Java 8 中引入的一个类,它实现了CompletionStage接口,提供了一组丰富的方法来处理异步操作和多个任务的结果。它支持链式操作,可以方便地处理任务的依赖关系和结果转换。相比于传统的Future接口,CompletableFuture更加灵活和强大。具体demo可以看官网示例 或者 看下面参考中的链接)

@Override
public void processElement(StreamRecord<IN> element) throws Exception {
   // add element first to the queue
   final ResultFuture<OUT> entry = addToWorkQueue(element);

   // 这里的ResultHandler就是对数据和ResultFuture的一个封装
   final ResultHandler resultHandler = new ResultHandler(element, entry);

   // register a timeout for the entry if timeout is configured
   if (timeout > 0L) {
      final long timeoutTimestamp = timeout + getProcessingTimeService().getCurrentProcessingTime();

      final ScheduledFuture<?> timeoutTimer = getProcessingTimeService().registerTimer(
         timeoutTimestamp,
         timestamp -> userFunction.timeout(element.getValue(), resultHandler));

      resultHandler.setTimeoutTimer(timeoutTimer);
   }

   // 调用用户编写的方法。 传入的resultHandler就是让用户在异步完成的时候传值用的
   userFunction.asyncInvoke(element.getValue(), resultHandler);
}
 
 
@Override
// resultHandler类内部的complete方法就是在用户自定义函数中传结果用的,最终执行结果会调用processInMainBox(results)方法,将结果发送给下游算子
public void complete(Collection<OUT> results) {
   Preconditions.checkNotNull(results, "Results must not be null, use empty collection to emit nothing");

   // already completed (exceptionally or with previous complete call from ill-written AsyncFunction), so
   // ignore additional result
   if (!completed.compareAndSet(false, true)) {
      return;
   }

   processInMailbox(results);
}

orderedWait 实现:

        有序的话很简单,就是创建一个队列,然后从队首取元素即可

public OrderedStreamElementQueue(int capacity) {
   Preconditions.checkArgument(capacity > 0, "The capacity must be larger than 0.");

   this.capacity = capacity;
   // 所有的元素都放在这么一个队列里面
   this.queue = new ArrayDeque<>(capacity);
}

@Override
public boolean hasCompletedElements() {
   // 然后FIFO就好了
   return !queue.isEmpty() && queue.peek().isDone();
}

unorderWait 实现:

        无序的话实现就会稍微复杂点。queue里面放的不是一条条数据,而是一个个segment。数据存放在segment中,中间使用watermark分隔(每条watermark都会有自己单独的segment)。

public UnorderedStreamElementQueue(int capacity) {
   Preconditions.checkArgument(capacity > 0, "The capacity must be larger than 0.");

   this.capacity = capacity;
   // most likely scenario are 4 segments <elements, watermark, elements, watermark>
   this.segments = new ArrayDeque<>(4);
   this.numberOfEntries = 0;
}
 
 
// 每个segment内部会有两个队列:未完成 和 已完成。未完成的数据在完成之后会放置到已完成队列里面,然后发送到下游算子
static class Segment<OUT> {
   /** Unfinished input elements. */
   private final Set<StreamElementQueueEntry<OUT>> incompleteElements;

   /** Undrained finished elements. */
   private final Queue<StreamElementQueueEntry<OUT>> completedElements;

   Segment(int initialCapacity) {
      incompleteElements = new HashSet<>(initialCapacity);
      completedElements = new ArrayDeque<>(initialCapacity);
   }

   /**
    * Signals that an entry finished computation.
    */
   void completed(StreamElementQueueEntry<OUT> elementQueueEntry) {
      // adding only to completed queue if not completed before
      // there may be a real result coming after a timeout result, which is updated in the queue entry but
      // the entry is not re-added to the complete queue
      if (incompleteElements.remove(elementQueueEntry)) {
         completedElements.add(elementQueueEntry);
      }
   }

一致性实现:

        一致性实现看起来很简单,就是将queue中未完成/已完成的数据备份下来。这里的queue就是上面的 OrderedStreamElementQueue 和 UnorderedStreamElementQueue:

@Override
public void snapshotState(StateSnapshotContext context) throws Exception {
   super.snapshotState(context);

   ListState<StreamElement> partitionableState =
      getOperatorStateBackend().getListState(new ListStateDescriptor<>(STATE_NAME, inStreamElementSerializer));
   partitionableState.clear();

   // 这里的queue == OrderedStreamElementQueue / UnorderedStreamElementQueue
   try {
      partitionableState.addAll(queue.values());
   } catch (Exception e) {
      partitionableState.clear();

      throw new Exception("Could not add stream element queue entries to operator state " +
         "backend of operator " + getOperatorName() + '.', e);
   }
}