图解Kafka-KafkaProcuder-BufferPool

主要属性

private final long totalMemory;//最大缓存空间 , 由配置文件指定
private final int poolableSize;//每个池的缓存空间大小
private final ReentrantLock lock; //重入锁
private final Deque<ByteBuffer> free; //空闲的ByteBuffer
private final Deque<Condition> waiters; //等待分配空间的线程
/** Total available memory is the sum of nonPooledAvailableMemory and the number of byte buffers in free * poolableSize.  */
private long nonPooledAvailableMemory; //ByteBuffer之外的缓冲区,设计为了适应突然的大数据量
//构造方法
public BufferPool(long memory, int poolableSize, Metrics metrics, Time time, String metricGrpName) {
    this.poolableSize = poolableSize; //指定的 poolableSize
    this.lock = new ReentrantLock();//初始化 ReentrantLock 锁
    this.free = new ArrayDeque<>(); //初始化一个 空(empty)的Array队列,存储内存
    this.waiters = new ArrayDeque<>(); //初始化一个空(empty)的array队列,存储等待线程
    this.totalMemory = memory;//总的内存
    this.nonPooledAvailableMemory = memory;//默认的池外内存,就是总的内存
    //下面是一些数据统计,不做分析
    this.metrics = metrics;
    this.time = time;
    this.waitTime = this.metrics.sensor(WAIT_TIME_SENSOR_NAME);
    MetricName rateMetricName = metrics.metricName("bufferpool-wait-ratio",
                                                metricGrpName,
                                                "The fraction of time an appender waits for space allocation.");
    MetricName totalMetricName = metrics.metricName("bufferpool-wait-time-total",
                                                metricGrpName,
                                                "The total time an appender waits for space allocation.");
    this.waitTime.add(new Meter(TimeUnit.NANOSECONDS, rateMetricName, totalMetricName));
}

allocate 方法

org.apache.kafka.clients.producer.internals.BufferPool#allocate

 /**
    * Allocate a buffer of the given size. This method blocks if there is not enough memory and the buffer pool
    * is configured with blocking mode.
    分配指定空间的缓存, 如果缓冲区中没有足够的空闲空间,那么会阻塞线程,
    直到超时或得到足够空间
    *
    * @param size The buffer size to allocate in bytes ,要获取的指定大小空间
    * @param maxTimeToBlockMs The maximum time in milliseconds to block for buffer memory to be available , 最大等待时长
    * @return The buffer
    * @throws InterruptedException If the thread is interrupted while blocked
    * @throws IllegalArgumentException if size is larger than the total memory controlled by the pool (and hence we would block
    *         forever)
    */
public ByteBuffer allocate(int size, long maxTimeToBlockMs) throws InterruptedException {
    //大于总缓冲区空间,抛出异常
    if (size > this.totalMemory) 
        throw new IllegalArgumentException("Attempt to allocate " + size
                                            + " bytes, but there is a hard limit of "
                                            + this.totalMemory
                                            + " on memory allocations.");

    ByteBuffer buffer = null;
    //会有线程争抢,所以需要锁
    this.lock.lock();
    try {
        // check if we have a free buffer of the right size pooled
        // 如果有空间大小正合适的空闲buffer, 走到获取并返回
        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
        // 判断是否有足够的空闲的内存
        int freeListSize = freeSize() * this.poolableSize;
        if (this.nonPooledAvailableMemory + freeListSize >= size) {
            // we have enough unallocated or pooled memory to immediately
            // 有足够的,未分配的空闲内存
            // satisfy the request, but need to allocate the buffer
            // 需要整理到一个buffer外空间中,从JVM Heap 中分配内存
            freeUp(size); // 循环释放 空闲的 buffer
            this.nonPooledAvailableMemory -= size;
        } else {
            // we are out of memory and will have to block
            // 没有足够空闲的 内存或 buffer
            int accumulated = 0; //累计已经释放的内存
            //阻塞自己,等待别的线程释放内存
            Condition moreMemory = this.lock.newCondition();
            try {
                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 {
                        waitingTimeElapsed = !moreMemory.await(remainingTimeToBlockNs, TimeUnit.NANOSECONDS);
                    } finally {
                        //还没到最大时长,被唤醒了。更新下已经等待的时长
                        long endWaitNs = time.nanoseconds();
                        timeNs = Math.max(0L, endWaitNs - startWaitNs);
                        recordWaitTime(timeNs);
                    }

                    if (waitingTimeElapsed) {
                        //等待超时了 , 不等了。抛出异常,结束
                        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
                    // 是否有释放的刚好足够的空间,否则的话,还得再调整空间
                    if (accumulated == 0 && size == this.poolableSize && !this.free.isEmpty()) {
                        // just grab a buffer from the free list
                        // 有,直接取一个byteBuffer ,返回 , 结束
                        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
                        // 没有足够空闲的,需要调整分配空间 , 如果分配多了,那么只需要得到 足够size的空间
                        // 例如: 需要 50 ,释放出来了 80 ,那么只取 其中的 50 。
                        freeUp(size - accumulated);
                        int got = (int) Math.min(size - accumulated, this.nonPooledAvailableMemory);
                        this.nonPooledAvailableMemory -= got;
                        accumulated += got;
                    }
                }
                // Don't reclaim memory on throwable since nothing was thrown
                accumulated = 0;
            } finally {
                // When this loop was not able to successfully terminate don't loose available memory
                // 在循环的过程中,有异常了。 那么已经释放出来的空间,再还回去。
                this.nonPooledAvailableMemory += accumulated;
                //把自己从等待队列中移除 , 并结束
                this.waiters.remove(moreMemory);
            }
        }
    } finally {
        // signal any additional waiters if there is more memory left
        // over for them
        // 后续处理 , 这里不管分配空间是成功还是失败,都会执行
        try {
            //三个条件
            // this.nonPooledAvailableMemory == 0 && this.free.isEmpty() : 池外内存为0 ,并且空闲的byteBuffer 没有了。 
            // 取反,就是 nonPooledAvailableMemory > 0 || this.free.isNotEmpty() : 池外有内存,或 有空闲的 ByteBuffer
            // !this.waiters.isEmpty() : 等待队列里有线程正在等待
            if (!(this.nonPooledAvailableMemory == 0 && this.free.isEmpty()) && !this.waiters.isEmpty())
                //唤醒队列里正在等待的线程
                this.waiters.peekFirst().signal();
        } finally {
            // Another finally... otherwise find bugs complains
            // 最后的最后,一定得解锁。否则就是BUG了
            lock.unlock();
        }
    }
    //到这里,说明空间足够,并且有足够空闲的了。可以执行真正的分配空间了。
    if (buffer == null)
        //没有正好的 buffer,从缓冲区外(JVM Heap)中直接分配内存
        return safeAllocateByteBuffer(size);
    else
        // 有正好的 buffer,返回buffer
        return buffer;
}


