绍圣--kafka之消费者(一)

此篇开始进入kafka的另外一侧:消费者。kafka中的消费者比生产者要复杂的多,里面涉及到的消费组,偏移量等概念。下面还是先从DEMO开始慢慢进入。

消费者客户端提供了两种消费模式:订阅(subscribe)和分配(assign)

订阅模式:消费者指定订阅的主题,由协调者为消费者分配动态的分区。

分配模式:消费者指定消费主题的特定分区。但是这种模式失去了协调者动态分配分区的功能。

订阅模式伪代码

KafkaConsumer<String,String> consumer = new KafkaConsumer<>(new Map());

consumer.subscribe(Lists.newArrayList(topic));

ConsumerRecords<String,String> 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<Void> 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<Void> 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<ClientResponse,Void> {

@Override public void onSuccess(ClientResponse resp, RequestFuture<Void> 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<Void> 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<ByteBuffer> 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<TopicPartition> 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<ByteBuffer>() {

@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<ByteBuffer> 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<JoinGroupResponse, ByteBuffer> { // 处理加入组响应

@Override

public JoinGroupResponse parse(ClientResponse response) {

return new JoinGroupResponse(response.responseBody());

}

@Override

public void handle(JoinGroupResponse joinResponse, RequestFuture<ByteBuffer> 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<ByteBuffer> onJoinLeader(JoinGroupResponse joinResponse) {

try {

Map<String,ByteBuffer> 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<ByteBuffer> onJoinFollower() {

SyncGroupRequest request = new SyncGroupRequest(groupId, generation.generationId, generation.memberId, Collections.emptyMap()); // Follower同步组请求,并带一个空的列表,因为Follower不需要进行分区分配操作

return sendSyncGroupRequest(request);

}

private RequestFuture<ByteBuffer> 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<SyncGroupResponse, ByteBuffer> { // 处理同步请求响应结果

@Override

public SyncGroupResponse parse(ClientResponse response) {

return new SyncGroupResponse(response.responseBody());

}

@Override

public void handle(SyncGroupResponse syncResponse, RequestFuture<ByteBuffer> 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<TopicPartition> assigned = new HashSet<>(subscriptions.assignedPartitions()); listener.onPartitionsAssigned(assigned);

} catch (WakeupException e) { throw e; } catch (Exception e) { } }

经过上面的步骤后,一个消费者才算是加入了一个消费组。

加入消费组成功后,消费者想要拉取消息,还得需要看看每一个分区有没有拉取偏移量。

为分区设置拉取偏移量

消费者订阅状态

订阅方法的参数是:topic,分配方法的参数是:分区。这个两个方法都会更新消费者订阅状态对象(SubscriptionState)

SubscriptionState:分配给消费者所有分区的状态。

public class SubscriptionState { 

private Set<String> subscription;  // 用户订阅的主题

private final Set<String> groupSubscription; // 消费组订阅的主题

private Set<TopicPartition> userAssignment; // 用户分配的分区

private final PartitionStates<TopicPartitionState> 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<TopicPartition> missingFetchPositions() { Set<TopicPartition> missing = new HashSet<>(); for (PartitionStates.PartitionState state : assignment.partitionStates()) { if (!state.value().hasValidPosition()) missing.add(state.topicPartition()); } return missing; }

// 获取存在拉取偏移量的分区集合

public List<TopicPartition> fetchablePartitions() { List<TopicPartition> 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<TopicPartition> partitions) { // partitions:没有拉取偏移量的分区

// 如果有重置策略就根据重置策略设置拉取偏移量

fetcher.resetOffsetsIfNeeded(partitions);

if (!subscriptions.hasAllFetchPositions()) { // 判断所有的分区是否都存在有效的拉取偏移量

// 初始化的时候(第一次启动运行的时候),需要从服务端的协调者那里获取最新提交偏移量赋值到拉取偏移量

// 让新的消费者从最新的提交偏移量开始拉取消息 coordinator.refreshCommittedOffsetsIfNeeded();

// 如果不是初始化(第一次启动运行的时候),在拉取器中更新拉取偏移量 fetcher.updateFetchPositions(partitions);

}

}

Fetcher.resetOffsetsIfNeeded():

public void resetOffsetsIfNeeded(Set<TopicPartition> 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<TopicPartition, OffsetAndMetadata> offsets = fetchCommittedOffsets(subscriptions.assignedPartitions());

for (Map.Entry<TopicPartition, OffsetAndMetadata> 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<TopicPartition> 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源码设计与实现

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,097评论 18 139
  • 学习kafka有一段时间了。关于它里面的知识还是需要总结一下,一来是能让自己对kafka能有一个比较成型的理解,二...
    绍圣阅读 1,022评论 0 3
  • 姓名:周小蓬 16019110037 转载自:http://blog.csdn.net/YChenFeng/art...
    aeytifiw阅读 34,518评论 13 425
  • 坚持下去,每一天都会越来越容易,否则,便是前功尽弃。 前两天日精进忘了打卡了,按照和小伙伴的约定,除了罚款之外,还...
    大爱小七阅读 181评论 0 0
  • 醒着, 踏着尘土走在路上 无论是否有方向 总是向着夕阳 一前一后 “保重!” 睡着, 化成尘土躺在路上 无论怎样的...
    和尚不念佛阅读 138评论 1 2