flink入门之StreaimingFileSink的使用

需求:用flink实时消费kafka信息,将信息存储到hdfs上。
方案:用flink提供的StreaimingFileSink方法。

forRowFormat方法

这个方法比较简单,就是把读到的信息按照行存储的格式写入hdfs上,我们这里直接看下官方提供的代码:(分桶策略后面再说)

import org.apache.flink.api.common.serialization.SimpleStringEncoder
import org.apache.flink.core.fs.Path
import org.apache.flink.streaming.api.functions.sink.filesystem.StreamingFileSink
import org.apache.flink.streaming.api.functions.sink.filesystem.rollingpolicies.DefaultRollingPolicy

val input: DataStream[String] = ...

val sink: StreamingFileSink[String] = StreamingFileSink
    .forRowFormat(new Path(outputPath), new SimpleStringEncoder[String]("UTF-8"))
    .withRollingPolicy(
    	//设置滚动策略
        DefaultRollingPolicy.builder()
       		 //15min 滚动,生成一个新文件
            .withRolloverInterval(TimeUnit.MINUTES.toMillis(15))
            //5min未接收到数据,滚动,生成一个新文件
            .withInactivityInterval(TimeUnit.MINUTES.toMillis(5))
            //文件大小达到1G,滚动,生成一个新文件
            .withMaxPartSize(1024 * 1024 * 1024)
            .build())
     //设置分桶策略
    .withBucketAssigner(dayAssigner)
    .build()

input.addSink(sink)

我们基本看代码就能知道这个sink的使用方法了。

forBulkFormat方法

但是除了上述的按照行存储的方式,我们往往需要指定其他的存储格式,例如:parquet,Avro,ORC等等…而且,我们还需要压缩写入的文件,针对这种情况,我们就需要换另一种方法,采用另一种编码器了。

Parquet格式

这里用parquet举例,其他情况类似。
首先我们要额外引入依赖:

> <dependency>   
> <groupId>org.apache.flink</groupId>  
> <artifactId>flink-parquet_2.11</artifactId>  
> <version>1.11.2</version> 
> </dependency>

然后代码:

import org.apache.flink.streaming.api.functions.sink.filesystem.StreamingFileSink
import org.apache.flink.formats.parquet.avro.ParquetAvroWriters

val input: DataStream[DemoBean] = ...

val sink: StreamingFileSink[DemoBean] = StreamingFileSink
    .forBulkFormat(outputBasePath, ParquetAvroWriters.forReflectRecord(classOf[DemoBean]))
    .withBucketAssigner(assigner)
    .build()

input.addSink(sink)

我们可以很明显的看出,编码方式由行存储的:

new SimpleStringEncoder[String]("UTF-8")

变成了:

ParquetAvroWriters.forReflectRecord(classOf[DemoBean])

然后我们发现这样的写入方式并没有设置文件的滚动策略,这里我们等下再说,然后现在,数据就可以以parquet的格式写入hdfs了。

Parquet格式+snappy压缩

上面我们说的两种方式,细心的朋友发现,在官网其实都可以找到,但是,实际生产中,我们经常还有一种需求,就是要求写入的文件要用上压缩,例如:Snappy,Lzo,Gzip等…官网并没有提供这种写法的示例,但我们可以自己实现。
首先需额外导入依赖:

		<dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-parquet_2.11</artifactId>
            <version>1.9.1</version>
        </dependency>

        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-avro</artifactId>
            <version>1.9.1</version>
        </dependency>

        <dependency>
            <groupId>org.apache.parquet</groupId>
            <artifactId>parquet-avro</artifactId>
            <version>1.10.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.parquet</groupId>
            <artifactId>parquet-hadoop</artifactId>
            <version>1.10.0</version>
        </dependency>

然后我们可以看一下AvroParquetWriter这个类的源码:

