kafka源码---消费者(1)

《kafka源码剖析》读书笔记

一,概念

1.offset

   问题:消费者如何确定自己消费到了分区的哪个位置?

   问题:如果消费者宕机了,当下一个消费者来继续处理这个分区的时候,如何继续消费?

       都指向了消费位移的概念,也就是我们需要一个变量来保存消费位置。我们将这个变量称为 offset。

  在旧版本中,消费者会把消费位置记录到zookeeper中,新版本为了缓解zookeeper的压力,在kafka服务端中添加了一个名为“_comsumer_offsets”的内部 topic,简称为offset topic,它保存消费者提交的offset,使用offset topic记录消费位置是默认选项。

   问题:消费者什么时候提交offset值?

1.自动提交方式,当使用poll()拉取的时候,把上次poll()消费的offset提交给offset topic。

    缺点:当消费者还没有消费完poll()拉取的消息,但是宕机了,自然没有提交offset,下一次继续消费的时候,会存在重复消费!

2.当poll()拉取消息的时候,还没有消费就把本次的offset提交了,

    缺点:避免了重复消费,但是如果还没有消费就宕机了,这就存在了消息丢失。

所以,出现了传递保证,三个级别:

   1.At most once:消息可能会丢,但绝对不会重复,对应上面的第二种方案

   2.At least once:消息不会丢失,但是可能会重复,对应上面的第一种方案

   3.Exactly once:每条消息只会被传递一次 (最好状态)

  很少有让消息丢失的情况,我们大多数都希望达到第三种情况,但是保证第三种情况,不仅仅是消费者,生产者也要保证消息只会被成功发送一次保存在服务器分区中,如果分区中已经存在同样的消息,自然无法避免重复消息(除非有一个判断机制,比如消息唯一id)

生产者确保exactly once

    1.每个分区只有一个生产者写入消息,当宕机,重启之后,自己去确认最后一次消息发送到了哪里,再决定是重发还是继续

   2.为每个消息设置一个全局主键,生产者不做其他处理,可以直接重传,消费者来进行去重。

消费者确保exactly once:

   关闭offset自动提交,不再使用offset topic这个内部topic,而是消费者自己保存offset,利用事务的原子性,当poll()一次,必须完整消费完,才提交offset,如果失败就回滚。将offset保存到数据库中,

2.消费组与消费者

    这里和其他的MQ有不同的地方,多了一个消费组,消费者是放在消费组里面的,分区是映射到消费者的,每个分区只能被一个消费组中的一个消费者所消费,如果要同一个分区要映射到多个消费者,这些消费者必然不是一个消费组的,但是一个消费者是可以任意映射分区,只要不重复。

    当一个消费组内加入新的消费者的时候,前面的消费者会释放一些分区出来,拿给新的消费者进行消费,而这里的和redis的集群里面很像,消费者分开处理topic的分区。

    每个不同的消费组中的消费者都去订阅同一个分区,那么就实现了广播模式。当有消费者宕机,或者消费者加入消费组,都会触发rebalance,在rebalance期间,消费者是不能消费的,直到rebalance完成,这个时候就需要使用第一小节中的offset重新开始消费。

问题:当消费组连接上服务器的时候,如何进行分区到消费者的映射分配呢?

方案1:kafka最开始使用zk的watcher实现,每个consumer group都在zk下维护一个/consumer/group_id/ids,记录消费者的id,同级别下,还有offsets节点,记录group在某个分区上的消费位置,owners记录分区与消费者的映射关系(重点),同样broker也在zk中有节点,保存了broker分区信息,leader信息,isr信息,

   缺点:zk负担很重,而且zk存在脑裂问题,不好!

方案2:kafka后续做出调整,将全部的consumer group 分成多个子集,每个consumer group 子集在服务端对应一个GroupCoordinator对其进行管理,消费者不再依赖zk,而只有GroupCoordinator在zk上面有watcher,这样就大大的减小了zk的负担,当有新的消费者加入,或者旧的宕机,就会改变zk的值,就会触发GroupCoordinator绑定的watcher,GroupCoordinator就会进行rebalance。

