我們當前的生產(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)圖中,可以看到 "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)
如上圖圖例所展示的, 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 實例,如下截圖所示:
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):
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_SIZE
和 DEFAULT_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
從 xharbor 啟動日志中的 netty 初始化信息看到,總算有了一個 DirectMemory Arena 。再次通過 xharbor 日志輸出觀察用于Socket讀寫的 ByteBuf 實例,這次總算是 Pooled 類型的 DirectByteBuf。
通過上面的代碼探險,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è)施供我們使用嗎? **
讓我們把問題留到本系列的下一篇吧!
本系列:
參考