读写文件
1 读取文件-readFile
Q:什么是文件数据源?
A:Apache Flink提供了一个可重置的数据源连接器,支持将文件中的数据提取成数据流。
(该文件系统是flink的一部分因此无需同Kafka那样添加依赖包)
val lineReader = new TextInputFormat(null)
streamLocal.readFile[String](
lineReader,
// "hdfs://master:8020/file/a.txt",
"file:///D:\\tmp\\a.txt",
FileProcessingMode.PROCESS_CONTINUOUSLY,
5000L)
.map(x => {
val value = x.split(",")
(value(0), value(1).toInt)
})
- TextInputFormat:会按(由换行符指定)行来读取文本文件。
- PROCESS_CONTINUOUSLY:会以时间间隔周期性的扫描文件,如果目标文件发生了修改,就会将修改过的文件重新读取一份。
(注意这里的读取是全部读取)
- PROCESS_ONE:不会以时间间隔扫描文件。
2 写入到文件-StreamingFileSink
应用配置了检查点,且读取的数据源可以提供在故障中恢复的功能,那么StreamFileSink就可以提供端到端的一次性保障。
2.1 在了解-StreamingFileSink之前你需要了解的知识点
-
进行,等待,和完成:
- 当数据写入文件时,文件会进入“进行” (part-0-0.inprogress)状态。当RollingPolicy决定生成文件时,原文件会关闭,并通过重命名(part-0-0.wait)进入等待状态,在下一次检查点完成后处于等待状态的文件将(再次重命名part-0-0)进入完成状态。
-
RollingPolicy:
- 使用行编码写入文件的时候,我们可以通过定义RollingPolicy来决定何时创建分块文件
(什么是分块文件会在下面的行编码中有讲到)
,但在批量编码中是无法选择RollingPolicy的。批量编码只能与检查点结合起来使用,即当每次生成一个检查点的时候,就会对应的生成一个新的分块文件。
- 使用行编码写入文件的时候,我们可以通过定义RollingPolicy来决定何时创建分块文件
2.1.1 结论
如果应用没有开启检查点,StreamingFlinkSink将永远不会把等待状态的文件变成完成状态。
2.2 行编码
val input = streamLocal.socketTextStream("master", 9999)
val sink = StreamingFileSink.forRowFormat(
new Path("file:///D:\\tmp"),
new SimpleStringEncoder[String]("UTF-8")
).build()
input.addSink(sink)
-
自定义BucketAssigner
- 默认:以程序写入的时间在tmp下生成每小时一个的目录
(该目录的形成方式是flink通过调用BucketAssigner来完成,我们可以通过修改BucketAssigner的方式来自定义。)
例如:
- 数据是2019:08:04 10:10:01秒写入的,就会在D:\tmp下生成一个2019-08-04 - - 10的一个目录。
- 数据是2019:08:04 11:10:01秒写入的,就会在D:\tmp下生成一个2019-08-04 - - 11的一个目录。
- 默认:以程序写入的时间在tmp下生成每小时一个的目录
-
目录下的分块文件
- 每一个目录(019-08-04 - -11)中都会包含很多分块文件(part file),如下,该分块文件的形成方式是通过StreamingFileSink的多个并行实例来进行并发写入
的:下面的分块文件代表的是:编号为0的的任务写出的第0个文件
- 每一个目录(019-08-04 - -11)中都会包含很多分块文件(part file),如下,该分块文件的形成方式是通过StreamingFileSink的多个并行实例来进行并发写入
-
上面的分块文件是如何创建的:RollingPolicy?
- RollingPolicy用来决定任务何时创建一个新的分块文件。默认是现有文件大小超过128M或打开时间超过60S就会创建一个新的分块文件。
2.2.1 行编码自定义-BucketAssigner
我们可以通过修改BucketAssigner的方式来自定义
val sink: StreamingFileSink[String] = StreamingFileSink.forRowFormat(
new Path("file:///D:\\tmp"),
new SimpleStringEncoder[String]("UTF-8")
)
.withBucketAssigner(new BucketAssigner[String, String] { //对应的是输入类型和BucketID(目录名称)类型
def getPartitions(element: String): String = {
val date_L = element.split(",")(1).toLong //将数据中的事件时间作为目录进行存储
val file_path = new SimpleDateFormat("yyyyMMdd").format(new Date(date_L));
s"dt=$file_path"
}
override def getBucketId(element: String, context: BucketAssigner.Context): String = {
var partitionValue = ""
try {
partitionValue = getPartitions(element)
} catch {
case e: Exception => partitionValue = "00000000"
}
partitionValue
}
override def getSerializer: SimpleVersionedSerializer[String] = {
SimpleVersionedStringSerializer.INSTANCE;
}
}).build()
2.3 批量编码
- 在前面的行编码中我们使用的写入方式是行写入。
- 在行编码中每条记录都会被单独进行编码然后添加到分块文件中,然而在批量编码模式下,记录会被攒成批,然后一次性的写入。Apache Pqrquet会以列式的方式组织和压缩,因此该文件格式需要以批量编码的方式写入。
/**
* 需要导入的依赖包
* "org.apache.flink" % "flink-parquet" % "1.7.0"
* "org.apache.parquet" % "parquet-avro" % "1.10.1"
* */
object 自定义批量写入文件 {
case class Avro_Pojo(name: String, age: Int)
def main(args: Array[String]): Unit = {
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
val streamLocal = StreamExecutionEnvironment.createLocalEnvironment(1)
//由于检查点为了支持容错会以固定间隔来进行创建,在这里我定义成周期为10秒
streamLocal.enableCheckpointing(5000L)
import org.apache.flink.streaming.api.scala._ //如果数据集是无限的可以引入这个包,来进行隐式转换操作
val input = streamLocal.socketTextStream("master", 9999)
.map(x=>{
val value = x.split(",")
val end = Avro_Pojo(value(0), value(1).toInt)
end
})
val sink = StreamingFileSink.forBulkFormat(
new Path("file:///D:\\tmp"),
//妈的注意这里是:forReflectRecord不是forSpecificRecord,小心被坑
ParquetAvroWriters.forReflectRecord(classOf[Avro_Pojo]))
.build()
input.addSink(sink).name("ceshi")
streamLocal.execute("write file")
}
}
2.3.1 批量编码自定义-BucketAssigner
批量编码自定义BucketAssigner,与上面的行编码自定义是一样的