30.DataStream API之Operators(Async IO)

flink 1.9

本页阐述了使用Flink的API来进行外部数据存储的异步I/O,对于不熟悉异步或者事件驱动编程的用户,学习一篇关于Future和事件驱动编程可能会很有用。

注意:关于异步I/O的详细设计和实现可以在异步I/O设计和实现这篇文章找到(FLIP-12: Asynchronous I/O Design and Implementation)。

 

The need for Asynchronous I/O Operations

在与外部系统交互时(例如,使用存储在数据库中的数据流事件),外部系统通信的延迟不应该堵塞流应用程序的其他处理工作,否则会影响流处理的吞吐量。

原始的访问外部系统中的数据,例如对于MapFunction类,通常是同步交互synchronous :将一个资源请求发送到数据库,MapFunction将会一直等待资源,直到接收到响应为止。通常这种情况下,资源的等待会占用函数执行的绝大部分时间。

与数据库的异步交互意味着一个并行函数实例可以同时处理多个请求任务,并同时接收多个响应任务。这样,等待的时间就可以被其他的请求和接收响应所占用,以便处理其他请求任务,从而提高流处理的吞吐量。

注:改进吞吐量只有增加MapFunction并行度,这在某些情况下是可能的,但通常存在非常高的资源成本:有更多的并行MapFunction实例意味着更多的任务,线程,Flink-internal网络连接,网络连接到数据库,缓冲区,和一般内部记帐开销。

 

Prerequisites前提

(1)首先需要一个客户端,数据库支持通过该客户端来进行异步调用请求,目前流行的数据库基本上都存在这样的一个客户端。

(2)对于没有这种客户端的数据库,用户可以通过创建多个客户端,然后使用线程池同步调用这些同步客户端,从而将其转换为有限的并发客户端。然而,这个方法通常比纯粹的异步客户端性能要低一些。

 

异步I/O API

Flink的Async I/O允许用户在数据流中使用异步的请求客户端,这个API会处理数据流间的交互行为,同时还处理数据的顺序、事件时间、容错等。

假标数据库已经具有异步客户端,要实现一个通过异步I/O来操作数据库的应用还需要三步:

1、实现一个用来分发请求的AsyncFunction类;

2、获取操作结果并将其传递给ResultFuture的回调callback函数;

3、将异步async I/O操作作为转换操作应用到DataStream中。

下面代码展示了这个基本模式:

// This example implements the asynchronous request and callback with Futures that have the
// interface of Java 8's futures (which is the same one followed by Flink's Future)

/**
 * An implementation of the 'AsyncFunction' that sends requests and sets the callback.
 */
class AsyncDatabaseRequest extends RichAsyncFunction<String, Tuple2<String, String>> {

    /** The database specific client that can issue concurrent requests with callbacks */
    private transient DatabaseClient client;

    @Override
    public void open(Configuration parameters) throws Exception {
        client = new DatabaseClient(host, post, credentials);
    }

    @Override
    public void close() throws Exception {
        client.close();
    }

    @Override
    public void asyncInvoke(String key, final ResultFuture<Tuple2<String, String>> resultFuture) throws Exception {

        // issue the asynchronous request, receive a future for result
        final Future<String> result = client.query(key);

        // set the callback to be executed once the request by the client is complete
        // the callback simply forwards the result to the result future
        CompletableFuture.supplyAsync(new Supplier<String>() {

            @Override
            public String get() {
                try {
                    return result.get();
                } catch (InterruptedException | ExecutionException e) {
                    // Normally handled explicitly.
                    return null;
                }
            }
        }).thenAccept( (String dbResult) -> {
            resultFuture.complete(Collections.singleton(new Tuple2<>(key, dbResult)));
        });
    }
}

// create the original stream
DataStream<String> stream = ...;

// apply the async I/O transformation
DataStream<Tuple2<String, String>> resultStream =
    AsyncDataStream.unorderedWait(stream, new AsyncDatabaseRequest(), 1000, TimeUnit.MILLISECONDS, 100);

注意:ResultFuture是在ResultFuture.complete的第一次调用中完成的。所有后续的complete 调用都将被忽略。

下面的两个参数控制了异步操作:

  • Timeout: timeout定义了异步操作过了多长时间后会被丢弃,此参数防止死请求/失败请求。
  • Capacity: 此参数定义同时可以处理多少异步请求。尽管异步I/O方法通常会带来更高的吞吐量,但是操作算子仍然可能成为flink流应用程序的瓶颈。限制并发请求的数量可以确保操作算子不会无限积压待处理的异步请求,当容量一旦耗尽,就会立即触发flink的背压机制。

 

