Kafka - 异常处理

Listener Error Handlers

监听器级别的error handler

  • KafkaListenerErrorHandler
@FunctionalInterface
public interface KafkaListenerErrorHandler {

	/**
	 * Handle the error.
	 * @param message the spring-messaging message.
	 * @param exception the exception the listener threw, wrapped in a
	 * {@link ListenerExecutionFailedException}.
	 * @return the return value is ignored unless the annotated method has a
	 * {@code @SendTo} annotation.
	 */
	Object handleError(Message<?> message, ListenerExecutionFailedException exception);

	default Object handleError(Message<?> message, ListenerExecutionFailedException exception,
			Consumer<?, ?> consumer) {

		return handleError(message, exception);
	}

}

Message是拥有消息头消息体的。

public interface Message<T> {

	/**
	 * Return the message payload.
	 */
	T getPayload();

	/**
	 * Return message headers for the message (never {@code null} but may be empty).
	 */
	MessageHeaders getHeaders();

}

ListenerExecutionFailedException继承于KafkaException, 拥有String messge, Throwable throwable, String groupId属性。
这里就不粘贴了。

  • ConsumerAwareListenerErrorHandler

可以额外访问Consumer。

@FunctionalInterface
public interface ConsumerAwareListenerErrorHandler extends KafkaListenerErrorHandler {

	@Override
	default Object handleError(Message<?> message, ListenerExecutionFailedException exception) {
		throw new UnsupportedOperationException("Container should never call this");
	}

	@Override
	Object handleError(Message<?> message, ListenerExecutionFailedException exception, Consumer<?, ?> consumer);

}

官方呢,也举了几个例子来讲解它的使用。

重置位移,重新消费出现异常的消息

@Bean
public ConsumerAwareListenerErrorHandler listen3ErrorHandler() {
    return (m, e, c) -> {
        this.listen3Exception = e;
        MessageHeaders headers = m.getHeaders();
        c.seek(new org.apache.kafka.common.TopicPartition(
                headers.get(KafkaHeaders.RECEIVED_TOPIC, String.class),
                headers.get(KafkaHeaders.RECEIVED_PARTITION_ID, Integer.class)),
                headers.get(KafkaHeaders.OFFSET, Long.class));
        return null;
    };
}

将批次中的所有位移都重置为批次中最小的位移。

@Bean
public ConsumerAwareListenerErrorHandler listen10ErrorHandler() {
    return (m, e, c) -> {
        this.listen10Exception = e;
        MessageHeaders headers = m.getHeaders();
        List<String> topics = headers.get(KafkaHeaders.RECEIVED_TOPIC, List.class);
        List<Integer> partitions = headers.get(KafkaHeaders.RECEIVED_PARTITION_ID, List.class);
        List<Long> offsets = headers.get(KafkaHeaders.OFFSET, List.class);
        Map<TopicPartition, Long> offsetsToReset = new HashMap<>();
        for (int i = 0; i < topics.size(); i++) {
            int index = i;
            offsetsToReset.compute(new TopicPartition(topics.get(i), partitions.get(i)),
                    (k, v) -> v == null ? offsets.get(index) : Math.min(v, offsets.get(index)));
        }
        offsetsToReset.forEach((k, v) -> c.seek(k, v));
        return null;
    };
}

最后呢,在@KafkaListener(errorHandler = “”),写上对应的bean的名字即可。

Container Error Handler

提供了两种error handler接口(ErrorHandler和BatchErrorHandler)。根据消息监听器的类型,配置合适的类型。

默认情况下,对于非事务,错误只是简单记录。对于事务,默认没有配置error handler,以至于异常会回滚事务。

如果想自定义一个error handler,当使用事务的时候,如果想回滚的话,必须抛出一个异常。

