Netty作为一款高性能网络应用程序框架,实现了一套高性能内存管理机制

通过学习其中的实现原理、算法、并发设计,有利于我们写出更优雅、更高性能的代码;当使用Netty时碰到内存方面的问题时,也可以更高效定位排查出来

本文基于Netty4.1.43.Final介绍其中的内存管理机制

ByteBuf分类

Netty使用ByteBuf对象作为数据容器,进行I/O读写操作,Netty的内存管理也是围绕着ByteBuf对象高效地分配和释放

当讨论ByteBuf对象管理,主要从以下方面进行分类:

  • Pooled 和 Unpooled

Unpooled,非池化内存每次分配时直接调用系统 API 向操作系统申请ByteBuf需要的同样大小内存,用完后通过系统调用进行释放

Pooled,池化内存分配时基于预分配的一整块大内存,取其中的部分封装成ByteBuf提供使用,用完后回收到内存池中

tips: Netty4默认使用Pooled的方式,可通过参数-Dio.netty.allocator.type=unpooled或pooled进行设置

  • Heap 和 Direct

    Heap,指ByteBuf关联的内存JVM堆内分配,分配的内存受GC 管理

    Direct,指ByteBuf关联的内存在JVM堆外分配,分配的内存不受GC管理,需要通过系统调用实现申请和释放,底层基于Java NIO的DirectByteBuffer对象

note: 使用堆外内存的优势在于,Java进行I/O操作时,需要传入数据所在缓冲区起始地址和长度,由于GC的存在,对象在堆中的位置往往会发生移动,导致对象地址变化,系统调用出错。为避免这种情况,当基于堆内存进行I/O系统调用时,需要将内存拷贝到堆外,而直接基于堆外内存进行I/O操作的话,可以节省该拷贝成本

池化(Pooled)对象管理

非池化对象(Unpooled),使用和释放对象仅需要调用底层接口实现,池化对象实现则复杂得多,可以带着以下问题进行研究:

  • 内存池管理算法是如何实现高效内存分配释放,减少内存碎片
  • 高负载下内存池不断申请/释放,如何实现弹性伸缩
  • 内存池作为全局数据,在多线程环境下如何减少锁竞争

1 算法设计

1.1 整体原理

Netty先向系统申请一整块连续内存,称为chunk,默认大小chunkSize = 16Mb,通过PoolChunk对象包装。为了更细粒度的管理,Netty将chunk进一步拆分为page,默认每个chunk包含2048个page(pageSize = 8Kb)

不同大小池化内存对象的分配策略不同,下面首先介绍申请内存大小在(pageSize/2, chunkSize]区间范围内的池化对象的分配原理,其他大对象和小对象的分配原理后面再介绍。在同一个chunk中,Netty将page按照不同粒度进行多层分组管理:

  • 第1层,分组大小size = 1*pageSize,一共有2048个组
  • 第2层,分组大小size = 2*pageSize,一共有1024个组
  • 第3层,分组大小size = 4*pageSize,一共有512个组

    ...

当请求分配内存时,将请求分配的内存数向上取值到最接近的分组大小,在该分组大小的相应层级中从左至右寻找空闲分组

例如请求分配内存对象为1.5 *pageSize,向上取值到分组大小2 * pageSize,在该层分组中找到完全空闲的一组内存进行分配,如下图:

当分组大小2 * pageSize的内存分配出去后,为了方便下次内存分配,分组被标记为全部已使用(图中红色标记),向上更粗粒度的内存分组被标记为部分已使用(图中黄色标记)

1.2 算法结构

Netty基于平衡树实现上面提到的不同粒度的多层分组管理

当需要创建一个给定大小的ByteBuf,算法需要在PoolChunk中大小为chunkSize的内存中,找到第一个能够容纳申请分配内存的位置

为了方便快速查找chunk中能容纳请求内存的位置,算法构建一个基于byte数组(memoryMap)存储的完全平衡树,该平衡树的多个层级深度,就是前面介绍的按照不同粒度对chunk进行多层分组:

树的深度depth从0开始计算,各层节点数,每个节点对应的内存大小如下:

depth = 0, 1 node,nodeSize = chunkSize
depth = 1, 2 nodes,nodeSize = chunkSize/2
...
depth = d, 2^d nodes, nodeSize = chunkSize/(2^d)
...
depth = maxOrder, 2^maxOrder nodes, nodeSize = chunkSize/2^{maxOrder} = pageSize

