Netty 内存管理探险: PoolArena 分配之谜

在本系列的上一篇《Netty 内存管理: PooledByteBufAllocator & PoolArena 代码探险》中,我们最终通过设置合适的 JVM 启动参数 —— DirectMemorySize最小应设置为chunkSize(16M)的6倍

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

这样 API网关 xharbor 启动后,得以使用 netty 的最佳内存管理策略:池化的堆外直接内存(Pooled DirectByteBuf)。古语云:知己知彼 百战不殆。在享受高效内存管理机制的大餐前,我们得先来看看,能否做到对内存的使用做到知根知底,也就是回答上篇文章留下的两个问题:

如何才能及时无误的查看 xharbor 中是否存在泄漏?
netty 中有什么的便利的设施供我们使用吗?

Netty 中内置的池化内存管理指标

代码是检验一切的标准。让我们再回到 netty 那优美而深邃的代码中去探险吧...... 只是因为在人群中多看了你一眼...... PoolArena 的定义,就有了大发现,原来 PoolArena 实现了一个名为 PoolArenaMetric 的接口,让我们把 PoolArenaMetric 的定义拿出来晒晒吧:

public interface PoolArenaMetric {
/** 返回所有通过该 arena 完成的内存分配动作数量,包括所有尺寸(tiny/small/normal/huge)
 * Return the number of allocations done via the arena. This includes all sizes.
 */
long numAllocations();

/** 返回通过该 arena 完成的 tiny 尺寸的内存分配动作数量
 * Return the number of tiny allocations done via the arena.
 */
long numTinyAllocations();

/** 返回通过该 arena 完成的 small 尺寸的内存分配动作数量
 * Return the number of small allocations done via the arena.
 */
long numSmallAllocations();

/** 返回通过该 arena 完成的 normal 尺寸的内存分配动作数量
 * Return the number of normal allocations done via the arena.
 */
long numNormalAllocations();

/** 返回通过该 arena 完成的 huge 尺寸的内存分配动作数量
 * Return the number of huge allocations done via the arena.
 */
long numHugeAllocations();

/** 返回所有通过该 arena 完成的内存释放动作数量,包括所有尺寸(tiny/small/normal/huge)
 * Return the number of deallocations done via the arena. This includes all sizes.
 */
long numDeallocations();

/** 返回通过该 arena 完成的 tiny 尺寸的内存释放动作数量
 * Return the number of tiny deallocations done via the arena.
 */
long numTinyDeallocations();

/** 返回通过该 arena 完成的 small 尺寸的内存释放动作数量
 * Return the number of small deallocations done via the arena.
 */
long numSmallDeallocations();

/** 返回通过该 arena 完成的 normal 尺寸的内存释放动作数量
 * Return the number of normal deallocations done via the arena.
 */
long numNormalDeallocations();

/** 返回通过该 arena 完成的 huge 尺寸的内存释放动作数量
 * Return the number of huge deallocations done via the arena.
 */
long numHugeDeallocations();

/** 返回该 arena 当前所有尺寸的活跃分配数量(已经分配但尚未释放)
 * Return the number of currently active allocations.
 */
long numActiveAllocations();

/** 返回该 arena 当前 tiny 尺寸的活跃分配数量
 * Return the number of currently active tiny allocations.
 */
long numActiveTinyAllocations();

/** 返回该 arena 当前 small 尺寸的活跃分配数量
 * Return the number of currently active small allocations.
 */
long numActiveSmallAllocations();

/** 返回该 arena 当前 normal 尺寸的活跃分配数量
 * Return the number of currently active normal allocations.
 */
long numActiveNormalAllocations();

/** 返回该 arena 当前 huge 尺寸的活跃分配数量
 * Return the number of currently active huge allocations.
 */
long numActiveHugeAllocations();

/** 当前从操作系统申请的有效内存总数量
 * Return the number of active bytes that are currently allocated by the arena.
 */
long numActiveBytes();

/** 返回持有该 arena 作为缓存的线程个数
 * Returns the number of thread caches backed by this arena.
 */
int numThreadCaches();

/** tiny类型页面个数
 * Returns the number of tiny sub-pages for the arena.
 */
int numTinySubpages();

/** small类型页面个数
 * Returns the number of small sub-pages for the arena.
 */
int numSmallSubpages();

/** chunk个数
 * Returns the number of chunk lists for the arena.
 */
int numChunkLists();

/** 返回所有 tiny 页面的链表
 * Returns an unmodifiable {@link List} which holds {@link PoolSubpageMetric}s for tiny sub-pages.
 */
List<PoolSubpageMetric> tinySubpages();

/** 返回所有 small 页面的链表
 * Returns an unmodifiable {@link List} which holds {@link PoolSubpageMetric}s for small sub-pages.
 */
List<PoolSubpageMetric> smallSubpages();

/** 返回所有chunk的链表
 * Returns an unmodifiable {@link List} which holds {@link PoolChunkListMetric}s.
 */
List<PoolChunkListMetric> chunkLists();
}

