【翻译】Flink 异步I / O访问外部数据

本文深入探讨了Apache Flink中的异步I/O机制,解释了如何利用异步I/O提升与外部数据存储交互的效率,特别是在处理数据库请求时。文章详细介绍了异步I/O的实现原理,包括其API、异步函数的使用、结果处理及容错机制。此外,还提供了使用Java和Scala实现异步I/O的具体代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文来自官网翻译: Asynchronous I/O for External Data Access

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

注:有关异步I / O实用程序的设计和实现的详细信息,请参阅提议和设计文档 FLIP-12:异步I / O设计和实现

需要异步I / O操作

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

访问外部数据库中的数据,例如在 MapFunction中,通常意味着同步交互:将请求发送到数据库并MapFunction等待直到收到响应。在许多情况下,这种等待构成了功能的绝大部分时间。

与数据库的异步交互意味着单个并行函数实例可以同时处理许多请求并同时接收响应。这样,等待时间可以覆盖发送其他请求和接收响应。至少,等待时间是在多个请求上分摊的。可以使大多数情况下流量吞吐量更高。

注意:MapFunction在某些情况下,仅通过扩展到非常高的并行度来提高吞吐量,但通常会产生非常高的资源成本:拥有更多并行MapFunction实例意味着更多任务,线程,Flink内部网络连接,网络与数据库,缓冲区和一般内部数据开销。

先决条件

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

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

异步I / O API

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

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

  • 实现AsyncFunction分派请求
  • 一个callback,它接收操作的结果并将其交给ResultFuture
  • 在DataStream上应用异步I / O操作作为转换

以下代码示例说明了基本模式:

Java

// 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);

scala

/**
 * An implementation of the 'AsyncFunction' that sends requests and sets the callback.
 */
class AsyncDatabaseRequest extends AsyncFunction[String, (String, String)] {

    /** The database specific client that can issue concurrent requests with callbacks */
    lazy val client: DatabaseClient = new DatabaseClient(host, post, credentials)

    /** The context used for the future callbacks */
    implicit lazy val executor: ExecutionContext = ExecutionContext.fromExecutor(Executors.directExecutor())


    override def asyncInvoke(str: String, resultFuture: ResultFuture[(String, String)]): Unit = {

        // issue the asynchronous request, receive a future for the result
        val resultFutureRequested: Future[String] = client.query(str)

        // set the callback to be executed once the request by the client is complete
        // the callback simply forwards the result to the result future
        resultFutureRequested.onSuccess {
            case result: String => resultFuture.complete(Iterable((str, result)))
        }
    }
}

// create the original stream
val stream: DataStream[String] = ...

// apply the async I/O transformation
val resultStream: DataStream[(String, String)] =
    AsyncDataStream.unorderedWait(stream, new AsyncDatabaseRequest(), 1000, TimeUnit.MILLISECONDS, 100)

重要提示:ResultFuture在第一次通话时完成ResultFuture.complete。所有后续complete调用都将被忽略。

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

  • Timeout:超时定义异步请求在被视为失败之前可能需要多长时间。此参数可防止死亡/失败请求。

  • Capacity:此参数定义可以同时进行的异步请求数。尽管异步I / O方法通常会带来更好的吞吐量,但异步I / O 的操作仍然可能成为流应用程序的瓶颈。限制并发请求的数量可确保操作不会不断累积,积压增加的待处理请求,一旦容量耗尽,它将触发反压。

超时处理

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

结果顺序

AsyncFunction一些未定义的顺序经常完成的并发请求,基于哪个请求首先完成。为了控制发出结果记录的顺序,Flink提供了两种模式:

  • 无序:异步请求完成后立即发出结果记录。在异步I / O运算符之后,流中记录的顺序与以前不同。当使用处理时间作为基本时间特性时,此模式具有最低延迟和最低开销。使用AsyncDataStream.unorderedWait(...)此模式。

  • 有序:在这种情况下,保留流顺序。结果记录的发出顺序与触发异步请求的顺序相同(运算符输入记录的顺序)。为此,运算符缓冲结果记录,直到其所有先前记录被发出(或超时)。这通常会在检查点中引入一些额外的延迟和一些开销,因为与无序模式相比,记录或结果在检查点状态下保持更长的时间。使用AsyncDataStream.orderedWait(...)此模式。

活动时间

当流应用程序与事件时间一起工作,异步I / O操作符将正确处理水印。这意味着两种订单模式具体如下:

  • Unordered:水印不会超过记录,反之亦然,这意味着水印建立了一个顺序边界。记录仅在水印之间无序发出。只有在发出水印后才会发出某个水印后发生的记录。反过来,只有在发出水印之前输入的所有结果记录之后才会发出水印。

    这意味着,在水印的存在,将无序的方式介绍了一些相同的延迟和管理开销的顺序模式一样。开销量取决于水印频率。

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

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

容错保证

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

实施技巧

