Flink源码解读--FlinkKafkaConsumer09

来源:互联网 发布:金蝶软件好用吗 编辑:程序博客网 时间:2024/05/22 11:38

1、简介

Flink消费Kafka的数据(这里默认kafka版本是0.9.x),非常简单,只需要提供以下几项即可:

1、maven依赖2、指定topic name(s)3、指定DeserializationSchema4、指定kafka的properties

其中,properties在kafka 0.9.x中,只需要配置两个选项即可,例如:

val kafkaProps = new Properties()  kafkaProps.setProperty("bootstrap.servers", "localhost:9092")  kafkaProps.setProperty("group.id", "flink consumer")

因此,Flink提供了一个high level的API来消费kafka的topic中的数据:

这里写图片描述

2、FlinkKafkaConsumer09解读

此类位于org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer09,提供了4个API来接受不同的参数输入:
这里写图片描述
其中,前3个API最终都被转换为第4个API,代码如下:

public FlinkKafkaConsumer09(String topic, DeserializationSchema<T> valueDeserializer, Properties props) {        this(Collections.singletonList(topic), valueDeserializer, props);    }
public FlinkKafkaConsumer09(String topic, KeyedDeserializationSchema<T> deserializer, Properties props) {        this(Collections.singletonList(topic), deserializer, props);    }
public FlinkKafkaConsumer09(List<String> topics, DeserializationSchema<T> deserializer, Properties props) {        this(topics, new KeyedDeserializationSchemaWrapper<>(deserializer), props);    }

这3个参数的说明如下:

1、指明kafka中要消费的topic的名字,或者topic名字的列表,用“,”隔开2、反序列化Schema,指明如何将kafka中的序列化的数据转换为Flink中的对象3、用于配置kafka(0.8.x还要指定zookeeper)consumer信息

如果第一个参数是一个string类型的topic name,那么将被转换为一个List类型的topic;如果第二个参数是一个DeserializationSchema类型,则new KeyedDeserializationSchemaWrapper(),该类继承自KeyedDeserializationSchema,而KeyedDeserializationSchema需要实现下列方法:

deserialize(byte[] messageKey, byte[] message, String topic, int partition, long offset) throws IOException;boolean isEndOfStream(T nextElement);

下面重点看下第4个API的实现,也是最终的实现:

