从0到1Flink的成长之路(二十一)-异步IO

Posted 熊老二-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从0到1Flink的成长之路(二十一)-异步IO相关的知识,希望对你有一定的参考价值。

异步IO

介绍
Async I/O是阿里巴巴贡献给社区的一个呼声非常高的特性,于1.2版本引入。主要目的是为了解决与外部系统交互时网络延迟成为了系统瓶颈的问题。

异步IO操作的需求

asynico.html

流计算系统中经常需要与外部系统进行交互,通常的做法,如向数据库发送用户A的查询请求,然后等待结果返回,在之前,程序无法发送用户B的查询请求。这是一种同步访问方式,如下图所示
在这里插入图片描述
左图所示:通常实现方式是向数据库发送用户a的查询请求(例如在MapFunction中),然后等待结果返回,在这之前,无法发送用户b的查询请求,这是一种同步访问的模式,图中棕色的长条标识等待时间,可以发现网络等待时间极大的阻碍了吞吐和延迟

右图所示:为了解决同步访问的问题,异步模式可以并发的处理多个请求和回复,可以连续的向数据库发送用户a、b、c、d等的请求,与此同时,哪个请求的回复先返回了就处理哪个回复,从而连续的请求之间不需要阻塞等待,这也正是Async I/O的实现原理

使用 Aysnc I/O 前提条件
数据库(或key/value存储系统)提供支持异步请求的client(如java的vertx);
没有异步请求客户端的话,也可以将同步客户端丢到线程池中执行作为异步客户端;

Async I/O API
Async I/O API 允许用户在数据流中使用异步客户端访问外部存储,该API处理与数据流的集成,以及消息顺序性(Order),事件时间(EventTime),一致性(容错)等脏活累活,用户只专注于业务。

在这里插入图片描述
如果目标数据库中有异步客户端,则三步即可实现异步流式转换操作(针对该数据库的异步):
实现用来分发请求的AsyncFunction,用来向数据库发送异步请求并设置回调
获取操作结果的callback,并将它提交给ResultFuture
将异步I/O操作应用于DataStream

在这里插入图片描述
案例:异步Redis

需求:
异步IO实现读入Redis中的数据
数据准备

/export/server/redis/bin/redis-cli -h node1 -p 6379

操作命令如下:

hset AsyncReadRedis beijing 1 hset AsyncReadRedis shanghai 2 hset
AsyncReadRedis guangzhou 3 hset AsyncReadRedis shenzhen 4 hset
AsyncReadRedis hangzhou 5 hset AsyncReadRedis wuhan 6 hset
AsyncReadRedis chengdu 7 hset AsyncReadRedis tianjin 8 hset
AsyncReadRedis chongqing 9 HGETALL AsyncReadRedis

ab.txt 文件中插入数据


> 1,beijing 
> 2,shanghai 
> 3,guangzhou 
> 4,shenzhen 
> 5,hangzhou
>  6,wuhan
> 7,chengdu 
> 8,tianjin 
> 9,chongqing

演示代码
异步请求Redis获取数据AsyncReadRedis:

package xx.xxxxxx.flink.asyncio;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.async.ResultFuture;
import org.apache.flink.streaming.api.functions.async.RichAsyncFunction;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
public class AsyncReadRedis extends RichAsyncFunction<String, String> {
// Jedis 连接池对象
private JedisPool jedisPool = null ;
// Jedis 对象
private Jedis jedis = null ;
@Override
public void open(Configuration parameters) throws Exception {
jedisPool = new JedisPool(
new JedisPoolConfig(), //
"node1.itcast.cn", //
6379, //
10000 //
);
jedis = jedisPool.getResource() ;
}
@Override
public void asyncInvoke(String input, ResultFuture<String> resultFuture) throws Exception {
System.out.println("Input: " + input);
// 发起异步请求,返回结果Future
CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
// 数据样本:1,beijing
String[] split = input.split(",");
// 从Redis获取值
String reply = jedis.hget("AsyncReadRedis", split[1]);
System.out.println("Output: " + reply);
return reply;
}
}).thenAccept((String dbResult) -> {
// 设置请求完成时的回调:将结果传递给collector
resultFuture.complete(Collections.singleton(dbResult));
});
}
@Override
public void close() throws Exception {
// 关闭Jedis连接
if(null != jedis) jedis.close();
}
}

Flink流式程序,从文本文件加载有界流数据,进行异步请求获取数据,代码如下:

package xx.xxxxxx.flink.asyncio;
import org.apache.flink.streaming.api.datastream.AsyncDataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import java.util.concurrent.TimeUnit;
public class StreamAsyncRedisRead {
public static void main(String[] args) throws Exception {
// 1. 初始化流计算运行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 2. 从文件读取数据
DataStreamSource<String> inputStream = env.readTextFile("datas/ab.txt");
// 3. 异步操作,将DataStream数据保存至Redis
SingleOutputStreamOperator<String> resultDataStream = AsyncDataStream.orderedWait(
inputStream, //
new AsyncReadRedis(),
90000,
TimeUnit.MICROSECONDS,
1
);
resultDataStream.print().setParallelism(2) ;
// 流式应用运行
env.execute(StreamAsyncRedisRead.class.getSimpleName()) ;
}
}

异步IO原理