Timeout Handling

当异步I/O请求超时时,默认情况下会引发异常并重新启动作业。如果要处理超时,可以重写AsyncFunction#timeout方法。

 

Order of Results

AsyncFunction发出的并发请求经常是以无序的形式完成,取决于哪个请求先完成。为了控制发出请求结果的顺序,Flink提供了两种模式:

  • Unordered:结果记录将在异步请求完成后发出,数据流中的记录的顺序通过异步I/O操作后会与先前的不一致。processing time流模式具有低延迟和低消耗特点。可以通过AsyncDataStream.unorderedWait(...)来使用这种模式。
  • Ordered:在这种情况下,流的顺序是保留的,结果记录发出的顺利与异步请求触发的顺序(算子输入记录的顺序)一致。为了实现这一点,算子会将结果记录缓存起来,直到所有的处理记录都被发出(或者超时)为止。这常常会导致一定程度的延迟和checkpoint的消耗,因此,跟非排序unordered 模式相比,记录或者结果会被长时间保存在checkpoint State中。通过AsyncDataStream.orderedWait(...)来使用这种模式。

 

Event Time

当流应用程序处理事件时间event time时,异步I/O操作算子将正确处理水印。具体地说,这两种有序模式是:

  • Unordered:水印不会超过记录,反之亦然(记录也不会超过水印),这意味着水印建立了一个有序边界。记录只在水印之间无序地发出。产生在某个水印之后的记录仅在该水印发出之后才会被发出。而水印只会在所有输入结果记录发出后才会发出,(即水印间的记录是无序,但是水印与水印之间的记录是有序的,局部无限,全局有序)。

这意味着,在存在水印的情况下,无序模式引入了一些与有序模式相同的延迟和管理开销。该开销的大小取决于水印的频率。

 

  • Ordered:保存记录的水印顺序,就像保存记录之间的顺序一样。与处理时间 processing time相比,开销没有显著变化。

请记住,插入Ingestion时间是事件时间的特殊情况,它根据源处理时间自动生成水印。

 

Fault Tolerance Guarantees

异步I/O操作提供了exactly-once容错性保证,它将异步请求的记录存储在checkpoint中,当从故障中恢复时,将会从checkpoint中恢复和重新触发异步请求。

 

使用提醒Implementation Tips

对于Futures 的实现使其具有用于回调的org.apache.flink.runtime.concurrent.Executors(或Scala中的ExecutionContext)类,我们建议使用DirectExecutor,因为回调通常只做很少的工作,而DirectExecutor可以避免额外的线程到线程切换开销。回调通常只将结果提交给ResultFuture,后者将其添加到输出缓冲区。从这里开始,包含记录发送和与检查点保存记录的交互的繁重逻辑将在专用线程池中进行。

public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {}

DirectExecutor可以通过org.apache.flink.runtime.concurrent.Executors.directExecutor()com.google.common.util.concurrent.MoreExecutors.directExecutor()获得。

 

The AsyncFunction is not called Multi-Threaded

这里我们要明确指出的一个常见容易混淆的概念:AsyncFunction不是以多线程方式的进行调用的。运行过程中,AsyncFunction只存在一个实例,并且对于流的各个分区中的每个记录,它都会被依次调用。除非asyncInvoke(…)方法能够快速返回和依赖一个回调callback(通过客户端回调),否则它将不会产生正确的异步I/O。

 

例如,以下模式将会导致asyncInvoke(…)函数阻塞,从而使异步行为无效:

  1. 使用其 lookup/query方法调用阻塞的数据库客户端,直到返回结果;
  2. 通过在future-type对象上进行阻塞/等待来返回asyncInvoke(…)方法中的异步客户端。

由于一致性的原因,AsyncFunction操作算子(AsyncWaitOperator)当前必须位于操作算子链的前端

由于问题FLINK-13063中给出的原因,我们目前必须打破AsyncWaitOperator的操作算子链,以防止潜在的一致性问题。这是对以前支持链接的行为的一个更改。需要旧行为并接受可能违反一致性保证的用户可以实例化AsyncWaitOperator并手动将其添加到作业图中,并将链接策略设置回通过AsyncWaitOperator#setChainingStrategy(ChainingStrategy.ALWAYS)

 

https://www.jianshu.com/p/a04f9ea5fe3d

https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/stream/operators/asyncio.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值