Kafka学习笔记:消息生产者、消费者以及消息发布的不同模型

目录

 

消息生产者、消费者以及消息发布的不同模型

Kafka Producer

Kafka Producer消息发送架构图

Kafka Consumer

Kafka Consumer Group

Kafka High Level Consumer Rebalance(重新分配消费)

Low Level Consumer


消息生产者、消费者以及消息发布的不同模型

Kafka Producer

  • Kafka Producer产生数据发送给KafkaServer,具体的分发逻辑和负载均衡逻辑,全部由producer维护
  • Producer不用连接ZooKeeper,而是直接发布信息,然后Topic会跟ZooKeeper更新数据

  • 所有的broker构成一个partition的list,partition会跨broker存在副本,副本中会有leader的角色,用来更新所有的副本,用以保证数据的一致性

Kafka Producer消息发送架构图

  • Producer有同步发送和异步发送两种策略,异步发送的意思就是客户端有个本地缓存区,消息先存放到本地缓存区,然后由后台进程来发送,在0.8.2版本之后,同步发送由异步发送间接实现
  • 异步发送的基本思路就是,send的时候,Producer把消息放到本地的消息队列RecordAccmulator,然后一个后台线程Sender不断循环,把消息发给Kafka集群。要实现这个操作,还得有一个前提条件,就是Producer/Sender都需要获取集群的配置信息Metadata,即每一个Topic的每个Partition对应的broker list,以及其中的leader,follower
  • 在以前的Kafka Client中,每条消息称为Message,在Java版Client中,称之为Record,同时又因为有批量发送累积功能,所以又称之为RecordAccumulator,RecordAccumulator最大的一个特性就是batch消息,队列中的多个消息会组成一个RecordBatch,然后由Sender一次性发送出去,由源码可以发现,每个TopicPartition对应一个deque,只有同一个TopicPartition的消息,才可能被batch
public final class RecordAccumulator {
    private final ConcurrentMap<TopicPartition, Deque<RecordBatch>> batches;

   ...
}
  • 那么什么时候消息会被batch,什么时候不会呢,可以看Kafka Producer的send方法