简述过程:

  1.当消费者准备计入Consumer Group,或者GroupCoordinator发生故障转移时,消费者并不知道GroupCoordinator网络位置,消费者会向任意broker发送一个ConmuserMetadataRequest请求,里面附带了GroupId,然后broker收到返回response,里面就带了groupId对应的CroupCoordinator信息。

  2.消费者根据ConsumerMetadataResponse中的GroupCoordinator信息,连接到GroupCoordinator并周期性发送HeartbeatReqeust,心跳检测,征明消费者还活着,如果GroupCoordinator长期收不到,就默认为消费者死了,会发起新一轮Rebalance。

  3.如果HeartbeatResponse中带有IllegalGeneration异常,说明GroupCoordinator发起了rebalance操作,此时消费者发送JoinGroupRequest给GroupCoordinator(通知GroupCoordinator,消费者要加入指定的GroupId)

  4.GroupCoordinator分配完成之后,将分配结果写入zookeeper,并通过JoinGroupResponse返回给消费者,消费者根据JoinGroupResponse结果进行消费数据。

  5.consumer计入Group之后,周期发送heartbeanRequest,如果发现异常返回就发送JoinGroupRequest,循环操作。

缺点:1.分区分配的操作在服务端的GroupCoordinator中完成,要求服务端实现Partition的分配策略,当使用新的分配策略,需要改动服务端的代码or配置,还需要重启,比较麻烦。

          2.不同的Rebalance策略有不同的验证需求,当需要自定义分区分配策略就会很麻烦

方案3:将rebalance工作放到了消费者处理,Consumer Group管理以然在GroupCoordinator中,当consumer发现GroupCoordinator后,就进入joinGroup阶段,发送JoinGroupRequest请求,服务端收齐所有消费者之后(这里如何确定所有消费者呢?),会从消费者中选出一个Leader,把所有消费者信息发送给leader,让它来进行分区分配,服务端只需要指定分配的方式就可以了,具体的操作交给leader来解决,leader把结果返回给GroupCoordinator,然后服务端再把结果 返回给Group中所有的消费者。

二,KafkaConuser分析

消费者的启动:

public void doWork() {
    //订阅 topic,可以一次订阅多个topic
    consumer.subscribe(Collections.singletonList(this.topic));
    // 从服务端拉取信息,每次poll()可以拉取多个信息  ★
    ConsumerRecords<Integer, String> records = consumer.poll(1000);
    //遍历,输出信息
    for (ConsumerRecord<Integer, String> record : records) {
        System.out.println("Received message: (" + record.key() + ", " + record.value() + ") at offset " + record.offset());
    }
}

 分为两步,1.订阅主题,记得上面我们说过,消费者构成消费组,一个消费组只能映射主题中的一个分区,但是这里直接订阅的主题topic,没有区分分区!

                   2.使用consumer进行消息拉取,也就是工作类:kafkaConsumer类

kafkaConsumer是线程不安全,而kafkaProducer是线程安全的,先看一下重要字段:

public class KafkaConsumer<K, V> implements Consumer<K, V> {
    private final String clientId;
    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 Time time;
    private final ConsumerNetworkClient client;   //负责通信
    private final SubscriptionState subscriptions; //订阅指定的topic
    private final Metadata metadata;
    private final long retryBackoffMs;
    private final long requestTimeoutMs;
    private volatile boolean closed = false;
    private List<PartitionAssignor> assignors;
...
}

目录结构:

  1.SubscriptionState             消费位置存储

  2.ConsumerNetworkClient   通信

  3.ConsumerCoordinator     负责与服务端GroupCoordinator交互

  4.PartitionAssignor             分区分配

2.1 SubscriptionState

第一步中,调用kafkaConsumer.subsribue()方法,实则是调用SubscriptionState类的subscribe()方法:

public void subscribe(Collection<String> topics, ConsumerRebalanceListener listener) {
   ... 检查topics 是否合法
        this.subscriptions.subscribe(new HashSet<>(topics), listener);  
        metadata.setTopics(subscriptions.groupSubscription());
}

 来到Subscriptions.subsribe()方法:

public void subscribe(Set<String> topics, ConsumerRebalanceListener listener) {
    //用户没有指定listener,则默认使用NoOpConsumerRebalanceListener,里面所有方法都是空的
    if (listener == null)
        throw new IllegalArgumentException("RebalanceListener cannot be null");
    //设置topic模式
    setSubscriptionType(SubscriptionType.AUTO_TOPICS);
    this.listener = listener;
    //【入】,将订阅的topics 放入GroupSubscribe集合
    changeSubscription(topics);
}

​
private void changeSubscription(Set<String> topicsToSubscribe) {
    if (!this.subscription.equals(topicsToSubscribe)) { //不相同才改变
        this.subscription = topicsToSubscribe;
        this.groupSubscription.addAll(topicsToSubscribe);
    }
}

​

 这里的订阅并没有发生网络连接,只是将topic存储了起来,设置了订阅模式,请看下面对SubscriptionState的详细分析

     consumer在拉取消息的时候,发送FetchRequest请求,里面要确定一个offset,而consumer为了快速获得这个值,使用SubscriptionState来追踪TopicPartitioni和offset的对应关系,

主要字段:

public class SubscriptionState {
    // 四种topic模式
    private enum SubscriptionType {
        NONE, AUTO_TOPICS, AUTO_PATTERN, USER_ASSIGNED
    }
    private SubscriptionType subscriptionType; // 订阅topic的模式

    // 表示AUTO_PATTREN模式,使用正则表达式对所有的topic进行匹配,对匹配成功的所有topic进行订阅
    private Pattern subscribedPattern;
    // 表示AUTO_TOPICS,AUTO_PATTERN模式,使用此集合记录所有订阅的topic
    private Set<String> subscription;
    // group leader记录所有消费者订阅的topic,其他consumer只保存自身订阅的topic
    private final Set<String> groupSubscription;
   // 表示AUTO_ASSIGNED模式
    private final PartitionStates<TopicPartitionState> assignment; //表示topicPartition的消费状态
    //是否需要从GroupCoordinator获取最近提交的offset
    private boolean needsFetchCommittedOffsets;
    //  默认为offsetResetStrategy策略
    private final OffsetResetStrategy defaultResetStrategy;
    //  用于监听分区分配操作 
    private ConsumerRebalanceListener listener;
...
}

1.SubscriptionType,四个枚举类型的含义:

1.NONE:                        初始值

2.AUTO_TOPICS:         按照指定的topic名字进行订阅,自动分配分区

3.AUTO_PATTERN:      按照正则表达式匹配topic,自动分配分区

4.USER_ASSIGNED:    用户手动指定消费者消费的topic以及分区编号

三种模式是互斥的。

2.TopicPartitionState 消费状态

他是本类中的内部类, 所有字段:

private static class TopicPartitionState {
    private Long position; /  下一次要拉取消息的offset
    private Long highWatermark; // the high watermark from last fetch
    private Long lastStableOffset;
    private OffsetAndMetadata committed;  / 最近一次提交的
    private boolean paused;  / 当前topicPartition是否处于暂停状态
    private OffsetResetStrategy resetStrategy;  / 枚举,重置position策略
...
}

2.2 ConsumerNetworkClient

  第一步订阅成功之后,第二步开始进行poll()拉取消息,使用client进行拉取。

ConsumerNetworkClient它封装了NetworkClient。  NetwordkClient 它依赖于Kselector,InFlightRequest,Metadata组件,负责管理客户端与kafka集群中各个Node间的连接,