从版本2.3.2开始,这些接口有一个default方法 isAckAfterHandle(), 由容器调用,用来决定如果error handler没有抛出异常返回,位移是否应当提交。从2.4版本开始,这个方法默认返回true。

	/**
	 * Return true if the offset should be committed for a handled error (no exception
	 * thrown).
	 * @return true to commit.
	 * @since 2.3.2
	 */
	default boolean isAckAfterHandle() {
		return true;
	}

在容器工厂中,设置全局的所有监听器的错误处理器

@Bean
public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<Integer, String>>
        kafkaListenerContainerFactory() {
    ConcurrentKafkaListenerContainerFactory<Integer, String> factory =
            new ConcurrentKafkaListenerContainerFactory<>();
    ...
    // Set the error handler to call when the listener throws an exception.
    factory.setErrorHandler(myErrorHandler);
    ...
    return factory;
}

设置全局的批次错误处理器

@Bean
public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<Integer, String>>
        kafkaListenerContainerFactory() {
    ConcurrentKafkaListenerContainerFactory<Integer, String> factory =
            new ConcurrentKafkaListenerContainerFactory<>();
    ...
    // Set the batch error handler to call when the listener throws an exception.
    factory.setBatchErrorHandler(myBatchErrorHandler);
    ...
    return factory;
}

默认情况下,如果标注注解的监听器方法抛出了一个异常,会抛给容器。消息会根据容器配置,进行处理。

如果使用Spring Boot,只需要在error handler中添加@Bean,Spring Boot会把它添加到自动配置的工厂中。

Consumer-Aware Container Error Handlers

容器级别的error handler(ErrorHandler、BatchErrorHandler)

  • ConsumerAwareErrorHandler
@FunctionalInterface
public interface ConsumerAwareErrorHandler extends ErrorHandler {

	@Override
	default void handle(Exception thrownException, ConsumerRecord<?, ?> data) {
		throw new UnsupportedOperationException("Container should never call this");
	}

	@Override
	void handle(Exception thrownException, ConsumerRecord<?, ?> data, Consumer<?, ?> consumer);

	@Override
	default void handle(Exception thrownException, List<ConsumerRecord<?, ?>> data, Consumer<?, ?> consumer,
			MessageListenerContainer container) {
		handle(thrownException, null, consumer);
	}

}
  • ConsumerAwareBatchErrorHandler
@FunctionalInterface
public interface ConsumerAwareBatchErrorHandler extends BatchErrorHandler {

	@Override
	default void handle(Exception thrownException, ConsumerRecords<?, ?> data) {
		throw new UnsupportedOperationException("Container should never call this");
	}

	@Override
	void handle(Exception thrownException, ConsumerRecords<?, ?> data, Consumer<?, ?> consumer);

	@Override
	default void handle(Exception thrownException, ConsumerRecords<?, ?> data, Consumer<?, ?> consumer,
			MessageListenerContainer container) {
		handle(thrownException, data, consumer);
	}

}

Unlike the listener-level error handlers, however, you should set the ackOnError container property to false (default) when making adjustments. Otherwise, any pending acks are applied after your repositioning.

Seek To Current Container Error Handlers

== RemainingRecordsErrorHandler==
可以处理上一次poll操作失败的和未处理的记录。如果这个处理器存在的话,那些记录不会传递给监听器。

@FunctionalInterface
public interface RemainingRecordsErrorHandler extends ConsumerAwareErrorHandler {

	@Override
	default void handle(Exception thrownException, ConsumerRecord<?, ?> data, Consumer<?, ?> consumer) {
		throw new UnsupportedOperationException("Container should never call this");
	}

	/**
	 * Handle the exception.
	 * @param thrownException the exception.
	 * @param records the remaining records including the one that failed.
	 * @param consumer the consumer.
	 */
	void handle(Exception thrownException, List<ConsumerRecord<?, ?>> records, Consumer<?, ?> consumer);

	@Override
	default void handle(Exception thrownException, List<ConsumerRecord<?, ?>> records, Consumer<?, ?> consumer,
			MessageListenerContainer container) {
		handle(thrownException, records, consumer);
	}

}