树的最大深度为maxOrder(最大阶,默认值11),通过这棵树,算法在chunk中的查找就可以转换为:

当申请分配大小为chunkSize/2^k的内存,在平衡树高度为k的层级中,从左到右搜索第一个空闲节点

数组的使用域从index = 1开始,将平衡树按照层次顺序依次存储在数组中,depth = n的第1个节点保存在memoryMap[2^n] 中,第2个节点保存在memoryMap[2^n+1]中,以此类推(下图代表已分配chunkSize/2)

可以根据memoryMap[id]的值得出节点的使用情况,memoryMap[id]值越大,剩余的可用内存越少

  • memoryMap[id] = depth_of_id:id节点空闲, 初始状态,depth_of_id的值代表id节点在树中的深度
  • memoryMap[id] = maxOrder + 1:id节点全部已使用,节点内存已完全分配,没有一个子节点空闲
  • depth_of_id < memoryMap[id] < maxOrder + 1:id节点部分已使用,memoryMap[id] 的值 x,代表id的子节点中,第一个空闲节点位于深度x,在深度[depth_of_id, x)的范围内没有任何空闲节点

1.3 申请/释放内存

当申请分配内存,会首先将请求分配的内存大小归一化(向上取值),通过PoolArena#normalizeCapacity()方法,取最近的2的幂的值​,例如8000byte归一化为8192byte( chunkSize/2^11 ),8193byte归一化为16384byte(chunkSize/2^10)

处理内存申请的算法在PoolChunk#allocateRun方法中,当分配已归一化处理后大小为chunkSize/2^d的内存,即需要在depth = d的层级中找到第一块空闲内存,算法从根节点开始遍历 (根节点depth = 0, id = 1),具体步骤如下:

  • 步骤1 判断是否当前节点值memoryMap[id] > d,或 depth_of_id > d

    如果是,则无法从该chunk分配内存,查找结束

  • 步骤2 判断是否节点值memoryMap[id] == d,且depth_of_id <= d

    如果是,当前节点是depth = d的空闲内存,查找结束,更新当前节点值为memoryMap[id] = max_order + 1,代表节点已使用,并遍历当前节点的所有祖先节点,更新节点值为各自的左右子节点值的最小值;如果否,执行步骤3

  • 步骤3 判断是否当前节点值memoryMap[id] <= d,且depth_of_id <= d

    如果是,则空闲节点在当前节点的子节点中,则先判断左子节点memoryMap[2 * id] <=d(判断左子节点是否可分配),如果成立,则当前节点更新为左子节点,否则更新为右子节点,然后重复步骤1,2

参考示例如下图,申请分配了chunkSize/2的内存

note:图中虽然index = 2的子节点memoryMap[id] = depth_of_id,但实际上节点内存已分配,因为算法是从上往下开始遍历,所以在实际处理中,节点分配内存后仅更新祖先节点的值,并没有更新子节点的值

释放内存时,根据申请内存返回的id,将 memoryMap[id]更新为depth_of_id,同时设置id节点的祖先节点值为各自左右节点的最小值

1.4 巨型对象内存管理

对于申请分配大小超过chunkSize的巨型对象(huge),Netty采用的是非池化管理策略,在每次请求分配内存时单独创建特殊的非池化PoolChunk对象进行管理,内部memoryMap为null,当对象内存释放时整个Chunk内存释放,相应内存申请逻辑在PoolArena#allocateHuge()方法中,释放逻辑在PoolArena#destroyChunk()方法中

1.5 小对象内存管理

当请求对象的大小reqCapacity <= 496,归一化计算后方式是向上取最近的16的倍数,例如15规整为16、40规整为48、490规整为496,规整后的大小(normalizedCapacity)小于pageSize的小对象可分为2类:

微型对象(tiny):规整后为16的整倍数,如16、32、48、...、496,一共31种规格

小型对象(small):规整后为2的幂的,有512、1024、2048、4096,一共4种规格

这些小对象直接分配一个page会造成浪费,在page中进行平衡树的标记又额外消耗更多空间,因此Netty的实现是:先PoolChunk中申请空闲page,同一个page分为相同大小规格的小内存进行存储

