KafkaConsumer实现了Consumer接口,Consumer接口中定义了KafkaConsumer对外的API,其核心方法可以分为下面六类。
subscribe()
:订阅指定的Topic,并未消费者自动分配分区;assign()
:用户手动订阅指定的Topic,并指定消费的分区;commit()
:提交消费者已经消费完成的offset;seek()
:指定消费者起始消费的位置;poll()
:负责从服务端获取消息;pause()、resume()方法
:暂停/继续Consumer,暂停后poll方法会返回空。public class KafkaConsumer<K, V> implements Consumer<K, V> {
private static final String CLIENT_ID_METRIC_TAG = "client-id";
private static final long NO_CURRENT_THREAD = -1L;
private static final String JMX_PREFIX = "kafka.consumer";
static final long DEFAULT_CLOSE_TIMEOUT_MS = 30 * 1000;
static final String DEFAULT_REASON = "rebalance enforced by user";
// Visible for testing
final Metrics metrics;
final KafkaConsumerMetrics kafkaConsumerMetrics;
private Logger log;
// Consumer的唯一标识
private final String clientId;
// Consumer所在consumer group id
private final Optional<String> groupId;
// 控制着Consumer与服务端GroupCoordinator之间的通信逻辑
private final ConsumerCoordinator coordinator;
// 反序列化器
private final Deserializer<K> keyDeserializer;
private final Deserializer<V> valueDeserializer;
// 负责从服务端获取消息
private final Fetcher<K, V> fetcher;
// 拦截器集合
private final ConsumerInterceptors<K, V> interceptors;
private final IsolationLevel isolationLevel;
private final Time time;
// 负责消费者与kafka服务端的网络通信
private final ConsumerNetworkClient client;
// 维护了消费者的消费状态
private final SubscriptionState subscriptions;
// 记录整个Kafka集群的元信息
private final ConsumerMetadata metadata;
private final long retryBackoffMs;
private final long requestTimeoutMs;
private final int defaultApiTimeoutMs;
private volatile boolean closed = false;
private List<ConsumerPartitionAssignor> assignors;
// currentThread holds the threadId of the current thread accessing KafkaConsumer
// and is used to prevent multi-threaded access
// 分别记录了当前使用KafkaConsumer的线程ID和重入次数,KafkaConsumer的acquire方法和release方式
// 实现了一个轻量级锁,并非真正的锁,仅是检测是否有多线程并发操作KafkaConsumer而已
private final AtomicLong currentThread = new AtomicLong(NO_CURRENT_THREAD);
// refcount is used to allow reentrant access by the thread who has acquired currentThread
private final AtomicInteger refcount = new AtomicInteger(0);
// to keep from repeatedly scanning subscriptions in poll(), cache the result during metadata updates
private boolean cachedSubscriptionHasAllFetchPositions;
}
ConsumerNetworkClient在NetworkClient上进行封装,提供了更高级别的功能和更易用的API。
public class ConsumerNetworkClient implements Closeable {
private static final int MAX_POLL_TIMEOUT_MS = 5000;
// the mutable state of this class is protected by the object's monitor (excluding the wakeup
// flag and the request completion queue below).
private final Logger log;
// NetworkClient对象
private final KafkaClient client;
// 缓冲队列,该对象内部维护了一个 unsent 属性,该属性是 ConcurrentMap>,
// key 是 Node 节点,value 是 ConcurrentLinkedQueue metadata:用于管理 Kafka 集群元数据。
private final UnsentRequests unsent = new UnsentRequests();
private final Metadata metadata;
private final Time time;
// 在尝试重试对给定主题分区的失败请求之前等待的时间量
private final long retryBackoffMs;
// 使用Kafka的组管理工具时,消费者协调器心跳之间的预期时间。
private final int maxPollTimeoutMs;
// 配置控制客户端等待请求响应的最长时间,如果超出则重试
private final int requestTimeoutMs;
// 由调用KafkaConsumer对象的消费者线程之外的其他线程设置,表示要中断KafkaConsumer线程
private final AtomicBoolean wakeupDisabled = new AtomicBoolean();
// We do not need high throughput, so use a fair lock to try to avoid starvation
private final ReentrantLock lock = new ReentrantLock(true);
// when requests complete, they are transferred to this queue prior to invocation. The purpose
// is to avoid invoking them while holding this object's monitor which can open the door for deadlocks.
// 当请求完成时,它们在调用之前被转移到这个队列,目的是避免在持有此对象的监视器时调用它们。
private final ConcurrentLinkedQueue<RequestFutureCompletionHandler> pendingCompletion = new ConcurrentLinkedQueue<>();
// 断开与协调器连接节点的队列
private final ConcurrentLinkedQueue<Node> pendingDisconnects = new ConcurrentLinkedQueue<>();
// 这个标志允许客户端被安全唤醒而无需等待上面的锁,为了同时启用它,避免需要获取上面的锁是原子的
// this flag allows the client to be safely woken up without waiting on the lock above. It is
// atomic to avoid the need to acquire the lock above in order to enable it concurrently.
private final AtomicBoolean wakeup = new AtomicBoolean(false);
}
1、trySend()
循环处理unsent中缓存的请求,对每个node节点,循环遍历其ClientRequest链表,每次循环都调用NetworkClient.ready方法检测消费者与此节点之间的连接,以及发送请求的条件。若符合条件,则调用NetworkClient.send方法将请求放入InFlightRequest中等待响应,同时,也放入KafkaChannel中的send字段等待发送,并将消息从缓存列表中删除。
long trySend(long now) {
long pollDelayMs = maxPollTimeoutMs;
// send any requests that can be sent now
for (Node node : unsent.nodes()) {
Iterator<ClientRequest> iterator = unsent.requestIterator(node);
if (iterator.hasNext())
// 计算超时时间,此超时时间由timeout和delayedTasks队列中最近要执行的定时任务的事件共同决定。
// 该事件会作为最长阻塞时长,避免影响定时任务的执行
pollDelayMs = Math.min(pollDelayMs, client.pollDelayMs(node, now));
while (iterator.hasNext()) {
ClientRequest request = iterator.next();
if (client.ready(node, now)) {
client.send(request, now);
iterator.remove();
} else {
// try next node when current node is not ready
break;
}
}
}
return pollDelayMs;
}
2、NetworkClient.poll方法
将KafkaChannel.send字段指定的消息发送出去,除此之外,poll方法可能会更新metadata使用一系列handle()*方法处理请求响应、连接断开、超时等情况。
3、checkDisConnected()
private void checkDisconnects(long now) {
// 检测消费者与每个Node之间的连接状态
for (Node node : unsent.nodes()) {
if (client.connectionFailed(node)) {
// 在调用请求回调之前删除,避免回调处理再次遍历未发送列表的协调器故障
Collection<ClientRequest> requests = unsent.remove(node);
for (ClientRequest request : requests) {
RequestFutureCompletionHandler handler = (RequestFutureCompletionHandler) request.callback();
AuthenticationException authenticationException = client.authenticationException(node);
// 调用clientRequest的回调函数
handler.onComplete(new ClientResponse(request.makeHeader(request.requestBuilder().latestAllowedVersion()),
request.callback(), request.destination(), request.createdTimeMs(), now, true,
null, authenticationException, null));
}
}
}
}
4、调用maybeTriggerWakeup方法
检测wakeup和wakeupDisabled,查看是否有其他线程中断,如果有中断请求,则跑出WakeupException异常,中断当前poll方法。
public void maybeTriggerWakeup() {
// 通过wakeupDisabled 检测是否在执行不可中断的方法,通过wakeup检测是否有中断请求。
if (!wakeupDisabled.get() && wakeup.get()) {
log.debug("Raising WakeupException in response to user wakeup");
wakeup.set(false);
throw new WakeupException();
}
}
5、再次调用trSend方法
在步骤2中调用了NetWorkClient.poll方法,在其中可能已经将KafkaChannel.send字段上的请求发送出去了,也可能已经新建
了与某些Node的网络连接,所以再次尝试调用trySend方法。
6、调用failExpireRequests
处理unsent中超时请求,它会循环遍历整个unsent集合,检测每个ClientRequest是否超时,调用超时ClientRequest的回调函数,
并将其从unsent集合中删除。
先来看下ConsumerNetworkClient.send方法,里面的逻辑会将待发送的请求封装成ClientRequest,然后保存到unsent集合中
等待发送。
public RequestFuture<ClientResponse> send(Node node,
AbstractRequest.Builder<?> requestBuilder,
int requestTimeoutMs) {
long now = time.milliseconds();
RequestFutureCompletionHandler completionHandler = new RequestFutureCompletionHandler();
ClientRequest clientRequest = client.newClientRequest(node.idString(), requestBuilder, now, true,
requestTimeoutMs, completionHandler);
unsent.put(node, clientRequest);
// wakeup the client in case it is blocking in poll so that we can send the queued request
// 唤醒客户端以防它在轮询中阻塞,以便我们可以发送排队的请求
client.wakeup();
return completionHandler.future;
}
private class RequestFutureCompletionHandler implements RequestCompletionHandler {
private final RequestFuture<ClientResponse> future;
private ClientResponse response;
private RuntimeException e;
}
核心方法和字段:
listeners
: 用来监听请求的完成情况,有onSuccess和onFailure两个方法,对应于请求正常完成和出现异常两种情况;
isDone()
:表示当前请求是否已经完成,不管是否正常完成,此字段都会被设置为true;
value()
:记录请求正常完成时收到的响应,与exception方法互斥,此字段非空表示正常完成,反之表示出现异常。
exception()
:记录导致请求异常完成的异常类,与value互斥,此字段非空表示出现异常,反之则表示正常完成。
compose()
:使用了适配器模式
chain()
:使用了责任链模式
public <S> RequestFuture<S> compose(final RequestFutureAdapter<T, S> adapter) {
// 适配之后的结果
final RequestFuture<S> adapted = new RequestFuture<>();
// 在当前RequestFuture上添加监听器
addListener(new RequestFutureListener<T>() {
@Override
public void onSuccess(T value) {
adapter.onSuccess(value, adapted);
}
@Override
public void onFailure(RuntimeException e) {
adapter.onFailure(e, adapted);
}
});
return adapted;
}
使用compose()方法进行适配后,回调时的调用过程,也可以认为是请求完成的事件传播流程。当调用RequestFuture对象的complete()或raise()方法时,会调用RequestFutureListener的onSuccess()或onFailure()方法,然后调用RequestFutureAdapterRequestFuture
对象的对应方法。
chain() 方法与 compose() 方法类似,也是通过 RequestFutureListener 在多个 RequestFuture 之间传递事件.
public void chain(final RequestFuture<T> future) {
addListener(new RequestFutureListener<T>() {
@Override
public void onSuccess(T value) {
// 通过监听器将value传递给下一个RequestFuture对象
future.complete(value);
}
@Override
public void onFailure(RuntimeException e) {
// 通过监听器将异常传递给下一个RequestFuture对象
future.raise(e);
}
});
}
KafkaConsumer从Kafka拉取消息时发送的请求是FetchRequest,在其中需要指定消费者希望拉取的起始消息的offset。
为了消费者快速获取这个值,KafkaConsumer使用SubscriptionState来追踪TopicPartition与offset对应关系。
public class SubscriptionState {
// 表示订阅Topic的模式
private enum SubscriptionType {
NONE, // SubscriptionState.subscriptionType的初始值。
AUTO_TOPICS, // 按照指定的Topic名字进行订阅,自动分配分区。
AUTO_PATTERN, //按照指定的正则表达式匹配Topic进行订阅,自动分配分区。
USER_ASSIGNED // 用户手动指定消费者消费的Topic以及分区编号。
}
// 表示订阅模式
private SubscriptionType subscriptionType;
// 使用AUTO_PATTERN 模式时,按照此字段记录的正则表达式对所有Topic进行匹配,对匹配符合的Topic进行订阅
private Pattern subscribedPattern;
// 如果使用AUTO_TOPICS或AUTO_PATTERN 模式,则使用此集合记录所有订阅的Topic
// 当metadata更新时会触发修改该集合
private Set<String> subscription;
// 在前面描述的协议中,Consumer Group中会选举一个Leader,Leader使用该集合记录Consumer Group
// 中所有消费者订阅的Topic,而其他Follower的该集合中只保存了其自身的订阅的Topic。
// groupSubscription集合收缩的场景
// 1、将消费者自身订阅的Topic添加到groupSubscription;
// 2、在Leader收到JoinGroupResponse时调用,在JoinGroupResponse中包含了全部消费者订阅的Topic,在此时将Topic信息添加到groupSubscribe集合。
// 3、是将groupSubscribe中其他消费者订阅的Topic删除,只留下自身订阅的Topic(即subscription集合)
private Set<String> groupSubscription;
// 记录每个TopicPartition的消费状态
private final PartitionStates<TopicPartitionState> assignment;
// 默认的位移重置策略
private final OffsetResetStrategy defaultResetStrategy;
// 消费者重平衡监听器
private ConsumerRebalanceListener rebalanceListener;
private int assignmentId = 0;
}
private static class TopicPartitionState {
//
private FetchState fetchState;
// 记录了下次要从Kafka服务端获取的消息的offset
private FetchPosition position; // last consumed position
// 高水位,处于水位之下的所有消息,consumer都是可以读取的,consumer无法读取水位以上的消息
private Long highWatermark; // the high watermark from last fetch
// 日志起始位移
private Long logStartOffset; // the log start offset
// 最新的已提交位移
private Long lastStableOffset;
// 当前TopicPartition是否处于暂停状态,与consumer接口的pause方法相关
private boolean paused; // whether this partition has been paused by the user
private boolean pendingRevocation;
// 重置position的策略
private OffsetResetStrategy resetStrategy; // the strategy to use if the offset needs resetting
private Long nextRetryTimeMs;
private Integer preferredReadReplica;
private Long preferredReadReplicaExpireTimeMs;
private boolean endOffsetRequested;
}
public synchronized boolean subscribe(Set<String> topics, ConsumerRebalanceListener listener) {
// 注册监听器
registerRebalanceListener(listener);
// 设置订阅模式
setSubscriptionType(SubscriptionType.AUTO_TOPICS);
// 修改订阅Topic集合
return changeSubscription(topics);
}
Kafka的coordiantor要做的事情就是group management,就是要对一个团队(或者叫组)的成员进行管理。Group management就是要做这些事情:
Kafka为其设计了一个协议,就收做Group Management Protocol.
很明显,consumer group所要做的事情,是可以用group management 协议做到的。而cooridnator, 及这个协议,也是为了实现不依赖Zookeeper的高级消费者而提出并实现的。只不过,Kafka对高级消费者的成员管理行为进行了抽象,抽象出来group management功能共有的逻辑,以此设计了Group Management Protocol, 使得这个协议不只适用于Kafka consumer(目前Kafka Connect也在用它),也可以作为其它"group"的管理协议。
那么,这个协议抽象出来了哪些group management共有的逻辑呢? Kafka Consumer的AbstractCoordinator的注释给出了一些答案。
首先,AbstractorCoordinator是位于broker端的coordinator的客户端。这段注释里的"The cooridnator"都是指broker端的那个cooridnator,而不是AbstractCoordiantor。AbstractCoordinator和broker端的coordinator的分工,可以从注释里大致看出来。这段注释说,Kafka的group management protocol包括以下的动作序列:
所有的成员要先向coordinator注册,由coordinator选出leader, 然后由leader来分配state。这里存在着3个角色, 它们也都是为了解决扩展性的问题。单个Kafka集群可能会存在着比broker的数量大得多的消费者和消费者组,而消费者的情况可能是不稳定的,可能会频繁变化,每次变化都需要一次协调,如果由broker来负责实际的协调工作,会给broker增加很多负担。所以,从group memeber里选出来一个做为leader,由leader来执行性能开销大的协调任务,这样把负载分配到client端,可以减轻broker的压力,支持更多数量的消费组。
所有group member都需要发心跳给coordinator,这样coordinator才能确定group的成员。为什么心跳不直接发给leader呢?或许是为了可靠性。毕竟,leader和follower之间是可能存在着网络分区的情况的。但是,coordinator作为broker,如果任何group member无法与coordinator通讯,那也就肯定不能作为这个group的成员了。这也决定了,这个Group Management Protocol不应依赖于follower和leader之间可靠的网络通讯,因为leader不应该与follower直接交互。而应该通过coordinator来管理这个组。
ConsumerCoordinator组件实现与服务端的GroupCoordinator的交互。
两者的关系:ConsumerCoordinator负责向ConsumerNetworkClient发起各种请求,再发给broker节点,
GroupCoordinator根据请求类型进行处理,所以说ConsumerCoordinator负责与GroupCoordinator交互,GroupCoordinator
才是处理真正的协调组。
public final class ConsumerCoordinator extends AbstractCoordinator {
private final static TopicPartitionComparator COMPARATOR = new TopicPartitionComparator();
private final GroupRebalanceConfig rebalanceConfig;
private final Logger log;
// 分配策略
private final List<ConsumerPartitionAssignor> assignors;
// 集群元数据
private final ConsumerMetadata metadata;
private final ConsumerCoordinatorMetrics sensors;
private final SubscriptionState subscriptions;
private final OffsetCommitCallback defaultOffsetCommitCallback;
// 是否开启了自动提交offset
private final boolean autoCommitEnabled;
// 自动提交间隔
private final int autoCommitIntervalMs;
private final ConsumerInterceptors<?, ?> interceptors;
private final AtomicInteger pendingAsyncCommits;
// this collection must be thread-safe because it is modified from the response handler
// of offset commit requests, which may be invoked from the heartbeat thread
private final ConcurrentLinkedQueue<OffsetCommitCompletion> completedOffsetCommits;
private boolean isLeader = false;
private Set<String> joinedSubscription;
// 用来存储元数据的快照信息,主要用来检测Topic是否发生了分区数量的变化。
private MetadataSnapshot metadataSnapshot;
private MetadataSnapshot assignmentSnapshot;
private Timer nextAutoCommitTimer;
private AtomicBoolean asyncCommitFenced;
private ConsumerGroupMetadata groupMetadata;
private final boolean throwOnFetchStableOffsetsUnsupported;
}
在ConsumerCoordinator的构造方法中,会为Metadata添加一个监听器,当Metadata更新时会做下面几件事:
如果是AUTO_PATTERN模式,则使用用户自定义的正则表达式过滤Topic,得到需要订阅的Topic集合后,设置到SubscriptionState的subscription
集合和groupSubscription集合中。
如果是AUTO_PATTERN或AUTO_TOPICS模式,为当前metadata做一个快照,将新旧快照进行比较,如果topic发送分区数量变化,则将
subscription的needsPartitionAssignment字段置为true,需要重新进行分区分配。
使用metadataSnapshot字段记录变化后的新快照。
也是用来存储Metadata的快照信息,不过是用来检测Partition分配的过程中有没有发生分区数量变化。具体是在Leader消费者开始分区分配操作前,使用此字段
记录Metadata快照;收到SyncGroupResponse后,会比较此字段记录的快照与当前Metadata是否发生变化。如果发生变化,则要重新进行分区分配。
ConsumerPartitionAssignor这个接口是用来定义KafkaConsumer所用的“分区分配策略”. 用户可以实现这个接口,以定义自己所需的策略。consumer group的成员把它们所订阅的topic发送给coordinator。然后coordinator来选择一个leader, 然后由coordinator把这个group的所有成员的订阅情况发给leader,由leader来执行分区的分配。leader调用ConsumerPartitionAssignor的assign方法,来执行分区,然后把结果发给coordinator。由coordinator来转发分配的结果到每个group的成员。有时候,需要利用各个consumer的额外的信息来决定分配结果,比如consumer所在的机架情况。这时候,在实现ConsumerPartitionAssignor时,
就可以覆盖subscription(Set)方法,在其返回的Subscription对象中提供自己需要的userData。
实现类:AbstractPartitionAssignor, AbstractStickyAssignor, CooperativeStickyAssignor, RangeAssignor, RoundRobinAssignor, StickyAssignor
assign()
:在给定成员订阅和当前集群元数据的情况下执行组分配。onAssignment()
:当组成员从leader那里收到其分配时调用的回调。