Netty 内存管理: PooledByteBufAllocator & PoolArena 代码探险

我们当前的生产系统是典型的微服务架构,其中的关键部分API网关 xharbor 自2014年初开始研发并在 github 上开源。 xharbor 中的网络层基于 netty ,而架构上重度使用 rxjava 定义模块间的响应式接口。xharbor 需要根据业务规则转发客户端的请求(request)到特定的后端服务,在后端服务处理完成后再将响应(response)发送回客户端,而在转发前后可能还需要进行请求/响应的重写。因此,有效的内存使用对 xharbor 的性能、稳定性和扩展性至关重要。在 xharbor 开发时,我们首先关注的是 netty 的内存管理和泄漏检测。

netty 架构图 —— 摘自 http://netty.io

在上面的 netty 架构图中,可以看到 "Zero-Copy-Capable Rich Byte Buffer"是其核心部分的坚固基石。而内存管理又是这一技术的重点。netty 内存管理的高性能主要依赖于两个关键点:

  • 内存的池化管理
  • 使用堆外直接内存(Direct Memory)

堆外直接内存的优势:Java 网络程序中使用堆外直接内存进行内容发送(Socket读写操作),可以避免了字节缓冲区的二次拷贝;相反,如果使用传统的堆内存(Heap Memory,其实就是byte[])进行Socket读写,JVM会将堆内存Buffer拷贝一份到堆外直接内存中,然后才写入Socket中。这样,相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。

而池化管理带来的性能提升参见下图,引用自Why Netty (by Norman Maurer at Netflix)

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

如上图图例所展示的, netty 基于两个维度:池化/非池化、Heap Memory/Direct Memory 的组合来确定最终使用的内存管理策略。对 netty 应用首先要能确定netty 到底采用了哪种内存管理策略,才能对各种情况下的性能表现有预期。根据 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 应用启动时,设置 JVM参数 -Dio.netty.allocator.type=pooled 设置池化管理策略,而根据 PlatformDependent 中的代码片段:

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

只要没有设置 -Dio.netty.noPreferDirect=true 并且运行在标准 Oracle JVM(sun.misc.Unsafe存在)中,就会优先使用 Direct Memory,当然还有一个前提是分配了一定数量的Direct Memory,本着省着过日子的想法,一开始 xharbor 中设定了64M的Direct Memory大小,-XX:MaxDirectMemorySize=64M,此时和 netty 相关的 JVM 启动参数为:

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

运行 xharbor ,通过特定日志输出观察用于Socket读写的 ByteBuf 实例,如下截图所示:

xharbor 运行日志截图

WTF! 不看不知道,一看吓一跳,怎么会是 Unpooled 类型的ByteBuf。反复检查了几次启动参数,确认无误。好吧,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 为空时,会直接产生 Unpooled 类型的ByteBuf。难道是 directArena 为空导致的?netty 启动时会详细输出各项配置,翻找之下,果然有所发现:

netty 启动时的日志输出

DirectMemory 的 Arena 数量为0,难怪 directArena 为空。继续在 PooledByteBufAllocator 的代码中查找原因,寻获相关代码片段如下:

    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 在没有特别设置的情况下,缺省值分别为 8192 和 11,因此, defaultChunkSize 的缺省大小是 8192 << 11 = 16M。根据上面的代码,PlatformDependent.maxDirectMemory() 得大于等于 16M * 2 * 3 = 96M 才能使 DEFAULT_NUM_DIRECT_ARENA =1。因此,调整 xharbor 的JVM 启动参数为:

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

netty 启动时的日志输出(2)

从 xharbor 启动日志中的 netty 初始化信息看到,总算有了一个 DirectMemory Arena 。再次通过 xharbor 日志输出观察用于Socket读写的 ByteBuf 实例,这次总算是 Pooled 类型的 DirectByteBuf。
xharbor 运行日志截图(2)

通过上面的代码探险,xharbor 总算有了一个不错的开始,我们通过设置适当的 DirectMemory 大小(>=96M)和内存管理策略(io.netty.allocator.type=pooled)使得 xharbor 用上了池化的堆外直接内存。但在一个高并发、重负载系统中,一旦出现内存泄漏,往往就意味着系统崩溃这样的致命问题,具体到 netty 中的ByteBuf,由于它使用了引用计数方式管理生命周期,使得问题排查更为复杂。那么:

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

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


本系列:

  1. Netty 内存管理探险: PoolArena 分配之谜
  2. Netty 内存管理探险: PoolArena 统计之BUG和解决

参考

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

推荐阅读更多精彩内容