这些page用PoolSubpage对象进行封装,PoolSubpage内部有记录内存规格大小(elemSize)、可用内存数量(numAvail)和各个小内存的使用情况,通过long[]类型的bitmap相应bit值0或1,来记录内存是否已使用

note:应该有读者注意到,Netty申请池化内存进行归一化处理后的值更大了,例如1025byte会归一化为2048byte,8193byte归一化为16384byte,这样是不是造成了一些浪费?可以理解为是一种取舍,通过归一化处理,使池化内存分配大小规格化,大大方便内存申请和内存、内存复用,提高效率

2 弹性伸缩

前面的算法原理部分介绍了Netty如何实现内存块的申请和释放,单个chunk比较容量有限,如何管理多个chunk,构建成能够弹性伸缩内存池?

2.1 PoolChunk管理

为了解决单个PoolChunk容量有限的问题,Netty将多个PoolChunk组成链表一起管理,然后用PoolChunkList对象持有链表的head

将所有PoolChunk组成一个链表的话,进行遍历查找管理效率较低,因此Netty设计了PoolArena对象(arena中文是舞台、场所),实现对多个PoolChunkList、PoolSubpage的管理,线程安全控制、对外提供内存分配、释放的服务

PoolArena内部持有6个PoolChunkList,各个PoolChunkList持有的PoolChunk的使用率区间不同:

// 容纳使用率 (0,25%) 的PoolChunk
private final PoolChunkList<T> qInit;
// [1%,50%)
private final PoolChunkList<T> q000;
// [25%, 75%)
private final PoolChunkList<T> q025;
// [50%, 100%)
private final PoolChunkList<T> q050;
// [75%, 100%)
private final PoolChunkList<T> q075;
// 100%
private final PoolChunkList<T> q100;

6个PoolChunkList对象组成双向链表,当PoolChunk内存分配、释放,导致使用率变化,需要判断PoolChunk是否超过所在PoolChunkList的限定使用率范围,如果超出了,需要沿着6个PoolChunkList的双向链表找到新的合适PoolChunkList,成为新的head;同样的,当新建PoolChunk并分配完内存,该PoolChunk也需要按照上面逻辑放入合适的PoolChunkList中

分配归一化内存normCapacity(大小范围在[pageSize, chunkSize]) 具体处理如下:

  • 按顺序依次访问q050、q025、q000、qInit、q075,遍历PoolChunkList内PoolChunk链表判断是否有PoolChunk能分配内存
  • 如果上面5个PoolChunkList有任意一个PoolChunk内存分配成功,PoolChunk使用率发生变更,重新检查并放入合适的PoolChunkList中,结束
  • 否则新建一个PoolChunk,分配内存,放入合适的PoolChunkList中(PoolChunkList扩容)

note:可以看到分配内存依次优先在q050 -> q025 -> q000 -> qInit -> q075的PoolChunkList的内分配,这样做的好处是,使分配后各个区间内存使用率更多处于[75,100)的区间范围内,提高PoolChunk内存使用率的同时也兼顾效率,减少在PoolChunkList中PoolChunk的遍历

当PoolChunk内存释放,同样PoolChunk使用率发生变更,重新检查并放入合适的PoolChunkList中,如果释放后PoolChunk内存使用率为0,则从PoolChunkList中移除,释放掉这部分空间,避免在高峰的时候申请过内存一直缓存在池中(PoolChunkList缩容)

PoolChunkList的额定使用率区间存在交叉,这样设计是因为如果基于一个临界值的话,当PoolChunk内存申请释放后的内存使用率在临界值上下徘徊的话,会导致在PoolChunkList链表前后来回移动

2.2 PoolSubpage管理

PoolArena内部持有2个PoolSubpage数组,分别存储tiny和small规格类型的PoolSubpage:

// 数组长度32,实际使用域从index = 1开始,对应31种tiny规格PoolSubpage
private final PoolSubpage<T>[] tinySubpagePools;
// 数组长度4,对应4种small规格PoolSubpage
private final PoolSubpage<T>[] smallSubpagePools;

相同规格大小(elemSize)的PoolSubpage组成链表,不同规格的PoolSubpage链表的head则分别保存在tinySubpagePools 或者 smallSubpagePools数组中,如下图:

当需要分配小内存对象到PoolSubpage中时,根据归一化后的大小,计算出需要访问的PoolSubpage链表在tinySubpagePools和smallSubpagePools数组的下标,访问链表中的PoolSubpage的申请内存分配,如果访问到的PoolSubpage链表节点数为0,则创建新的PoolSubpage分配内存然后加入链表