对于使用Executor(或Scala中的ExecutionContext)进行回调的Futures实现,我们建议使用DirectExecutor,因为回调通常只做最小的工作,而DirectExecutor避免了额外的线程到线程的切换开销。 回调通常只将结果传递给ResultFuture,后者将其添加到输出缓冲区。 从那里开始,包括记录发射和与检查点簿记交互的重要逻辑无论如何都发生在专用线程池中。

警告

AsyncFunction不是为多线程

我们想在这里明确指出的常见混淆AsyncFunction是以多线程方式调用。只存在一个实例,AsyncFunction并且对于流的相应分区中的每个记录顺序调用它。除非该asyncInvoke(...)方法快速返回并依赖于回调(由客户端),否则它将不会导致适当的异步I / O.

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

  • 使用其查找/查询方法调用阻塞的数据库客户端,直到收到结果为止

  • 阻止/等待asyncInvoke(...)方法内异步客户端返回的future-type对象

 实例:Flink 异步IO访问外部数据(mysql篇)

转载于:https://www.cnblogs.com/Springmoon-venn/p/11081201.html

<think>好的,用户想知道如何在Flink中通过JDBC实现异步sink操作,特别是请求一个示例。首先,我需要回忆Flink异步I/O的相关知识。Flink异步I/O允许在流处理中异步访问外部系统,比如数据库,从而提高吞吐量,避免同步等待带来的延迟。 用户提到的JDBC通常是同步的,因为JDBC本身是阻塞的。但结合异步I/O,可以将JDBC操作放到异步函数中执行。可能需要使用AsyncFunction,然后通过AsyncWaitOperator来处理异步请求。同时,需要确保数据库连接池是异步的,或者将JDBC调用放在单独的线程池中,避免阻塞主线程。 接下来,我需要检查用户提供的引用内容。引用[1]和[2]提到了CompletableFuture和异步处理的回调机制,引用[3]解释了异步函数和同步函数的区别,以及如何通过返回异步函数来提高吞吐率。这些引用指出,实现异步访问需要用到异步函数,并在数据准备好时回调complete方法。 因此,实现JDBC异步sink的大致步骤应该是:创建一个实现AsyncSinkFunction的类,重写asyncInvoke方法,在其中执行JDBC操作异步化。可能需要结合线程池来管理数据库连接,使用CompletableFuture来处理异步结果,并在完成时调用resultFuture.complete()或处理异常。 需要注意正确管理数据库连接,避免资源泄漏。另外,要处理重试机制和错误情况,比如网络波动导致的连接失败。可能还需要考虑批处理提交,减少频繁的数据库访问,提高效率。 最后,用户需要一个具体的示例,所以需要编写一个代码示例,展示如何继承RichAsyncFunction,使用线程池执行JDBC插入,并在回调中完成结果。同时,要提醒用户配置合适的超时时间和容量,以及数据库连接池的参数优化。</think>在Flink中实现JDBC异步sink需要结合异步I/O接口和连接池技术,以下是具体实现步骤和示例: ### 一、实现原理 1. **异步I/O机制**:通过实现$RichAsyncFunction$接口,将JDBC操作封装为异步非阻塞调用[^3] 2. **线程池管理**:使用$ExecutorService$创建专用线程池执行数据库操作,避免阻塞Flink任务管理器线程 3. **连接池优化**:需配合HikariCP等高性能连接池,建议设置最小连接数等于最大并行度 ### 二、核心代码示例 ```java public class AsyncJdbcSink extends RichAsyncFunction<String, Void> { private transient DataSource dataSource; private transient ExecutorService executorService; @Override public void open(Configuration parameters) { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/test"); config.setUsername("root"); config.setPassword("root"); config.setMaximumPoolSize(10); dataSource = new HikariDataSource(config); executorService = Executors.newFixedThreadPool(5); } @Override public void asyncInvoke(String record, ResultFuture<Void> resultFuture) { CompletableFuture .supplyAsync(() -> executeInsert(record), executorService) .thenAccept(result -> resultFuture.complete(null)) .exceptionally(e -> { resultFuture.completeExceptionally(e); return null; }); } private Void executeInsert(String record) { try (Connection conn = dataSource.getConnection(); PreparedStatement stmt = conn.prepareStatement( "INSERT INTO logs(content) VALUES (?)")) { stmt.setString(1, record); stmt.executeUpdate(); } catch (SQLException e) { throw new RuntimeException("Insert failed", e); } return null; } @Override public void close() { executorService.shutdown(); ((HikariDataSource)dataSource).close(); } } ``` ### 三、使用配置要点 1. **超时设置**:通过$.setAsyncTimeout(60_000)$配置异步操作超时时间 2. **容量控制**:使用$.setBufferTimeout(100)$限制未完成请求的最大数量 3. **异常处理**:建议重写$AsyncFunction#timeout$方法处理超时异常 ### 四、性能优化建议 1. **批量写入**:采用`addBatch()`实现批量提交 $$ \sum_{i=1}^{n} batchSize_i \leq maxBatchSize $$ 2. **连接复用**:单个TaskManager共享连接池 3. **背压处理**:监控$inFlightRequests$指标,设置合理的缓冲区大小
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值