【Apache Kafka3.2】KafkaConsumer源码分析

KafkaConsumer分析

方法概述

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

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<Node, ConcurrentLinkedQueue<ClientRequest>>,
    // key 是 Node 节点,value 是 ConcurrentLinkedQueue<ClientRequest> 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);
}
poll 流程

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集合中删除。

回调对象——RequestFutureCompletionHandler

先来看下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;
    }
RequestFuture

核心方法和字段:

  • listeners: 用来监听请求的完成情况,有onSuccess和onFailure两个方法,对应于请求正常完成和出现异常两种情况;

  • isDone():表示当前请求是否已经完成,不管是否正常完成,此字段都会被设置为true;

  • value():记录请求正常完成时收到的响应,与exception方法互斥,此字段非空表示正常完成,反之表示出现异常。

  • exception():记录导致请求异常完成的异常类,与value互斥,此字段非空表示出现异常,反之则表示正常完成。

  • compose():使用了适配器模式

  • chain():使用了责任链模式

RequestFuture.compose
    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()方法,然后调用RequestFutureAdapter<T,S>的对应方法,最终调用RequestFuture<S>对象的对应方法。

RequestFuture.chain()

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);
            }
        });
    }

SubscriptionState

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;
}
subscribe方法
    public synchronized boolean subscribe(Set<String> topics, ConsumerRebalanceListener listener) {
        // 注册监听器
        registerRebalanceListener(listener);
        // 设置订阅模式
        setSubscriptionType(SubscriptionType.AUTO_TOPICS);
        // 修改订阅Topic集合
        return changeSubscription(topics);
    }

ConsumerCoordinator

Group Management Protocol

Kafka的coordiantor要做的事情就是group management,就是要对一个团队(或者叫组)的成员进行管理。Group management就是要做这些事情:

  • 维持group的成员组成。这包括允许新的成员加入,检测成员的存活性,清除不再存活的成员。
  • 协调group成员的行为。

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的注释给出了一些答案。

AbstractCoordinator

首先,AbstractorCoordinator是位于broker端的coordinator的客户端。这段注释里的"The cooridnator"都是指broker端的那个cooridnator,而不是AbstractCoordiantor。AbstractCoordinator和broker端的coordinator的分工,可以从注释里大致看出来。这段注释说,Kafka的group management protocol包括以下的动作序列:

  • Group Registration:Group的成员需要向cooridnator注册自己,并且提供关于成员自身的元数据(比如,这个消费成员想要消费的topic)
  • Group/Leader Selection:cooridnator确定这个group包括哪些成员,并且选择其中的一个作为leader。
  • State Assignment: leader收集所有成员的metadata,并且给它们分配状态(state,可以理解为资源,或者任务)。
  • Group Stabilization: 每个成员收到leader分配的状态,并且开始处理。

所有的成员要先向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

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;
}
metadataSnapshot

在ConsumerCoordinator的构造方法中,会为Metadata添加一个监听器,当Metadata更新时会做下面几件事:

  • 如果是AUTO_PATTERN模式,则使用用户自定义的正则表达式过滤Topic,得到需要订阅的Topic集合后,设置到SubscriptionState的subscription
    集合和groupSubscription集合中。

  • 如果是AUTO_PATTERN或AUTO_TOPICS模式,为当前metadata做一个快照,将新旧快照进行比较,如果topic发送分区数量变化,则将
    subscription的needsPartitionAssignment字段置为true,需要重新进行分区分配。

  • 使用metadataSnapshot字段记录变化后的新快照。

assignmentSnapshot

也是用来存储Metadata的快照信息,不过是用来检测Partition分配的过程中有没有发生分区数量变化。具体是在Leader消费者开始分区分配操作前,使用此字段
记录Metadata快照;收到SyncGroupResponse后,会比较此字段记录的快照与当前Metadata是否发生变化。如果发生变化,则要重新进行分区分配。

ConsumerPartitionAssignor

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那里收到其分配时调用的回调。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Apache Kafka是一个分布式流数据平台,用于处理和传输大规模的实时数据流。它最初由LinkedIn开发,并且现在成为Apache软件基金会的顶级项目。 Kafka的设计目标是提供一个可靠、高吞吐量、可扩展的平台来处理实时数据流。它以分布式发布-订阅消息系统的方式工作,允许多个生产者将消息发布到一个或多个主题(topics),并允许多个消费者从这些主题中订阅并处理消息。 Kafka的核心概念包括主题(topics)、分区(partitions)、生产者(producers)、消费者(consumers)和代理(brokers)。主题是消息的分类,每个主题可以被分为多个分区。生产者负责发布消息到主题,而消费者则可以订阅一个或多个主题并处理收到的消息。代理是Kafka集群中的服务器节点,负责存储和处理消息。 Kafka的特点包括: 1. 高吞吐量:Kafka能够处理大规模的实时数据流,每秒可以处理成千上万条消息。 2. 可扩展性:Kafka的分布式架构允许在需要时简单地增加节点来提高处理能力。 3. 持久性:Kafka将消息持久化到磁盘,因此即使消费者离线,消息也不会丢失。 4. 可靠性:Kafka保证消息会被正确地传递和处理,支持消息的复制和容错。 5. 实时性:Kafka能够以毫秒级的延迟传递消息,使得实时数据流处理成为可能。 总之,Apache Kafka是一个非常强大的流数据平台,广泛应用于大规模数据处理、日志收集、事件驱动架构等场景。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值