上述接口方法重排了顺序,并根据内部实现进行中文翻译

原来在 PoolArena 中存在着巨大的宝库,它已经内置了详细的内存管理指标。我们暂时先把注意力集中到前面这15个内存分配/释放/活跃的指标上来,怀着激动的心情,我们再次分三组列出这些 可爱的探针:

# numXXXAllocations
numAllocations:          已完成的内存分配动作数量,包括所有尺寸(tiny/small/normal/huge)
numTinyAllocations:      已完成的 tiny 尺寸的内存分配动作数量
numSmallAllocations:     已完成的 small 尺寸的内存分配动作数量
numNormalAllocations:    已完成的 normal 尺寸的内存分配动作数量
numHugeAllocations:      已完成的 huge 尺寸的内存分配动作数量

# numXXXDeallocations
numDeallocations:        已完成的内存释放动作数量,包括所有尺寸(tiny/small/normal/huge)
numTinyDeallocations:    已完成的 tiny 尺寸的内存释放动作数量
numSmallDeallocations:   已完成的 small 尺寸的内存释放动作数量
numNormalDeallocations:  已完成的 normal 尺寸的内存释放动作数量
numHugeDeallocations:    已完成的 huge 尺寸的内存释放动作数量

# numActiveXXXAllocations
numActiveAllocations:       当前所有尺寸的活跃分配数量(已经分配但尚未释放)
numActiveTinyAllocations:   当前 tiny 尺寸的活跃分配数量
numActiveSmallAllocations:  当前 small 尺寸的活跃分配数量
numActiveNormalAllocations: 当前 normal 尺寸的活跃分配数量
numActiveHugeAllocations:   当前 huge 尺寸的活跃分配数量

如果上面中文翻译的意思理解无误的话,我们来推测下这三组15个指标的特性:

  • numXXXAllocations 和 numXXXDeallocations 均为只增指标,随着分配和释放行为的进行而增加,数值不断增加
  • 如果没有内存泄漏,经过一段时间,系统的业务都处理完毕且没有新的业务产生,numXXXAllocations 应该和名称对应的 numXXXDeallocations 数值严格相等
  • numActiveXXXAllocations 是当前时刻,对应的 numXXXAllocations 与 numXXXDeallocations 的差值

备注:关于 PoolArena 的四种内存分配尺寸,请参见 Netty内存池原理分析,此处不再赘述原因,只列出这四种内存分配类型的对应尺寸范围(bytes):

内存分配类型 最小字节数(含) 最大字节数(含)
tiny 1 511
small 512 8191(pageSize-1)
normal 8192(pageSize) 16777216(chunkSize)
huge 16777217(chunkSize+1) 无上限

展现 Netty 的内存管理指标

有了这些探针,接下来该把它们都拉出来干活了。Java系统中,如果说到展现 JVM 内信息,首推 JMX

备注:JMX在Java编程语言中定义了应用程序以及网络管理和监控的体系结构、设计模式、应用程序接口以及服务。通常使用JMX来监控系统的运行状态或管理系统的某些方面。

