圖解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)
    • 隊列

如果文章有幫助到您,請點個贊,您的反饋會讓我感到文章是有價值的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章