SparkStreaming读取Kafka报OffsetOutOfRangeException

·  阅读 2776

最近项目上在跑的Spark流计算程序突然报错停止,并且由于其他原因导致未能重启,导致过了一天才发现,差点酿成大祸。

未解之谜

查看spark日志发现报错如下:

 java.lang.IllegalArgumentException: requirement failed: numRecords must not be negative
复制代码

这个错误主要是kafka的消费offset比生产offset的最大值大,导致在计算这一批次消费多少条numRecords(numRecords=untilOffset - fromOffset)时,结果为负数。我查了很多资料,都是说在删掉原来topic并且重建了一个相同的topic时发生的问题,我们之前没有对kafka有过相关操作。很可惜,这个问题目前没法复现,还没找到具体的原因。如果有大佬有任何可疑方向的想法,可以评论告诉我。

进入正题

新的报错

报错停机的原因暂时找不到,但是程序得先重新跑起来,不然客户得杀上门了。但是在SparkStreaming程序重新跑起来之后,又报错停机了。还和之前的错误不一样

org.apache.kafka.clients.consumer.OffsetOutOfRangeException: Offsets out of range with no configured reset policy for partitions: {kafkaTest3-0=4406}
    at org.apache.kafka.clients.consumer.internals.Fetcher.parseCompletedFetch(Fetcher.java:970)
    at org.apache.kafka.clients.consumer.internals.Fetcher.fetchedRecords(Fetcher.java:490)
    at org.apache.kafka.clients.consumer.KafkaConsumer.pollForFetches(KafkaConsumer.java:1259)
    at org.apache.kafka.clients.consumer.KafkaConsumer.poll(KafkaConsumer.java:1187)
    ...
复制代码

跟踪源码

原来是offset的越界了,跟进源码,看看这个offset是从哪获取的。
最后在#Fetcher.prepareFetchRequests()方法中,可以看到offset从subscriptions变量中获取

long position = this.subscriptions.position(partition);
复制代码

subscriptions变量里的数据是在#ConsumerCoordinator.refreshCommittedOffsetsIfNeeded()方法中通过this.subscriptions.seek(tp, offset)设进去的

public boolean refreshCommittedOffsetsIfNeeded(final long timeoutMs) {
        final Set<TopicPartition> missingFetchPositions = subscriptions.missingFetchPositions();

        final Map<TopicPartition, OffsetAndMetadata> offsets = fetchCommittedOffsets(missingFetchPositions, timeoutMs);
        if (offsets == null) return false;

        for (final Map.Entry<TopicPartition, OffsetAndMetadata> entry : offsets.entrySet()) {
            final TopicPartition tp = entry.getKey();
            final long offset = entry.getValue().offset();
            log.debug("Setting offset for partition {} to the committed offset {}", tp, offset);
            this.subscriptions.seek(tp, offset);
        }
        return true;
    }
复制代码

跟进fetchCommittedOffsets(missingFetchPositions, timeoutMs)方法

//获取一组分区的提交偏移量。
future = sendOffsetFetchRequest(partitions);
client.poll(future, remainingTimeAtLeastZero(timeoutMs, elapsedTime));
if (future.succeeded()) {
    return future.value();
}
复制代码

所以最后是从kafka去获取的当前partiton的消费offset。

找到原因

错误的原因也呼之欲出了,因为项目上Kafka消息的保存时间设为1天,报错停机到现在重新拉起SparkStreaming的时间超过了一天,所以导致消费offset对应的消息丢了,对应的offset已经不在当前Kafka所拥有的offset范围里了。

解决办法

知道了原因录,接下来就是修改kafka中的消费offset记录了。

Kafka常用命令

有两种方法:

  1. 直接设置为当前分区最早(earliest)的offset

  2. 第一种方法有极小可能会在设置完新的offset之后,在重启SparkStreaming的过程中,数据又过期了。所以可以查询Kafka生产者的数据日志,通过下一个index文件来确定offset。或者再次执行第一种方法即可。

//查询该组下所有topic的offset信息
./kafka-consumer-groups.sh  --bootstrap-server 192.168.19.128:9092 --describe --group spark-kafka

//修改某个topic某个组下的消费offset为当前生产者earliest-offset
./kafka-consumer-groups.sh --bootstrap-server 192.168.19.128:9092 --group spark-kafka --topic kafkaTest3 --execute --reset-offsets --to-earliest

//修改某个topic某个组下的消费offset为1500(指定offset)
./kafka-consumer-groups.sh --bootstrap-server 192.168.19.128:9092 --group spark-kafka --topic kafkaTest3 --execute --reset-offsets --to-offset 1500

//查询某topic下各个分区当前最大的消息位移值(注意,这里的位移不是consumer端的位移,而是指消息在每个分区的位置)
./kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list 192.168.19.128:9092 --topic kafkaTest3 --time -1

//表示去获取当前各个分区的最小位移(earliest)。把运行上一条命令的结果与这条命令的结果相减就是集群中该topic的当前消息总数。
./kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list 192.168.19.128:9092 --topic kafkaTest3 --time -2
复制代码
分类:
后端
标签:
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改