SeekToCurrentErrorHandler
可以定位当前的消费位移(包括上一次没有处理的记录)。

ackError一定要设置false。否则如果容器在seek之后被停止,但是在记录被处理之前,这个记录在容器重新启动的时候会被调过。

ackError这个属性,我有看ContainerProperties#setAckError这个方法给的注释。

当监听器抛出异常的时候,容器是否提交位移(确认消息)。当Kafka设置enable.auto.commit为false时,这个参数才有效(也就是手动提交)。设置为true,所有处理过的(有可能处理失败,或者抛出异常),都会提交位移。设置为false,只有成功处理的才会提交位移。
不要在事务中使用这个属性。

@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
    ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory();
    factory.setConsumerFactory(consumerFactory());
    factory.getContainerProperties().setAckOnError(false);
    factory.getContainerProperties().setAckMode(AckMode.RECORD);
    factory.setErrorHandler(new SeekToCurrentErrorHandler());
    return factory;
}

As an example; if the poll returns six records (two from each partition 0, 1, 2) and the listener throws an exception on the fourth record, the container acknowledges the first three messages by committing their offsets. The SeekToCurrentErrorHandler seeks to offset 1 for partition 1 and offset 0 for partition 2. The next poll() returns the three unprocessed records.

这里呢,官方又给了解释。如上所述。

从2.2版本开始,SeekToCurrentErrorHandler可以恢复处理失败的记录。默认是10次,10次之后呢,会被日志记录(error级别)。可以配置自定义的recoverer(BiConsumer)和maximum failures。

从2.2.4版本开始,容器配置成AckMode.MANUAL_IMMEDIATE,错误处理器可以提交恢复的记录。还需要设置commitRecovered为true。

AckMode.MANUAL_IMMEDIATE: 使用AcknowledgingMessageListener确认消息。客户端会立刻处理这个确认。

下面贴一下handler方法的源码:

	@Override
	public void handle(Exception thrownException, List<ConsumerRecord<?, ?>> records,
			Consumer<?, ?> consumer, MessageListenerContainer container) {

		if (!SeekUtils.doSeeks(records, consumer, thrownException, true, this.failureTracker::skip, LOGGER)) {
			throw new KafkaException("Seek to current after exception", thrownException);
		}
		else if (this.commitRecovered) {
			if (container.getContainerProperties().getAckMode().equals(AckMode.MANUAL_IMMEDIATE)) {
				ConsumerRecord<?, ?> record = records.get(0);
				Map<TopicPartition, OffsetAndMetadata> offsetToCommit = Collections.singletonMap(
						new TopicPartition(record.topic(), record.partition()),
						new OffsetAndMetadata(record.offset() + 1));
				if (container.getContainerProperties().isSyncCommits()) {
					consumer.commitSync(offsetToCommit);
				}
				else {
					OffsetCommitCallback commitCallback = container.getContainerProperties().getCommitCallback();
					if (commitCallback == null) {
						commitCallback = LOGGING_COMMIT_CALLBACK;
					}
					consumer.commitAsync(offsetToCommit, commitCallback);
				}
			}
			else {
				LOGGER.warn("'commitRecovered' ignored, container AckMode must be MANUAL_IMMEDIATE");
			}
		}
	}

在2.3版本开始,SeekToCurrentErrorHandler会视以下异常为错误,遇到如下异常,重试直接被跳过。

  • DeserializationException
  • MessageConversionException
  • MethodArgumentResolutionException
  • NoSuchMethodException
  • ClassCastException
@Bean
public SeekToCurrentErrorHandler errorHandler(BiConsumer<ConsumerRecord<?, ?>, Exception> recoverer) {
    SeekToCurrentErrorHandler handler = new SeekToCurrentErrorHandler(recoverer);
    handler.addNotRetryableException(IllegalArgumentException.class);
    return handler;
}