public class ConsumerNetworkClient implements Closeable {
    private final KafkaClient client;      //NetworkClient对象
    private final UnsentRequests unsent = new UnsentRequests();  //缓冲队列
    private final Metadata metadata;  //用于管理kafka集群元数据
    private final Time time;
    private final long retryBackoffMs;  
    private final long unsentExpiryMs;  //缓冲的超时 时长
    private final AtomicBoolean wakeupDisabled = new AtomicBoolean();
...
}

 最核心方法poll(),拉取消息进行消费

 public void poll(long timeout, long now, PollCondition pollCondition, boolean disableWakeup) {
        firePendingCompletedRequests();

        synchronized (this) {
            /1.使用client 进行send() request
            trySend(now);
            if (pollCondition == null || pollCondition.shouldBlock()) {
                if (client.inFlightRequestCount() == 0)
                    timeout = Math.min(timeout, retryBackoffMs);
                //3,
                client.poll(Math.min(MAX_POLL_TIMEOUT_MS, timeout), now);
                now = time.milliseconds();
            } else {
                client.poll(0, now);
            }
            //4.
            checkDisconnects(now);
            if (!disableWakeup) {
                //5.
                maybeTriggerWakeup();
            }
            // throw InterruptException if this thread is interrupted
            maybeThrowInterruptException();
            //6.
            trySend(now);
            //7.
            failExpiredRequests(now);
             unsent.clean();
        }
        firePendingCompletedRequests();
    }

1.trySend(),发送clientRequest请求,unsent是ConsumerNetworkClient内部类

private boolean trySend(long now) {
    boolean requestsSent = false;
    for (Node node : unsent.nodes()) {
            /获取迭代器,一个node有多个request?
        Iterator<ClientRequest> iterator = unsent.requestIterator(node);
        while (iterator.hasNext()) {
            ClientRequest request = iterator.next();
            /检查 是否可以发送请求
            if (client.ready(node, now)) {
                client.send(request, now);  /发送
                iterator.remove();  //移除
                requestsSent = true;  //发送成功
            }
        }
    }
    return requestsSent;
}

unsent缓冲队列, client的内部类,里面保存了一个集合:

private final ConcurrentMap<Node, ConcurrentLinkedQueue<ClientRequest>> unsent;

存放的是node对应的request请求

2.checkDisconnects()检测连接状态

把断开的Node,从unsent中移除

    private void checkDisconnects(long now) {
        for (Node node : unsent.nodes()) {
            if (client.connectionFailed(node)) {
                // Remove entry before invoking request callback to avoid callbacks handling
                // coordinator failures traversing the unsent list again.
                Collection<ClientRequest> requests = unsent.remove(node);
                for (ClientRequest request : requests) {
                    RequestFutureCompletionHandler handler = (RequestFutureCompletionHandler) request.callback();
                    AuthenticationException authenticationException = client.authenticationException(node);
                    if (authenticationException != null)
                        handler.onFailure(authenticationException);
                    else
                        handler.onComplete(new ClientResponse(request.makeHeader(request.requestBuilder().latestAllowedVersion()),
                            request.callback(), request.destination(), request.createdTimeMs(), now, true,
                            null, null));
                }
            }
        }
    }

2.3 ConsumerCoordinator

通过consumerCoordinator组件与服务端的GroupCoordinator进行交互,它继承了AbstractCoordinator抽象类

1.AbstractCoordinator

public abstract class AbstractCoordinator implements Closeable {
    public static final String HEARTBEAT_THREAD_PREFIX = "kafka-coordinator-heartbeat-thread";

    private enum MemberState {
        UNJOINED,    // the client is not part of a group
        REBALANCING, // the client has begun rebalancing
        STABLE,      // the client has joined and is sending heartbeats
    }

    private final Logger log;
    private final int sessionTimeoutMs;
    private final boolean leaveGroupOnClose;
    private final GroupCoordinatorMetrics sensors;
    private final Heartbeat heartbeat;      //心跳任务的辅助类
    protected final int rebalanceTimeoutMs;
    protected final String groupId;        //当前consumer所属grou的id
    protected final ConsumerNetworkClient client;  //负责网络通信和执行定时任务
    protected final Time time;
    protected final long retryBackoffMs;

