Flink目前對(duì)于外部Exectly-Once支持提供了兩種的connector,一個(gè)是Flink-Kafka Connector,另一個(gè)是Flink-Hdfs Connector,這兩種connector實(shí)現(xiàn)的Exectly-Once都是基于Flink checkpoint提供的hook來(lái)實(shí)現(xiàn)的兩階段提交模式來(lái)保證的,主要應(yīng)用在實(shí)時(shí)數(shù)倉(cāng)、topic拆分、基于小時(shí)分析處理等場(chǎng)景下。本篇將會(huì)介紹StreamingFileSink的基本用法、如何壓縮數(shù)據(jù)以及合并產(chǎn)生的小文件。
一、StreamingFileSink基本用法
StreamingFileSink提供了基于行、列兩種文件寫入格式,用法:
//行
StreamingFileSink.forRowFormat(new Path(path),
new SimpleStringEncoder<T>())
.withBucketAssigner(new PaulAssigner<>()) //分桶策略
.withRollingPolicy(new PaulRollingPolicy<>()) //滾動(dòng)策略
.withBucketCheckInterval(CHECK_INTERVAL) //檢查周期
.build();
//列 parquet
StreamingFileSink.forBulkFormat(new Path(path),
ParquetAvroWriters.forReflectRecord(clazz))
.withBucketAssigner(new PaulBucketAssigner<>())
.withBucketCheckInterval(CHECK_INTERVAL)
.build();
這兩種寫入格式除了文件格式的不同,另外一個(gè)很重要的區(qū)別就是回滾策略的不同,forRowFormat行寫可基于文件大小、滾動(dòng)時(shí)間、不活躍時(shí)間進(jìn)行滾動(dòng),但是對(duì)于forBulkFormat列寫方式只能基于checkpoint機(jī)制進(jìn)行文件滾動(dòng),即在執(zhí)行snapshotState方法時(shí)滾動(dòng)文件,如果基于大小或者時(shí)間滾動(dòng)文件,那么在任務(wù)失敗恢復(fù)時(shí)就必須對(duì)處于in-processing狀態(tài)的文件按照指定的offset進(jìn)行truncate,我想這是由于列式存儲(chǔ)是無(wú)法針對(duì)文件offset進(jìn)行truncate的,因此就必須在每次checkpoint使文件滾動(dòng),其使用的滾動(dòng)策略實(shí)現(xiàn)是OnCheckpointRollingPolicy。
二、文件壓縮
通常情況下生成的文件用來(lái)做按照小時(shí)或者天進(jìn)行分析,但是離線集群與實(shí)時(shí)集群是兩個(gè)不同的集群,那么就需要將數(shù)據(jù)寫入到離線集群中,在這個(gè)過(guò)程中數(shù)據(jù)流量傳輸成本會(huì)比較高,因此可以選擇parquet文件格式,然而parquet存儲(chǔ)格式默認(rèn)是不壓縮格式:
//ParquetWriter.Builder中
private CompressionCodecName codecName = DEFAULT_COMPRESSION_CODEC_NAME;
在Flink中的ParquetAvroWriters未提供壓縮格式的入口,但是可以自定義一個(gè)ParquetAvroWriters,在創(chuàng)建ParquetWriter時(shí),指定壓縮算法:
public class PaulParquetAvroWriters {
public static <T extends SpecificRecordBase> ParquetWriterFactory<T> forSpecificRecord(Class<T> type,CompressionCodecName compressionCodecName) {
final String schemaString = SpecificData.get().getSchema(type).toString();
final ParquetBuilder<T> builder = (out) -> createAvroParquetWriter(schemaString, SpecificData.get(), out,compressionCodecName);
return new ParquetWriterFactory<>(builder);
}
//compressionCodecName 壓縮算法
public static ParquetWriterFactory<GenericRecord> forGenericRecord(Schema schema,CompressionCodecName compressionCodecName) {
final String schemaString = schema.toString();
final ParquetBuilder<GenericRecord> builder = (out) -> createAvroParquetWriter(schemaString, GenericData.get(), out,compressionCodecName);
return new ParquetWriterFactory<>(builder);
}
//compressionCodecName 壓縮算法
public static <T> ParquetWriterFactory<T> forReflectRecord(Class<T> type,CompressionCodecName compressionCodecName) {
final String schemaString = ReflectData.get().getSchema(type).toString();
final ParquetBuilder<T> builder = (out) -> createAvroParquetWriter(schemaString, ReflectData.get(), out,compressionCodecName);
return new ParquetWriterFactory<>(builder);
}
//compressionCodecName 壓縮算法
private static <T> ParquetWriter<T> createAvroParquetWriter(
String schemaString,
GenericData dataModel,
OutputFile out,
CompressionCodecName compressionCodecName) throws IOException {
final Schema schema = new Schema.Parser().parse(schemaString);
return AvroParquetWriter.<T>builder(out)
.withSchema(schema)
.withDataModel(dataModel)
.withCompressionCodec(compressionCodecName)//壓縮算法
.build();
}
private PaulParquetAvroWriters() {}
}
那么在使用時(shí)根據(jù)實(shí)際情況傳入SNAPPY、LZO、GZIP等壓縮算法,但是需要注意的壓縮雖然減少了io的消耗,帶來(lái)的卻是cpu的更多消耗,在實(shí)際使用中進(jìn)行權(quán)衡。
三、小文件處理
不管是Flink還是SparkStreaming寫hdfs不可避免需要關(guān)注的一個(gè)點(diǎn)就是如何處理小文件,眾多的小文件會(huì)帶來(lái)兩個(gè)影響:
Hdfs NameNode維護(hù)元數(shù)據(jù)成本增加
下游hive/spark任務(wù)執(zhí)行的數(shù)據(jù)讀取成本增加
理想狀態(tài)下是按照設(shè)置的文件大小滾動(dòng),那為什么會(huì)產(chǎn)生小文件呢?這與文件滾動(dòng)周期、checkpoint時(shí)間間隔設(shè)置相關(guān),如果滾動(dòng)周期較短、checkpoint時(shí)間也比較短或者數(shù)據(jù)流量有低峰期達(dá)到文件不活躍的時(shí)間間隔,很容易產(chǎn)生小文件,接下來(lái)介紹幾種處理小文件的方式:
-
減少并行度
回顧一下文件生成格式:part+subtaskIndex+connter,其中subtaskIndex代表著任務(wù)并行度的序號(hào),也就是代表著當(dāng)前的一個(gè)寫task,越大的并行度代表著越多的subtaskIndex,數(shù)據(jù)就越分散,如果我們減小并行度,數(shù)據(jù)寫入由更少的task來(lái)執(zhí)行,寫入就相對(duì)集中,這個(gè)在一定程度上減少的文件的個(gè)數(shù),但是在減少并行的同時(shí)意味著任務(wù)的并發(fā)能力下降;
-
增大checkpoint周期或者文件滾動(dòng)周期
以parquet寫分析為例,parquet寫文件由processing狀態(tài)變?yōu)閜ending狀態(tài)發(fā)生在checkpoint的snapshotState階段中,如果checkpoint周期時(shí)間較短,就會(huì)更快發(fā)生文件滾動(dòng),增大checkpoint周期,那么文件就能積累更多數(shù)據(jù)之后發(fā)生滾動(dòng),但是這種增加時(shí)間的方式帶來(lái)的是數(shù)據(jù)的一定延時(shí);
-
下游任務(wù)合并處理
待Flink將數(shù)據(jù)寫入hdfs后,下游開(kāi)啟一個(gè)hive或者spark定時(shí)任務(wù),通過(guò)改變分區(qū)的方式,將文件寫入新的目錄中,后續(xù)任務(wù)處理讀取這個(gè)新的目錄數(shù)據(jù)即可,同時(shí)還需要定時(shí)清理產(chǎn)生的小文件,這種方式雖然增加了后續(xù)的任務(wù)處理成本,但是其即合并了小文件提升了后續(xù)任務(wù)分析速度,也將小文件清理了減小了對(duì)NameNode的壓力,相對(duì)于上面兩種方式更加穩(wěn)定,因此也比較推薦這種方式。
四、總結(jié)
本文重點(diǎn)分析了StreamingFileSink用法、壓縮與小文件合并方式,StreamingFileSink支持行、列兩種文件寫入格式,對(duì)于壓縮只需要自定義一個(gè)ParquetAvroWriters類,重寫其createAvroParquetWriter方法即可,對(duì)于小文件合并比較推薦使用下游任務(wù)合并處理方式。