? ? 注:本文依賴于kafka-0.10.0.1-src
? ? 我們都知道kafka生產者send一條記錄(record)后并沒有直接發(fā)送到kafka服務端,而是先將它保存到內存(RecordAccumulator)中,用于壓縮之后批量發(fā)送,這里內存的創(chuàng)建和釋放是比較消耗資源的,為了實現(xiàn)內存的高效利用,基本上每個成熟的框架或者工具都有一套內存管理機制,kafka的生產者使用BufferPool來實現(xiàn)內存(Java NIO的ByteBuffer)的復用。
? ? BufferPool是什么呢?如圖1:
? ? 圖1包含兩個部分,紅色和綠色的總和代表BufferPool的總量,用totalMemory表示(由buffer.memory配置);綠色代表可使用的空間,它又包括兩個部分:上半部分代表未申請未使用的部分,用availableMemory表示;下半部分代表已經申請但沒有使用的部分,用一個ByteBuffer隊列(Deque<ByteBuffer>)表示,我們稱這個隊列為free,隊列中的ByteBuffer的大小用poolableSize表示(由batch.size配置)。
? ? 下圖2總結了從BufferPool中分配固定size大小的內存的步驟:
? ? 從圖2可以看出申請size大小的內存有這么幾種結束方式(紅色框部分),1、異常結束,比如申請的內存過大超過總量限定2、直接用隊列中的ByteBuffer分配內存;3、用avaliableMemory分配內存。
? ? 藍色框內的為大多數的內存分配方式,就是從隊列中直接拿想要的ByteBuffer,也是kafka希望的分配方式;黃色的框為分配內存時隊列中的內存不符合其分配的條件(隊列為空或大小不匹配),從availableMemory中分配;綠色框為當前內存池中內存不足時阻塞等待的情況,具體就是有一個累加器accumulated,如果累加器沒有累加到size大小,說明還沒有足夠的內存釋放出來,所以就會阻塞等待內存釋放,內存釋放之后會喚醒阻塞的線程,將可以分配的內存大小累加到累加器accumulated上,這樣直到累加器accumulated大小滿足size,就直接分配。這里面還有一個原則就是如果還沒給累加器accumulated累加過一次的話,也就是accumulated==0的時候,那么會優(yōu)先嘗試從隊列中獲取內存(有可能釋放的內存釋放到隊列中)。
? ? 釋放內存的話就比較簡單了,如果釋放的大小等于poolableSize的話,就把它放入free隊列,否則釋放到availableMemory中(availableMemory+=size)。所以只有固定大小的內存塊被釋放后才會進入池化列表,非常規(guī)釋放后只會增加可用內存大小。
? ? BufferPool是線程安全的,用一個ReentrantLock來保證,并且用一個Deque<Condition> waiters隊列來記錄申請不到足夠空間而阻塞的線程,此隊列中實際記錄的是阻塞線程對應的Condition對象,如圖3所示,將阻塞線程對應的Condition加入隊列,等待喚醒,喚醒的順序根據入隊順序決定(先進先出)。
? ? 總結:可以看到BufferPool只針對特定大小(poolableSize)的ByteBuffer進行管理,對于其它大小的并不會緩存進來。因此如果超大消息比較多(大于poolableSize),就不會很好的利用內存池,頻繁的申請回收內存效率會降低,并可能帶來Full GC或者Out Of Memory Error,這個時候就要調整好batch.size的配置了。