/**
   * Create a new {@link AvroParquetWriter}.
   *
   * @param file The file name to write to.
   * @param writeSupport The schema to write with.
   * @param compressionCodecName Compression code to use, or CompressionCodecName.UNCOMPRESSED
   * @param blockSize the block size threshold.
   * @param pageSize See parquet write up. Blocks are subdivided into pages for alignment and other purposes.
   * @param enableDictionary Whether to use a dictionary to compress columns.
   * @param conf The Configuration to use.
   * @throws IOException
   */
  AvroParquetWriter(Path file, WriteSupport<T> writeSupport,
                           CompressionCodecName compressionCodecName,
                           int blockSize, int pageSize, boolean enableDictionary,
                           boolean enableValidation, WriterVersion writerVersion,
                           Configuration conf)
      throws IOException {
    super(file, writeSupport, compressionCodecName, blockSize, pageSize,
        pageSize, enableDictionary, enableValidation, writerVersion, conf);
  }

可以看到,是可以传入一个CompressionCodecName的参数的,但是我们上面调用的ParquetAvroWriters.forReflectRecord(classOf[DemoBean])方法并没有要求去传一个压缩格式,所以我们需要重新写这个方法,然后调用:

StreamingFileSink.forBulkFormat(
      outputBasePath,
      MyParquetAvroWriterSnappy.forReflectRecord(CompressionCodecName.SNAPPY, classOf[DemoBean])
      )
     .withBucketAssigner(dayAssigner)
     .build()

MyParquetAvroWriterSnappy类Scala实现:

import org.apache.avro.Schema
import org.apache.avro.generic.GenericData
import org.apache.avro.reflect.ReflectData
import org.apache.flink.formats.parquet.{ParquetBuilder, ParquetWriterFactory}
import org.apache.parquet.avro.AvroParquetWriter
import org.apache.parquet.hadoop.ParquetWriter
import org.apache.parquet.hadoop.metadata.CompressionCodecName
import org.apache.parquet.io.OutputFile

object ParquetAvroWriterSnappyScala {
  def forReflectRecord(compressionCodecName:CompressionCodecName,t1:Class[DemoBean]): ParquetWriterFactory[DemoBean] ={
    val schemaString = ReflectData.get().getSchema(t1).toString()
    val builder = new MyParquet(schemaString,compressionCodecName)
    new ParquetWriterFactory(builder)

  }

  def createAvroParquetWriter(schemaString:String,dataModel:GenericData,out:OutputFile,compressionCodecName:CompressionCodecName): ParquetWriter[DemoBean] ={
    val schema = new Schema.Parser().parse(schemaString)
    val builder = AvroParquetWriter.builder[DemoBean](out)
      .withSchema(schema)
      .withDataModel(dataModel)
      .withCompressionCodec(compressionCodecName)
      .build()
    builder
  }
}

private class MyParquet(schemaString:String,compressionCodecName: CompressionCodecName) extends ParquetBuilder[DemoBean]{
  override def createWriter(out: OutputFile): ParquetWriter[DemoBean] = {
    ParquetAvroWriterSnappyScala.createAvroParquetWriter(schemaString,ReflectData.get(),out,compressionCodecName)
  }
}

MyParquetAvroWriterSnappy类java实现:

import org.apache.avro.Schema;
import org.apache.avro.generic.GenericData;
import org.apache.avro.reflect.ReflectData;
import org.apache.flink.formats.parquet.ParquetBuilder;
import org.apache.flink.formats.parquet.ParquetWriterFactory;
import org.apache.parquet.avro.AvroParquetWriter;
import org.apache.parquet.hadoop.ParquetWriter;
import org.apache.parquet.hadoop.metadata.CompressionCodecName;
import org.apache.parquet.io.OutputFile;

import java.io.IOException;

public class ParquetAvroWriterSnappyDemo {

    public static <T> ParquetWriterFactory<T> forReflectRecord(Class<T> type, CompressionCodecName codecName) {
        final String schemaString = ReflectData.get().getSchema(type).toString();
        final ParquetBuilder<T> builder = (out) -> createAvroParquetWriter(schemaString, ReflectData.get(), out, codecName);
        return new ParquetWriterFactory<>(builder);
    }