可以通过如上方式,添加不可重试异常。

SeekToCurrentBatchErrorHandler
可以定位批次中第一个消息的消费位移。但是不支持恢复消息。

在seek之后,如果有一个ListenerExecutionFailerException抛出,如果设置了开启事务,就会造成事务回滚。

从2.3版本开始,可以在SeekToCurrentErrorHandler和DefaultAfterRollbackProcessor设置BackOff。有两种开箱即用的,FixedBackOff和ExponentialBackOff。最大的回退时间不要超过max.poll.interval.ms客户端属性,避免重新平衡(rebalance)。

从2.3.2版本开始,在一个记录被恢复之后,它的位移会被提交。为了恢复到从前的行为,设置错误处理器的ackAfterHandle属性为false。

Container Stopping Error Handlers

==ContainerStoppingErrorHandler == --使用record listeners
如果监听器抛出了异常,会停止容器的运行。
如果AckMode为RECORD, 会提交已经处理完的记录的位移。
如果为任何手动的值,提交已经确认的记录的位移。
如果为BATCH,(如果设置开启了事务,只有未处理的记录会重新获取并处理),当容器重启,整个批次都会replay。

public class ContainerStoppingErrorHandler implements ContainerAwareErrorHandler {

	private final Executor executor;

	public ContainerStoppingErrorHandler() {
		this.executor = new SimpleAsyncTaskExecutor();
	}

	public ContainerStoppingErrorHandler(Executor executor) {
		Assert.notNull(executor, "'executor' cannot be null");
		this.executor = executor;
	}

	@Override
	public void handle(Exception thrownException, List<ConsumerRecord<?, ?>> records, Consumer<?, ?> consumer,
			MessageListenerContainer container) {
		this.executor.execute(() -> container.stop());
		// isRunning is false before the container.stop() waits for listener thread
		int n = 0;
		while (container.isRunning() && n++ < 100) { // NOSONAR magic #
			try {
				Thread.sleep(100); // NOSONAR magic #
			}
			catch (InterruptedException e) {
				Thread.currentThread().interrupt();
				break;
			}
		}
		throw new KafkaException("Stopped container", thrownException);
	}

}

ContainerStoppingBatchErrorHandler --使用batch listeners。

public class ContainerStoppingBatchErrorHandler implements ContainerAwareBatchErrorHandler {

	private final Executor executor;

	public ContainerStoppingBatchErrorHandler() {
		this.executor = new SimpleAsyncTaskExecutor();
	}

	public ContainerStoppingBatchErrorHandler(Executor executor) {
		Assert.notNull(executor, "'executor' cannot be null");
		this.executor = executor;
	}

	@Override
	public void handle(Exception thrownException, ConsumerRecords<?, ?> data, Consumer<?, ?> consumer,
			MessageListenerContainer container) {
		this.executor.execute(() -> container.stop());
		// isRunning is false before the container.stop() waits for listener thread
		int n = 0;
		while (container.isRunning() && n++ < 100) { // NOSONAR magic #
			try {
				Thread.sleep(100); // NOSONAR magic #
			}
			catch (InterruptedException e) {
				Thread.currentThread().interrupt();
				break;
			}
		}
		throw new KafkaException("Stopped container", thrownException);
	}

}

Publishing Dead-letter Records

可以将失败的消息发布到死信队列。

默认情况下:
topic name : .DLT
partition:和原始分区相同。

下面是自定义解析器:

DeadLetterPublishingRecoverer recoverer = new DeadLetterPublishingRecoverer(template,
        (r, e) -> {
            if (e instanceof FooException) {
                return new TopicPartition(r.topic() + ".Foo.failures", r.partition());
            }
            else {
                return new TopicPartition(r.topic() + ".other.failures", r.partition());
            }
        });
ErrorHandler errorHandler = new SeekToCurrentErrorHandler(recoverer, new FixedBackOff(0L, 2L));

