Flink-StreaimingFileSink-自定义序列化-Parquet批量压缩

Flink-StreaimingFileSink-自定义序列化-Parquet批量压缩

Flink系列文章

1 Maven依赖

Flink有内置方法可用于为Avro数据创建Parquet writer factory。

要使用ParquetBulkEncoder,需要添加以下Maven依赖:

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

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-statebackend-rocksdb_${scala.binary.version}</artifactId>
    <version>1.10.0</version>
</dependency>

<dependency>
	<groupId>org.apache.avro</groupId>
	<artifactId>avro</artifactId>
	<version>1.8.2</version>
</dependency>

<dependency>
	<groupId>org.apache.parquet</groupId>
	<artifactId>parquet-avro</artifactId>
	<exclusions>
		<exclusion>
			<groupId>org.apache.hadoop</groupId>
			<artifactId>hadoop-client</artifactId>
		</exclusion>
		<exclusion>
			<groupId>it.unimi.dsi</groupId>
			<artifactId>fastutil</artifactId>
		</exclusion>
	</exclusions>
	<version>1.10.0</version>
</dependency>

2 代码

2.1 main

从Kafka读数据,写入Parquet文件,并采用LZO压缩例子:

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

  def main(args: Array[String]) {
 	// read parameter from command line
    val parameter = ParameterTool.fromArgs(args)

    // 1. set up the streaming execution environment
    val env = StreamExecutionEnvironment.getExecutionEnvironment
      // checkpoint every 5 minute
      .enableCheckpointing(5 * 60 * 1000)
	  .setStateBackend(new RocksDBStateBackend(path, true))
	  
    val checkpointConfig = env.getCheckpointConfig
    checkpointConfig.setMinPauseBetweenCheckpoints(2 * 60 * 1000)
    checkpointConfig.setCheckpointTimeout(3 * 60 * 1000)
    checkpointConfig.enableExternalizedCheckpoints(ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION)

    // 2. kafka consumer`s config
    val kafkaConsumerConfig = new Properties()
    kafkaConsumerConfig.setProperty("bootstrap.servers", parameter.get("bootstrap-servers","192.168.1.1:9092"))
    kafkaConsumerConfig.setProperty("group.id", parameter.get("groupid","Kafka2hive"))
    kafkaConsumerConfig.setProperty("auto.offset.reset", parameter.get("offset","latest"))
    // 动态分区默认关闭,需要设置,以下表示10秒探测一次
    kafkaConsumerConfig.setProperty("flink.partition-discovery.interval-millis", "10000")

    // 3. create a kafka consumer
    val kafkaConsumer = new FlinkKafkaConsumer(
      "test_topic",
      new JSONKeyValueSilentDeserializationSchema,
      kafkaConsumerConfig)
    // 设置为用groupId放在ZK的offset,如果找不到就用配置文件中的auto.offset.reset来决定
    // 注意,该方法不影响此行为:从checkpoint/savepoint恢复时用存在这里面的offset消费
    .setStartFromGroupOffsets()
    // 1.9.0版本必须设这个值,虽然默认true,但是实测中并未提交offset到kafka
    // 设为true后已经观察到offset正常提交到kafka,便于监控
    .setCommitOffsetsOnCheckpoints(true)

    // 4. create the stream with kafka source, test_topic must return Student!
    val kafkaStream: DataStream[Student] = env
      .addSource(kafkaConsumer)

	 // 5. 构建StreamingFileSink,指定BasePath和序列化Encoder
	val sink: StreamingFileSink[Student] = StreamingFileSink
	    .forBulkFormat(outputBasePath, ParquetAvroCompressionWriters.forReflectRecord(classOf[Student], CompressionCodecName.LZO))
	    .withBucketAssigner(new EventDateTimeBucketAssigner("yyyyMMdd"))
	    .build()
	
	// 6. 添加Sink到InputDataSteam即可
    kafkaStream.addSink(sink)

    // 7. execute program
    env.execute("Kafka to Parquet")
  }   

2.2 JSONKeyValueSilentDeserializationSchema

该类十分重要。他继承自KafkaDeserializationSchema(他可用来访问Kafka消息的key、value和元数据。),负责解析FlinkKafkaConsumer从Kafka中读取到的ConsumerRecord。具体来说,会调用其deserialize方法。我们可以自定义处理逻辑,将其转为一个特定类型,再交由下一个算子处理。这里,我们是解析为了一个自定义Java Bean再由StreamingFileSink

