前提已经有一定flink基础
上一篇文章 环境搭建Demo运行 已经完成基础的Demo试跑
接下来测试 精确一次 语义
source 为kafka
sink 为print
主要测试算子状态和checkpoint、savepoint的情况
阅读官网,可以知道很多Connector支持 精确一次 语义
而且checkpoint和savepoint对于异常重试和重新发布对于数据不丢不重是很关键的基础配置
结合上state的管理,可以极大限度的保证数据的完成性
我们这里使用阿里云的OSS作为state的存储与管理
需要容器可访问的Kafka
需要容器可访问的OSS
可以通过ping的方式查看连通性
阅读官网可知
为使用 flink-oss-fs-hadoop,在启动 Flink 之前,将对应的 JAR 文件从 opt 目录复制到 Flink 发行版中的 plugin 目录下的一个文件夹中,例如:
mkdir ./plugins/oss-fs-hadoop
cp ./opt/flink-oss-fs-hadoop-1.11.2.jar ./plugins/oss-fs-hadoop/
所以需要重新制作Docker镜像,把对应Jar包放到指定目录下,上篇文章的资源中已经做好相关镜像 flink:oss 版本
使用此镜像进行Session的启动,启动参数添加三个条件
-Dfs.oss.endpoint=<OSS-ENDPOINT> \
-Dfs.oss.accessKeyId=<OSS-ACCESSKEYID> \
-Dfs.oss.accessKeySecret=<OSS-ACCESSKEYSECRET>
启动后查看对应flink-conf.yaml可以看到已经增加的K8s的ConfigMap中
从官网DEMO中初始化应用
增加KAFKA依赖pom
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-connector-kafka_2.11artifactId>
<version>1.11.2version>
dependency>
启动类
package TestStreaming
import java.util.Properties
import java.util.concurrent.TimeUnit
import org.apache.flink.api.common.restartstrategy.RestartStrategies
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.api.common.time.Time
import org.apache.flink.core.fs.Path
import org.apache.flink.runtime.state.filesystem.FsStateBackend
import org.apache.flink.streaming.api.CheckpointingMode
import org.apache.flink.streaming.api.environment.CheckpointConfig.ExternalizedCheckpointCleanup
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer
/**
* @author xuanqisong
* @date 2021/2/22 15:43
* @document
*/
object TestKafka {
def main(args: Array[String]): Unit = {
// init kafka
val kafkaProperties = new Properties()
kafkaProperties.setProperty("bootstrap.servers", "20.20.0.185:9092")
kafkaProperties.setProperty("group.id", "flink-test-kafka")
val kafkaConsumer = new FlinkKafkaConsumer[String]("test", new SimpleStringSchema(), kafkaProperties)
// init flink env
val flinkEnv = StreamExecutionEnvironment.getExecutionEnvironment
flinkEnv.setParallelism(1)
// check point
// 5S 一次检测
flinkEnv.enableCheckpointing(5000)
// 设置模式为精确一次 (这是默认值)
flinkEnv.getCheckpointConfig.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE)
// Checkpoint 必须在一分钟内完成,否则就会被抛弃
flinkEnv.getCheckpointConfig.setCheckpointTimeout(60000)
// 是否自动起立checkpoint文件
flinkEnv.getCheckpointConfig.enableExternalizedCheckpoints(ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION) // 保留
// flinkEnv.getCheckpointConfig.enableExternalizedCheckpoints(ExternalizedCheckpointCleanup.DELETE_ON_CANCELLATION) // 自动删除
flinkEnv.setStateBackend(new FsStateBackend("oss://bucket-flink//flink-k8s-test//checkpoint//test-kafka"))
// flinkEnv.setStateBackend(new FsStateBackend(new Path("file:///D:/checkpoint/testkafka/")))
// 重启策略
flinkEnv.setRestartStrategy(RestartStrategies.fixedDelayRestart(10000, Time.of(1, TimeUnit.SECONDS))) // 5表示最大重试次数为5次,10s为延迟时间
val dataStream = flinkEnv
.addSource(kafkaConsumer).name("kafka_source").uid("kafka_source")
.map(s => (1, s)).name("map_key").uid("map_key")
.keyBy(t => t._1)
.map(new CheckWordMap).name("map_and_error").uid("map_and_error")
.print().name("print").uid("print")
flinkEnv.execute("test-kafka-job")
}
}
map函数
package TestStreaming
import org.apache.flink.api.common.functions.RichMapFunction
import org.apache.flink.api.common.state.{MapState, MapStateDescriptor, ValueState, ValueStateDescriptor}
import org.apache.flink.api.common.typeinfo.TypeInformation
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.scala.createTypeInformation
import org.apache.flink.api.common.state.StateTtlConfig
import org.apache.flink.api.common.state.ValueStateDescriptor
import org.apache.flink.api.common.time.Time
/**
* @author xuanqisong
* @date 2021/2/23 14:44
* @document
*/
class CheckWordMap extends RichMapFunction[(Int, String), String] {
private var wordSum: ValueState[String] = _
private var errorCheck: MapState[String, Long] = _
override def map(in: (Int, String)): String = {
val tempWordSum = wordSum.value
val currentWordSum = if (tempWordSum != null) {
tempWordSum
} else {
""
}
val newWordSum = currentWordSum + "," + in._2
wordSum.update(newWordSum)
if (errorCheck.contains(in._2)){
throw new Exception("Stop thread")
}else{
errorCheck.put(in._2, 0L)
}
wordSum.value()
}
override def open(parameters: Configuration): Unit = {
super.open(parameters)
wordSum = getRuntimeContext.getState(
new ValueStateDescriptor[String]("wordSum", TypeInformation.of(classOf[String]))
)
val mapStateDescriptor = new MapStateDescriptor[String, Long]("errorCheck", TypeInformation.of(classOf[String]), TypeInformation.of(classOf[Long]))
// 主动异常MapState的过期时间,测试异常后checkpoint重启对于数据的影响
val ttlConfig = StateTtlConfig
.newBuilder(Time.seconds(20))
.setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
.setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
.build
mapStateDescriptor.enableTimeToLive(ttlConfig)
errorCheck = getRuntimeContext.getMapState(
mapStateDescriptor
)
}
}
这里需要注意的是,一定要使用Rich相关函数进行继承,这样才可以使用getRunTimeContext的上下文,对状态进行注册
进行打包发布
mvn clean package
在target文件夹中复制jar到服务器上进行发布
./flink run -c TestStreaming.TestKafka -d -e kubernetes-session -Dkubernetes.namespace=<name-space> -Dkubernetes.cluster-id=<cluster-id> <jar-path>
这里发布后在监控页面就能看到对应Session中已经存在启动的Job
向kafka对应的topic内推送数据
a
b
c
我们通过最后的Sink点击进去到TaskManager中查看对应的输出
此测试样例中 如果推送的数据在20S内出现重复就会主动触发一个异常
Stop Thread 异常,可以通过任务的异常栈看到具体的输出
由于MapState有20秒自动清理机制,所以在超过20S后重复数据依然会写入到ValueState中
至此我们可以看到checkpoint对于 精确一次 语义的支持,而且在对应的oss中可以看到对应的checkpoint产生的数据
Savepoint是用户管理的数据,用作应用的更新迭代操作
官网可知,如果对应用算子结构/并发度进行了相应的更改,需要通过算子UID进行重新分配,所以上述测试代码中已经在每个算子中增加了name/uid的名称
进行savepoint的触发
./flink stop -p oss://bucket-flink//flink-k8s-test//savepoint//test-kafka -e kubernetes-session -Dkubernetes.namespace=<name-space> -Dkubernetes.cluster-id=<cluster-id> <JOB-ID>
写入savepoint 的oss地址进行触发
执行后可以发现OSS已经存在了对应的savepoint的备份
进行savepoint恢复
./flink run -d -s <SAVE-POINT-PATH> -e kubernetes-session -Dkubernetes.namespace=<name-space> -Dkubernetes.cluster-id=<cluster-id> -c TestStreaming.TestKafka <jar-path>
启动后,可以看Session中又恢复了对应的Job
我们再继续向kafka推送数据
可以看到JobManager的输出数据是根据上一次的数据累加得到
至此已经完成对checkpoint/savepoint的测试