Kafka如何通過經典的內存緩衝池設計來優化JVM GC問題?

公衆號後臺回覆“面試”,獲取精品學習資料

掃描下方海報瞭解專欄詳情

本文來源:朱小廝的博客

《Java工程師面試突擊(第3季)》重磅升級,由原來的70講增至160講,內容擴充一倍多,升級部分內容請參見文末

大家都知道Kafka是一個高吞吐的消息隊列,是大數據場景首選的消息隊列,這種場景就意味着發送單位時間消息的量會特別的大,那麼Kafka如何做到能支持能同時發送大量消息的呢?

答案是Kafka通過批量壓縮和發送做到的。

我們知道消息肯定是放在內存中的,大數據場景消息的不斷髮送,內存中不斷存在大量的消息,很容易引起GC

頻繁的GC特別是full gc是會造成“stop the world”,也就是其他線程停止工作等待垃圾回收線程執行,繼而進一步影響發送的速度影響吞吐量,那麼Kafka是如何做到優化JVM的GC問題的呢?看完本篇文章你會get到。

Kafka的內存池

下面介紹下Kafka客戶端發送的大致過程,如下圖:

Kafka的kafkaProducer對象是線程安全的,每個發送線程在發送消息時候共用一個kafkaProducer對象來調用發送方法,最後發送的數據根據Topic和分區的不同被組裝進某一個RecordBatch中。

發送的數據放入RecordBatch後會被髮送線程批量取出組裝成ProduceRequest對象發送給Kafka服務端。

可以看到發送數據線程和取數據線程都要跟內存中的RecordBatch打交道,RecordBatch是存儲數據的對象,那麼RecordBatch是怎麼分配的呢?

下面我們看下Kafka的緩衝池結構,如下圖所示:

名詞解釋:緩衝池:BufferPool(緩衝池)對象,整個KafkaProducer實例中只有一個BufferPool對象。內存池總大小,它是已使用空間和可使用空間的總和,用totalMemory表示(由buffer.memory配置,默認32M)。

可使用的空間:它包含包括兩個部分,綠色部分代表未申請未使用的部分,用availableMemory表示

黃色部分代表已經申請但沒有使用的部分,用一個ByteBuffer雙端隊列(Deque)表示,在BufferPool中這個隊列叫free,隊列中的每個ByteBuffer的大小用poolableSize表示(由batch.size配置,默認16k),因爲每次free申請內存都是以poolableSize爲單位申請的,申請poolableSize大小的bytebuffer後用RecordBatch來包裝起來。

已使用空間:代表緩衝池中已經裝了數據的部分。

根據以上介紹,我們可以知道,總的BufferPool大小=已使用空間+可使用空間;free的大小=free.size * poolableSize(poolsize就是單位batch的size)。

數據的分配過程 總的來說是判斷需要存儲的數據的大小是否free裏有合適的recordBatch裝得下

如果裝得下則用recordBatch來存儲數據,如果free裏沒有空間但是availableMemory+free的大小比需要存儲的數據大(也就是說可使用空間比實際需要申請的空間大),說明可使用空間大小足夠,則會用讓free一直釋放byteBuffer空間直到有空間裝得下要存儲的數據位置,如果需要申請的空間比實際可使用空間大,則內存申請會阻塞直到申請到足夠的內存爲止。

整個申請過程如下圖:

數據的釋放過程 總的來說有2個入口,釋放過程如下圖:

再來看段申請空間代碼:

//判斷需要申請空間大小,如果需要申請空間大小比batchSize小,那麼申請大小就是batchsize,如果比batchSize大,那麼大小以實際申請大小爲準
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());
//這個過程可以參考圖3
ByteBuffer buffer = free.allocate(size, maxTimeToBlock);

再來段回收的核心代碼:

public void deallocate(ByteBuffer buffer, int size) {
    lock.lock();
    try {
        //只有標準規格(bytebuffer空間大小和poolableSize大小一致的才放入free)
        if (size == this.poolableSize && size == buffer.capacity()) {
            //注意這裏的buffer是直接reset了,重新reset後可以重複利用,沒有gc問題
            buffer.clear();
            //添加進free循環利用
            this.free.add(buffer);
        } else {
            //規格不是poolableSize大小的那麼沒有進行重製,但是會把availableMemory增加,代表整個可用內存空間增加了,這個時候buffer的回收依賴jvm的gc
            this.availableMemory += size;
        }
        //喚醒排在前面的等待線程
        Condition moreMem = this.waiters.peekFirst();
        if (moreMem != null)
            moreMem.signal();
    } finally {
        lock.unlock();
    }
}

通過申請和釋放過程流程圖以及釋放空間代碼,我們可以得到一個結論

就是如果用戶申請的數據(發送的消息)大小都是在poolableSize(由batch.size配置,默認16k)以內,並且申請時候free裏有空間,那麼用戶申請的空間是可以循環利用的空間,可以減少gc,但是其他情況也可能存在直接用堆內存申請空間的情況,存在gc的情況。

如何儘量避免呢,如果批量消息裏面單個消息都是超過16k,可以考慮調整batchSize大小。

如果沒有使用緩衝池,那麼用戶發送的模型是下圖5,由於GC特別是Full GC的存在,如果大量發送,就可能會發生頻繁的垃圾回收,導致的工作線程的停頓,會對整個發送性能,吞吐量延遲等都有影響。

使用緩衝池後,整個使用過程可以縮略爲下圖:

總結

Kafka通過使用內存緩衝池的設計,讓整個發送過程中的存儲空間循環利用,有效減少JVM GC造成的影響,從而提高發送性能,提升吞吐量。

作者簡介:黃益明,來自滴滴出行kafka團隊,對kafka有一年多的研究實踐,負責滴滴內部雲平臺的架構設計和Kafka特性研發工作

END

《Java工程師面試突擊第三季》加餐部分大綱:(注:1-66講的大綱請掃描文末二維碼,在課程詳情頁獲取)

詳細的課程內容,大家可以掃描下方二維碼瞭解:

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