netty4 总结
话说,最近,要在内部推广 mainstay3 所以, 需要先深度分享下 netty
以便让大家能放心使用 mainstay3 with netty.
这里总结在自己blog里面, netty的版本为 4.1.xx。
EventloopGroup
对于开发者来说,主要关心紫色圈出部分。其余的都已经封装完毕
有几个关键点。
- Eventloopsize 建议设置为 2 的次方,dispatch 使用位移,更快。
- 侦听一个端口,只会绑定到 BossEventLoopGroup 中的一个 Eventloop,所以, BossEventLoopGroup 配置多个也无用。
EventLoop
只使用 tcp 和 异步阻塞的话主要关心以下2个 EventLoop
NioEventLoop - 基于java 原生nio
level-triggered (水平触发)
EpollEventLoop - native jni 直接调用 epoll, only work on linux
edge-triggered (边缘触发)更少的系统调用
C代码,更少GC,更少synchronized
暴露了更多的Socket配置参数
流程图
关键点
- 整个loop 干的事情就是
select -> processIO -> runAllTask
- 这是一个死循环
- 那么这个loop 如何自己优雅退出? noway,只能通过外部添加 CloseTask, 比如添加到 MpscQueue
- deadline 为 定时任务的触发时间,避免 select 阻塞, 让 定时任务不能及时执行。
- 在select 这一步 解决 epollbug
关于解决 epoll bug的原理是 应当 “阻塞”的 select 变得不再阻塞。
所以只需要统计下 select 次数就行了
部分关键代码:
1 | for(;;){ |
ChannelPipeline
读写链, 运行于 eventloop 之内, 内部是一个双向链表。
以下面的代码举例子。
1 | public void initChannel(SocketChannel ch) throws Exception { |
从上面可以看出几个点
- 对于
pipeline.fireChannelRead
这种fire
开头的API 的请求从 HeadContext 开始找 链的next Handler 且继承了 ChannelInboundHandler 的 Handler - 对于
pipeline.write
(还有bind
,connect
,flush
这种非fire
开头) 的请求相反从 TailContext 开始找 链的 perv Handler 且继承了 ChannelOutboundHandler 的 Handler - 对于内部直接
ctx.xxx
自然不是从 tail / head 开始, 路径更短,更快
ByteBufPool
netty 的 ByteBufPool 是 jemelloc 的精简版,大体流程是类似的。都有一个 threadlocal 的 cache, 以及公有的 Arena。
从cache 取内存无锁,从 Arena取内存有锁。所以一般建议将 Arena 的数量设置为 EventLoop 数量,以达到无竞争。
关系图如下:
关于 参数的设定参看 io.netty.buffer.PooledByteBufAllocator
这个类.
如果先不管 Arena 是什么的话,Cache 是如何缓存的?
- 给每个 申请的内存块的大小 都分配一个Queue (一样大小的都recycle 都放回一个 Queue)
- 如果给每个大小都定义个Queue 反而浪费内存了,所以 可申请的内存块大小是指定规格的。
- 对申请的内存根据预定义的规格向上取整
- 所有的规格都放在一个数组里面,寻找起来 O1 的速度。
根据默认参数的话规格如下:
1 | tiny: [16 32 48 64 80 ... 496] |
举个例子: 当申请的内存为 10个byte 则向上取整 16 , 分配16byte, 取 tiny[0]
里取内存即可
Arena 为分配器,是预申请内存块的管理者。 jemelloc 的初衷为了减少内存碎片,那么如何减少?
当申请比如 1 byte 的话就会预申请一大块内存,然后分配其中一部分给 1byte。
为什么是分配 一部分? 不然 内存管理花费的内存开销将大于内存实际的开销。。
Arena 是一个分配器,内部管理的内存块 为 Chunk
, 申请大于 Chunk
大小的内存将直接向系统申请。
Chunk
内部由 n 个 page
组成, page
是一个虚拟的概念,因为 page
还可以切分为 n 个 SubPage
他们的关系图如下:
ps: 左侧为默认参数
这里说一下Arena 需要做的事情
首先, 一个 Arena 用 ChunkList 管理着 n 个 Chunk, 一个Chunk 默认 16mb, 当一个 Chunk 分配不了时,新建一个新的 Chunk
Chunk & ChunkList
Arena 内部还对 Chunk 做了 使用量的 监测。
Arena 新建了 6个 ChunkList 分别为 qInit
, q000
, q025
, q050
, q075
, q100
她们依次组成一个双向链表,分别又一个 最小使用量 和 最大使用量 的阀值
1 | // PoolChunkList<T> nextList, int minUsage, int maxUsage, int chunkSize |
然后在每次 allocate 和 deallocate 时判断 Chunk 是否触及 阀值,然后分别 上移/下移 到合适的 ChunkList
为了减小内存碎片, 分配的优先级如下:
q050 > q025 > q000 > qInit > q075
代码如下
1 | private synchronized void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) { |
为什么这么弄?
首先, 可以分配的内存时这样的, 回到之前的规格。 对于 8192x2^11
byte 相当于 100% 的 chunk 了。
而 比他小的分别为 50%, 25%, 12.5%, 6.25%, 3.125% …
所以如果将优先级 的第一第二换个位置则可能造成 大量的 75% 。。。
Page & SubPage
Chunk 由 2048 个 Page 组成。
一个 Page 默认 8k。
Chunk 里面将这 2048 个 page 用其 2 倍的空间 byte[4096] memoryMap
以完全二叉树管理。
深度为 11 的共 2048 个元素表示该 page 有没有被分配
深度 1-10 的节点都是索引
每个节点的 value 默认为该节点在树的深度。
当 page 被分配掉时, 则将自己的 value + 1 也就是变成12
每次被分配后都要同时更新自己的 parent 的 value, parent 的 value 取 2 个 child 中小的一个。
1 | private void updateParentsAlloc(int id) { |
比如当要分配 8k 的大小时直接找 第 11 层找 第一个 val 为 11 的(unused), 找到后更新自己以及parent节点。
这样当比如需要分配 8MB 内存是,立即找 depth 为 1 的2个节点, 只要她的 value 不等于 depth 就知道该节点的 child 被分配过了。
然后 Subpage 的作用是用来标示分配小于一个 page 的内存块。
但是 Subpage 不再是用二叉树管理了。而是用更方便的方式。
一个 Subpage 只能分配一种 size 大小, 然后用 3个long 共 8x8x8
个bit 标示是否被使用。
比如 如果 page 为 8k , 由于共 512 个标示可用,所以最小分配单元 为 16 byte.
但是 如果 最小分配单元为 32 byte, 则 共只需要 256 个标示即可。(3个 long 不需要全部用完)
Subpage 也是一个链表, 相同规格的subpage 链在一起。
然后 PoolArena 初始化了 n 个size 的 Subpage 的链表头, 并用来维护。
1 | private final PoolSubpage<T>[] tinySubpagePools; |
每次新创建 Subpage 会吧 subpage 链到 相应的 idx 的 PoolSubpage 内。
1 | private long allocateSubpage(int normCapacity) { |
图如下
内存开销
最后算一下开销。
如果 一个 netty server 配置了 1 个 acceptor Thread,32个 IO Thread, 我们这里仅仅考虑默认配置。
为了让 arena 不被锁影响,则需要33 个 arena, 那么就会需要 33个 chunk, 共计 33 * 16mb 共计 528MB (默认配置)
由于 ByteBufPool 默认是使用堆外内存的, 而 jdk 默认堆外内存 64 mb, 所以需要手动修改。
不然 netty 会使用 堆内存。 不经计算,贸然使用可能会造成full gc。
内存分配需要根据实际情况自己计算参数:
举一个例子
算一台服务 2k qps的服务,每个包 2k 的话 预计需要
1 | In [13]: 2000 * 2000 / 1024/1024.0 |
ps 这是没有算上 请求包的,就算 * 2 8mb 因该够了。
可以将 pageSize 调为一半,节省内存 io.netty.allocator.pageSize = 4096
于是一个 chunk 8mb 可以少分配一半的内存。
reference
http://www.jianshu.com/p/c4bd37a3555b
http://www.jianshu.com/p/d91060311437
http://www.jianshu.com/p/a1debfe4ff02
http://www.jianshu.com/p/4856bd30dd56
http://calvin1978.blogcn.com/articles/netty-performance.html
http://calvin1978.blogcn.com/articles/netty-performance2.html