关于此类,有几点值得注意:

  • 如果出现读Kafka数据中文乱码,可尝试按如下方式解析:
new String(record.value, StandardCharsets.UTF_8)
  • 一定要做异常处理,出错时务必返回null来跳过该条异常记录。否则会造成报错=>程序重启=>继续消费这条记录=>再次报错=>再次重启。。。的死循环中。

2.3 ParquetAvroCompressionWriters

ParquetAvroCompressionWriters是以Avro格式来定义Parquet元数据,写入Parquet文件。

  • ParquetAvroCompressionWriters.forReflectRecord(classOf[Student])
    Student类来生成该Avro的schema,用来生成Parquet的元数据。

2.4 AvroSchema定义

我们以Avro格式来定义Parquet元数据,写入Parquet文件。这里我们可以自定义类SensorPojo:

class SensorPojo extends Serializable{
  @BeanProperty @Nullable var kafka_partition:java.lang.Integer = _
  @BeanProperty @Nullable var kafka_offset:java.lang.Long = _
  @BeanProperty @Nullable var name:java.lang.Integer = _
  @BeanProperty @Nullable var age:String = _
  @BeanProperty @Nullable var time:java.lang.Long = _
}  

重点如下:

  • Serializable
    定义类时,请extends Serializable

  • @Nullable
    如果有字段为空,务必记得使用来自Avro项目的@Nullable注解允许有空值,否则遇到空值将会报错!

  • 如果使用scala,可以使用@BeanProperty字段注解,生成java风格的getset方法。

  • 如果要写入Avro以外的Parquet兼容的数据格式,请实现ParquetBuilder接口来创建ParquetWriterFactory。

  • 关于Parquet更多内容,可参考:

2.5 Hive-Parquet

如果要配合Hive使用,则在建表时直接指定STORED AS parquet即可(具体看hive版本)

2.6 Bucket

Bucket为按yyyyMMdd天格式生成,可以和Hive表分区目录配合。而且这里我们是自定义的EventDateTimeBucketAssigner,其他都跟BucketAssigner相同除了getBucketId方法自己实现根据特定字段获取BucketId:

public class EventDateTimeBucketAssigner implements BucketAssigner<Student, String> {
	@Override
    public String getBucketId(Student element, Context context) {
        if (dateTimeFormatter == null) {
            dateTimeFormatter = DateTimeFormatter.ofPattern(formatString).withZone(zoneId);
        }
        try {
        	// Student类中的特定字段的get方法
            Long eventTime = element.getTime();
            if(eventTime != null && eventTime > 0){
                return dateTimeFormatter.format(Instant.ofEpochMilli(eventTime));
            }
        } catch (Exception e) {
            LOGGER.error("an error happened while dateTimeFormatter.format context.timestamp():", e);
        }
        return dateTimeFormatter.format(Instant.ofEpochMilli(context.currentProcessingTime()));
    }
}

3 总结

3.1 关于压缩

可参考:

这里采用LZO,需要建立index才能对MR任务切分。

我们实测中,采用flume+LZO方式写文件和Flink+Parquet方式相比,总大小差距不大,所以必须在Parquet基础上加上LZO。

3.2 小文件

3.2.1 概述

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

3.2.2 增加Checkpoint周期

因为使用BulkEncoding时只能用OnCheckpointRollingPolicy,所以我们调大Checkpoint间隔可以减少总的part-file文件数量。

但调大以后,会增加每次Checkpoint时间,以及增长数据可见周期,需要权衡。

相关设置:

val env = StreamExecutionEnvironment.getExecutionEnvironment
env.enableCheckpointing(5 * 60 * 1000)
   .setStateBackend(new RocksDBStateBackend(path, TernaryBoolean.TRUE))
	  
val checkpointConfig = env.getCheckpointConfig
checkpointConfig.setMinPauseBetweenCheckpoints(2 * 60 * 1000)
checkpointConfig.setCheckpointTimeout(3 * 60 * 1000)

3.2.3 减小并发subtask

每个文件在每个Checkpoint周期都会写一个自己的文件,所以可以调小并发减少文件总量。

但这会导致数据处理能力下降,请做出权衡。

3.2.4 后期合并

用定时任务合并小文件。

比如我们StreamingFileSink程序写入临时分区,而用SparkSql定时任务,将临时分区的文件读取后写入正式分区目录,用户全部读取正式分区。

这个方法增加了处理成本,但提升了后续其他读取任务处理速度。

更多好文

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值