发送到死信队列中的记录会有以下header:

  • KafkaHeaders.DLT_EXCEPTION_FQCN: The Exception class name.

  • KafkaHeaders.DLT_EXCEPTION_STACKTRACE: The Exception stack trace.

  • KafkaHeaders.DLT_EXCEPTION_MESSAGE: The Exception message.

  • KafkaHeaders.DLT_ORIGINAL_TOPIC: The original topic.

  • KafkaHeaders.DLT_ORIGINAL_PARTITION: The original partition.

  • KafkaHeaders.DLT_ORIGINAL_OFFSET: The original offset.

  • KafkaHeaders.DLT_ORIGINAL_TIMESTAMP: The original timestamp.

  • KafkaHeaders.DLT_ORIGINAL_TIMESTAMP_TYPE: The original timestamp type.

从2.3版本开始,这个publisher当跟ErrorHandlingDeserializer2一起使用时,publisher会重置记录在死信队列中的value()(之前的值反序列化失败)。之前的话,value()是空的,然后用户代码不得不从消息头解码DeserializationException。

@Bean
public DeadLetterPublishingRecoverer publisher(KafkaTemplate<?, ?> stringTemplate,
        KafkaTemplate<?, ?> bytesTemplate) {

    Map<Class<?>, KafkaTemplate<?, ?>> templates = new LinkedHashMap<>();
    templates.put(String.class, stringTemplate);
    templates.put(byte[].class, bytesTemplate);
    return new DeadLetterPublishingRecoverer(templates);
}

After-rollback Processor

使用事务的时候,如果监听器抛出异常,事务会回滚。默认情况下,没有处理的记录会在下一次poll操作中重新获取。
这在DefaultAfterRollbackProcessor由seek操作实现。从2.2版本开始,DefaultAfterRollbackProcessor可以恢复失败的记录。

AfterRollbackProcessor<String, String> processor =
    new DefaultAfterRollbackProcessor((record, exception) -> {
        // recover after 3 failures, with no back off - e.g. send to a dead-letter topic
    }, new FixedBackOff(0L, 2L));

当没有使用事务的时候,可以通过配置SeekToCurrentErrorHandler实现。

从2.2.5版本开始,DefaultAfterRollbackProcessor可以在一个新的事务中被调用(在失败的事务回滚后开始)
然后使用DeadLetterPublishingRecoverer去发布一个失败的记录,这个处理器会发送恢复的记录到事务中,通过设置
DefaultAfterRollbackProcessor的commitRecovered和kafkaTemplate属性开启。

从2.3.1版本开始遇到如下异常,自动跳过:

  • DeserializationException

  • MessageConversionException

  • MethodArgumentResolutionException

  • NoSuchMethodException

  • ClassCastException

通过如下代码修改,增加不可重试异常类:

@Bean
public DefaultAfterRollbackProcessor errorHandler(BiConsumer<ConsumerRecord<?, ?>, Exception> recoverer) {
    DefaultAfterRollbackProcessor processor = new DefaultAfterRollbackProcessor(recoverer);
    processor.addNotRetryableException(IllegalArgumentException.class);
    return processor;
}

Kerberos

Starting with version 2.0, a KafkaJaasLoginModuleInitializer class has been added to assist with Kerberos configuration.

@Bean
public KafkaJaasLoginModuleInitializer jaasConfig() throws IOException {
    KafkaJaasLoginModuleInitializer jaasConfig = new KafkaJaasLoginModuleInitializer();
    jaasConfig.setControlFlag("REQUIRED");
    Map<String, String> options = new HashMap<>();
    options.put("useKeyTab", "true");
    options.put("storeKey", "true");
    options.put("keyTab", "/etc/security/keytabs/kafka_client.keytab");
    options.put("principal", "kafka-client-1@EXAMPLE.COM");
    jaasConfig.setOptions(options);
    return jaasConfig;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值