背景
Async I/O 是阿里巴巴贡献给社区的一个呼声非常高的特性,于1.2版本引入。主要目的是为了解决与外部系统交互时网络延迟成为了系统瓶颈的问题。
流计算系统中经常需要与外部系统进行交互,比如需要查询外部数据库以关联上用户的额外信息。通常,我们的实现方式是向数据库发送用户的查询请求,然后等待结果返回,在这之前,我们无法发送用户的查询请求。这是一种同步访问的模式,如下图左边所示。
图中棕色的长条表示等待时间,可以发现网络等待时间极大地阻碍了吞吐和延迟。为了解决同步访问的问题,异步模式可以并发地处理多个请求和回复。也就是说,你可以连续地向数据库发送用户a、b、c等的请求,与此同时,哪个请求的回复先返回了就处理哪个回复,从而连续的请求之间不需要阻塞等待,如上图右边所示。这也正是 Async I/O 的实现原理。
提高吞吐量的两种方式对比
前提
使用 Async I/O 的前提是需要一个支持异步请求的客户端。当然,没有异步请求客户端的话也可以将同步客户端丢到线程池中执行作为异步客户端。Flink 提供了非常简洁的API,让用户只需要关注业务逻辑,一些脏活累活比如消息顺序性和一致性保证都由框架处理了
开发步骤
如果目标数据库有异步客户端,则三步可以实现异步流式转换操作
实现用来分发请求的AsyncFunction,用来向数据库发送异步请求并设置回调
获取结果操作的callback,并将它提交给ResultFurture
将异步I/O操作应用于DataStream
代码实现:
数据源产生:
/**
* 每10毫秒发送一个数字
*/
private class SimpleSource(val maxNum: Int) extends SourceFunction[Integer] with ListCheckpointed[Integer] {
var counter = maxNum
var isRunning = true
var start = 0
override def snapshotState(checkpointId: Long, timestamp: Long): util.List[Integer] = {
Collections.singletonList(start)
}
override def restoreState(list: util.List[Integer]): Unit = {
this.start = list.size()
}
override def run(ctx: SourceFunction.SourceContext[Integer]): Unit = {
while ((start < counter || counter == -1) && isRunning) {
ctx.getCheckpointLock synchronized ctx.collect(start)
start += 1
if (start == Integer.MAX_VALUE) start = 0
Thread.sleep(10L)
}
}
override def cancel(): Unit = {
isRunning = false
}
}
AsyncIO代码实现:
object SampleAsyncFunction {
val executorService: ExecutorService = Executors.newFixedThreadPool(30)
}
class SampleAsyncFunction(val sleepFactor: Long, val failRatio: Float, val shutdownWaitTS: Long) extends AsyncFunction[Integer, String] {
// ExecutorUtils.gracefulShutdown(shutdownWaitTS, java.util.concurrent.TimeUnit.MILLISECONDS, executorService)
override def asyncInvoke(input: Integer, resultFuture: ResultFuture[String]): Unit = {
SampleAsyncFunction.executorService.submit(new Runnable {
override def run() = {
val sleep: Float = ThreadLocalRandom.current.nextFloat * sleepFactor
try {
Thread.sleep(sleep.asInstanceOf[Long])
if (ThreadLocalRandom.current.nextFloat < failRatio) {
resultFuture.completeExceptionally(new Exception("wahahahaha..."))
} else {
resultFuture.complete(Iterable("key-" + (input % 10)))
}
} catch {
case e: InterruptedException =>
resultFuture.complete(Iterable())
}
}
})
}
}
主程序如下:
AsyncDataStream是一个工具类,用于将AsyncFunction应用到DataStream;
AsyncFunction内部的并发请求是无序的,为了控制在异步处理算子中处理完成后,外部接收的顺序和发送给下游的顺序,AsyncDataStream提供了两种方法,orderedWait(有序) 和 unorderedWait(无序),unorderedWait又分为ProcessTime无序和EventTime无序。代码如下
import java.util
import java.util.Collections
import java.util.concurrent.{ ExecutorService, Executors, ThreadLocalRandom, TimeUnit}
import org.apache.flink.api.java.utils.ParameterTool
import org.apache.flink.runtime.state.filesystem.FsStateBackend
import org.apache.flink.streaming.api.checkpoint.ListCheckpointed
import org.apache.flink.streaming.api.functions.source.SourceFunction
import org.apache.flink.streaming.api.{CheckpointingMode, TimeCharacteristic}
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.async.{AsyncFunction, ResultFuture}
import org.apache.flink.util.Collector
object AsyncIOExample {
private val EXACTLY_ONCE_MODE = "exactly_once"
private val EVENT_TIME = "EventTime"
private val INGESTION_TIME = "IngestionTime"
private val ORDERED = "ordered"
private def printUsage(): Unit = {
System.out.println("To customize example, use: AsyncIOExample [--fsStatePath <path to fs state>] " + "[--checkpointMode <exactly_once or at_least_once>] " + "[--maxCount <max number of input from source, -1 for infinite input>] " + "[--sleepFactor <interval to sleep for each stream element>] [--failRatio <possibility to throw exception>] " + "[--waitMode <ordered or unordered>] [--waitOperatorParallelism <parallelism for async wait operator>] " + "[--eventType <EventTime or IngestionTime>] [--shutdownWaitTS <milli sec to wait for thread pool>]" + "[--timeout <Timeout for the asynchronous operations>]")
}
@throws[Exception]
def main(args: Array[String]): Unit = { // obtain execution environment
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
//1.解析参数
val params: ParameterTool = ParameterTool.fromArgs(args)
var statePath: String = null
var cpMode: String = null
var maxCount = 0
var sleepFactor: Long = 0L
var failRatio: Float = .0F
var mode: String = null
var taskNum = 0
var timeType: String = null
var shutdownWaitTS = 0L
var timeout = 0L
try {
statePath = params.get("fsStatePath", null)
cpMode = params.get("checkpointMode", "exactly_once")
maxCount = params.getInt("maxCount", 100000)
sleepFactor = params.getLong("sleepFactor", 100)
failRatio = params.getFloat("failRatio", 0.001f)
mode = params.get("waitMode", "ordered")
taskNum = params.getInt("waitOperatorParallelism", 1)
timeType = params.get("eventType", "EventTime")
shutdownWaitTS = params.getLong("shutdownWaitTS", 20000)
timeout = params.getLong("timeout", 10000L)
} catch {
case e: Exception =>
printUsage()
throw e
}
//2.配置程序参数
val configStringBuilder = new StringBuilder
val lineSeparator: String = System.getProperty("line.separator")
configStringBuilder.append("Job configuration").append(lineSeparator).append("FS state path=").append(statePath).append(lineSeparator).append("Checkpoint mode=").append(cpMode).append(lineSeparator).append("Max count of input from source=").append(maxCount).append(lineSeparator).append("Sleep factor=").append(sleepFactor).append(lineSeparator).append("Fail ratio=").append(failRatio).append(lineSeparator).append("Waiting mode=").append(mode).append(lineSeparator).append("Parallelism for async wait operator=").append(taskNum).append(lineSeparator).append("Event type=").append(timeType).append(lineSeparator).append("Shutdown wait timestamp=").append(shutdownWaitTS)
if (statePath != null) {
env.setStateBackend(new FsStateBackend(statePath))
}
if (EXACTLY_ONCE_MODE == cpMode) env.enableCheckpointing(1000L, CheckpointingMode.EXACTLY_ONCE)
else env.enableCheckpointing(1000L, CheckpointingMode.AT_LEAST_ONCE)
if (EVENT_TIME == timeType) env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
else if (INGESTION_TIME == timeType) env.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime)
//3.设置数据源
val inputStream: DataStream[Integer] = env.addSource(new SimpleSource(maxCount))
//4.定义异步处理规则
val function = new SampleAsyncFunction(sleepFactor, failRatio, shutdownWaitTS)
var result: DataStream[String] = null
if (ORDERED == mode) {
result = AsyncDataStream
.orderedWait(inputStream, function, timeout, TimeUnit.MILLISECONDS, 20)
.setParallelism(taskNum)
} else {
result = AsyncDataStream
.unorderedWait(inputStream, function, timeout, TimeUnit.MILLISECONDS, 20)
.setParallelism(taskNum)
}
//5.数据统计操作
result.flatMap[(String, Int)]((item: String, col: Collector[(String, Int)]) => {
col.collect((item, 1))
}).keyBy(0)
.sum(1)
.print()
//6.执行流式程序
env.execute("Async IO Example")
}
}
打印结果:
3> (key-6,31)
4> (key-2,31)
1> (key-3,31)
3> (key-7,31)
1> (key-4,31)
2> (key-8,31)
2> (key-9,31)
4> (key-5,31)
1> (key-4,32)
2> (key-8,32)
AsyncFunction超时处理
当异步IO请求超时时,默认情况下会抛出异常并重新启动job,如果希望自己处理超时,可以覆盖AsyncFunction的timeout,如下代码:
trait AsyncFunction[IN, OUT] extends java.lang.Object with org.apache.flink.api.common.functions.Function {
def asyncInvoke(input : IN, resultFuture : org.apache.flink.streaming.api.scala.async.ResultFuture[OUT]) : scala.Unit
def timeout(input : IN, resultFuture : org.apache.flink.streaming.api.scala.async.ResultFuture[OUT]) : scala.Unit = { /* compiled code */ }
}
Async IO原理
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 无序也比较简单,它使用两个队列就能实现: uncompletedQueue 和 completedQueue。
所有新进入该算子的元素,同样的包装成 Promise 并放入 uncompletedQueue 队列,当uncompletedQueue队列中任意的Promise返回了数据,则将该 Promise 移到 completedQueue 队列中,并通知 Emitter 消费。如下图所示:
EventTime 无序
uncompletedQueue存储的是每一个Promiss集合,对于大集合基于watermark有序,对于内部每一个小集合无序。数据发送到completedQueue 队列的规则是基本每一个watermark顺序,过程如下图: