对于异步IO的需求
在与外部系统交互(用数据库中的数据扩充流数据)的时候,需要考虑与外部系统的通信延迟对整个流处理应用的影响。简单地访问外部数据库的数据,比如使用
MapFunction
,通常意味着同步交互: MapFunction
向数据库发送一个请求然后一直等待,直到收到响应。在许多情况下,等待占据了函数运行的大部分时间。与数据库异步交互是指一个并行函数实例可以并发地处理多个请求和接收多个响应。这样,函数在等待的时间可以发送其他请求和接收其他响应。至少等待的时间可以被多个请求摊分。大多数情况下,异步交互可以大幅度提高流处理的吞吐量。
注意:仅仅提高 MapFunction 的并行度(parallelism)在有些情况下也可以提升吞吐量,但是这样做通常会导致非常高的资源消耗:更多的并行 MapFunction 实例意味着更多的 Task、更多的线程、更多的 Flink 内部网络连接、 更多的与数据库的网络连接、更多的缓冲和更多程序内部协调的开销。异步IO API
Flink 的异步 I/O API 允许用户在流处理中使用异步请求客户端。API 处理与数据流的集成,同时还能处理好顺序、事件时间和容错等。在具备异步数据库客户端的基础上,实现数据流转换操作与数据库的异步 I/O 交互需要以下三部分:-
实现分发请求的
AsyncFunction
-
获取数据库交互的结果并发送给
ResultFuture
的 回调 函数 -
将异步 I/O 操作应用于
DataStream
作为DataStream
的一次转换操作。
重要提示:第一次调用 ResultFuture.complete
后 ResultFuture
就完成了。后续的 complete
调用都将被忽略。
下面两个参数控制异步操作:
-
Timeout:超时参数定义了异步请求发出多久后未得到响应即被认定为失败。它可以防止一直等待得不到响应的请求。
-
Capacity:容量参数定义了可以同时进行的异步请求数。即使异步 I/O 通常带来更高的吞吐量,执行异步 I/O 操作的算子仍然可能成为流处理的瓶颈。限制并发请求的数量可以确保算子不会持续累积待处理的请求进而造成积压,而是在容量耗尽时触发反压。
结果的顺序
AsyncFunction
发出的并发请求经常以不确定的顺序完成,这取决于请求得到响应的顺序。Flink 提供两种模式控制结果记录以何种顺序发出。-
无序模式:异步请求一结束就立刻发出结果记录。流中记录的顺序在经过异步 I/O 算子之后发生了改变。当使用 处理时间 作为基本时间特征时,这个模式具有最低的延迟和最少的开销。此模式使用
AsyncDataStream.unorderedWait(...)
方法。 -
有序模式: 这种模式保持了流的顺序。发出结果记录的顺序与触发异步请求的顺序(记录输入算子的顺序)相同。为了实现这一点,算子将缓冲一个结果记录直到这条记录前面的所有记录都发出(或超时)。由于记录或者结果要在 checkpoint 的状态中保存更长的时间,所以与无序模式相比,有序模式通常会带来一些额外的延迟和 checkpoint 开销。此模式使用
AsyncDataStream.orderedWait(...)
方法。
封装线程池工具类
使用双重锁的单例获取线程池实例package com.duo.utils
import java.util.concurrent.{LinkedBlockingQueue, ThreadPoolExecutor, TimeUnit}
/**
* Author z
* Date 2021-03-23 11:05:18
*/
object ThreadPoolUtil {
private var pool: ThreadPoolExecutor = _;
/**
* corePoolSize:指定了线程池中的线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去;
* maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量;
* keepAliveTime:当线程池中空闲线程数量超过corePoolSize时,多余的线程会在多长时间内被销毁;
* unit:keepAliveTime的单位
* workQueue:任务队列,被添加到线程池中,但尚未被执行的任务
*
* @return
*/
def getInstance(): ThreadPoolExecutor = {
if (pool == null) {
this.synchronized {
if (pool == null) {
pool = new ThreadPoolExecutor(
5,
50,
500,
TimeUnit.SECONDS,
new LinkedBlockingQueue(Int.MaxValue))
}
}
}
pool
}
}
参数说明:
-
corePoolSize:
指定了线程池中的线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去;
-
maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量;
-
keepAliveTime:当线程池中空闲线程数量超过corePoolSize时,多余的线程会在多长时间内被销毁;
-
unit:keepAliveTime的单位
-
workQueue:任务队列,被添加到线程池中,但尚未被执行的任务
封装Mysql工具类
package com.duo.utils
import java.sql.{Connection, DriverManager, PreparedStatement, ResultSet}
import com.alibaba.fastjson.JSONObject
import scala.collection.mutable
import scala.collection.mutable.ListBuffer
/**
* Author z
* Date 2021-03-24 16:56:16
*/
object MySqlUtil {
Class.forName("com.mysql.jdbc.Driver")
def queryList(sql: String) = {
var conn: Connection = null;
var ps: PreparedStatement = null;
var rs: ResultSet = null;
val resultList = new ListBuffer[JSONObject]
try {
conn = DriverManager.getConnection(
"jdbc:mysql://hadoop01:3306/mall?characterEncoding=utf-8&useSSL=false",
"root",
"123456")
ps = conn.prepareStatement(sql)
rs = ps.executeQuery()
val metaData = rs.getMetaData
while (rs.next()) {
val nObject = new JSONObject
for (i <- 1 to metaData.getColumnCount) {
nObject.put(metaData.getColumnName(i), if (rs.getObject(i) == null) "null" else rs.getObject(i))
}
resultList += nObject
}
} catch {
case e: Exception => println(e)
}
finally {
if (rs != null) {
try {
rs.close()
} catch {
case e: Exception => println(e.printStackTrace())
}
}
if (ps != null) {
try {
ps.close()
} catch {
case e: Exception => println(e.printStackTrace())
}
}
if (conn != null) {
try {
conn.close()
} catch {
case e: Exception => println(e.printStackTrace())
}
}
}
resultList
}
/**
* 根据传入的表,传入的查询条件,查询mysql
* @param table:表
* @param key:where条件,可以是多个
* @return 返回集合
*/
def queryListByKey(table: String, key: ListBuffer[(String, String)]) = {
if(table!=null && key.nonEmpty) {
val sb = new mutable.StringBuilder(
s"""
|select * from ${table} where 1=1
|""".stripMargin)
key.foreach {
// k:字段
// v:查询值
// 如:id=1
case (k, v) => {
sb ++= " and "
sb ++= k
sb ++= " = "
sb ++= " '" + v + "' "
}
}
queryList(sb.toString)
}else{
new ListBuffer[JSONObject]()
}
}
}
封装AsyncDbRequest的特质DimProcessTrait
package com.duo.mytrait
import com.alibaba.fastjson.JSONObject
import scala.collection.mutable.ListBuffer
/**
* Author z
* Date 2021-03-29 14:13:37
*/
trait DimProcessTrait {
/**
* 从流中获取需要关联 维度表的主键
* @param i:输入流
* @return 返回关联的key列表
*/
def getKey(i: JSONObject): ListBuffer[(String,String)]
/**
* 输入流 join 维表
* @param i:输入流
* @param dimListJSON:根据输入流的key,在维表中找到维表数据
* @param dimFields :要获取的维表的字段,以及别名
* @return
*/
def join(i: JSONObject, dimListJSON: ListBuffer[JSONObject],dimFields: ListBuffer[(String,String)]): ListBuffer[JSONObject]
}
封装AsyncDbRequest抽象类
为什么是抽象类,为了通用,获取维表的key和join维表延伸在子类中实现,更加通用package com.duo.utils
import java.util.concurrent.ExecutorService
import com.alibaba.fastjson.JSONObject
import com.duo.mytrait.DimProcessTrait
import org.apache.flink.runtime.concurrent.Executors
import org.apache.flink.streaming.api.scala.async.{AsyncFunction, ResultFuture}
import scala.collection._
import scala.collection.mutable.ListBuffer
import scala.concurrent.ExecutionContext
/**
* Author z
* Date 2021-03-23 10:47:21
*/
abstract class AsyncDbRequest extends
AsyncFunction[JSONObject, ListBuffer[JSONObject]]
// 混入我们自己定义的特质
with DimProcessTrait {
lazy val executorService: ExecutorService = ThreadPoolUtil.getInstance()
private var tableName: String = _
private var dimFields = new ListBuffer[(String, String)]()
/**
* 构造器
* @param tableName:子类传入表名字
* @param dimFields:查询条件
*/
def this(tableName: String, dimFields: ListBuffer[(String, String)]) = {
this
this.tableName = tableName
this.dimFields = dimFields
}
def asyncInvoke(in: JSONObject, resultFuture: ResultFuture[ListBuffer[JSONObject]]) = {
executorService.submit(
new Runnable {
override def run(): Unit = {
try {
//1. 获取 关联的 key和value
val keys = getKey(in)
//2. 根据多个关联条件,从mysql中查询维表
val dimList = MySqlUtil.queryListByKey(tableName, keys)
//3. 关联维表
val result = join(in, dimList, dimFields)
//4. 将结果向下传递
resultFuture.complete(Iterable(result))
} catch {
case e: Exception => println(e.getStackTrace)
}
}
})
}
}
数据准备
-
mysql中维表数据
-
从集合中获取流
package com.duo.model
/**
* Author z
* Date 2021-04-03 17:05:34
*/
case class StudentTask(
id: Int,
name: String,
// 维表主键
stuId: Int,
ts: Long
)
val env=...
val ds = env.fromElements(
// StudentTask依次为:主键id,活动,维表id,时间戳
StuToJson(StudentTask(1, "吃饭", 1, System.currentTimeMillis())),
StuToJson(StudentTask(2, "看电影", 1, System.currentTimeMillis())),
StuToJson(StudentTask(3, "约会", 3, System.currentTimeMillis())),
StuToJson(StudentTask(4, "逛街", 5, System.currentTimeMillis())),
StuToJson(StudentTask(5, "吃饭", 6, System.currentTimeMillis())),
StuToJson(StudentTask(6, "写代码", 7, System.currentTimeMillis())),
StuToJson(StudentTask(7, "看女朋友", 8, System.currentTimeMillis())),
StuToJson(StudentTask(8, "吃饭", 7, System.currentTimeMillis()))
)
...
主程序
package com.duo.mytest
import java.util.concurrent.TimeUnit
import com.alibaba.fastjson.JSONObject
import com.duo.model.StudentTask
import com.duo.utils.AsyncDbRequest
import org.apache.flink.api.common.functions.MapFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.table.api.EnvironmentSettings
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment
import scala.collection.mutable.ListBuffer
/**
* Author z
* Date 2021-04-03 10:10:03
*/
object FlinkAsyncIO {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val settings = EnvironmentSettings
.newInstance()
.useBlinkPlanner()
.inStreamingMode()
.build()
val tableEnv = StreamTableEnvironment.create(env, settings)
val ds = env.fromElements(
StuToJson(StudentTask(1, "吃饭", 1, System.currentTimeMillis())),
StuToJson(StudentTask(2, "看电影", 1, System.currentTimeMillis())),
StuToJson(StudentTask(3, "约会", 3, System.currentTimeMillis())),
StuToJson(StudentTask(4, "逛街", 5, System.currentTimeMillis())),
StuToJson(StudentTask(5, "吃饭", 6, System.currentTimeMillis())),
StuToJson(StudentTask(6, "写代码", 7, System.currentTimeMillis())),
StuToJson(StudentTask(7, "看女朋友", 8, System.currentTimeMillis())),
StuToJson(StudentTask(8, "吃饭", 7, System.currentTimeMillis()))
)
val dimDS = AsyncDataStream.unorderedWait(
ds,
new AsyncDbRequest(
// 表名字
"student",
// 需要返回的维表字段,_1:维表字段,_2:返回结果维表字段对应别名
ListBuffer(
("id", "stu_Id"),
("name", "stu_name"),
("age", "stu_age"))
) {
override def getKey(i: JSONObject): ListBuffer[(String, String)] = {
ListBuffer(("id", i.getString("stuId")))
}
override def join(i: JSONObject, dimListJSON: ListBuffer[JSONObject], dimFields: ListBuffer[(String, String)]): ListBuffer[JSONObject] = {
val list = new ListBuffer[JSONObject]()
if (dimListJSON != null && dimListJSON.nonEmpty && dimFields.nonEmpty) {
// 遍历 查询到的维表 数据
dimListJSON.foreach(x => {
val value = i.clone().asInstanceOf[JSONObject]
// 遍历 需要的维表字段集合
dimFields.foreach(y => {
// 获取数据根据维表字段
// put时key是传入的别名
value.put(y._2, x.getString(y._1))
})
list += value
})
list
} else {
dimFields.foreach(y => {
i.put(y._2, "null")
})
list += i
}
}
}, 60L, TimeUnit.SECONDS, 100)
dimDS.print()
env.execute()
}
def StuToJson(stu: StudentTask) = {
val data = new JSONObject()
data.put("id", stu.id)
data.put("name", stu.name)
data.put("stuId", stu.stuId)
data.put("ts", stu.ts)
data
}
}
程序结果
其中,stu_Id,stu_name,stu_age 为mysql维表数据6> ListBuffer({"stu_Id":"1","stuId":1,"name":"吃饭","stu_age":"100","stu_name":"张三","id":1,"ts":1617444151021})
7> ListBuffer({"stu_Id":"1","stuId":1,"name":"看电影","stu_age":"100","stu_name":"张三","id":2,"ts":1617444151029})
8> ListBuffer({"stu_Id":"3","stuId":3,"name":"约会","stu_age":"3","stu_name":"3","id":3,"ts":1617444151029})
3> ListBuffer({"stu_Id":"7","stuId":7,"name":"写代码","stu_age":"77","stu_name":"7","id":6,"ts":1617444151029})
5> ListBuffer({"stu_Id":"7","stuId":7,"name":"吃饭","stu_age":"77","stu_name":"7","id":8,"ts":1617444151029})
2> ListBuffer({"stu_Id":"6","stuId":6,"name":"吃饭","stu_age":"6","stu_name":"6","id":5,"ts":1617444151029})
1> ListBuffer({"stu_Id":"5","stuId":5,"name":"逛街","stu_age":"5","stu_name":"5","id":4,"ts":1617444151029})
4> ListBuffer({"stu_Id":"8","stuId":8,"name":"看女朋友","stu_age":"8","stu_name":"8","id":7,"ts":1617444151029})