此篇开始进入kafka的另外一侧:消费者。kafka中的消费者比生产者要复杂的多,里面涉及到的消费组,偏移量等概念。下面还是先从DEMO开始慢慢进入。
消费者客户端提供了两种消费模式:订阅(subscribe)和分配(assign)
订阅模式:消费者指定订阅的主题,由协调者为消费者分配动态的分区。
分配模式:消费者指定消费主题的特定分区。但是这种模式失去了协调者动态分配分区的功能。
订阅模式伪代码
KafkaConsumer
consumer = new KafkaConsumer<>(new Map()); consumer.subscribe(Lists.newArrayList(topic));
ConsumerRecords
records = consumer.poll(timeout); for (ConsumerRecord record : records) {
System.out.println("拉取到消息-->from partition:" + record.partition() + ", offset: " + record.offset() + "," + record.key() + ":" + record.value());
}
和KafkaProducer一样,对外提供的API是比较简洁的。但是内部却做了非常多的事情。
消费者轮询的准备工作
消费者有组的概念,一个消费者想要消费数据的前提是必须成功加入一个消费组,并获取分配到的tp。所以在轮询前确保一个消费者必须是在一个消费组中。消费者要加入到消费组是通过消费者的协调者(ConsumerCoordinator)与服务端的协调者(GroupCoordinator)通信来完成的。消费者向协调者申请加入消费组,服务端管理消费组的协调者将消费者加入到消费组,协调者为所有的消费者分配分区,消费者从协调者获得分配给他的分区,消费者开始拉取消息。
消费者拉取消息之前的准备工作:连接服务端的协调者,向服务端的协调者发送请求加入组,从服务端的协调者获得分配的分区。
属于同一个组的每个消费者都需要向服务端的协调者发送加入消费组的请求:JoinGroupRequest,协调者会知道消费组里面有多少消费者,才可以执行分区分配算法。至于消费者什么时候分配到分区由协调者决定,如果协调者的分配算法还没有执行完成,消费者就无法得到分配给它的分区。
GroupCoordinator
服务端协调者。GroupCoordinator:运行在kafka broker上的服务,每台broker运行一个GroupCoordinator服务,它负责进行消费组的成员管理和offset管理(每一个GroupCoordinator只管理一部分的消费组和offset信息)。
消费者从协调者节点获取分区,向协调者节点发送心跳,提交偏移量。其中提交偏移量主要和消息处理有关。协调者只是作为偏移量的存储介质。
接下来看看消费者的协调者怎么和服务端的协调者进行交互。
ConsumerCoordinator.poll
public void poll(long now) {
invokeCompletedOffsetCommitCallbacks(); // 测试
// 消费者采用订阅模式,并且连接不上GroupCoordinator所在的节点,就连接GroupCoordinator
if (subscriptions.partitionsAutoAssigned() && coordinatorUnknown()) { ensureCoordinatorReady(); // 发送GroupCoordinator请求(GROUP_COORDINATOR请求),并建立连接
now = time.milliseconds(); }
// 判断是否需要重新加入group,如订阅的partition变化或者分配的partition变化时需要重新加入
if (needRejoin()) {
if (subscriptions.hasPatternSubscription())
client.ensureFreshMetadata();
ensureActiveGroup(); // 发送JoinGroup请求,获取分配的分区
now = time.milliseconds();
}
pollHeartbeat(now); // 检查心跳线程运行是否正常,如果心跳线程失败则抛出异常,反正更新poll调用时间
maybeAutoCommitOffsetsAsync(now); // 自动commit时:当定时达到时,进行自动commit
}
消费者协调者连接服务端协调者
ensureCoordinatorReady(),选择一个连接数小的节点,发送GroupCoordinator请求,并建立连接。
调用过程:ensureCoordinatorReady()-->lookupCoordinator()-->sendGroupCoordinatorRequest()
ensureCoordinatorReady()
public synchronized void ensureCoordinatorReady() {
while (coordinatorUnknown()) {
RequestFuture future = lookupCoordinator(); // 获取GroupCoordinator,并建立连接 client.poll(future);
if (future.failed()) { // 获取的过程中失败了
if (future.isRetriable())
client.awaitMetadataUpdate();
else throw future.exception();
} else if (coordinator != null && client.connectionFailed(coordinator)) {
coordinatorDead();
time.sleep(retryBackoffMs);
}
}
}
lookupCoordinator()
protected synchronized RequestFuture
lookupCoordinator() { if (findCoordinatorFuture == null) {
Node node = this.client.leastLoadedNode(); // 找连接数最小的节点
if (node == null) {
return RequestFuture.noBrokersAvailable();
} else
findCoordinatorFuture = sendGroupCoordinatorRequest(node); // 发送请求,并对response进行处理
}
return findCoordinatorFuture;
}
sendGroupCoordinatorRequest()
private RequestFuture
sendGroupCoordinatorRequest(Node node) { // 发送GroupCoordinator的请求 GroupCoordinatorRequest metadataRequest = new GroupCoordinatorRequest(this.groupId);
return client.send(node, ApiKeys.GROUP_COORDINATOR, metadataRequest) .compose(new GroupCoordinatorResponseHandler());
// GroupCoordinatorResponseHandler对GroupCoordinator的response进行处理
}
GroupCoordinatorResponseHandler
private class GroupCoordinatorResponseHandler extends RequestFutureAdapter
{ @Override public void onSuccess(ClientResponse resp, RequestFuture
future) { GroupCoordinatorResponse groupCoordinatorResponse = new GroupCoordinatorResponse(resp.responseBody()); Errors error = Errors.forCode(groupCoordinatorResponse.errorCode()); clearFindCoordinatorFuture();
if (error == Errors.NONE) { // 如果正确获取 GroupCoordinator时,建立连接,并更新心跳时间
synchronized (AbstractCoordinator.this) {
AbstractCoordinator.this.coordinator = new Node( Integer.MAX_VALUE - groupCoordinatorResponse.node().id(), groupCoordinatorResponse.node().host(), groupCoordinatorResponse.node().port());
client.tryConnect(coordinator); // 初始化tcp连接 heartbeat.resetTimeouts(time.milliseconds()); // 更新心跳时间
}
future.complete(null);
} else if (error == Errors.GROUP_AUTHORIZATION_FAILED) {
future.raise(new GroupAuthorizationException(groupId));
} else { future.raise(error);
}
}
@Override public void onFailure(RuntimeException e, RequestFuture
future) { clearFindCoordinatorFuture(); super.onFailure(e, future);
}
}
消费者加入消费组
消费者连接上服务端协调者(GroupCoordinator)后,就可以发送加入组消费组请求。
调用过程:ensureActiveGroup()-->ensureCoordinatorReady()-->startHeartbeatThreadIfNeeded()-->joinGroupIfNeeded()
ensureActiveGroup()
public void ensureActiveGroup() {
ensureCoordinatorReady(); // 再次确保GroupCoordinator已经连接上 startHeartbeatThreadIfNeeded(); // 启动心跳发送线程(并不一定发送心跳,满足条件后才会发送心跳)
joinGroupIfNeeded(); // 发送JoinGroup请求,并对返回的信息进行处理
}
重要的方法joinGroupIfNeeded:
void joinGroupIfNeeded() {
while (needRejoin() || rejoinIncomplete()) { // 需要加入,再次判断,并且使用循环保证请求的完成
ensureCoordinatorReady();
// needsJoinPrepare:准备加入消费组,初始为true,执行一次后更新为false,
if (needsJoinPrepare) {
// 消费者加入消费组之前先暂停定时提交任务,并调用一次同步提交偏移量 onJoinPrepare(generation.generationId, generation.memberId);
needsJoinPrepare = false;
}
// 初始化JoinGroup请求,并发送该请求
RequestFuture
future = initiateJoinGroup(); client.poll(future); // 客户端轮询确保异步请求完成后才会返回
resetJoinGroupFuture(); // 重置“重新加入消费组是否完成”对象为空
if (future.succeeded()) {
// 加入组请求完成,实际上同步组请求也已经成功
needsJoinPrepare = true;
// 完成加入
onJoinComplete(generation.generationId, generation.memberId, generation.protocol, future.value());
} else {
RuntimeException exception = future.exception();
if (exception instanceof UnknownMemberIdException || exception instanceof RebalanceInProgressException || exception instanceof IllegalGenerationException) continue;
else if (!future.isRetriable())
throw exception;
time.sleep(retryBackoffMs);
}
}
}
在重新加入消费组之前,需要提交一次提交偏移量
onJoinPrepare()
protected void onJoinPrepare(int generation, String memberId) { maybeAutoCommitOffsetsSync();
ConsumerRebalanceListener listener = subscriptions.listener();
try {
Set
revoked = new HashSet<>(subscriptions.assignedPartitions()); listener.onPartitionsRevoked(revoked); } catch (WakeupException e) {
throw e;
} catch (Exception e) { }
isLeader = false;
subscriptions.resetGroupSubscription();
}
private void maybeAutoCommitOffsetsSync() {
if (autoCommitEnabled) {
try {
commitOffsetsSync(subscriptions.allConsumed()); // 阻塞,等待提交完成
} catch (WakeupException e) {
throw e;
} catch (Exception e) { }
}
}
接下来就可以发送加入组请求
initiateJoinGroup
private synchronized RequestFuture initiateJoinGroup() {
if (joinFuture == null) {
disableHeartbeatThread(); // 重新加入消费组的时候,心跳线程停止
state = MemberState.REBALANCING; // 标记为 rebalance
joinFuture = sendJoinGroupRequest(); // 发送JoinGroup请求
joinFuture.addListener(new RequestFutureListener
() { @Override public void onSuccess(ByteBuffer value) {
synchronized (AbstractCoordinator.this) {
state = MemberState.STABLE; // 标记 Consumer 为 stable
if (heartbeatThread != null)
heartbeatThread.enable();
}
}
@Override public void onFailure(RuntimeException e) {
synchronized (AbstractCoordinator.this) {
state = MemberState.UNJOINED; // 标记 Consumer 为 Unjoined
}
}
});
}
return joinFuture;
}
private RequestFuture
sendJoinGroupRequest() { if (coordinatorUnknown())
return RequestFuture.coordinatorNotAvailable();
JoinGroupRequest request = new JoinGroupRequest( groupId, this.sessionTimeoutMs, this.rebalanceTimeoutMs, this.generation.memberId, protocolType(), metadata());
return client.send(coordinator, ApiKeys.JOIN_GROUP, request) .compose(new JoinGroupResponseHandler());
}
private class JoinGroupResponseHandler extends CoordinatorResponseHandler
{ // 处理加入组响应 @Override
public JoinGroupResponse parse(ClientResponse response) {
return new JoinGroupResponse(response.responseBody());
}
@Override
public void handle(JoinGroupResponse joinResponse, RequestFuture
future) { Errors error = Errors.forCode(joinResponse.errorCode());
if (error == Errors.NONE) {
sensors.joinLatency.record(response.requestLatencyMs());
synchronized (AbstractCoordinator.this) {
if (state != MemberState.REBALANCING) { // 此时消费者的状态不是rebalacing,就引起异常 future.raise(new UnjoinedGroupException());
} else {
AbstractCoordinator.this.generation = new Generation(joinResponse.generationId(), joinResponse.memberId(), joinResponse.groupProtocol()); AbstractCoordinator.this.rejoinNeeded = false; // 加入消费组成功后需要进行同步请求,获取分配的分区列表
if (joinResponse.isLeader()) {
onJoinLeader(joinResponse).chain(future);
} else { onJoinFollower().chain(future);
} } } } else if (error == Errors.GROUP_LOAD_IN_PROGRESS) { future.raise(error); } else if (error == Errors.UNKNOWN_MEMBER_ID) { resetGeneration(); future.raise(Errors.UNKNOWN_MEMBER_ID); } else if (error == Errors.GROUP_COORDINATOR_NOT_AVAILABLE || error == Errors.NOT_COORDINATOR_FOR_GROUP) { coordinatorDead(); future.raise(error); } else if (error == Errors.INCONSISTENT_GROUP_PROTOCOL || error == Errors.INVALID_SESSION_TIMEOUT || error == Errors.INVALID_GROUP_ID) { future.raise(error); } else if (error == Errors.GROUP_AUTHORIZATION_FAILED) { future.raise(new GroupAuthorizationException(groupId)); } else { future.raise(new KafkaException("Unexpected error in join group response: " + error.message())); } } }
加入消费组成功后需要进行同步组请求,获取分配的分区列表。如果是消费者被选为leader,那么此消费者要进行分区分配操作(一般第一个加入消费组成功的消费者被选择为Leander)。leader将分配后的结果以发送同步请求的方式给GroupCoordinator。作为follower的消费者发送给GroupCoordinator的同步请求是一个空列表。GroupCoordinator在接收到leader发来的请求后,会将分配结果返回给所有发送了同步请求的消费者。
private RequestFuture
onJoinLeader(JoinGroupResponse joinResponse) { try {
Map
groupAssignment = performAssignment(joinResponse.leaderId(), joinResponse.groupProtocol(), joinResponse.members()); // 进行分区分配 SyncGroupRequest request = new SyncGroupRequest(groupId, generation.generationId, generation.memberId, groupAssignment); return sendSyncGroupRequest(request); // 发送同步请求带有分配分区的结果
} catch (RuntimeException e) {
return RequestFuture.failure(e);
}
}
private RequestFuture
onJoinFollower() { SyncGroupRequest request = new SyncGroupRequest(groupId, generation.generationId, generation.memberId, Collections.emptyMap()); // Follower同步组请求,并带一个空的列表,因为Follower不需要进行分区分配操作
return sendSyncGroupRequest(request);
}
private RequestFuture
sendSyncGroupRequest(SyncGroupRequest request) { // 发送 SyncGroup请求 if (coordinatorUnknown())
return RequestFuture.coordinatorNotAvailable();
return client.send(coordinator, ApiKeys.SYNC_GROUP, request) .compose(new SyncGroupResponseHandler());
}
private class SyncGroupResponseHandler extends CoordinatorResponseHandler
{ // 处理同步请求响应结果 @Override
public SyncGroupResponse parse(ClientResponse response) {
return new SyncGroupResponse(response.responseBody());
}
@Override
public void handle(SyncGroupResponse syncResponse, RequestFuture
future) { Errors error = Errors.forCode(syncResponse.errorCode());
if (error == Errors.NONE) { // 同步成功 sensors.syncLatency.record(response.requestLatencyMs()); future.complete(syncResponse.memberAssignment());
} else {
requestRejoin(); // join 的标志位设置为 true
if (error == Errors.GROUP_AUTHORIZATION_FAILED) { future.raise(new GroupAuthorizationException(groupId)); } else if (error == Errors.REBALANCE_IN_PROGRESS) { // group 正在进行 rebalance,任务失败 future.raise(error); } else if (error == Errors.UNKNOWN_MEMBER_ID || error == Errors.ILLEGAL_GENERATION) { resetGeneration(); future.raise(error); } else if (error == Errors.GROUP_COORDINATOR_NOT_AVAILABLE || error == Errors.NOT_COORDINATOR_FOR_GROUP) { coordinatorDead(); future.raise(error); } else { future.raise(new KafkaException("Unexpected error from SyncGroup: " + error.message())); } } } }
onJoinComplete
一个消费者加入消费组成功后,会触发onJoinComplete方法:更新订阅的 tp 列表等操作。
protected void onJoinComplete(int generation, String memberId, String assignmentStrategy, ByteBuffer assignmentBuffer) {
if (!isLeader)
assignmentSnapshot = null;
PartitionAssignor assignor = lookupAssignor(assignmentStrategy);
if (assignor == null)
throw new IllegalStateException("Coordinator selected invalid assignment protocol: " + assignmentStrategy);
Assignment assignment = ConsumerProtocol.deserializeAssignment(assignmentBuffer); subscriptions.needRefreshCommits(); // 更新分配结果 subscriptions.assignFromSubscribed(assignment.partitions()); assignor.onAssignment(assignment);
this.nextAutoCommitDeadline = time.milliseconds() + autoCommitIntervalMs;
// 加入组之后,消费者分配到新的分区,触发自定义监听器的回调 ConsumerRebalanceListener listener = subscriptions.listener();
try {
Set
assigned = new HashSet<>(subscriptions.assignedPartitions()); listener.onPartitionsAssigned(assigned); } catch (WakeupException e) { throw e; } catch (Exception e) { } }
经过上面的步骤后,一个消费者才算是加入了一个消费组。
加入消费组成功后,消费者想要拉取消息,还得需要看看每一个分区有没有拉取偏移量。
为分区设置拉取偏移量
消费者订阅状态
订阅方法的参数是:topic,分配方法的参数是:分区。这个两个方法都会更新消费者订阅状态对象(SubscriptionState)
SubscriptionState:分配给消费者所有分区的状态。
public class SubscriptionState {
private Set
subscription; // 用户订阅的主题 private final Set
groupSubscription; // 消费组订阅的主题 private Set
userAssignment; // 用户分配的分区 private final PartitionStates
assignment; // 分配给消费者的每个分区的最新状态 // 获取指定分区的状态对象
private TopicPartitionState assignedState(TopicPartition tp) { TopicPartitionState state = this.assignment.stateValue(tp); if (state == null) throw new IllegalStateException("No current assignment for partition " + tp); return state; }
// 更新分区的提交偏移量
public void committed(TopicPartition tp, OffsetAndMetadata offset) { assignedState(tp).committed(offset); }
// 获取分区最新提交的提交偏移量
public OffsetAndMetadata committed(TopicPartition tp) { return assignedState(tp).committed; }
// 定位指定的分区状态,更新拉取偏移量
public void seek(TopicPartition tp, long offset) { assignedState(tp).seek(offset); }
// 设置分区的拉取偏移量
public void position(TopicPartition tp, long offset) { assignedState(tp).position(offset); }
// 获取分区的拉取偏移量,拉取消息时使用最新的拉取位置
public Long position(TopicPartition tp) { return assignedState(tp).position; }
// 暂停拉取分区
public void pause(TopicPartition tp) { assignedState(tp).pause(); }
//恢复拉取分区
public void resume(TopicPartition tp) { assignedState(tp).resume(); }
// 判断所有的分区是否都存在有效的拉取偏移量
public boolean hasAllFetchPositions() { for (TopicPartitionState state : assignment.partitionStateValues()) if (!state.hasValidPosition()) return false; return true; }
// 找出没有拉取偏移量的分区
public Set
missingFetchPositions() { Set missing = new HashSet<>(); for (PartitionStates.PartitionState state : assignment.partitionStates()) { if (!state.value().hasValidPosition()) missing.add(state.topicPartition()); } return missing; } // 获取存在拉取偏移量的分区集合
public List
fetchablePartitions() { List fetchable = new ArrayList<>(); for (PartitionStates.PartitionState state : assignment.partitionStates()) { if (state.value().isFetchable()) fetchable.add(state.topicPartition()); } return fetchable; } }
消费者在拉取消息之前,会先判断所有的分区是否有拉取偏移量(调用hasAllFetchPositions()),如果有的分区没有拉取偏移量,这找出相应的分区(missingFetchPositions()),然后调用方法(updateFetchPositions())更新这些分区。消费者在创建拉取请求时,只会选择允许拉取的存在拉取偏移量的分区(fetchablePartitions())
TopicPartitionState:分区状态对象,记录分区的offset信息,包含拉取偏移量和提交偏移量的状态信息。
private static class TopicPartitionState {
private Long position; // 拉取偏移量:Fetcher下次去拉取时的offset,Fecher在拉取时需要知道这个值
private OffsetAndMetadata committed; // 提交偏移量:消费者已经处理完的最新一条消息的offset,消费者主动调用offset-commit时会更新这个值
private boolean paused; // 分区是否被暂停拉取
private OffsetResetStrategy resetStrategy; // tp offset重置的策略,重置之后,此策略就会变成NULL,以防止再次操作
// 重置拉取偏移量(第一次分配给消费者时调用)
private void awaitReset(OffsetResetStrategy strategy) {
this.resetStrategy = strategy; // 设置重置策略
this.position = null; // 清空拉取偏移量
}
// 开始重置(第一次读取协调者后更新)
private void seek(long offset) {
this.position = offset; // 设置拉取偏移量
this.resetStrategy = null; // 清空重置策略
}
// 更新拉取偏移量(拉取线程在拉取到消息后调用) ,每次拉取到消息后更新
private void position(long offset) {
if (!hasValidPosition()) // 当前拉取偏移量必须有效,才可以更新拉取偏移量
this.position = offset;
}
// 更新提交偏移量(定时提交任务调用)
private void committed(OffsetAndMetadata offset) { this.committed = offset; }
// 分区没有暂停,且拉取偏移量有效才可以拉取
private boolean isFetchable() { return !paused && hasValidPosition(); }
}
关于分区状态更新偏移量的相关方法在什么地方调用:参考(Kafka技术内幕:图文详解Kafka源码设计与实现)中的图:
分区状态的拉取偏移量(position)表示对分区的拉取进度,它的值不能为空,消费者才可以拉取分区消息。拉取线程工作时,要确保及时的更新TopicPartitionState的拉取偏移量,每次构建的拉取请求都以拉取偏移量为准。seek()方法看作第一次更新拉取偏移量,position()方法看作每次拉取到消息后更新拉取偏移量。
拉取偏移量:用于在发送拉取请求时指定从分区的哪里开始拉取消息。
提交偏移量:消费者处理分区消息的进度。
在发生再平衡,分区分配给新的消费者的时候,会把提交偏移量的值赋值给拉取偏移量。因为新的消费者之前在本地没有记录这个分区的消费进度,它要获取拉取偏移量需要从协调者获取这个分区的提交偏移量,把这个分区的提交偏移量作为分区的起始拉取偏移量。消费者在每次轮询时,如果发现有分区没有拉取偏移量,就通过消费者的协调者对象发送获取偏移量请求(OFFSET_FETCH)给服务端的协调者节点,获取偏移量请求返回的结果表示这个分区在协调者节点已经记录的提交偏移量。(服务端记录的这个偏移量可能是同一个消费组其他消费者提交的)。
KafkaConsumer.pollOnce():
// 判断所有的分区是否都存在有效的拉取偏移量
// 更新那些没有拉取偏移量的分区
if (!subscriptions.hasAllFetchPositions()) updateFetchPositions(this.subscriptions.missingFetchPositions());
this.subscriptions.missingFetchPositions():找出没有拉取偏移量的分区。
KafkaConsumer.updateFetchPositions()
private void updateFetchPositions(Set
partitions) { // partitions:没有拉取偏移量的分区 // 如果有重置策略就根据重置策略设置拉取偏移量
fetcher.resetOffsetsIfNeeded(partitions);
if (!subscriptions.hasAllFetchPositions()) { // 判断所有的分区是否都存在有效的拉取偏移量
// 初始化的时候(第一次启动运行的时候),需要从服务端的协调者那里获取最新提交偏移量赋值到拉取偏移量
// 让新的消费者从最新的提交偏移量开始拉取消息 coordinator.refreshCommittedOffsetsIfNeeded();
// 如果不是初始化(第一次启动运行的时候),在拉取器中更新拉取偏移量 fetcher.updateFetchPositions(partitions);
}
}
Fetcher.resetOffsetsIfNeeded():
public void resetOffsetsIfNeeded(Set
partitions) { for (TopicPartition tp : partitions) {
if (subscriptions.isAssigned(tp) && subscriptions.isOffsetResetNeeded(tp))
resetOffset(tp);
}
}
private void resetOffset(TopicPartition partition) {
OffsetResetStrategy strategy = subscriptions.resetStrategy(partition);
final long timestamp;
if (strategy == OffsetResetStrategy.EARLIEST) // 偏移量置为最早的值
timestamp = ListOffsetRequest.EARLIEST_TIMESTAMP;
else if (strategy == OffsetResetStrategy.LATEST) // 偏移量置为最新的值
timestamp = ListOffsetRequest.LATEST_TIMESTAMP;
else throw new NoOffsetForPartitionException(partition);
// 根据策略获取拉取偏移量
long offset = getOffsetsByTimes(Collections.singletonMap(partition, timestamp), Long.MAX_VALUE).get(partition).offset();
if (subscriptions.isAssigned(partition)) this.subscriptions.seek(partition, offset); }
ConsumerCoordinator.refreshCommittedOffsetsIfNeeded()
public void refreshCommittedOffsetsIfNeeded() {
if (subscriptions.refreshCommitsNeeded()) { // 初始化的时候需要从服务端的协调者获取最新的提交偏移量
// 发送OFFSET_FETCH请求给服务端协调者,获取分区已经提交偏移量
Map
offsets = fetchCommittedOffsets(subscriptions.assignedPartitions()); for (Map.Entry
entry : offsets.entrySet()) { TopicPartition tp = entry.getKey();
if (subscriptions.isAssigned(tp))
// 更新分区的提交偏移量
this.subscriptions.committed(tp, entry.getValue());
}
this.subscriptions.commitsRefreshed();
// 设置需要从服务端的协调者获取最新的提交偏移量的变量为false
}
}
Fetcher.updateFetchPositions()
public void updateFetchPositions(Set
partitions) { for (TopicPartition tp : partitions) {
if (!subscriptions.isAssigned(tp) || subscriptions.isFetchable(tp))
continue;
// 有重置策略的使用重置策略更新拉取偏移量
if (subscriptions.isOffsetResetNeeded(tp)) {
resetOffset(tp);
} else if (subscriptions.committed(tp) == null) { // 提交偏移量为空 subscriptions.needOffsetReset(tp); // 需要重置
resetOffset(tp);
} else { // 分区状态中提交偏移量不为空,直接使用它作为拉取偏移量
long committed = subscriptions.committed(tp).offset();
subscriptions.seek(tp, committed); // 用已提交偏移量来更新拉取偏移量
}
}
}
在执行refreshCommittedOffsetsIfNeeded()中,消费者接收到获取偏移量请求的结果(OFFSET_FETCH请求),会通过SubscriptionState.committed方法更新分区的提交偏移量,执行到updateFetchPositions里面会判断分区是否有提交偏移量,如果有就直接使用提交偏移量赋值给拉取偏移量(seek方法);如果没有就需要调用重置策略进行拉取偏移量的拉取。
获取拉取偏移量请求的结果有可能为空(OFFSET_FETCH请求),说明服务端的协调者所在的节点并没有记录这个分区的提交偏移量。这样就不能把提交偏移量赋值给拉取偏移量了。这时就需要根据重置策略向分区的主副本所在的节点发送列举偏移量(LIST_OFFSETS请求),获取分区偏移量。
OFFSET_FETCH请求是由消费者的协调者发送给管理消费组的服务端协调节点;LIST_OFFSETS请求是由拉取器发送给分区的主副本所在节点。协调节点和分区主副本是不同的服务端节点,协调节点保存了消费组的相关数据(分区的提交偏移量);分区主副本保存分区的日志文件,日志文件里面保存了消息的偏移量。
更新订阅状态中分区的拉取偏移量:参考《Kafka技术内幕:图文详解Kafka源码设计与实现》
到这里,消费者拉取消息的前期准备工作就完成了。
参考资料:
Kafka 源码解析之 Consumer 如何加入一个 Group(六) | Matt's Blog
Kafka技术内幕:图文详解Kafka源码设计与实现