Flink
Flink 的测试工具只有 local cluster fake,而且它需要用户花费大量的精力手动生成所有那些与测试中的每个特定功能相关的流(及其相应的输出)。
Flink文档中描述的测试流程序的策略建议使用经典的单元测试框架来测试单个运算符,并使用 local cluster fake 来测试一个完整的程序,同时也参考了一些具有不稳定接口的内部测试工具来测试检查点和状态处理。
然而,在所有这些情况下,用户都需要手动介绍输入流和预期输出,一般来说,输入流和预期输出可能是巨大的,这使得这些测试技术变得繁琐而容易出错。
此外,文档本身指出,状态处理测试 “由于时间上的依赖性,状态处理测试可能很棘手”,这就强调了面向流的测试技术的必要性。
Flink Check
Flink Check 是 Apache Flink 的一个基于属性的测试库,它扩展了 ScalaCheck
的线性时序逻辑运算符,适用于测试 Flink 数据流转换。
基于属性的测试(PBT)是一种自动的黑盒测试技术,它通过生成随机输入并检查获得的输出是否满足给定的属性来测试功能。
FlinkCheck 提供了一个有边界的时间逻辑,用于生成函数的输入和声明属性。这个逻辑是为流媒体系统设计的,它允许用户定义流如何随时间变化,以及哪些属性应该验证相应的输出。
FlinkCheck随机生成指定数量的有限输入流前缀,并对Flink运行时产生的输出流进行评估。
Flink Check 是基于 sscheck,这是 Apache Spark 的一个基于属性的测试库,所以它依赖于 sscheck-core 项目,其中包含了 sscheck 和 Flink Check 共同的代码,特别是系统所基于的 LTLss 逻辑的实现。
LTLss
是一种有限字的离散时间线性时序逻辑,在 Spark Streaming 的 Property-based testing for Spark Streaming 的论文中有详细介绍。
Getting started
Flink Check 已经用 Scala 2.11.8
和 Apache Flink 1.8.0
进行了测试。
Flink Check 可作为 Maven 依赖使用
<dependency>
<groupId>es.ucm.fdi</groupId>
<artifactId>flink-check_2.11</artifactId>
<version>0.0.2</version>
<type>pom</type>
</dependency>
LTLss & FlinkCheck
LTLssgenerators and their FlinckCheck counterparts
LTLss generator | FlinkCheck generator | 含义 |
---|---|---|
𝑋 g | WindowGen.next(g) | 生成一个空窗口和一个使用g生成的窗口 |
☐n g | WindowGen.always(g,n) | 生成连续不断的n个窗口 |
♢𝗇 g | WindowGen.eventually(g,n) | 生成1到n-1个空窗口,使用g生成最后一个窗口 |
g1 𝑈𝗇 g2 | WindowGen.until(g1,g2,n) | 使用g1生成少于n个窗口并使用g2一次 |
LTLss formulas and their FlinkCheck counterparts.
LTLss formula | FlinkCheck formula | 含义 |
---|---|---|
Boolean expr.b | Solved(b) | 基本的公式是布尔函数和常量 |
𝑋 f | Formula.next(f) | f对下一个窗口保持成立 |
☐n f | Formula.always(f) during n | f对前n个窗口保持成立 |
♢𝗇 f | Formula.eventually(f) during n | f对前n个窗口中的任意一个保持成立 |
f1 𝑈𝗇 f2 | f1.until(f2) during n | f1对前k个窗口(0≤k<n)和f2对k+1窗口保持成立 |
λᵗ₍ℹ,𝑜₎ f | Formula.consume(fr) | ℹ当前输入窗口,𝑜当前输出窗口,ᵗ当前时间戳 |
How properties are executed
属性是通过扩展特征 DataStreamTLProperty
来定义的,该特征包括用于定义属性的 forAllDataStream
方法,该属性为每个测试用例生成 DataStream[in]
,应用 testSubject
以生成 DataStream[out]
,并检查这些数据流是否满足指定的 formula
。
type TSeq[A] = Seq[TimedElement[A]]
type TSGen[A] = Gen[TSeq[A]]
type Letter[In, Out] = (DataSet[TimedElement[In]], DataSet[TimedElement[Out]])
def forAllDataStream[In : TypeInformation,
Out : TypeInformation](generator: TSGen[In])
(testSubject: (DataStream[In]) => DataStream[Out])
(formula: FlinkFormula[Letter[In, Out]])
(implicit pp1: TSGen[In] => Pretty): Prop
每个测试用例分两个阶段执行
- 运行测试用例:
- 在内存中为
case class TimedElement[T](TIMESTAMP:LONG,VALUE:T)
生成测试用例为Seq[TimedElement[Input]]
- 将其转换为
DataStream[In]
并应用测试对象以获取DataStream[Out]
- 将输入和输出的数据流存储到 Flink 配置的默认文件系统中
- 由于生成的测试用例是有限序列,因此结果流也是有限的
- 在内存中为
- 评估测试用例:
- 为了检查
formula
,我们通过从文件系统读取输入流和输出流来评估测试用例 - 每个流都被分割成窗口,作为
case class TimedWindow[T](timestamp: Long, data: DataSet[TimedElement[T]])
中的一个Iterator[TimedWindow[T]]
- 我们对输入流和输出流的窗口迭代器进行压缩,获取一个向
formula
提供消息来检查测试用例是否通过的TimedWindow
对序列
- 为了检查
How time is handled in properties
由于 LTLss 逻辑使用离散时间,而 Flink 流是连续的时间,所以我们必须在两个方向上执行离散和连续的转换。
- 离散生成器连续化
sscheck-core
中的生成器对象WindowGen
和PStreamGen
生成值序列,其中每个嵌套序列都是一个窗口。FlinkGenerators.tumblingTimeWindows
方法通过将每个嵌套序列理解为为一个指定大小的滚动窗口,将其转换为Gen[Seq[TimedElement[A]]]
- 在每个窗口内,每个元素的时间戳通过均匀的分布获得一个从窗口开始(包括)到窗口结束(不包括)之间的随机值
- 可指定开始时间,但通常以0作为标准,因为它使得测试执行日志变得更容易解释
- 除了将来自 sscheck-core 的生成器与
tumblingTimeWindows
相结合外,还可以使用Seq[TimedElement[in]]
的任何生成器。唯一的要求是,元素是按时间戳的递增顺序生成的。
- 为了评估测试用例,连续的输入和输出的 Flink 流被离散化,所以我们可以将其理解为 LTLss logic 中的
letters
。为此必须为formula
指定StreamDiscretizer
,但这只是把数据流分割成窗口的一个标准。目前我们支持tumbling time windows
和sliding time windows
因此,离散和连续世界之间转换是基于时间的窗口标准来执行的。 在一个属性中,我们可以对 generator
和 formula
使用不同的窗口化条件,尽管两者应该使用相同的开始时间–在所有标准的默认情况都以0为开始时间。
How we use event time
Flink Check 依赖于 event time 来完成所有工作。使用 DataStream.assignAscendingTimestamps
将 TimedElement
中的时间戳指定为生成的输入流中的 event time,因为我们按该顺序生成元素,因此可以很好地工作。我们这样做是为了使测试更具可预测性,因为事件计时不那么依赖于运行测试的硬件的性能。测试对象不需要知道事件时间,并且通过特征 DataStreamTLProperty
的默认配置为每个测试用例创建 StreamExecutionEnvironment
(可以通过复写方法 buildFreshStreamExecutionEnvironment
来更改)已经配置为使用 event time。
对于事件时间相关的场景,我们有两个选择。
- 我们可以使用方法
FlinkGenerators.eventTimeToFieldAssigner
来指定方法fieldAssigner: Long => A => A
根据分配的事件时间设置生成元素的相关字段,使 event time 与生成的时间戳一致。- 例如,对于
case class SensorData(timestamp: Long, sensor_id: Int, concentration: Double)
我们使用ts => _.copy(timestamp = ts)
作为字段分配器。
- 例如,对于
- 使用
Seq[TimedElement[in]]
的自定义生成器,其中我们可以完全控制时间戳