(十)Flink Datastream API 编程指南 算子-5 外部数据访问的异步I/O

文章目录

  • 需要异步I/O操作
  • 前提条件
  • Async I/O API
    • 超时处理
    • 结果的顺序
    • Event Time
    • 容错担保机制
    • Implementation Tips
    • 说明

本页面解释了使用Flink的API与外部数据存储进行异步I/O。对于不熟悉异步或事件驱动编程的用户,一篇关于Futures和事件驱动编程的文章可能是有用的准备。

注意:关于异步I/O实用程序的设计和实现的详细信息可以在建议和设计文档FLIP-12:异步I/O设计和实现中找到。

需要异步I/O操作

当与外部系统交互时(例如,当使用存储在数据库中的数据丰富流事件时),需要注意与外部系统的通信延迟不会主导流应用程序的全部工作。

单纯地访问外部数据库中的数据,例如在MapFunction中,通常意味着同步交互:一个请求被发送到数据库,MapFunction等待直到接收到响应。在许多情况下,这种等待占据了函数的绝大多数时间。

与数据库的异步交互意味着单个并行函数实例可以并发地处理许多请求并并发地接收响应。这样,等待时间可以与发送其他请求和接收响应重叠。至少,等待时间可以分摊给多个请求。这在大多数情况下导致更高的流吞吐量。
(十)Flink Datastream API 编程指南 算子-5 外部数据访问的异步I/O_第1张图片

注意:通过将MapFunction扩展到一个非常高的并行度来提高吞吐量在某些情况下也是可能的,但通常会以非常高的资源成本:拥有更多的并行MapFunction实例意味着更多的任务、线程、flink -内部网络连接、到数据库的网络连接、缓冲区和一般的内部簿记开销。

前提条件

如上一节所述,对数据库(或键/值存储)实现适当的异步I/O需要支持异步请求的数据库的客户端。许多流行的数据库都提供这样的客户机。

在没有这种客户机的情况下,可以通过创建多个客户机并使用线程池处理同步调用来尝试将同步客户机转换为有限并发客户机。但是,这种方法的效率通常低于适当的异步客户机。

Async I/O API

Flink的异步I/O API允许用户对数据流使用异步请求客户端。该API处理与数据流的集成,以及处理顺序、事件时间、容错等。

假设目标数据库有一个异步客户端,使用异步I/O对数据库实现流转换需要三个部分:

  • 分发请求的AsyncFunction的实现
  • 获取操作结果并将其传递给ResultFuture的回调
  • 在DataStream上应用异步I/O操作作为转换

下面的代码示例演示了基本模式:

// 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完成的。所有随后完成的调用将被忽略。

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

  • Timeout: 超时定义异步请求在被认为失败之前可能需要的时间。这个参数防止死亡/失败的请求。
  • Capacity:这个参数定义了有多少异步请求可以同时进行。尽管异步I/O方法通常会带来更好的吞吐量,但算子仍然可能成为流应用程序中的瓶颈。限制并发请求的数量可以确保算子不会积压越来越多的待处理请求,但一旦容量耗尽,就会触发反压力。

超时处理

当异步I/O请求超时时,默认情况下会抛出异常并重新启动作业。如果你想处理超时,你可以重写asyncfunction# timeout方法。确保在重写时调用ResultFuture.complete()或Resultfuture.completeExceptionally(),以便向Flink表明对该输入记录的处理已经完成。如果您不想在超时发生时发出任何记录,可以调用ResultFuture.complete(Collections.emptyList())。

结果的顺序

由AsyncFunction发出的并发请求通常以某种未定义的顺序完成,这取决于哪个请求先完成。为了控制结果记录发出的顺序,Flink提供了两种模式:

  • 无序:异步请求一完成就会发出结果记录。在异步I/O operator之后,流中记录的顺序与之前不同。当使用处理时间作为基本时间特性时,这种模式具有最低的延迟和最低的开销。此模式使用AsyncDataStream.unorderedWait(…)
  • 有序:在这种情况下,将保留流的顺序。触发结果记录的顺序与触发异步请求的顺序相同(算子输入记录的顺序)。为了实现这一点,operator缓冲一个结果记录,直到它前面的所有记录都发出(或超时)。这通常会在检查点中引入一些额外的延迟和开销,因为与无序模式相比,记录或结果在检查点状态中维护的时间更长。此模式使用AsyncDataStream.orderedWait(…)。

Event Time

当流应用程序处理事件时间时,异步I/O操作符将正确处理水印。这对两阶模态的具体含义如下:

  • 无序:水印不超过记录,反之亦然,这意味着水印建立一个有序的边界。记录仅在水印之间无序发出。在某个水印之后出现的记录只有在该水印发出之后才会发出。接着,只有在水印发出之前的所有输入结果记录之后,水印才会发出。
    这意味着,在存在水印的情况下,无序模式会引入一些与有序模式相同的延迟和管理开销。该开销的大小取决于水印频率。
  • 顺序:保留水印和记录的顺序,就像保留记录之间的顺序一样。与处理时间相比,开销没有显著变化。

请记住,摄取时间是事件时间的一种特殊情况,自动生成的水印基于源处理时间。

容错担保机制

异步I/O operator提供完全恰好一次的容错保证。它在检查点中存储正在进行的异步请求的记录,并在从故障中恢复时恢复/重新触发请求。

Implementation Tips

对于有一个Executor(或者Scala中的ExecutionContext)用于回调的Futures实现,我们建议使用一个DirectExecutor,因为回调通常只做很少的工作,而且DirectExecutor可以避免额外的线程到线程切换开销。回调通常只将结果传递给ResultFuture, ResultFuture将结果添加到输出缓冲区。从那时起,包括记录发射和与检查点簿记的交互在内的繁重逻辑无论如何都发生在专用的线程池中。

一个DirectExecutor可以通过org.apache.flink.util.concurrent. executor . DirectExecutor()或com.google.common.util.concurrent. moreexecutor . DirectExecutor()获得。

说明

AsyncFunction不是多线程的
我们在这里要明确指出的一个常见的困惑是,AsyncFunction不是以多线程的方式调用的。AsyncFunction只存在一个实例,它会对流各自分区中的每条记录顺序调用。除非asyncInvoke(…)方法快速返回并依赖于回调(由客户端),否则它不会导致正确的异步I/O。

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

  • 使用其查找/查询方法调用阻塞的数据库客户机,直到接收到结果为止
  • 阻塞/等待异步客户端在asyncInvoke(…)方法中返回的future类型对象

AsyncFunction(AsyncWaitOperator)可以在作业图的任何地方使用,除非它不能链接到SourceFunction/SourceStreamTask。

你可能感兴趣的:(flink,flink)