背景
在Flink任务中需要将计算结果数据先存入HBase中(Phoenix),在保证HBase写入成功后再转发到Kafka中。在数据量较大的情况下由于串行写入HBase会出现严重反压,造成整体数据计算链路的整体数据产出效率大大降低。
目标
使用异步算子与外部系统交互,提高吞吐量,降低由于写入数据库造成的延迟。
描述
在本地测试时验证出,一条数据写入的HBase的请求完整耗时在40ms左右(仅作为参考,不同环境耗时差异较大),但是在Flink任务中上游算子的耗时基本都在几毫秒内,因此写入HBase是整体链路的明显短板。在一个写入HBase的Task中,由于写入请求是同步执行的,在大部分的情况下等待请求相应占据了该算子的大部分时间,因此通过并发的实现处理多个请求和接收多个响应,将等待的时间分摊到每个请求。
注:仅仅提高
Function的并行度(parallelism)在有些情况下也可以提升吞吐量,但是这样做通常会导致非常高的资源消耗:更多的并行Function实例意味着更多的 Task、更多的线程、更多的 Flink 内部网络连接、 更多的与数据库的网络连接、更多的缓冲和更多程序内部协调的开销。--Apache Flink Documentation
两个参数控制异步操作:
Timeout: 超时参数定义了异步请求发出多久后未得到响应即被认定为失败。 它可以防止一直等待得不到响应的请求。
Capacity: 容量参数定义了可以同时进行的异步请求数。 即使异步 I/O 通常带来更高的吞吐量,执行异步 I/O 操作的算子仍然可能成为流处理的瓶颈。 限制并发请求的数量可以确保算子不会持续累积待处理的请求进而造成积压,而是在容量耗尽时触发反压。
注意点:
- 在使用与外部数据库交互的客户端时,需使用支持异步操作的客户端,若没有则可以使用线程池实现异步提交的请求,若使用同步的客户端仍会阻塞当前异步算子。
- 在使用异步算子时,Capacity参数、线程池最大连接数、数据库连接池最大连接数要合理配置,避免由于某个参数设置的过小使请求的并发数受限,无法达到期望并发。
- 官方文档的中已对 超时处理、结果的顺序、事件时间、容错保证做了明确的解释,放在文档最后便于查看。
代码
package app;
import com.alibaba.fastjson.JSONObject;
import functions.MyAsyncFunction;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.streaming.api.datastream.AsyncDataStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import java.util.concurrent.TimeUnit;
public class AsyncTestJob {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
SingleOutputStreamOperator<JSONObject> stream = env.socketTextStream("localhost", 8888)
.map((MapFunction<String, JSONObject>) JSONObject::parseObject);
AsyncDataStream.orderedWait(stream,
//使用异步算子
new MyAsyncFunction(50),
//超时时间
30000,
//超时时间单位
TimeUnit.MILLISECONDS,
//capacity为异步请求数参数。建议:该参数应小于等于线程池、连接池的最大连接数
50)
.print();
env.execute();
}
}
package functions;
import com.alibaba.fastjson.JSONObject;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Collections;
import java.util.Properties;
import java.util.concurrent.*;
public class MyAsyncFunction extends RichAsyncFunction<JSONObject, String> {
private final int maxConnTotal;
private transient ExecutorService executorService;
private HikariDataSource phoenixDataSource;
private final static Logger logger = LoggerFactory.getLogger(AsyncUpsertHBase.class);
public MyAsyncFunction(Integer maxConnTotal) {
this.maxConnTotal = maxConnTotal;
}
@Override
public void open(Configuration parameters) throws Exception {
executorService = Executors.newFixedThreadPool(maxConnTotal);
phoenixDataSource = getPhoenixDataSource();
}
@Override
public void close() throws Exception {
executorService.shutdown();
phoenixDataSource.close();
}
@Override
public void asyncInvoke(JSONObject json, ResultFuture<String> resultFuture) {
//使用线程池实现异步提交请求
Future<String> future = executorService.submit(() -> upsertHBase(json));
CompletableFuture.supplyAsync(() -> {
try {
return future.get();
} catch (InterruptedException | ExecutionException e) {
return null;
}
}).thenAccept((String dbResult) -> resultFuture.complete(Collections.singleton(dbResult)));
}
/**
* 标签写入HBase
*
* @param json 标签数据
* @return 标签数据字符串
* @throws SQLException SQLException
*/
private String upsertHBase(JSONObject json) throws SQLException {
Long id = json.getLong("id");
String key = json.getString("key");
Integer value = json.getInteger("data");
String SQL = "UPSERT INTO TEST_TABLE (ID," + key + ") VALUES (" + id + "," + value + ")";
try (
Connection conn = phoenixDataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(SQL)
) {
ps.executeUpdate();
conn.commit();
return json.toJSONString();
} catch (Exception e) {
logger.error(e.getMessage(), e);
throw e;
}
}
private HikariDataSource getPhoenixDataSource() {
HikariConfig config = new HikariConfig();
config.setDriverClassName("org.apache.phoenix.jdbc.PhoenixDriver");
config.setJdbcUrl("jdbc:phoenix:10.xxx.xxx.xxx:2181");
// 最大活跃连接数
config.setMaximumPoolSize(50);
//最小空闲连接数
config.setMinimumIdle(10);
//连接超时时间,单位毫秒
config.setConnectionTimeout(30000);
//空闲连接最大存活时间,单位毫秒
config.setIdleTimeout(5 * 60 * 1000);
//初始化失败时的超时时间,单位秒
config.setInitializationFailTimeout(1);
//池名称
config.setPoolName("PhoenixHikariCP");
// 允许池在达到最大大小时增长,如果当前所有连接都在使用中,并且执行者正在等待一个连接
config.setAllowPoolSuspension(true);
Properties properties = new Properties();
properties.setProperty("phoenix.schema.mapSystemTablesToNamespace", "true");
properties.setProperty("phoenix.schema.isNamespaceMappingEnabled", "true");
properties.setProperty("phoenix.query.timeoutMs", "1200000");
properties.setProperty("hbase.rpc.timeout", "1200000");
properties.setProperty("hbase.client.scanner.timeout.period", "1200000");
config.setDataSourceProperties(properties);
return new HikariDataSource(config);
}
}
官方文档
超时处理 #
当异步 I/O 请求超时的时候,默认会抛出异常并重启作业。 如果你想处理超时,可以重写
AsyncFunction#timeout方法。结果的顺序 #
AsyncFunction发出的并发请求经常以不确定的顺序完成,这取决于请求得到响应的顺序。 Flink 提供两种模式控制结果记录以何种顺序发出。
无序模式: 异步请求一结束就立刻发出结果记录。 流中记录的顺序在经过异步 I/O 算子之后发生了改变。 当使用 处理时间 作为基本时间特征时,这个模式具有最低的延迟和最少的开销。 此模式使用
AsyncDataStream.unorderedWait(...)方法。有序模式: 这种模式保持了流的顺序。发出结果记录的顺序与触发异步请求的顺序(记录输入算子的顺序)相同。为了实现这一点,算子将缓冲一个结果记录直到这条记录前面的所有记录都发出(或超时)。由于记录或者结果要在 checkpoint 的状态中保存更长的时间,所以与无序模式相比,有序模式通常会带来一些额外的延迟和 checkpoint 开销。此模式使用
AsyncDataStream.orderedWait(...)方法。事件时间 #
当流处理应用使用事件时间时,异步 I/O 算子会正确处理 watermark。对于两种顺序模式,这意味着以下内容:
无序模式: Watermark 既不超前于记录也不落后于记录,即 watermark 建立了顺序的边界。 只有连续两个 watermark 之间的记录是无序发出的。 在一个 watermark 后面生成的记录只会在这个 watermark 发出以后才发出。 在一个 watermark 之前的所有输入的结果记录全部发出以后,才会发出这个 watermark。
这意味着存在 watermark 的情况下,无序模式 会引入一些与有序模式 相同的延迟和管理开销。开销大小取决于 watermark 的频率。
有序模式: 连续两个 watermark 之间的记录顺序也被保留了。开销与使用处理时间 相比,没有显著的差别。
请记住,摄入时间 是一种特殊的事件时间,它基于数据源的处理时间自动生成 watermark。
容错保证 #
异步 I/O 算子提供了完全的精确一次容错保证。它将在途的异步请求的记录保存在 checkpoint 中,在故障恢复时重新触发请求。
实现提示 #
在实现使用 Executor(或者 Scala 中的 ExecutionContext)和回调的 Futures 时,建议使用
DirectExecutor,因为通常回调的工作量很小,DirectExecutor避免了额外的线程切换开销。回调通常只是把结果发送给ResultFuture,也就是把它添加进输出缓冲。从这里开始,包括发送记录和与 chenkpoint 交互在内的繁重逻辑都将在专有的线程池中进行处理。
DirectExecutor可以通过org.apache.flink.util.concurrent.Executors.directExecutor()或com.google.common.util.concurrent.MoreExecutors.directExecutor()获得。警告 #
Flink 不以多线程方式调用 AsyncFunction
我们想在这里明确指出一个经常混淆的地方:
AsyncFunction不是以多线程方式调用的。 只有一个AsyncFunction实例,它被流中相应分区内的每个记录顺序地调用。除非asyncInvoke(...)方法快速返回并且依赖于(客户端的)回调, 否则无法实现正确的异步 I/O。例如,以下情况导致阻塞的
asyncInvoke(...)函数,从而使异步行为无效:
- 使用同步数据库客户端,它的查询方法调用在返回结果前一直被阻塞。
- 在
asyncInvoke(...)方法内阻塞等待异步客户端返回的 future 类型对象目前,出于一致性的原因,AsyncFunction 的算子(异步等待算子)必须位于算子链的头部
根据
FLINK-13063给出的原因,目前我们必须断开异步等待算子的算子链以防止潜在的一致性问题。这改变了先前支持的算子链的行为。需要旧有行为并接受可能违反一致性保证的用户可以实例化并手工将异步等待算子添加到作业图中并将链策略设置回通过异步等待算子的ChainingStrategy.ALWAYS方法进行链接。
6565

被折叠的 条评论
为什么被折叠?