    private static <T> ParquetWriter<T> createAvroParquetWriter(
            String schemaString,
            GenericData dataModel,
            OutputFile out,
            CompressionCodecName codecName) throws IOException {

        final Schema schema = new Schema.Parser().parse(schemaString);

        return AvroParquetWriter.<T>builder(out)
                .withSchema(schema)
                .withDataModel(dataModel)
                .withCompressionCodec(codecName)
                .build();
    }

    private ParquetAvroWriterSnappyDemo() {}
}

我们重新自定义了带压缩的写入方法后,就可以指定压缩名字传入了,我们这里分别用parquet写入和parquet+snappy压缩写入10万条相同数据验证代码的正确性:

886.0 K /tmp/test/2020-11-11-uncompress
449.8 K /tmp/test/2020-11-11-compress

虽然数据较少,但是也能看出来,压缩设置确实生效了。

自定义分桶策略

Flink 提供了两个分桶策略:

  • BasePathBucketAssigner,不分桶,所有文件写到根目录;
  • DateTimeBucketAssigner,基于系统时间(yyyy-MM-dd–HH)分桶。

我们在实际生产中往往根据不同要求调整分桶策略(说白了就是自定义文件写入的文件夹的名字)例如按天的自定义分桶:

private class DayBucketAssigner(formatString: String,zoneId: ZoneId) extends BucketAssigner[DemoBean,String] {
    private val serialVersionUID = 10000L
    // DateTimeFormatter被用来通过当前系统时间和DateTimeFormat来生成时间字符串
    private var dateTimeFormatter:DateTimeFormatter = null
    override def getBucketId(element: DemoBean, context: BucketAssigner.Context): String = {
      if(dateTimeFormatter == null){
        dateTimeFormatter = DateTimeFormatter.ofPattern(formatString).withZone(zoneId)
      }
      val dateStr = dateTimeFormatter.format(Instant.ofEpochMilli(context.currentProcessingTime()))
      dateStr
    }

    override def getSerializer: SimpleVersionedSerializer[String] = {
      SimpleVersionedStringSerializer.INSTANCE
    }
  }

滚动策略

分桶策略我们就可以简单的理解为针对文件夹的,而滚动策略是针对文件的。
Flink 提供了两个滚动策略:

  • DefaultRollingPolicy 当超过最大桶大小(默认为 128 MB),或超过了滚动周期(默认为 60秒),或未写入数据处于不活跃状态超时(默认为 60 秒)的时候,滚动文件;
  • OnCheckpointRollingPolicy 当 checkpoint 的时候,滚动文件。

我们需要特别注意:

  • 当使用forRowFormat方法时,我们可以手动指定滚动策略(包括多久生成新文件,文件达到多大生成新文件等)
  • 当使用forBulkFormat方法时,我们只能选择OnCheckpointRollingPolicy的滚动策略,详见文档[1]:

文档[1]

优化

因为每个subtask在一个checkpoint周期就会生成一个文件,所以在并发高时小文件数量很大,不仅增加NameNode维护元数据成本,也影响下游其他任务读取效率(大量小文件大量磁盘IO)。常见调优方式介绍如下:

1、增加Checkpoint周期
因为使用BulkEncoding时只能用OnCheckpointRollingPolicy,所以我们调大Checkpoint间隔可以减少总的part-file文件数量。
但调大以后,会增加每次Checkpoint时间,以及增长数据可见周期

2、减小并发subtask
每个文件在每个Checkpoint周期都会写一个自己的文件,所以可以调小并发减少文件总量。但这会导致数据处理能力下降。

3、后期合并
我们可以另外写个程序,按周或者按月定时去读StreamingFileSink写入的未压缩文件,然后按照压缩写出,这个方法增加了处理成本,但提升了后续其他读取任务处理速度。

StreamingFileSink的用法就说到这里,如果有写的不对的地方,欢迎大家指正

参考文档

Flink-1.9-StreamingFileSink
Flink-1.11-StreamingFileSink
Flink学习-DataStream-HDFSConnector(StreamingFileSink)

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值