在1.5.4中SpringBoot对应的Spring kafka的版本
org.springframework.kafka:spring-kafka:1.1.6.RELEASE
在此版本的Spring对于kafka发生重平衡过程对消费线程的处理,会在awakeup之后打断消费线程,也就做优雅关闭的处理(设置消费线程的状态为interpred),具体的测试代码和现象如下
在消费session是15秒的基础下,我将业务的代码逻辑设置为100秒等待,显然这种情况会因为消费超时而发生重平衡,那么在较低版本的kafka的依赖支持上,那么会在执行awakeup之后执行消费线程的 interpret,具体的重平衡的效果会报错如下
这是因为在
org.springframework.kafka:spring-kafka:1.1.6.RELEASE
中对kafka的重平衡,会先将所有的consumer的thread状态设置为interpred,具体的源码如下

所以在低版本的spring for kafka 中对于重平衡的处理,Spring 并不保证当前正在执行的消费线程能够正常执行完成,如果在过程中业务代码使用了Lock 、线程池可能会出现InterpretException的地方,那么业务执行就会被打断(比较致命),当然肯定很多人说一般应该避免非必要重平衡的发生,但是我现在的一个项目在重平衡过程中依然在消费订单,导致每一次的上下线,就会出现问题因为线程被中断
在2.7.18中SpringBoot对应的Spring kafka的版本是
org.springframework.kafka:spring-kafka:2.8.11
高版本的doStop的方法比较简单,kafka的支持 2.x的版本的代码更加的复杂

执行的流程是
-
检查isRunning() - 确保容器正在运行
-
注册StopCallback - 设置停止完成后的回调
-
设置running=false - 通知消费线程停止
-
调用wakeup() - 唤醒可能阻塞的消费线程
-
记录停止类型 - 标记为正常停止
-
消费线程检测到停止标志
-
完成当前消息处理
-
关闭consumer连接
-
Future完成,触发StopCallback
-
执行回调,通知重平衡完成
对应的源码主要是
kotlin
public void run() {
ListenerUtils.setLogOnlyMetadata(this.containerProperties.isOnlyLogRecordMetadata());
publishConsumerStartingEvent();
this.consumerThread = Thread.currentThread();
setupSeeks();
KafkaUtils.setConsumerGroupId(this.consumerGroupId);
this.count = 0;
this.last = System.currentTimeMillis();
initAssignedPartitions();
publishConsumerStartedEvent();
Throwable exitThrowable = null;
while (isRunning()) {
try {
pollAndInvoke();
}
catch (NoOffsetForPartitionException nofpe) {
this.fatalError = true;
ListenerConsumer.this.logger.error(nofpe, "No offset and no reset policy");
exitThrowable = nofpe;
break;
}
catch (AuthenticationException | AuthorizationException ae) {
if (this.authExceptionRetryInterval == null) {
ListenerConsumer.this.logger.error(ae,
"Authentication/Authorization Exception and no authExceptionRetryInterval set");
this.fatalError = true;
exitThrowable = ae;
break;
}
else {
ListenerConsumer.this.logger.error(ae,
"Authentication/Authorization Exception, retrying in "
+ this.authExceptionRetryInterval.toMillis() + " ms");
// We can't pause/resume here, as KafkaConsumer doesn't take pausing
// into account when committing, hence risk of being flooded with
// GroupAuthorizationExceptions.
// see: https://github.com/spring-projects/spring-kafka/pull/1337
sleepFor(this.authExceptionRetryInterval);
}
}
catch (FencedInstanceIdException fie) {
this.fatalError = true;
ListenerConsumer.this.logger.error(fie, "'" + ConsumerConfig.GROUP_INSTANCE_ID_CONFIG
+ "' has been fenced");
exitThrowable = fie;
break;
}
catch (StopAfterFenceException e) {
this.logger.error(e, "Stopping container due to fencing");
stop(false);
exitThrowable = e;
}
catch (Error e) { // NOSONAR - rethrown
this.logger.error(e, "Stopping container due to an Error");
this.fatalError = true;
wrapUp(e);
throw e;
}
catch (Exception e) {
handleConsumerException(e);
}
finally {
clearThreadState();
}
}
wrapUp(exitThrowable);
}
可见,在stop的时候 running直接退出,对于被invokeIfHaveRecords的数据会被等待执行完成 ,并不会影响正在执行的消费线程。