AsyncDataStream
AsyncDataStream是一个工具类,用于将AsyncFunction应用于DataStream,AsyncFunction发出的并发请求都是无序的,该顺序基于哪个请求先完成,为了控制结果记录的发出顺序,flink提供了两种模式,分别对应AsyncDataStream的两个静态方法,OrderedWait和unorderedWait

orderedWait(有序):消息的发送顺序与接收到的顺序相同(包括 watermark ),也就是
先进先出。
unorderWait(无序):
在ProcessingTime中完全无序,即哪个请求先返回结果就先发送(最低延迟和最低消耗)

在EventTime中,以watermark为边界,介于两个watermark之间的消息可以乱序,但是
watermark和消息之间不能乱序,这样既认为在无序中又引入了有序,这样就有了与有
序一样的开销。

AsyncDataStream.(un)orderedWait 的主要工作就是创建了一个 AsyncWaitOperator。
AsyncWaitOperator 是支持异步 IO 访问的算子实现,该算子会运行 AsyncFunction 并处理异步返回的结果,其内部原理如下图所示。

在这里插入图片描述
如图所示,AsyncWaitOperator 主要由两部分组成:
StreamElementQueue
Emitter

StreamElementQueue 是一个 Promise 队列,所谓 Promise 是一种异步抽象表示将来会有一个值(海底捞排队给你的小票),这个队列是未完成的 Promise 队列,也就是进行中的请求队列。Emitter 是一个单独的线程,负责发送消息(收到的异步回复)给下游。

图中E5表示进入该算子的第五个元素(”Element-5”)

在执行过程中首先会将其包装成一个 “Promise” P5,然后将P5放入队列

最后调用 AsyncFunction 的 ayncInvoke 方法,该方法会向外部服务发起一个异步的请求,
并注册回调

回调会在异步请求成功返回时调用 AsyncCollector.collect 方法将返回的结果交给框架处理。

实际上 AsyncCollector 是一个 Promise ,也就是 P5,在调用 collect 的时候会标记
Promise 为完成状态,并通知 Emitter 线程有完成的消息可以发送了。

Emitter 就会从队列中拉取完成的 Promise ,并从 Promise 中取出消息发送给下游。

消息的顺序性

上文提到 Async I/O 提供了两种输出模式。其实细分有三种模式:
有序
ProcessingTime 无序
EventTime 无序

Flink 使用队列来实现不同的输出模式,并抽象出一个队列的接口(StreamElementQueue),这种分层设计使得AsyncWaitOperator和Emitter不用关心消息的顺序问题。StreamElementQueue
有两种具体实现,分别是 OrderedStreamElementQueue 和UnorderedStreamElementQueue。
UnorderedStreamElementQueue 比较有意思,它使用了一套逻辑巧妙地实现完全无序和
EventTime 无序。

有序
有序比较简单,使用一个队列就能实现。所有新进入该算子的元素(包括 watermark),都会包装成 Promise 并按到达顺序放入该队列。如下图所示,尽管P4的结果先返回,但并不会发送,只有 P1 (队首)的结果返回了才会触发 Emitter 拉取队首元素进行发送。

在这里插入图片描述
ProcessingTime 无序

ProcessingTime 无序也比较简单,因为没有 watermark,不需要协调 watermark 与消息的顺序
性,所以使用两个队列就能实现,一个 uncompletedQueue 一个 completedQueue。所有新进入该算子的元素,同样的包装成 Promise 并放入 uncompletedQueue 队列,当
uncompletedQueue队列中任意的Promise返回了数据,则将该 Promise 移
到 completedQueue 队列中,并通知 Emitter 消费。如下图所示:
在这里插入图片描述
EventTime 无序
EventTime 无序类似于有序与 ProcessingTime 无序的结合体。因为有 watermark,需要协调watermark与消息之间的顺序性,所以uncompletedQueue中存放的元素从原先的 Promise 变成了 Promise 集合。

如果进入算子的是消息元素,则会包装成 Promise 放入队尾的集合中

如果进入算子的是 watermark,也会包装成 Promise 并放到一个独立的集合中,再将
该集合加入到 uncompletedQueue 队尾,最后再创建一个空集合加
到 uncompletedQueue 队尾

这样,watermark 就成了消息顺序的边界。

只有处在队首的集合中的 Promise 返回了数据,才能将该 Promise 移到
completedQueue

队列中,由 Emitter 消费发往下游。

只有队首集合空了,才能处理第二个集合。

这样就保证了当且仅当某个 watermark 之前所有的消息都已经被发送了,该 watermark 才能被
发送。过程如下图所示:

在这里插入图片描述

以上是关于从0到1Flink的成长之路(二十一)-异步IO的主要内容,如果未能解决你的问题,请参考以下文章

从0到1Flink的成长之路(二十一)-Flink Table API 与 SQL

从0到1Flink的成长之路(二十一)-Flink+Kafka:End-to-End Exactly-Once代码示例

从0到1Flink的成长之路(二十一)-Flink+Kafka:End-to-End Exactly-Once

从0到1Flink的成长之路(二十一)-Flink+Kafka实现End-to-End Exactly-Once代码示例

从0到1Flink的成长之路(二十)-Flink 高级特性之Checkpoint 配置方式

从0到1Flink的成长之路(二十)-案例:时间会话窗口