//KafkaProducer
    public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
        try {
            // first make sure the metadata for the topic is available
            long waitedOnMetadataMs = waitOnMetadata(record.topic(), this.maxBlockTimeMs);

            ...

            RecordAccumulator.RecordAppendResult result = accumulator.append(tp, serializedKey, serializedValue, callback, remainingWaitMs);   //核心函数:把消息放入队列

            if (result.batchIsFull || result.newBatchCreated) {
                log.trace("Waking up the sender since topic {} partition {} is either full or getting a new batch", record.topic(), partition);
                this.sender.wakeup();
            }
            return result.future;

从上面代码可以看到,batch逻辑都在accumulator.append函数里面

public RecordAppendResult append(TopicPartition tp, byte[] key, byte[] value, Callback callback, long maxTimeToBlock) throws InterruptedException {
        appendsInProgress.incrementAndGet();
        try {
            if (closed)
                throw new IllegalStateException("Cannot send after the producer is closed.");
            Deque<RecordBatch> dq = dequeFor(tp);  //找到该topicPartiton对应的消息队列
            synchronized (dq) {
                RecordBatch last = dq.peekLast(); //拿出队列的最后1个元素
                if (last != null) {  
                    FutureRecordMetadata future = last.tryAppend(key, value, callback, time.milliseconds()); //最后一个元素, 即RecordBatch不为空,把该Record加入该RecordBatch
                    if (future != null)
                        return new RecordAppendResult(future, dq.size() > 1 || last.records.isFull(), false);
                }
            }

            int size = Math.max(this.batchSize, Records.LOG_OVERHEAD + Record.recordSize(key, value));
            log.trace("Allocating a new {} byte message buffer for topic {} partition {}", size, tp.topic(), tp.partition());
            ByteBuffer buffer = free.allocate(size, maxTimeToBlock);
            synchronized (dq) {
                // Need to check if producer is closed again after grabbing the dequeue lock.
                if (closed)
                    throw new IllegalStateException("Cannot send after the producer is closed.");
                RecordBatch last = dq.peekLast();
                if (last != null) {
                    FutureRecordMetadata future = last.tryAppend(key, value, callback, time.milliseconds());
                    if (future != null) {
                        // Somebody else found us a batch, return the one we waited for! Hopefully this doesn't happen often...
                        free.deallocate(buffer);
                        return new RecordAppendResult(future, dq.size() > 1 || last.records.isFull(), false);
                    }
                }

                //队列里面没有RecordBatch,建一个新的,然后把Record放进去
                MemoryRecords records = MemoryRecords.emptyRecords(buffer, compression, this.batchSize);
                RecordBatch batch = new RecordBatch(tp, records, time.milliseconds());
                FutureRecordMetadata future = Utils.notNull(batch.tryAppend(key, value, callback, time.milliseconds()));

                dq.addLast(batch);
                incomplete.add(batch);
                return new RecordAppendResult(future, dq.size() > 1 || batch.records.isFull(), true);
            }
        } finally {
            appendsInProgress.decrementAndGet();
        }
    }

    private Deque<RecordBatch> dequeFor(TopicPartition tp) {
        Deque<RecordBatch> d = this.batches.get(tp);
        if (d != null)
            return d;
        d = new ArrayDeque<>();
        Deque<RecordBatch> previous = this.batches.putIfAbsent(tp, d);
        if (previous == null)
            return d;
        else
            return previous;
    }

从上面代码可以看出batch的策略:

1.如果是同步发送,每次去队列取,RecordBatch都会为空,这个时候消息就不会被batch,一个Record作为一个RecordBatch
2.当Producer入队速率 < Sender出队速率 && lingerMs=0
3.Producer 入队速率 > Sender出对速率, 消息会被batch
4.lingerMs > 0,这个时候Sender会等待,直到lingerMs > 0 或者 队列满了,或者超过了一个RecordBatch的最大值,就会发送。这个逻辑在RecordAccumulator的ready函数里面。

ReadyCheckResult ready(Cluster cluster, long nowMs) {
        Set<Node> readyNodes = new HashSet<Node>();
        long nextReadyCheckDelayMs = Long.MAX_VALUE;
        boolean unknownLeadersExist = false;

        boolean exhausted = this.free.queued() > 0;
        for (Map.Entry<TopicPartition, Deque<RecordBatch>> entry : this.batches.entrySet()) {
            TopicPartition part = entry.getKey();
            Deque<RecordBatch> deque = entry.getValue();

            Node leader = cluster.leaderFor(part);
            if (leader == null) {
                unknownLeadersExist = true;
            } else if (!readyNodes.contains(leader)) {
                synchronized (deque) {
                    RecordBatch batch = deque.peekFirst();
                    if (batch != null) {
                        boolean backingOff = batch.attempts > 0 && batch.lastAttemptMs + retryBackoffMs > nowMs;
                        long waitedTimeMs = nowMs - batch.lastAttemptMs;
                        long timeToWaitMs = backingOff ? retryBackoffMs : lingerMs;
                        long timeLeftMs = Math.max(timeToWaitMs - waitedTimeMs, 0);
                        boolean full = deque.size() > 1 || batch.records.isFull();
                        boolean expired = waitedTimeMs >= timeToWaitMs;
                        boolean sendable = full || expired || exhausted || closed || flushInProgress();  //关键的一句话
                        if (sendable && !backingOff) {
                            readyNodes.add(leader);
                        } else {

                            nextReadyCheckDelayMs = Math.min(timeLeftMs, nextReadyCheckDelayMs);
                        }
                    }
                }
            }
        }

        return new ReadyCheckResult(readyNodes, nextReadyCheckDelayMs, unknownLeadersExist);
    }

(这一部分暂未彻底弄懂,先记结论)

  • 为什么要使用Deque(即双端队列,双端队列中的元素可以从两端弹出,其限定插入和删除操作在表的两端进行)?这其实是为了处理“发送失败,重试”的问题,当消息发送失败要重发的时候,需要把消息优先放入队列头部重新发送,这就需要用到双端队列,在头部而不是尾部加入,即便如此,消息发送出去的顺序还是和Producer放进去的顺序不一致了
  • Recordbatch被Sender发送给NetworkClient之后,NetworkClient将其封装成类似于Socket通信的存在,即将deque里的batch封装成ClientRequest,NetworkClient是一个内部类,用于实现面向用户的生产者和消费者客户端。然后发送给Selector,传到Cluster

Kafka Consumer

  • Consumer以订阅形式获取Kafka数据
  • Kafka提供了两种Consumer API,分别是:High Level Consumer API和Lower Level Consumer API(Simple Consumer API)
API 原理 优点 缺点
High Level Consumer API(入口类:ConsumerConnector) 将底层具体获取数据、更新offset、设置偏移量等操作屏蔽掉,直接将操作数据流的处理工作提供给编写程序的人员 操作简单 可操作性差,无法按照自己的业务场景选择处理方式
Lower Level Consumer API(入口类:SimpleConsumer) 通过直接操作底层API获取数据的方式获取Kafka中的数据,需要自行给定分区、偏移量等属性 可操作性强 代码比较复杂

Kafka Consumer Group

  • High Level Consumer将从某个Partition读取的最后一条消息的offset存于Zookeeper中(从0.8.2开始同时支持将offset存于Zookeeper中和专用的Kafka Topic中)。 
  • 这个offset基于客户程序提供给Kafka的名字来保存,这个名字被称为Consumer Group。换句话说,并不是每个topic都会分很多consumer group,每一个consumer group中的consumer都可以消费多个topic,同时,一个topic可以被多个consumer group消费。
  • Consumer Group是整个Kafka集群全局唯一的,而非针对某个Topic的。 
  • 每个High Level Consumer实例都属于一个Consumer Group,若不指定则属于默认的Group。
  • 消息被消费后,并不会被删除,只是相应的offset加一(对于p2p消息系统,消息一旦被消费,就会被删除,保证queue比较小,提高效率;但是对于kafka这种发布订阅系统来说,消息被消费后,并不会立即被删除,因为消息是顺序的,而且,删除后,其他的consumer就无法消费了) 。
  • 对于每条消息,在同一个Consumer Group里只会被一个Consumer消费。
  • 不同Consumer Group可消费同一条消息。

Kafka High Level Consumer Rebalance(重新分配消费)

当有Consumer加入或退出、coodinator挂了(0.9之后用于管理Consumer Group的角色)、以及partition的改变(如broker加入或退出)时会触发rebalance,Consumer Group通过Rebalance提供HA特性

  • Consumer启动及Rebalance流程
  1. High Level Consumer启动时将其ID注册到其Consumer Group下,在Zookeeper上的路径为/consumers/[consumer group]/ids/[consumer id]
  2. 在/consumers/[consumer group]/ids上注册Watch,看看有没有其他的consumer加入或者退出
  3. 在/brokers/ids上注册Watch,有没有broker crash了,因为有些broker crash了,他的partition就不可用了或者需要重新分配。
  4. 如果Consumer通过Topic Filter创建消息流,则它会同时在/brokers/topics上也创建Watch
  5. 强制自己在其Consumer Group内启动Rebalance流程
  • Consumer Rebalance算法
  1. 将目标Topic下的所有Partirtion排序,存于集合P中
  2. 对某Consumer Group下所有Consumer排序,存于集合C ,第i个Consumer记为C[i]
  3. N=size(P)/size(C) ,向上取整
  4. 解除C[i]对原来分配的Partition的消费权(i从0开始)
  5. 将第 i∗N 到(i+1)∗N−1个Partition分配给C[i]

     举例:
     topic有4个partition[p0,p1,p2,p3],2个consumer[c0,c1]
     将所有partition排序,存在集合P中,Consumer排序也存在集合C中
     N=size(P)/size(C)=4/2=2
     根据公式可以知道分配
     C[0]->p0,p1
     C[2]->p2,p3

     当consumer加入[c0,c1,c2,c3]
     N=4/4=1
     Rebalance之后
     C[0]->p0
     C[1]->p1
     C[2]->p2
     C[3]->p3

  • Consumer Rebalance算法缺陷及改进
  1. Herd Effect:任何Broker或者Consumer的增减都会触发所有的Consumer的Rebalance
  2. Split Brain:每个Consumer分别单独通过Zookeeper判断哪些Broker和Consumer宕机,同时Consumer在同一时刻从Zookeeper“看”到的View可能不完全一样,这是由Zookeeper的特性决定的。
  3. 调整结果不可控 所有Consumer分别进行Rebalance,彼此不知道对应的Rebalance是否成功

Low Level Consumer

使用Low Level Consumer (Simple Consumer)的主要原因是:用户希望比Consumer Group更好的控制数据的消费,如:

  1. 同一条消息读多次,方便Replay
  2. 只消费某个Topic的部分Partition
  3. 管理事务,从而确保每条消息被处理一次(Exactly once)

与High Level Consumer相对,Low Level Consumer要求用户做大量的额外工作:

  1. 在应用程序中跟踪处理offset,并决定下一条消费哪条消息
  2. 获知每个Partition的Leader
  3. 处理Leader的变化
  4. 处理多Consumer的协作

参考文章:
Kafka源码深度解析:https://blog.csdn.net/chunlongyu/article/category/6417583
kafka Consumer Pull vs Push & Low level API vs High level API:https://blog.csdn.net/qq_37502106/article/details/80260546
kafka学习笔记:知识点整理:https://www.cnblogs.com/cyfonly/p/5954614.html

猜你喜欢

转载自blog.csdn.net/lrxcmwy2/article/details/82945343