Netty 中没有将这些指标导出为MBean,那么该程序员上了。在 xharbor 的依赖库 jocean-http 中,我添加了一个 POJO 类:PooledAllocatorStats 来达到这一目的,并通过 Spring 将其导出为 MBean: pooledstats,摘录部分代码如下:

public class PooledAllocatorStats {
public Map<String, Object> getMetrics() {
    final PooledByteBufAllocator allocator = PooledByteBufAllocator.DEFAULT;
    final Map<String, Object> metrics = new HashMap<>();
    {
        int idx = 0;
        for (PoolArenaMetric poolArenaMetric : allocator.directArenas()) {
            metrics.put("1_DirectArena[" + idx++ + "]", 
                        metricsOfPoolArena(poolArenaMetric));
        }
    }
    {
        int idx = 0;
        for (PoolArenaMetric poolArenaMetric : allocator.heapArenas()) {
            metrics.put("2_HeapArena[" + idx++ + "]", 
                        metricsOfPoolArena(poolArenaMetric));
        }
    }
    return metrics;
}

private static Map<String, Object> metricsOfPoolArena(
        final PoolArenaMetric poolArenaMetric) {
    final Map<String, Object> metrics = new HashMap<>();
    metrics.put("2_0_numAllocations", 
                poolArenaMetric.numAllocations());
    metrics.put("2_1_numTinyAllocations", 
                poolArenaMetric.numTinyAllocations());
    ......
}

PooledAllocatorStats.java代码片段

<bean id="pooledstats" 
      class="org.jocean.http.util.PooledAllocatorStats"/>
<bean class="org.springframework.jmx.export.MBeanExporter"
      lazy-init="false">
    <property name="beans">
        <map>
            <entry key="org.jocean:name=pooledstats" 
                   value-ref="pooledstats"/>
        </map>
    </property>
</bean>

Spring配置文件片段

简言之,其实现思路是获取 PooledByteBufAllocator 的全局实例 DEFAULT, 将heap类型和direct类型的 PoolArena 各项指标通过Map方式输出。
如何连接并展现 JVM 上的JMX,有多种成熟的实现:从 JDK 自带的 JConsoleJava Mission Control 到大型的商业工具。而我们的生产系统解决方案是在后端系统中整合 Jolokia 将 JMX 暴露为 REST API,并通过一个集中式的WEB服务:xbeacon 进行访问和展现。

关于 xbeaconJolokia 的整合将是另一个故事,将单开系列进行介绍,敬请期待......

PoolArena 分配之谜

有了趁手的工具,把这一切组装起来,上测试环境跑跑。在运行了一段时间,怀着忐忑的心情,打开 xbeacon 的MBean展示界面,定睛一看:

xharbor 的Direct PoolArena 分配指标(1) by xbeacon

当时的界面截图如上,WTF!,为啥是这样的,有一路跟着开动脑筋的读者也应该明白了吧?竟然还有 202 个活跃分配存在,居然一个 tiny 和 small 类型的内存释放都没有,normal 类型的内存分配倒是有对应的释放,但也才一半左右的分配释放掉了。有机灵的读者或许会提示: 是不是业务没有全部完成啊? BUT,通过另外的业务指标,我确认以及肯定,当前的API网关转发业务已经全部完成,而且没有的新的业务导入(阿里云SLB 中已经将这个 xharbor 实例的服务器移除了BTW,我们在重度使用阿里云,不过这是另外一个话题了)。
难道 xharbor 的代码中对于 ByteBuf 的使用一直有泄漏?并且漏的一塌糊涂,tiny 漏,small 漏,连大于 8K 的 normal 都源源不断的在漏?基于现网环境已经很少出现ByteBuf LEAK 提示了:

netty 应用的生产系统如果不断出现这样的提示:
ERROR |-i.n.u.ResourceLeakDetector:171 - LEAK: ByteBuf.release() was not called before it's garbage-collected. See http://netty.io/wiki/reference-counted-objects.html for more information.
稍有责任心的程序员都会战战惶惶 汗出如浆的......

抑制住内心的惶恐和急于翻查代码的冲动,再一次从 netty 的代码中寻找线索。终于接连从这几处地方找到提示:

tiny cache 分配

if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {
    // was able to allocate out of the cache so move on
    return;
}

small cache 分配

if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) {
    // was able to allocate out of the cache so move on
    return;
}

