Netty 內(nèi)存管理: PooledByteBufAllocator & PoolArena 代碼探險

我們當前的生產(chǎn)系統(tǒng)是典型的微服務(wù)架構(gòu),其中的關(guān)鍵部分API網(wǎng)關(guān) xharbor 自2014年初開始研發(fā)并在 github 上開源。 xharbor 中的網(wǎng)絡(luò)層基于 netty ,而架構(gòu)上重度使用 rxjava 定義模塊間的響應(yīng)式接口。xharbor 需要根據(jù)業(yè)務(wù)規(guī)則轉(zhuǎn)發(fā)客戶端的請求(request)到特定的后端服務(wù),在后端服務(wù)處理完成后再將響應(yīng)(response)發(fā)送回客戶端,而在轉(zhuǎn)發(fā)前后可能還需要進行請求/響應(yīng)的重寫。因此,有效的內(nèi)存使用對 xharbor 的性能、穩(wěn)定性和擴展性至關(guān)重要。在 xharbor 開發(fā)時,我們首先關(guān)注的是 netty 的內(nèi)存管理和泄漏檢測。

netty 架構(gòu)圖 —— 摘自 http://netty.io

在上面的 netty 架構(gòu)圖中,可以看到 "Zero-Copy-Capable Rich Byte Buffer"是其核心部分的堅固基石。而內(nèi)存管理又是這一技術(shù)的重點。netty 內(nèi)存管理的高性能主要依賴于兩個關(guān)鍵點:

  • 內(nèi)存的池化管理
  • 使用堆外直接內(nèi)存(Direct Memory)

堆外直接內(nèi)存的優(yōu)勢:Java 網(wǎng)絡(luò)程序中使用堆外直接內(nèi)存進行內(nèi)容發(fā)送(Socket讀寫操作),可以避免了字節(jié)緩沖區(qū)的二次拷貝;相反,如果使用傳統(tǒng)的堆內(nèi)存(Heap Memory,其實就是byte[])進行Socket讀寫,JVM會將堆內(nèi)存Buffer拷貝一份到堆外直接內(nèi)存中,然后才寫入Socket中。這樣,相比于堆外直接內(nèi)存,消息在發(fā)送過程中多了一次緩沖區(qū)的內(nèi)存拷貝。

而池化管理帶來的性能提升參見下圖,引用自Why Netty (by Norman Maurer at Netflix)

https://blog.twitter.com/2013/netty-4-at-twitter-reduced-gc-overhead

如上圖圖例所展示的, netty 基于兩個維度:池化/非池化、Heap Memory/Direct Memory 的組合來確定最終使用的內(nèi)存管理策略。對 netty 應(yīng)用首先要能確定netty 到底采用了哪種內(nèi)存管理策略,才能對各種情況下的性能表現(xiàn)有預(yù)期。根據(jù) ByteBufUtil 代碼:

    String allocType = SystemPropertyUtil.get(
            "io.netty.allocator.type", 
            PlatformDependent.isAndroid() ? "unpooled" : "pooled");
    allocType = allocType.toLowerCase(Locale.US).trim();

    ByteBufAllocator alloc;
    if ("unpooled".equals(allocType)) {
        alloc = UnpooledByteBufAllocator.DEFAULT;
        logger.debug("-Dio.netty.allocator.type: {}", allocType);
    } else if ("pooled".equals(allocType)) {
        alloc = PooledByteBufAllocator.DEFAULT;
        logger.debug("-Dio.netty.allocator.type: {}", allocType);
    } else {
        alloc = PooledByteBufAllocator.DEFAULT;
        logger.debug("-Dio.netty.allocator.type: pooled (unknown: {})", allocType);
    }

需要在 netty 應(yīng)用啟動時,設(shè)置 JVM參數(shù) -Dio.netty.allocator.type=pooled 設(shè)置池化管理策略,而根據(jù) PlatformDependent 中的代碼片段:

private static final boolean DIRECT_BUFFER_PREFERRED =
        HAS_UNSAFE && !SystemPropertyUtil.getBoolean(
        "io.netty.noPreferDirect", false);

只要沒有設(shè)置 -Dio.netty.noPreferDirect=true 并且運行在標準 Oracle JVM(sun.misc.Unsafe存在)中,就會優(yōu)先使用 Direct Memory,當然還有一個前提是分配了一定數(shù)量的Direct Memory,本著省著過日子的想法,一開始 xharbor 中設(shè)定了64M的Direct Memory大小,-XX:MaxDirectMemorySize=64M,此時和 netty 相關(guān)的 JVM 啟動參數(shù)為:

    -XX:MaxDirectMemorySize=64M
    -Dio.netty.allocator.type=pooled

