【kafka】kafka RecordAccumulator封装消息流程

403 篇文章 639 订阅 ¥99.90 ¥299.90
本文深入解析Kafka生产者中的RecordAccumulator如何封装消息,探讨CopyOnWriteMap数据结构的应用,以及数据如何写入批次并采用分段加锁策略保证并发安全性。同时,介绍了内存池的设计,以实现内存高效复用减少GC。
摘要由CSDN通过智能技术生成

在这里插入图片描述

1.概述

转载:【生产者分析四】RecordAccumulator封装消息流程
并且做一定的修改

1.1 整体架构

在这里插入图片描述

1.1 RecordAccumulator封装消息流程初探

下面我们来分析下生产者核心流程中RecordAccumulator是如何封装消息的
在这里插入图片描述
简图如下,这里有个数据结构大致如下

在这里插入图片描述
核心代码KafkaProducer当中doSend方法定义以下内容

/**
  *  todo: 第七步:把消息放入Accumulator(32M的一个内存)
  * 然后由accumulator把消息封装成为一个批次一个批次的去发送。
  */
 RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey, serializedValue, interceptCallback, remainingWaitMs);
           

accumulator.append方法实现的核心流程

 public RecordAppendResult append(TopicPartition tp,
                                     long timestamp,
                                     byte[] key,
                                     byte[] value,
                                     Callback callback,
                                     long maxTimeToBlock) throws InterruptedException {
        // We keep track of the number of appending thread to make sure we do not miss batches in
        // abortIncompleteBatches().
        appendsInProgress.incrementAndGet();
        try {
            // check if we have an in-progress batch
            /**
             * 步骤一:
             * 先根据分区获取该消息所属的队列中
             *      如果有已经存在的队列,那么我们就使用存在队列
             *      如果队列不存在,那么我们新创建一个队列
             */
            Deque<RecordBatch> dq = getOrCreateDeque(tp);
            synchronized (dq) {
                if (closed)
                    throw new IllegalStateException("Cannot send after the producer is closed.");
                /**
                 * 步骤二:
                 * 尝试向队列里面的批次中添加数据
                 * 这里需要说明一下:目前我们只是有了队列,数据是存储在批次中(批次对象是需要分配内存的)
                 * 目前还没有分配到内存,所以添加数据会失败
                 */
                RecordAppendResult appendResult = tryAppend(timestamp, key, value, callback, dq);
                if (appendResult != null)
                    return appendResult;
            }

            // we don't have an in-progress record ba tch try to allocate a new batch
            /**
             * 步骤三:计算一个批次的大小
             * 在消息的大小和批次的大小之间取一个最大值,用这个值作为当前这个批次的大小
             *
             * 有可能一个消息比设定好的大小还要打
             * 默认一个批次的大小是16K。
             * 所以我们看到这段代码以后,应该有一个启发
             * 如果我们生产者发送数据的时候,如果我们的消息大小超过了16K, 那么消息就是一条一条的发送出去的,
             * 这样的话批次这个概念的设计也就没有意义了
             *  结论:所有大家一定要根据自己公司的数据大小来设置批次的大小
             */
            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.");

                /**
                 *  步骤五:再次尝试把数据写入到批次中
                 *  代码第一次执行到这里,依然还是失败的,
                 *  目前只是有内存了,目前还没有创建批次,还没有把内存分配给批次
                 */
                RecordAppendResult appendResult = tryAppend(timestamp, key, value, callback, dq);
                //appendResult 还是等于null
                if (appendResult != null) {
                    // Somebody else found us a batch, return the one we waited for! Hopefully this doesn't happen often...
                    free.deallocate(buffer);
                    return appendResult;
                }

                /**
                 * 步骤六: 根据内存大小封装批次
                 */
                MemoryRecords records = MemoryRecords.emptyRecords(buffer, compression, this.batchSize);
                RecordBatch batch = new RecordBatch(tp, records, time.milliseconds());

                //尝试往这个批次写数据,到这个时候,代码会执行成功
                FutureRecordMetadata future = Utils.notNull(batch.tryAppend(timestamp, 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();
        }
    }

1.2 生产者源码之CopyOnWriteMap数据结构使用

了解了RecordAccumulator 封装消息的流程后,下面我们来具体分析每一个步骤的详细逻辑和底层设计

核心代码RecordAccumulator当中的append方法

 /**
  * 步骤一:
  * 先根据分区获取该消息所属的队列中
  *      如果有已经存在的队列,那么我们就使用存在队列
  *      如果队列不存在,那么我们新创建一个队列
  */
 Deque<RecordBatch> dq = getOrCreateDeque(tp);

getOrCreateDeque方法实现

 private Deque<RecordBatch> getOrCreateDeque(TopicPartition tp) {
     //todo: 直接从batches里面获取当前分区对应的存储队列
     Deque<RecordBatch> d = this.batches.get(tp);
     //我们是以场景驱动的方式,代码第一次执行到这里的话,获取不到队列,该变量d为null
     if (d != null)
         return d;

     //创建出一个新的空队列
     d = new ArrayDeque<>();
     //把空的队列存入到batches这个map数据结构中
     //使用putIfAbsent方法添加键值对,如果map集合中没有该key对应的值,则直接添加,并返回null,
     // 如果已经存在对应的值,则依旧为原来的值。
     Deque<RecordBatch> previous = this.batches.putIfAbsent(tp, d);
     if (previous == null)
         return d;
     else
         //直接返回新的结果
         return previous;
 }

这里的this.batches其本质就是一个Map数据结构

  //TopicPartition 分区-------》  Deque<RecordBatch> 队列
  private final ConcurrentMap<TopicPartition, Deque<RecordBatch>> batches;


在RecordAccumulator构造方法中给batches 进行初始化

 //todo: CopyOnWriteMap这样的一个数据类型
 //这个数据结构在jdk里面是没有,是kafka自己设计的
 //这个数据结构设计的很好,有了这个数据结构,整体的提升了封装批次的这个流程性能
 this.batches = new CopyOnWriteMap<>();

1.2.1 CopyOnWriteMap

下面我们来学习下CopyOnWriteMap这种数据结构的底层使用

CopyOnWriteMap类

public class CopyOnWriteMap<K, V> implements ConcurrentMap<K, V> {

    /**
     * 核心的变量就是一个map
     *
     *  这个map有个特点,它的修饰符是volatile关键字
     *  作用;在多线程的情况下,如果这个map的值发生变化,其他线程是可见的
     */
    private volatile Map<K, V> map;
    
   /**
     * (1) 整个方法是使用synchronized去修饰的,它是线程安全的
     *     即使加了锁,这段代码的性能依然很好,因为里面都是纯内存操作的
     *
     * (2) 读数据的流程和写数据的流程分离了,这里采用了读写分离的设计思想
     *     读操作和写操作是互不影响
     *     所以我们读数据的操作就是线程安全的
     *
     * (3) 最后把之赋值给了map,map是使用了volatile去修饰
     *     说明这个map是具有可见性的,如果map的值发生了变化,进行get操作的时候,是可以感知到的
     *
     */
    @Override
    public synchronized V put(K k, V v) {
        //开辟新的内存空间
        Map<K, V> copy = new HashMap<K, V>(this.map);
        //插入数据
        V prev = copy.put(k, v);
        //赋值给map
        this.map = Collections.unmodifiableMap(copy);
        return prev;
    }
    
 	/**
     *  没有加锁,读取数据的时候性能很高(高并发场景下,性能肯定很好)
     *  并且是线程安全的(由于采用了读写分离的思想)
     */
    @Override
    public V get(Object k) {
        return map.get(k);
    }

    
        @Override
    public synchronized V putIfAbsent(K k, V v) {
        //如果k不存在,就调用内部的put方法
        if (!containsKey(k))
            return put(k, v);
        else
        //如果存在就返回结果
            return get(k);
    }

 
}

1.3 生产者源码之数据写入到对应的批次(分段加锁)

前面已经创建好了队列,但是队列内部目前来说还是没有批次,也就是说构建好一个空的队列,下面我们来学习数据写如何入到对应的批次中

核心代码RecordAccumulator核心类当中的append方法

 synchronized (dq) {
     if (closed)
         throw new IllegalStateException("Cannot send after the producer is closed.");
     /**
      * 步骤二:
      * 尝试向队列里面的批次中添加数据
      * 这里需要说明一下:目前我们只是有了队列,数据是存储在批次中(批次对象是需要分配内存的)
      * 目前还没有分配到内存,所以添加数据会失败
      */
     RecordAppendResult appendResult = tryAppend(timestamp, key, value, callback, dq);
     //第一次进来appendResult返回结果为null
     if (appendResult != null)
         return appendResult;
 }

tryAppend方法分析RecordAccumulator核心类当中的append方法

    /**
     * If `RecordBatch.tryAppend` fails (i.e. the record batch is full), close its memory records to release temporary
     * resources (like compression streams buffers).
     */
    private RecordAppendResult tryAppend(long timestamp, byte[] key, byte[] value, Callback callback, Deque<RecordBatch> deque) {
        //todo: 获取到队列中最后一个批次
        RecordBatch last = deque.peekLast();
        //todo: 第一次进来是没有批次的,if不会执行,直接返回null
        if (last != null) {
            FutureRecordMetadata future = last.tryAppend(timestamp, key, value, callback, time.milliseconds());
            if (future == null)
                last.records.close();
            else
                return new RecordAppendResult(future, deque.size() > 1 || last.records.isFull(), false);
        }
        //todo: 第一次直接返回null
        return null;
    }

由于之前我们已经分析过了,由于队列中没有批次,第一次添加数据会失败,然后继续执行后面的逻辑,计算一个批次的大小------->根据批次大小分配内存--------->再次尝试把数据写入到批次中-------->根据内存大小封装批次,此时此刻,队列有了,队列中的批次批次也构建好了,最后第三次把数据写入到批次中会执行成功。

1.3.1 分析

这里发现代码中会出现申请内存(free.allocate)和释放内存(free.deallocate)的操作,那么什么时候会去申请内存,什么时候会去释放内存,还有在代码中会看到有些代码中使用了synchronized进行加锁,有些并没有实现synchronized进行加锁。

​ 这里其本质就是用到了分段加锁的设计思想,该加锁的地方就加锁,不该加锁的地方就不加锁,好处就是为了在支持高并发的场景下,既要保证线程的安全,还有尽可能的提升代码的性能。

1.4 生产者源码之内存池设计

给批次分配对应大小的内存,这是用到了内存池的设计,通过内存池的使用,可以实现内存的复用,不需要进行频繁的GC。下面我们来学习下kafka底层高效的内存池的设计思路。

在这里插入图片描述

1.4.1 核心代码

RecordAccumulator.append方法中的核心代码逻辑

  /**
    * 步骤四:根据批次大小分配内存
    */
     ByteBuffer buffer = free.allocate(size, maxTimeToBlock);

free.allocate申请内存方法分析

 public ByteBuffer allocate(int size, long maxTimeToBlockMs) throws InterruptedException {
        //todo: 如果要申请的内存大小超过了 32M 就报异常
        if (size > this.totalMemory)
            throw new IllegalArgumentException("Attempt to allocate " + size
                                               + " bytes, but there is a hard limit of "
                                               + this.totalMemory
                                               + " on memory allocations.");

        //加锁
        this.lock.lock();
        try {
            // check if we have a free buffer of the right size pooled
            //todo: poolableSize就是一个批次的大小,默认就是16KB
            //如果size我们要申请的批次大小正好等于一个批次默认的大小,并且内存池不为空

            //todo:代码第一次进来的话,内存池中是没有内存的,这里是获取不到
            if (size == poolableSize && !this.free.isEmpty())
                //直接从内存池拿出一块内存就可以了
                return this.free.pollFirst();

            // now check if the request is immediately satisfiable with the
            // memory on hand or if we need to block
            //todo: 内存块的个数* 批次的大小=内存池的内存大小
            int freeListSize = this.free.size() * this.poolableSize;

            /**
             * size是我们要申请的内存
             *  this.availableMemory + freeListSize 目前可用的总内存
             */
            if (this.availableMemory + freeListSize >= size) {
                // we have enough unallocated or pooled memory to immediately
                // satisfy the request
                freeUp(size);
                //todo: 进行内存扣减
                this.availableMemory -= size;
                lock.unlock();
                //todo: 直接分配内存
                return ByteBuffer.allocate(size);
            } else {
                // we are out of memory and will have to block
                /**
                 * 还有一种情况是,比如内存池中还剩10K的内存,但是需要申请32K的内存
                 */

                //统计分配的内存
                int accumulated = 0;
                ByteBuffer buffer = null;
                Condition moreMemory = this.lock.newCondition();
                long remainingTimeToBlockNs = TimeUnit.MILLISECONDS.toNanos(maxTimeToBlockMs);
                /**
                 * 等待别人释放内存
                 */
                this.waiters.addLast(moreMemory);
                // loop over and over until we have a buffer or have reserved
                // enough memory to allocate one
                /**
                 *  总的分配内存思路:
                 *   可能一下子给不了这么大的内存,我可以先一点一点的分配
                 */

                //如果分配的内存的大小,还是没有达到我需要申请的内存大小,内存池就会一直不断的一点一点的去分配
                //等待别人释放内存
                while (accumulated < size) {
                    long startWaitNs = time.nanoseconds();
                    long timeNs;
                    boolean waitingTimeElapsed;
                    try {
                        //todo:等待别人释放内存,如果这儿的代码是等待wait操作
                        //假设其他线程归还了内存,那么这里的代码就可以被唤醒,代码就继续往下执行
                        waitingTimeElapsed = !moreMemory.await(remainingTimeToBlockNs, TimeUnit.NANOSECONDS);
                    } catch (InterruptedException e) {
                        this.waiters.remove(moreMemory);
                        throw e;
                    } finally {
                        long endWaitNs = time.nanoseconds();
                        timeNs = Math.max(0L, endWaitNs - startWaitNs);
                        this.waitTime.record(timeNs, time.milliseconds());
                    }

                    if (waitingTimeElapsed) {
                        this.waiters.remove(moreMemory);
                        throw new TimeoutException("Failed to allocate memory within the configured max blocking time " + maxTimeToBlockMs + " ms.");
                    }

                    remainingTimeToBlockNs -= timeNs;
                    // check if we can satisfy this request from the free list,
                    // otherwise allocate memory
                    //todo: 再次尝试看看内存池中是否有内存了,如果有,并且你申请的内存大小就是一个批次的大小
                    if (accumulated == 0 && size == this.poolableSize && !this.free.isEmpty()) {
                        // just grab a buffer from the free list
                        //todo: 这里就是可以直接获取到内存
                        buffer = this.free.pollFirst();
                        accumulated = size;
                    } else {
                        // we'll need to allocate memory, but we may only get
                        // part of what we need on this iteration
                        freeUp(size - accumulated);
                        //todo: 可以给你分配的内存
                        int got = (int) Math.min(size - accumulated, this.availableMemory);
                        //todo: 内存扣减
                        this.availableMemory -= got;
                        //todo: 统计已经分配的内存大小
                        accumulated += got;
                    }
                }

                // remove the condition for this thread to let the next thread
                // in line start getting memory
                Condition removed = this.waiters.removeFirst();
                if (removed != moreMemory)
                    throw new IllegalStateException("Wrong condition: this shouldn't happen.");

                // signal any additional waiters if there is more memory left
                // over for them
                if (this.availableMemory > 0 || !this.free.isEmpty()) {
                    if (!this.waiters.isEmpty())
                        this.waiters.peekFirst().signal();
                }

                // unlock and return the buffer
                lock.unlock();
                if (buffer == null)
                    return ByteBuffer.allocate(size);
                else
                    return buffer;
            }
        } finally {
            if (lock.isHeldByCurrentThread())
                lock.unlock();
        }
    }

free.deallocate释放内存方法分析

                /**
                 *  步骤五:再次尝试把数据写入到批次中
                 *  代码第一次执行到这里,依然还是失败的,
                 *  目前只是有内存了,目前还没有创建批次,还没有把内存分配给批次
                 */
                RecordAppendResult appendResult = tryAppend(timestamp, key, value, callback, dq);
                //appendResult 还是等于null
                if (appendResult != null) {
                    // Somebody else found us a batch, return the one we waited for! Hopefully this doesn't happen often...
                    //释放内存
                    free.deallocate(buffer);
                    return appendResult;
                }

    public void deallocate(ByteBuffer buffer, int size) {
        //加锁
        lock.lock();
        try {
            //todo:需要申请的内存等于一个批次的内存大小,就把内存归还给内存池
            //该判断可以有效的提高内存的利用率,等于批次大小的内存直接归还内存池利用率才是最高的
            if (size == this.poolableSize && size == buffer.capacity()) {
                //todo: 清空数据
                buffer.clear();
                //todo:内存放入到内存池中
                this.free.add(buffer);
            } else {
            //todo:否则不相等,就直接归还给可用的内存中
                this.availableMemory += size;
            }
            Condition moreMem = this.waiters.peekFirst();
            if (moreMem != null)
                //todo: 内存释放后,需要唤醒处于wait等待内存的线程----> !moreMemory.await(remainingTimeToBlockNs, TimeUnit.NANOSECONDS);
                moreMem.signal();
        } finally {
            lock.unlock();
        }
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值