    private HeartbeatThread heartbeatThread = null;
    private boolean rejoinNeeded = true;       //是否重新发送JoinGroupRequest请求的条件之一
    private boolean needsJoinPrepare = true;   //是否需要执行发送JoinGroupRequest请求前的准备操作
    private MemberState state = MemberState.UNJOINED;
    private RequestFuture<ByteBuffer> joinFuture = null;
    private Node coordinator = null;
    private Generation generation = Generation.NO_GENERATION;

    private RequestFuture<Void> findCoordinatorFuture = null;

2.ConsumerCoordinator

public final class ConsumerCoordinator extends AbstractCoordinator {
    private final Logger log;
    private final List<PartitionAssignor> assignors; //分区策略,在consumer发送出去的JoinGroupRequest中带着,server会从所有消费者都支持的Assignor中选择一个策略来进去分区。
    private final Metadata metadata;                  //kafka集群元数据
    private final ConsumerCoordinatorMetrics sensors;
    private final SubscriptionState subscriptions;      //之前分析过,但是这里有一种冗余的感觉
    private final OffsetCommitCallback defaultOffsetCommitCallback;
    private final boolean autoCommitEnabled;            //是否自动提交offset
    private final int autoCommitIntervalMs;             //自动提交的时间
    private final ConsumerInterceptors<?, ?> interceptors;  //拦截器
    private final boolean excludeInternalTopics;            //排除内部topic
    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;
    private MetadataSnapshot metadataSnapshot;  //存储metadata快照信息,用来检测topic是否发生分区数量变化,添加一个监听器,
    private MetadataSnapshot assignmentSnapshot;  //用来存储metadata快照信息,用来检测partition分配的过程中,有没有分区数量的变化。
    private long nextAutoCommitDeadline;
...
}

3.metadata的listener

//为metadata添加 监听器
private void addMetadataListener() {
    this.metadata.addListener(new Metadata.Listener() {
        @Override
        public void onMetadataUpdate(Cluster cluster, Set<String> unavailableTopics) {
            // if we encounter any unauthorized topics, raise an exception to the user 验证
            if (!cluster.unauthorizedTopics().isEmpty())
                throw new TopicAuthorizationException(new HashSet<>(cluster.unauthorizedTopics()));
            //更新 集合
            if (subscriptions.hasPatternSubscription())
                updatePatternSubscription(cluster);

            // check if there are any changes to the metadata which should trigger a rebalance
            //检测是否为Auto_Pattern或者auto_topics模式
            if (subscriptions.partitionsAutoAssigned()) {
                MetadataSnapshot snapshot = new MetadataSnapshot(subscriptions, cluster); //创建快照
                if (!snapshot.equals(metadataSnapshot))   //比较快照
                    metadataSnapshot = snapshot;          //记录快照
            }

            if (!Collections.disjoint(metadata.topics(), unavailableTopics))
                metadata.requestUpdate();
        }
    });
}

2.4 PartitionAssignor

   Leader消费者收到JoinGroupResponse后,会按照其中指定的分区分配策略进行分区分配,每个分区分配策略就是一个PartitionAssignor接口的实现,该接口定义了两个内部类:Assignment和Subscriptioni两个内部类,

    class Subscription {
        private final List<String> topics;  //对应的主题,到底对应哪个分区,还不确定,待分配
        private final ByteBuffer userData;

   ...
}
    class Assignment {
        private final List<TopicPartition> partitions;  //表示的是分配给某消费者的topicPartition集合
        private final ByteBuffer userData;            //用户自定义的数据
...
}

  分区分配需要两个重要的数据:metadata元数据信息,每个member的订阅信息,将用户的订阅信息封装成Subscriptioin。

  Assignment保存了分区的分配结果,partitions表示的是分配给某消费者的topicPartition集合,userData是用户自定义的数据。

  assign()完成分配的方法,onAssignment()是给每个消费者收到leader分配结果时的回调函数。

1.assign()