運行 xharbor ,通過特定日志輸出觀察用于Socket讀寫的 ByteBuf 實例,如下截圖所示:

xharbor 運行日志截圖

WTF! 不看不知道,一看嚇一跳,怎么會是 Unpooled 類型的ByteBuf。反復(fù)檢查了幾次啟動參數(shù),確認無誤。好吧,Talk is cheap,Show me the code代碼是檢驗一切的標準。找到 PooledByteBufAllocator.newDirectBuffer,摘錄如下:

protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
    PoolThreadCache cache = threadCache.get();
    PoolArena<ByteBuffer> directArena = cache.directArena;

    ByteBuf buf;
    if (directArena != null) {
        buf = directArena.allocate(cache, initialCapacity, maxCapacity);
    } else {
        if (PlatformDependent.hasUnsafe()) {
            buf = UnsafeByteBufUtil.newUnsafeDirectByteBuf(this, initialCapacity, maxCapacity);
        } else {
            buf = new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
        }
    }

    return toLeakAwareBuffer(buf);
}

在上面的代碼邏輯中,當 directArena 為空時,會直接產(chǎn)生 Unpooled 類型的ByteBuf。難道是 directArena 為空導(dǎo)致的?netty 啟動時會詳細輸出各項配置,翻找之下,果然有所發(fā)現(xiàn):

netty 啟動時的日志輸出

DirectMemory 的 Arena 數(shù)量為0,難怪 directArena 為空。繼續(xù)在 PooledByteBufAllocator 的代碼中查找原因,尋獲相關(guān)代碼片段如下:

    final int defaultMinNumArena = runtime.availableProcessors() * 2;
    final int defaultChunkSize = DEFAULT_PAGE_SIZE << DEFAULT_MAX_ORDER;
    ......
    DEFAULT_NUM_DIRECT_ARENA = Math.max(0,
            SystemPropertyUtil.getInt(
                    "io.netty.allocator.numDirectArenas",
                    (int) Math.min(
                            defaultMinNumArena,
                            PlatformDependent.maxDirectMemory() 
                            / defaultChunkSize / 2 / 3)));

常量 DEFAULT_PAGE_SIZEDEFAULT_MAX_ORDER 在沒有特別設(shè)置的情況下,缺省值分別為 8192 和 11,因此, defaultChunkSize 的缺省大小是 8192 << 11 = 16M。根據(jù)上面的代碼,PlatformDependent.maxDirectMemory() 得大于等于 16M * 2 * 3 = 96M 才能使 DEFAULT_NUM_DIRECT_ARENA =1。因此,調(diào)整 xharbor 的JVM 啟動參數(shù)為:

    -XX:MaxDirectMemorySize=96M
    -Dio.netty.allocator.type=pooled

netty 啟動時的日志輸出(2)

從 xharbor 啟動日志中的 netty 初始化信息看到,總算有了一個 DirectMemory Arena 。再次通過 xharbor 日志輸出觀察用于Socket讀寫的 ByteBuf 實例,這次總算是 Pooled 類型的 DirectByteBuf。
xharbor 運行日志截圖(2)

通過上面的代碼探險,xharbor 總算有了一個不錯的開始,我們通過設(shè)置適當?shù)?DirectMemory 大小(>=96M)和內(nèi)存管理策略(io.netty.allocator.type=pooled)使得 xharbor 用上了池化的堆外直接內(nèi)存。但在一個高并發(fā)、重負載系統(tǒng)中,一旦出現(xiàn)內(nèi)存泄漏,往往就意味著系統(tǒng)崩潰這樣的致命問題,具體到 netty 中的ByteBuf,由于它使用了引用計數(shù)方式管理生命周期,使得問題排查更為復(fù)雜。那么:

  • 如何才能及時無誤的查看 xharbor 中是否存在泄漏?
  • **netty 中有什么的便利的設(shè)施供我們使用嗎? **

讓我們把問題留到本系列的下一篇吧!


本系列:

  1. Netty 內(nèi)存管理探險: PoolArena 分配之謎
  2. Netty 內(nèi)存管理探險: PoolArena 統(tǒng)計之BUG和解決

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容