PoolSubpage链表存储的PoolSubpage都是已分配部分内存,当内存全部分配完或者内存全部释放完的PoolSubpage会移出链表,减少不必要的链表节点;当PoolSubpage内存全部分配完后再释放部分内存,会重新将加入链表

PoolArean内存池弹性伸缩可用下图总结:

3 并发设计

内存分配释放不可避免地会遇到多线程并发场景,无论是PoolChunk的平衡树标记或者PoolSubpage的bitmap标记都是多线程不安全,如何在线程安全的前提下尽量提升并发性能?

首先,为了减少线程间的竞争,Netty会提前创建多个PoolArena(默认生成数量 = 2 * CPU核心数),当线程首次请求池化内存分配,会找被最少线程持有的PoolArena,并保存线程局部变量PoolThreadCache中,实现线程与PoolArena的关联绑定(PoolThreadLocalCache#initialValue()方法)

note:Java自带的ThreadLocal实现线程局部变量的原理是:基于Thread的ThreadLocalMap类型成员变量,该变量中map的key为ThreadLocal,value-为需要自定义的线程局部变量值。调用ThreadLocal#get()方法时,会通过Thread.currentThread()获取当前线程访问Thread的ThreadLocalMap中的值

Netty设计了ThreadLocal的更高性能替代类:FastThreadLocal,需要配套继承Thread的类FastThreadLocalThread一起使用,基本原理是将原来Thead的基于ThreadLocalMap存储局部变量,扩展为能更快速访问的数组进行存储(Object[] indexedVariables),每个FastThreadLocal内部维护了一个全局原子自增的int类型的数组index

此外,Netty还设计了缓存机制提升并发性能:当请求对象内存释放,PoolArena并没有马上释放,而是先尝试将该内存关联的PoolChunk和chunk中的偏移位置(handler变量)等信息存入PoolThreadLocalCache中的固定大小缓存队列中(如果缓存队列满了则马上释放内存);

当请求内存分配,PoolArena会优先访问PoolThreadLocalCache的缓存队列中是否有缓存内存可用,如果有,则直接分配,提高分配效率

总结

Netty池化内存管理的设计借鉴了Facebook的jemalloc,同时也与Linux内存分配算法Buddy算法和Slab算法也有相似之处,很多分布式系统、框架的设计都可以在操作系统的设计中找到原型,学习底层原理是很有价值的

下一篇,介绍Netty堆外内存泄漏问题的排查

参考

《scalable memory allocation using jemalloc —— Facebook》

https://engineering.fb.com/core-data/scalable-memory-allocation-using-jemalloc/

《Netty入门与实战:仿写微信 IM 即时通讯系统》

https://juejin.im/book/5b4bc28bf265da0f60130116?referrer=598ff735f265da3e1c0f9643

更多精彩,欢迎关注公众号 分布式系统架构

支撑百万级并发,Netty如何实现高性能内存管理的更多相关文章

  1. 总结:如何使用redis缓存加索引处理数据库百万级并发

    前言:事先说明:在实际应用中这种做法设计需要各位读者自己设计,本文只提供一种思想.准备工作:安装后本地数redis服务器,使用mysql数据库,事先插入1000万条数据,可以参考我之前的文章插入数据, ...

  2. 使用redis缓存加索引处理数据库百万级并发

    使用redis缓存加索引处理数据库百万级并发 前言:事先说明:在实际应用中这种做法设计需要各位读者自己设计,本文只提供一种思想.准备工作:安装后本地数redis服务器,使用mysql数据库,事先插入1 ...

  3. 基于openstack搭建百万级并发负载均衡器的解决方案

    最近,喜欢研究一些国外技术大咖们的文章,而这篇文章是基于openstack负载均衡器的解决方案,做的一些总结~希望能够给小伙伴带来一些灵感或者帮助. openstack现有的负载均衡解决方案,无论是l ...

  4. dns 服务架构优化 - 百万级并发不是梦 - bind+namedmanager+dnsmasq

    bind: DNS服务端. namedmanager: DNS web管理页面. dnsmasq: 并发查询上游dns域名解析. 问题:作为消息推送业务,单台业务机器域名解析并发达到上万次.业务机器集 ...

  5. Netty(RPC高性能之道)原理剖析

    转载:http://blog.csdn.net/zhiguozhu/article/details/50517551 1,Netty简述 Netty 是一个基于 JAVA NIO 类库的异步通信框架, ...

  6. Netty系列之Netty百万级推送服务设计要点

    1. 背景 1.1. 话题来源 最近很多从事移动互联网和物联网开发的同学给我发邮件或者微博私信我,咨询推送服务相关的问题.问题五花八门,在帮助大家答疑解惑的过程中,我也对问题进行了总结,大概可以归纳为 ...

  7. Netty_Netty系列之Netty百万级推送服务设计要点

    1. 背景 1.1. 话题来源 最近很多从事移动互联网和物联网开发的同学给我发邮件或者微博私信我,咨询推送服务相关的问题.问题五花八门,在帮助大家答疑解惑的过程中,我也对问题进行了总结,大概可以归纳为 ...

  8. 【netty】Netty系列之Netty百万级推送服务设计要点

    1. 背景 1.1. 话题来源 最近很多从事移动互联网和物联网开发的同学给我发邮件或者微博私信我,咨询推送服务相关的问题.问题五花八门,在帮助大家答疑解惑的过程中,我也对问题进行了总结,大概可以归纳为 ...

  9. Netty系列之Netty百万级推送服务设计要点(转)

    1. 背景 1.1. 话题来源 最近很多从事移动互联网和物联网开发的同学给我发邮件或者微博私信我,咨询推送服务相关的问题.问题五花八门,在帮助大家答疑解惑的过程中,我也对问题进行了总结,大概可以归纳为 ...

随机推荐

  1. 高可用Kubernetes集群原理介绍

    ■ 文/ 天云软件 云平台开发工程师 张伟 1. 背景 Kubernetes作为容器应用的管理中心,对集群内部所有容器的生命周期进行管理,结合自身的健康检查及错误恢复机制,实现了集群内部应用层的高可用 ...

  2. poj 1655 Balancing Act 求树的重心【树形dp】

    poj 1655 Balancing Act 题意:求树的重心且编号数最小 一棵树的重心是指一个结点u,去掉它后剩下的子树结点数最少. (图片来源: PatrickZhou 感谢博主) 看上面的图就好 ...

  3. 【Flask源码分析——请求上下文与应用上下文】

    Flask中有两种上下文,请求上下文和应用上下文.两者的作用域都处于一个请求的局部中. 查看源代码,上下文类在flask.ctx模块中定义 AppContext类定义应用上下文,app是当前应用Web ...

  4. phpexecl

    <?phpnamespace Admin\Controller;use Think\Controller;class InoutController extends Controller { p ...

  5. ArcGIS中的连接和关联表

    大多数数据库的设计指导方针都倾向于将数据库组织成多个表 - 每个表关注一个特定的主题 - 而非一个包含所有必要字段的大型表.设置多个表可以避免数据库中的信息发生重复,因为只会将信息在一个表中存储一次. ...

  6. python的if循环和嵌套

    1.    if 条件:    if语句块   执行流程:判断条件是否为真. 如果真. 执行if语句块 money = int(input('请输入你兜里的钱:')) if money >500 ...

  7. HZOJ 走格子

    作者的正解: 对于100%的数据:行动可以分为两种: 1. 步行,花费一个单位的时间移动到4联通的相邻格子中去. 2. 使用传送门,指定一个方向的墙的前面的一个格子,步行至最近的一个墙的面前,使用传送 ...

  8. HZOJ 那一天我们许下约定

    比较好想的一道题,只是那个组合数比较恶心. 先说一下我最开始想的$n^4$的沙雕dp: 设f[i][j][k]为前i天给了j个,第i天给了k个,则f[i][j][k]=∑f[i-1][j-k][o]; ...

  9. hdu 2410 Barbara Bennett's Wild Numbers

    Problem - 2410 挺好玩的一道题目.这道题的意思是给出一个模糊值以及一个确定值,要求求出模糊值中大于确定值的个数有多少. 这题我是直接用dfs的方法搜索的,对于每一位如果之前位置的形成的数 ...

  10. SuperSocket 中内置的 Flash/Silverlight 策略服务器

    关键字: 策略服务器, Flash策略服务器, Silverlight策略服务器, Policy Server, Flash Policy Server, Silverlight Policy Ser ...