最近项目上在跑的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记录了。
有两种方法:
-
直接设置为当前分区最早(earliest)的offset
-
第一种方法有极小可能会在设置完新的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