public FlinkKafkaConsumer09(List<String> topics, KeyedDeserializationSchema<T> deserializer, Properties props) {        super(deserializer);        checkNotNull(topics, "topics");        this.properties = checkNotNull(props, "props");        setDeserializer(this.properties);        // configure the polling timeout        try {            if (properties.containsKey(KEY_POLL_TIMEOUT)) {                this.pollTimeout = Long.parseLong(properties.getProperty(KEY_POLL_TIMEOUT));            } else {                this.pollTimeout = DEFAULT_POLL_TIMEOUT;            }        }        catch (Exception e) {            throw new IllegalArgumentException("Cannot parse poll timeout for '" + KEY_POLL_TIMEOUT + '\'', e);        }        // read the partitions that belong to the listed topics        final List<KafkaTopicPartition> partitions = new ArrayList<>();        try (KafkaConsumer<byte[], byte[]> consumer = new KafkaConsumer<>(this.properties)) {            for (final String topic: topics) {                // get partitions for each topic                List<PartitionInfo> partitionsForTopic = consumer.partitionsFor(topic);                // for non existing topics, the list might be null.                if (partitionsForTopic != null) {                    partitions.addAll(convertToFlinkKafkaTopicPartition(partitionsForTopic));                }            }        }        if (partitions.isEmpty()) {            throw new RuntimeException("Unable to retrieve any partitions for the requested topics " + topics);        }        // we now have a list of partitions which is the same for all parallel consumer instances.        LOG.info("Got {} partitions from these topics: {}", partitions.size(), topics);        if (LOG.isInfoEnabled()) {            logPartitionInfo(LOG, partitions);        }        // register these partitions        setSubscribedPartitions(partitions);    }

首先,调用一个super(deserializer),即父类FlinkKafkaConsumerBase中的方法checkNotNull,如果传入的deserializer为空,则返回一个“valueDeserializer”的空指针异常错误,否则返回deserializer。此方法就是验证传入的deserializer是否为空。

同时,验证传入的topics和props是否为空。setDeserializer(this.properties)方法用于将ByteArrayDeserializer注册到props中,即添加key.deserializer和value.deserializer。

之后,配置polling超时时间。此参数的含义是:拉取的超时毫秒数,即如果没有新的消息可供拉取,consumer会等待指定的毫秒数,到达超时时间后会直接返回一个空的结果集。如果指定的poll time不是Long类型会报错,如果没有指定,那么默认为100毫秒。

然后,遍历topic list,对每一个topic,获取其partition的数量,然后把(topic_name, partition_id)存入KafkaTopicPartition类型的List中。举个例子,假如我们的topic名字是T,在kafka中一共有4个partition,那么这个List的内容类似于如下的格式:

T0),(T,1),(T,2),(T,3

最后,将这个列表中的内容注册,依然调用父类FlinkKafkaConsumerBase的方法setSubscribedPartitions,即消费kafka的数据。

3、说明

这里讨论下kafka中topic的partition数量与Flink中consumer的线程数的关系,通过上面的代码分析看出,一个topic最后生成的list的个数就是partition的数量,如果Flink消费时,consumer数量大于partition数量,则多余的consumer不会消费到任何数据,也就是说,consumer的线程数,最好是等于topic的partition的数量,这样可以保证低延迟下达到最高的吞吐量。而且,每一个consumer线程只能保证消费的partition内的数据是有序的,并不保证全局topic是有序消费的。

4、consumer

当我们反序列化kafka中的对象时,需要实现DeserializationSchema方法,此方法继承自ResultTypeQueryable,要实现其getProducedType方法,下面是一个简单的例子:

override def isEndOfStream(t: T): Boolean = ???override def deserialize(bytes: Array[Byte]): T = ???override def getProducedType: TypeInformation[T] = ???

isEndOfStream指定是否接收结束,返回false即可;deserialize方法就是如何反序列化kafka中的数据到Flink中的对象;getProducedType方法是告诉Flink,kafka中的序列化之前的数据是什么类型。

具体的实现的例子如下:

class TXDeserialization extends DeserializationSchema[TX]{  override def isEndOfStream(t: TX): Boolean = {    false  }  override def deserialize(bytes: Array[Byte]): TX = {    JSON.parseObject(bytes,classOf[TX])  }  override def getProducedType: TypeInformation[TX] = {    TypeInformation.of(new TypeHint[TX] {})  }

Flink中consumer也做了容错,即通过检查点,定时将consumer消费某topic的offset信息写入快照中,以便恢复时可以重新从记录的offset重新消费kafka的数据,做到exactly once。如果没有设置检查点,当job失败时,Flink只能从zookeeper中获取consumer的offset信息,但是可能不是最新的,而且不能做到exactly once。

Flink Kafka Consumer同样允许在消费kafka数据时,指定时间戳并发射水位线,示例方法如下:

val properties = new Properties();properties.setProperty("bootstrap.servers", "localhost:9092");// only required for Kafka 0.8properties.setProperty("zookeeper.connect", "localhost:2181");properties.setProperty("group.id", "test");val myConsumer = new FlinkKafkaConsumer08[String]("topic", new SimpleStringSchema(), properties);myConsumer.assignTimestampsAndWatermarks(new CustomWatermarkEmitter());stream = env    .addSource(myConsumer)    .print

当然,也可以消费之后,对DataStream进行简单的map或filter之后,再进行watermark的的emit和timestamp的extract。

5、总结

Flink提供了消费kafka数据的high level API,在内部实现时,则是通过我们配置的properties属性获取consumer上一次的offset以及partition信息,并从记录的offset开始消费。消费时,按照(topic,partition_id)对的形式对每个partition顺序消费。

0 0
原创粉丝点击