normal cache 分配

if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {
    // was able to allocate out of the cache so move on
    return;
}

而上述代码片段中的 cache 其实就是上一篇中已经看到的线程局部缓存,让我们再来回顾一下:

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);
}

在 Netty 主要贡献者:Norman Maurer 的一篇slide"Why Netty"我们也找到了线程局部缓存的说明:

好吧,假定上述的释放和分配的巨大差距是由于线程局部缓存造成的,摆在我们面前的有两个解决方法:

  1. 将所有线程中的有效局部缓存也纳入统计
  2. 完全禁用线程局部缓存这一机制

机智如我们程序员,会选择哪种方式嘞?对我相信绝大部分读者此时会像我一样...... 选择...... 禁用线程局部缓存统计所有线程中局部缓存,引入的复杂性太高,性价比完全不合适么,再说,搞清楚了不存在泄漏,还可以再次打开线程局部缓存呀
OK,再次上路。从 PooledByteBufAllocator 中找到如何禁用线程局部缓存的设置,参见相关代码如下:

DEFAULT_TINY_CACHE_SIZE = 
    SystemPropertyUtil.getInt(
    "io.netty.allocator.tinyCacheSize", 512);
DEFAULT_SMALL_CACHE_SIZE =
    SystemPropertyUtil.getInt(
    "io.netty.allocator.smallCacheSize", 256);
DEFAULT_NORMAL_CACHE_SIZE = 
    SystemPropertyUtil.getInt(
    "io.netty.allocator.normalCacheSize", 64);

将这三个属性都设置为0,即可禁用对应的线程局部缓存,因此连同上一篇的启动参数在内,此时我们将 xharbor 的 JVM 启动参数扩展为如下这5项:

-XX:MaxDirectMemorySize=96M
-Dio.netty.allocator.type=pooled
-Dio.netty.allocator.tinyCacheSize=0
-Dio.netty.allocator.smallCacheSize=0
-Dio.netty.allocator.normalCacheSize=0

加完上述的启动参数,再次将 xharbor 点火... 启动... 导入业务... 一段时间后... 关闭业务导入... 查看 xbeacon 上的指标:

xharbor 的Direct PoolArena 分配指标(2) by xbeacon

一个直观的感受是在大致相同的业务压力下,禁用线程布局缓存后,内存的分配多了好几倍,所以,缓存的确极大的减少了实际内存分配行为。另外,不错哦,numAllocations 和 numDeallocations 总算相同了,均为 2404。但隐隐总觉得哪里还有些...不对?!


xharbor 的Direct PoolArena 分配指标(2) 着色版 by xbeacon

如上图,我们用颜色把不太对劲的地方标注出来。似乎还有问题啊?

小结

本篇中,我们通过代码探险找到了 Netty 中内置的内存分配指标:** PoolArenaMetric**,并基于JMX将其可视的展现出来。接下来从指标的异常,找到了第一个坑:线程局部缓存,为了消除这一影响,我们将上一篇总结的 xharbor 启动参数从2个扩充为5个:

-XX:MaxDirectMemorySize=96M
-Dio.netty.allocator.type=pooled
-Dio.netty.allocator.tinyCacheSize=0
-Dio.netty.allocator.smallCacheSize=0
-Dio.netty.allocator.normalCacheSize=0

这下内存管理指标看起来已经合理多了,但其中还是夹带着些许小误差:
**为啥 **

   (tiny deallocation - tiny allocation) 
+  (small deallocation - small allocation) 
== (normal allocation - normal deallocation) 

让我们把问题留到本系列的下一篇吧!


本系列:

  1. Netty 内存管理: PooledByteBufAllocator & PoolArena 代码探险
  2. Netty 内存管理探险: PoolArena 统计之BUG和解决

参考:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容