  public Map<String, Assignment> assign(Cluster metadata, Map<String, Subscription> subscriptions) {
        Set<String> allSubscribedTopics = new HashSet<>();
        for (Map.Entry<String, Subscription> subscriptionEntry : subscriptions.entrySet()) //取出所有topic
            allSubscribedTopics.addAll(subscriptionEntry.getValue().topics());

        Map<String, Integer> partitionsPerTopic = new HashMap<>();
        for (String topic : allSubscribedTopics) {    //遍历所有topic
            Integer numPartitions = metadata.partitionCountForTopic(topic);  //topic下的partition count
            if (numPartitions != null && numPartitions > 0)            //合法,就放入集合中,
                partitionsPerTopic.put(topic, numPartitions);
            else
                log.debug("Skipping assignment for topic {} since no metadata is available", topic);
        }
        //传入 topic-partitionCount + subscriptions信息
        //返回结果:consumerId-消费的分区(可能不是同一个topic的)
        Map<String, List<TopicPartition>> rawAssignments = assign(partitionsPerTopic, subscriptions); //【入】委派模式,不同策略不同实现

        // this class maintains no user data, so just wrap the results
        //整理分区结果
        Map<String, Assignment> assignments = new HashMap<>();
        for (Map.Entry<String, List<TopicPartition>> assignmentEntry : rawAssignments.entrySet())
            assignments.put(assignmentEntry.getKey(), new Assignment(assignmentEntry.getValue()));
        return assignments;
    }

2.RangeAssignor中的assign()

对每个topic,n=分区数/消费者数量,剩下的前m个消费者+1.最简单的平均分配方法 存放的问题,这个存放topic隔离,意味着,前面m个消费者,可能在很多个topic中都是n+1状态。

public Map<String, List<TopicPartition>> assign(Map<String, Integer> partitionsPerTopic,
                                                    Map<String, Subscription> subscriptions) {
        Map<String, List<String>> consumersPerTopic = consumersPerTopic(subscriptions);
        Map<String, List<TopicPartition>> assignment = new HashMap<>();
        for (String memberId : subscriptions.keySet())
            assignment.put(memberId, new ArrayList<TopicPartition>());

        for (Map.Entry<String, List<String>> topicEntry : consumersPerTopic.entrySet()) {
            String topic = topicEntry.getKey();
            List<String> consumersForTopic = topicEntry.getValue();

            Integer numPartitionsForTopic = partitionsPerTopic.get(topic);
            if (numPartitionsForTopic == null)
                continue;

            Collections.sort(consumersForTopic);

            int numPartitionsPerConsumer = numPartitionsForTopic / consumersForTopic.size();
            int consumersWithExtraPartition = numPartitionsForTopic % consumersForTopic.size();

            List<TopicPartition> partitions = AbstractPartitionAssignor.partitions(topic, numPartitionsForTopic);
            for (int i = 0, n = consumersForTopic.size(); i < n; i++) {
                int start = numPartitionsPerConsumer * i + Math.min(i, consumersWithExtraPartition);
                int length = numPartitionsPerConsumer + (i + 1 > consumersWithExtraPartition ? 0 : 1);
                assignment.get(consumersForTopic.get(i)).addAll(partitions.subList(start, start + length));
            }
        }
        return assignment;
    }

3.RoundRobinAssignor中的assign()方法

它是通过轮询的方式,把【所有】的topic中的partition按字典顺序排序,然后对每个consumer进行轮询分配,如此,每个consumer绑定的partition count就比较平均

    public Map<String, List<TopicPartition>> assign(Map<String, Integer> partitionsPerTopic,
                                                    Map<String, Subscription> subscriptions) {
        Map<String, List<TopicPartition>> assignment = new HashMap<>();
        for (String memberId : subscriptions.keySet())
            assignment.put(memberId, new ArrayList<TopicPartition>());

        CircularIterator<String> assigner = new CircularIterator<>(Utils.sorted(subscriptions.keySet()));
        for (TopicPartition partition : allPartitionsSorted(partitionsPerTopic, subscriptions)) {
            final String topic = partition.topic();
            while (!subscriptions.get(assigner.peek()).topics().contains(topic))
                assigner.next();
            assignment.get(assigner.next()).add(partition);
        }
        return assignment;
    }

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值