/**
    * Allocate a buffer.  If buffer allocation fails (e.g. because of OOM) then return the size count back to
    * available memory and signal the next waiter if it exists.
    */
private ByteBuffer safeAllocateByteBuffer(int size) {
    boolean error = true;
    try {
        //分配空间
        ByteBuffer buffer = allocateByteBuffer(size);
        error = false;
        //返回buffer
        return buffer;
    } finally {
        if (error) {
            //分配失败了, 加锁,操作内存pool
            this.lock.lock();
            try {
                //归还空间给 池外内存
                this.nonPooledAvailableMemory += size;
                if (!this.waiters.isEmpty())
                //有其他在等待的线程的话,唤醒其他线程
                    this.waiters.peekFirst().signal();
            } finally {
                // 加锁不忘解锁
                this.lock.unlock();
            }
        }
    }
}

// Protected for testing.
protected ByteBuffer allocateByteBuffer(int size) {
    // 从JVM Heap 中分配空间,并得到持有空间的ByteBuffer对象
    return ByteBuffer.allocate(size);
}

/**
    * Attempt to ensure we have at least the requested number of bytes of memory for allocation by deallocating pooled
    * buffers (if needed)
    */
private void freeUp(int size) {
    while (!this.free.isEmpty() && this.nonPooledAvailableMemory < size)
    //循环把 free 里的 byteBuffer 全捞出来,给 nonPooledAvailableMemory
        this.nonPooledAvailableMemory += this.free.pollLast().capacity();
}

deallocate

org.apache.kafka.clients.producer.internals.BufferPool#deallocate(ByteBuffer, int)

/**
* Return buffers to the pool. If they are of the poolable size add them to the free list, otherwise just mark the
* memory as free.
* 归还 buffer 到 pool 里,即 buffer放回到 free 队列中。
* 其他的直接标记为 空闲内存就可以了
* @param buffer The buffer to return
* @param size The size of the buffer to mark as deallocated, note that this may be smaller than buffer.capacity
*             since the buffer may re-allocate itself during in-place compression
*/
public void deallocate(ByteBuffer buffer, int size) {
    //照例先加锁
    lock.lock();
    try {
        if (size == this.poolableSize && size == buffer.capacity()) {
            //如果是完整的buffer,放回到队列里
            buffer.clear();
            this.free.add(buffer);
        } else {
            //不是完整的buffer,标记为空闲内存就可以了。
            this.nonPooledAvailableMemory += size;
        }
        //如果有内存的线程,唤醒线程
        Condition moreMem = this.waiters.peekFirst();
        if (moreMem != null)
            moreMem.signal();
    } finally {
        //解锁
        lock.unlock();
    }
}

主要逻辑:

  • 如果 size == poolableSize , 就放到 free 中
  • 如果 size != poolableSize , 归还到 nonPooledAvailableMemory 中. buffer 对象没有引用。等待GC释放
  • 有等待线程的话,唤醒线程

free 分析

free 的生产和归还

free 对象的使用有点绕,在初始化时,是一个空的Array队列。 allocate() 方法是从 free 中取 buffer 或 释放 buffer , deallocate() 是归还 buffer 到 free 中。

  • 当 free 空时,从 allocate() 中生产 buffer 对象
  • deallocate() 方法将 buffer 放到 free 中

free 为什么是双向队列

  • 获取 buffer 是从一头取
  • freeUp() 方法释放 buffer 是从另一头

理论上 allocate() 方法是单线程访问。怕是以防万一吧,一边获取一边释放。

free的最大化使用

// RecordAccumulator 的 this.batchSize == BufferPool.poolableSize
int size = Math.max(this.batchSize, AbstractRecords.estimateSizeInBytesUpperBound(maxUsableMagic, compression, key, value, headers));
buffer = bufferPool.allocate(size, maxTimeToBlock);

在传入的参数中,在 size 和 poolableSize 中 , 取最大值。

  • <= poolableSize的,可以直接使用一个ByteBuffer。
  • poolableSize 的,就需要开新的内存了。

所以,对于内存来说,poolableSize的大小设置很重要。尽可能的重复利用 缓存 byteBuffer

经验之谈的话,大概取 80% 左右的比例。最大有 100 的数据,那么poolableSize 设置为 80 。当然还要具体情况具体分析。

总结

  • 共享变量的使用:
    • Lock 锁
  • 先进先出(FIFO)
    • 队列

如果文章有帮助到您,请点个赞,您的反馈会让我感到文章是有价值的

posted @ 2020-09-07 20:32  孙行者、  阅读(603)  评论(0编辑  收藏  举报