概念理解
流计算系统中经常需要与外部系统进行交互,比如需要查询外部数据库以关联上用户的额外信息。通常,我们的实现方式是向数据库发送用户a的查询请求(例如在MapFunction中),然后等待结果返回,在这之前,我们无法发送用户b的查询请求。这是一种同步访问的模式,如下图左边所示。
图中棕色的长条表示等待时间,可以发现网络等待时间极大地阻碍了吞吐和延迟。为了解决同步访问的问题,异步模式可以并发地处理多个请求和回复。也就是说,你可以连续地向数据库发送用户a、b、c等的请求,与此同时,哪个请求的回复先返回了就处理哪个回复,从而连续的请求之间不需要阻塞等待,如上图右边所示。这也正是 Async I/O 的实现原理。
2 Flink异步IO
2.1 先决条件
如上一节所示,对数据库(或键/值存储)实现适当的异步I / O需要客户端访问支持异步请求的数据库。许多流行的数据库提供这样的客户端在没有这样的客户端的情况下,可以通过创建多个客户端并使用线程池处理同步调用来尝试将同步客户端转变为有限的并发客户端。但是,这种方法通常比适当的异步客户端效率低。
2.2 异步I/O API
Flink的Async I / O API允许用户将异步请求客户端与数据流一起使用。API处理数据流的集成,以及结果顺序,事件时间,容错等。
假设有一个目标数据库的异步客户端,需要三个部分来实现对数据库的异步I / O流转换:
AsyncFunction的一个实现,用来分派请求
获取操作结果并将其传递给ResultFuture的回调
将异步I/O操作作为转换应用于DataStream
代码示例:
//可以先了解下java 的Future模式
* 'AsyncFunction'实现,向数据库发送异步请求并设置回调 。
*/
class AsyncDatabaseRequest extends RichAsyncFunction<String, Tuple2<String,
String>> {
/** 可以异步请求的特定数据库的客户端 */
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 {
// 发起一个异步请求,返回结果的Future对象
final Future<String> result = client.query(key);
// 设置请求完成时的回调: 将结果传递给callback
// callback直接将结果传递给ResultFuture
CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
try {
return result.get();
} catch (InterruptedException | ExecutionException e) {
return null;
}
}
}).thenAccept( (String dbResult) -> {
//结果收集
resultFuture.complete(Collections.singleton(new Tuple2<>(key,
dbResult)));
});
}
}
// 创建一个流
DataStream<String> stream = ...;
// 添加一个async I/O转换
DataStream<Tuple2<String, String>> resultStream =
AsyncDataStream.unorderedWait(stream, new AsyncDatabaseRequest(), 1000,
TimeUnit.MILLISECONDS, 100);//(流,异步转换算子,超时时间,时间单位,进行异步请求的最大数
量)
以下两个参数控制异步操作:
超时:超时定义异步请求在被视为失败之前可能需要多长时间。此参数可防止死/失败请求。
容量:此参数定义可以同时进行的异步请求数。尽管异步I / O方法通常会带来更好的吞吐量,但运营商仍然可能成为流应用程序的瓶颈。限制并发请求的数量可确保操作员不会累积不断增长的待处理请求积压,但一旦容量耗尽,它将触发背压。
2.3 超时处理
当异步I / O请求超时时,默认情况下会引发异常并重新启动作业。如果要处理超时,可以覆盖该AsyncFunction#timeout 方法。
2.4 结果顺序
AsyncFunction发出的并发请求通常以某种未定义的顺序完成,该顺序基于哪个请求先完成。为了控制结果记录的发出顺序,Flink提供了两种模式:
无序:异步请求完成后立即发出结果记录。在异步I / O运算符之后,流中记录的顺序与以前不同。当使用处理时间作为基本时间特性时,此模式具有最低延迟和最低开销。此模式使用AsyncDataStream.unorderedWait(…) 。
有序:在这种情况下,保留流顺序。结果记录的发出顺序与触发异步请求的顺序相同(运算符输入记录的顺序)。为此,运算符缓冲结果记录,直到其所有先前记录被发出(或超时)。这通常会在检查点中引入一些额外的延迟和一些开销,因为与无序模式相比,记录或结果在检查点状态下保持更长的时间。此模式使用 AsyncDataStream.orderedWait(…) 。
2.5 事件时间
当流应用程序与事件时间一起工作时,异步I/O操作符将正确处理水位线。具备以下两种排序模式:
无序:水位线不会超过记录,反之亦然,这意味着水位线会建立一个有序的边界。记录只在水位线之间无序地发出。在某个水位线之后出现的记录只会在该水位线发出之后才会发出。然后,只有在发出水位线之前的输入的所有结果记录之后,才会发出水位线。这意味着,在存在水位线的情况下,无序模式引入了与有序模式相同的延迟和管理开销。这种开销的大小取决于水位线频率。
有序:保留记录的水位线顺序,就像保留记录之间的顺序一样。与处理时间相比,开销没有显著变化。请记住,摄取时间(Ingestion Time)是事件时间的一种特殊情况,它根据源处理时间自动生成水位线
2.6 容错保证
异步I / O运算符提供完全一次的容错保证。它在检查点中存储正在进行的异步请求的记录,并在从故障中恢复时恢复/重新触发请求。
2.7 实施建议
对于具有用于回调的执行器(或scala中的ExecutionContext)的 Futures 的实现,我们建议使用DirectExecutor ,因为回调通常只做最少的工作,并且 DirectExecutor 避免了额外的线程到线程切换开销。回调通常只将结果传递给 ResultFuture ,后者将其添加到输出缓冲区。包含记录发送和与检查点bookkeeping的交互的繁重逻辑在专用的线程池中发生。
DirectExecutor 可以通过
org.apache.flink.runtime.concurrent.Executors.directExecutor() 或
com.google.common.util.concurrent.MoreExecutors.directExecutor() 获得。
2.8 警告
AsyncFunction不是多线程的例如,以下模式会导致阻塞asyncInvoke(…)函数,从而使异步行为无效:
使用 lookup/query方法调用阻塞的数据库客户机,直到收到返回的结果为止
阻塞/等待asyncInvoke(…)方法中的异步客户机返回的future-type对象
3 实例验证
3.1 非异步IO实例
采用默认的Map方法来模拟关联维度表,预期记录输出应该是按照记录产生顺序严格匹配的。
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.api.java.tuple.Tuple3;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.table.expressions.In;
public class AsyncTest01 {
public static void main(String[] args) throws Exception {
//初始化上下文
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
//设置并行度
env.setParallelism(1);
//添加Source
DataStream<Tuple2<Integer, String>> ds = env.addSource(new
SourceFunction<Tuple2<Integer, String>>() {
private boolean isRunning = true;
private int count = 0;
@Override
public void run(SourceContext<Tuple2<Integer, String>> ctx) throws
Exception {
//每100ms产生一条数据
while (isRunning) {
ctx.collect(new Tuple2<>(count, "user" + count));
Thread.sleep(100);
count++;
}
}
@Override
public void cancel() {
isRunning = false;
}
});
//对数据进行Map操作,模拟关联维度信息
/**
* 测试结果
* 可以看到,打印的结果是按照记录产生的顺序输出的,即数据处理等待数据库返回后才继续进行后续处
* 理。
*/
DataStream<Tuple3<Integer, String, String>> ds2 = ds.map(new
MapFunction<Tuple2<Integer, String>, Tuple3<Integer, String, String>>() {
@Override
public Tuple3<Integer, String, String> map(Tuple2<Integer, String>
value) throws Exception {
System.out.println("维度关联开始:" + value.f1);
System.out.println("查询数据库中......");
//模拟查询耗时
Thread.sleep(1000);
System.out.println("查询结束!");
return new Tuple3<>(value.f0, value.f1, value.f0 + "Dim");
}
});
//打印结果
ds2.print();
env.execute("同步测试");
}
}
测试结果
可以看到,打印的结果是按照记录产生的顺序输出的,即数据处理等待数据库返回后才继续进行后续处理。
3.2 异步I/O实例
采用异步I/O,主程序不会等待处理结果完成
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.api.java.tuple.Tuple3;
import org.apache.flink.streaming.api.datastream.AsyncDataStream;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.async.ResultFuture;
import org.apache.flink.streaming.api.functions.async.RichAsyncFunction;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.table.expressions.In;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
public class AsyncTest02 {
public static void main(String[] args) throws Exception {
//初始化上下文
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
//设置并行度
env.setParallelism(1);
//添加Source
DataStream<Tuple2<Integer, String>> ds = env.addSource(new
SourceFunction<Tuple2<Integer, String>>() {
private boolean isRunning = true;
private int count = 0;
@Override
public void run(SourceContext<Tuple2<Integer, String>> ctx) throws
Exception {
while (isRunning) {
//每100ms产生一条数据
ctx.collect(new Tuple2<>(count, "user" + count));
Thread.sleep(100);
count++;
}
}
@Override
public void cancel() {
isRunning = false;
}
});
//使用异步I/O处理数据
DataStream<Tuple3<Integer, String, String>> ds2 =
AsyncDataStream.unorderedWait(ds, new MyAsyncFunc(), 50000,
TimeUnit.MICROSECONDS, 100);
//打印结果
ds2.print();
env.execute("异步I/O测试");
结果:
可以看到查询数据库操作不会阻塞数据处理。
}
static class MyAsyncFunc extends RichAsyncFunction<Tuple2<Integer, String>,
Tuple3<Integer, String, String>> {
@Override
public void asyncInvoke(Tuple2<Integer, String> input,
ResultFuture<Tuple3<Integer, String, String>> resultFuture) throws Exception {
System.out.println(input.f0 + "维度关联开始:" + input.f1);
//异步处理
CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
try {
System.out.println(input.f0 + "查询数据库中......");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "dim";
}
}).thenAccept((String dbResult) -> {
System.out.println("---->" + input.f0 + ":查询结束!");
resultFuture.complete(Collections.singleton(Tuple3.of(input.f0,
input.f1, input.f0 + dbResult)));
});
}
}
}
异步维表查询
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import io.vertx.core.Vertx;
import io.vertx.core.VertxOptions;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.jdbc.JDBCClient;
import io.vertx.ext.sql.ResultSet;
import io.vertx.ext.sql.SQLConnection;
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 java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class AsyncDBTest {
static class MyDBAsyncFunction extends RichAsyncFunction<JsonObject,
JsonObject> {
//Caffeine 缓存
private Cache<String, String> cache;
//异步JDBC客户端
private transient JDBCClient jdbcClient;
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
cache = Caffeine
.newBuilder()//构造器
.maximumSize(2048)//缓存最大数量
.expireAfterAccess(20, TimeUnit.MINUTES)//缓存过期时间:20分钟
没有访问则过期
.build();
//数据库连接配置
JsonObject dbConfig = new JsonObject();
dbConfig.put("url", "jdbc:mysql://localhost:3306/test")
.put("driver_class", "com.mysql.jdbc.Driver")
.put("max_pool_size", 20)
.put("user", "root")
.put("max_idle_time", 1000)
.put("password", "123456");
VertxOptions vo = new VertxOptions();
vo.setEventLoopPoolSize(10).setWorkerPoolSize(20);
Vertx vertx = Vertx.vertx(vo);
jdbcClient = JDBCClient.createShared(vertx, dbConfig);
}
@Override
public void close() throws Exception {
super.close();
if (jdbcClient != null) {
jdbcClient.close();
}
if (cache != null) {
//清理缓存
cache.cleanUp();
}
}
@Override
public void asyncInvoke(JsonObject input, ResultFuture<JsonObject>
resultFuture) throws Exception {
String key = input.getString("device_id");
String result = cache.getIfPresent(key);
if (result != null) {
input.put("device_code", result);
resultFuture.complete(Collections.singleton(input));
return;
} else {
jdbcClient.getConnection(res -> {
if (res.failed()) {
resultFuture.completeExceptionally(res.cause());
return;
}
if (res.succeeded()) {
SQLConnection conn = res.result();
String query = "select device_code from
dwr_dim.dim_facilty_instance_d where device_id = ?limit 1 ";
conn.queryWithParams(query, new JsonArray().add(key),
res2 -> {
if (res2.failed()) {
resultFuture.completeExceptionally(res2.cause());
return;
}
if (res2.succeeded()) {
ResultSet rs = res2.result();
List<JsonObject> rows = rs.getRows();
if (rows.size() == 0) {
resultFuture.complete(null);
return;
}
String deviceCode =
rows.get(0).getString("device_code");
cache.put(key, deviceCode);
resultFuture.complete(Collections.singleton(input.put("device_code",
deviceCode)));
}
});
}
});
}
}
@Override
public void timeout(JsonObject input, ResultFuture<JsonObject>
resultFuture) throws Exception {
resultFuture.completeExceptionally(new Exception("TimeOut"));
}
}
}
实时数仓代码:
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidPooledConnection;
import com.alibaba.fastjson.JSONObject;
import com.atguigu.utils.DimUtil;
import com.atguigu.utils.DruidDSUtil;
import com.atguigu.utils.JedisPoolUtil;
import com.atguigu.utils.ThreadPoolUtil;
import lombok.SneakyThrows;
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 java.util.Collections;
import java.util.concurrent.ThreadPoolExecutor;
public abstract class DimAsyncFunction <T> extends RichAsyncFunction<T,T> implements AsyncJoinFunction<T> {
private JedisPool jedisPool;
private DruidDataSource druidDataSource;
private ThreadPoolExecutor threadPoolExecutor;
private String tableName;
//tablename可以从外部传入
public DimAsyncFunction(String tableName) {
this.tableName = tableName;
}
@Override
public void open(Configuration parameters) throws Exception {
//使用构建的工具类初始化连接
jedisPool = JedisPoolUtil.getJedisPool();
druidDataSource = DruidDSUtil.createDataSource();
threadPoolExecutor = ThreadPoolUtil.getThreadPoolExecutor();
}
@Override
public void asyncInvoke(T input, ResultFuture<T> resultFuture) throws Exception {
//开启线程,在线程中完成关联及补充
threadPoolExecutor.execute(new Runnable() {
@SneakyThrows
@Override
public void run() {
//获取连接
Jedis jedis = jedisPool.getResource();
DruidPooledConnection connection = druidDataSource.getConnection();
//通过构建一个抽象方法,外部在实现这个抽象类时必定要重写抽象方法,此时将想要的值传进来
//调用这个函数的时候一定不再是泛型,所以可以拿到所有想拿到的值
String key = getKey(input);
//查询维表
JSONObject dimInfo = DimUtil.getDimInfo(jedis, connection, tableName, key);
//补充信息
if (dimInfo != null){
//从dimInfo中取字段加入input中
join(input,dimInfo);
}
//归还连接
jedis.close();
connection.close();
//输出补充完信息的数据
resultFuture.complete(Collections.singletonList(input));
}
});
}
//超时数据,关联不上也输出
@Override
public void timeout(T input, ResultFuture<T> resultFuture) throws Exception {
System.out.println("TimeOut:"+input);
}
}
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidPooledConnection;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.atguigu.common.GmallConfig;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.sql.Connection;
import java.util.List;
public class DimUtil {
public static JSONObject getDimInfo(Jedis jedis, Connection connection, String tableName, String key) throws Exception {
//使用redis做查询缓存
//读取Redis中的维表数据,rediskey用表名+主键(可以唯一代表一行数据),所以用String类型即可
//redis会公用,所以加个DIM标识
String redisKey = "DIM:" + tableName + ":" + key;
//string类型,使用get读取即可
String dimInfoStr = jedis.get(redisKey);
if (dimInfoStr != null) {
//重置过期时间,因为非热词不必一直缓存
jedis.expire(redisKey,24*60*60);
return JSON.parseObject(dimInfoStr);
}
//拼接SQL
String querysql = "select * from " + GmallConfig.HBASE_SCHEMA + "." + tableName + " where id='" + key + "'";
//查询
List<JSONObject> queryList = JdbcUtil.queryList(connection, querysql, JSONObject.class, false);
JSONObject dimInfo = queryList.get(0);
//将从Phoenix查询到的数据写入Redis一份(第一次查询时)
jedis.set(redisKey,dimInfo.toJSONString());
//设置过期时间
jedis.expire(redisKey,24*60*60);
//返回结果
return dimInfo;
}
//若是维表出现更新则删除redis中的老数据
public static void delDimInfo(Jedis jedis,String tableName, String key){
String redisKey = "DIM:" + tableName + ":" + key;
jedis.del(redisKey);
}
//测试
public static void main(String[] args) throws Exception {
//获取连接
DruidDataSource dataSource = DruidDSUtil.createDataSource();
DruidPooledConnection connection = dataSource.getConnection();
JedisPool jedisPool = JedisPoolUtil.getJedisPool();
Jedis jedis = jedisPool.getResource();
long start = System.currentTimeMillis();
//System.out.println(getDimInfo(connection, "DIM_BASE_TRADEMARK", "15")); //217ms 184ms
System.out.println(getDimInfo(jedis,connection, "DIM_BASE_TRADEMARK", "15")); // redis缓存有了之后 58ms 58ms
long end = System.currentTimeMillis();
//System.out.println(getDimInfo(connection, "DIM_BASE_TRADEMARK", "15")); //本地缓存有了之后,10ms 11ms
System.out.println(getDimInfo(jedis,connection, "DIM_BASE_TRADEMARK", "15")); //redis缓存有了之后,0ms 1ms 1ms
long end2 = System.currentTimeMillis();
System.out.println(end - start);
System.out.println(end2 - end);
connection.close();
dataSource.close();
}
}
import com.alibaba.fastjson.JSONObject;
public interface AsyncJoinFunction<T> {
//抽出来,方便其他需求使用,直接实现接口即可
String getKey(T input);
void join(T input, JSONObject dimInfo);
}