当前位置:首页 » 硬盘大全 » netty缓存池大小
扩展阅读
webinf下怎么引入js 2023-08-31 21:54:13
堡垒机怎么打开web 2023-08-31 21:54:11

netty缓存池大小

发布时间: 2023-05-27 01:08:36

1. Netty理论四:TCP vs UDP

从数据结构上可以看出来,TCP比UDP要复杂的多。

我们上面说tcp是面向连接的,这是啥意思呢,简单的说,tcp要发送数据,首先得先建立连接,而udp不需要,直接发送数据就行了。

TCP是全双工的,即客户端在给服务器端发送信息的同时,服务器端也可以给客户端发送信息。
而半双工的意思是A可以给B发,B也可以给A发,但是A在给B发的时候,B不能给A发,即不同时,为半双工。

为什么采用三次握手?而不是二次?
是为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误;比如有以下场景,当客户端发出第一个
连接请求1并没有丢失,而是在某些网络节点长时间滞留了,这时客户端会认为超时,会再次发送连接请求2,服务端在接收到连接请求2以后建立了正常的连接,这时候失效请求1又到达了服务端,服务端会误以为是客户端又发出的一个连接请求,于是会再次建立连接,假定不采用三次握手,那服务端发出确认,新的连接就建立了。但是由于客户端并没有发出新的建立连接的请求,因此客户端不会再新的连接上发送数据,而服务端却以为新的连接已经建立了,在一直等待客户端的数据,由此会导致服务端许多资源的浪费。采用了三次握手后,可以防止这个现象发生,在客户端收到服务端对于自己的无效连接的应答后,并不会向服务端发出确认,服务端由于收不到确认,就可以认为此次连接是无效的。

第二次袭搭挥手完成后,客户端到服务端的连接已经释放,B不会再接收数据,A也不会再发送数据。这个时候只是客户端不再发送数据,但是 B 可能还有未发送完的数据,所以需要等待服务端也主动关闭。

为什么是四次?
关闭连接时,当Server收到FIN报文时,很可能并不会立即关闭socket,因为Server端可能还有消息未发出,所有其只能先回复一个ACK报文,告诉Client端,你的关闭请求我收到了;当Server把锋唤所有的报文都发送完以后,Server才能给Client端发送FIN报文关闭连接,Client收到后应答ASK。所以需要四次握手

在四次握手后,Server端先进入TIME_WAIT状态,然后过2MS(最大报文生存时间)才能进入CLOSE状态。为啥?
因为网络是不可靠的,客户端在第四次握手的ACK可能会丢失,所以TIME_WAIT状态就是用来重发可能丢失的ACK报文

TCP是顺序性,是通过协议中的序号来保证,每个包都有一个序号ID,
在建立连接的时候会商定起始 序号ID 是什么,然后按照 序号ID 一个个发送。

tcp是通过应答/确认/ACK 以及 重传机制来保证消息可靠传输的。

即在消息包发送后,要进行确认,当然,这个确认不是一个一个来的,而是会确认某个之前的 ID,表示都收到了,这种模式成为累计应答或累计确认。
确认是通过报文头里面的确认序号来保证的。

为了记录所有发送的包和接收的包,TCP 需要在发送端和接收端分别来缓存这些记录,发送端的缓存里是按照包的 ID 一个个排列,根据处理的情况分成四个部分

流量控制指的是发送端不能无限的往接收端发送数据(UDP就可以),为啥呢?

因为在 TCP 里,接收端在发送 ACK 的时候会带上接收端缓冲区的窗口大小,叫 Advertised window,超过这个窗口,银禅凯接收端就接收不过来了,发送端就不能发送数据了。这个窗口大概等于上面的第二部分加上第三部分,即 发送未确认 + 未发送可发送。

流量控制是点对点通信量的控制,是一个端到端的问题,主要就是抑制发送端发送数据的速率,以便接收端来得及接收。

tcp接收端缓冲区的大小是可以调试的,见 Netty高级功能(五):IoT百万长连接性能调优

TCP通过一个定时器(timer)采样了RTT并计算RTO,但是,**如果网络上的延时突然增加,那么,TCP对这个事做出的应对只有重传数据,然而重传会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这就导致了恶性循环,最终形成“网络风暴” —— TCP的拥塞控制机制就是用于应对这种情况。 **
拥塞控制的问题,也是通过窗口的大小来控制的,即为了在发送端调节所要发送的数据量,定义了一个“拥塞窗口”(Congestion Window),在发送数据时,将拥塞窗口的大小与接收端ack的窗口大小做比较,取较小者作为发送数据量的上限。

所以拥塞控制是防止过多的数据注入到网络中,可以使网络中的路由器或链路不致过载,是一个全局性的过程。

TCP 拥塞控制主要来避免两种现象,包丢失和超时重传,一旦出现了这些现象说明发送的太快了,要慢一点。

具体的方法就是发送端慢启动,比如倒水,刚开始倒的很慢,渐渐变快。然后设置一个阈值,当超过这个值的时候就要慢下来

通过 TCP 连接传输的数据无差错,不丢失,不重复,且按顺序到达。

第一层:物理层
第二层:数据链路层 802.2、802.3ATM、HDLC、FRAME RELAY
第三层:网络层 IP 、IPX、ARP、APPLETALK、ICMP
第四层:传输层 TCP、UDP 、SPX
第五层:会话层 RPC、SQL 、NFS 、X WINDOWS、ASP
第六层:表示层 ASCLL、PICT、TIFF、JPEG、 MIDI、MPEG
第七层:应用层 HTTP,FTP ,SNMP等

2. netty内存池

1:PooledByteBufAllocator    内存池入口,应用通过该类从内存池中申请内存

PoolThreadCache:线程缓存池

Recycler:上文的Recycler是个对象池,储存的是相应类型的堆中对象的集合。

2:PooledByteBufAllocator获取bytebuf步骤:

      1)如果是pool类型,先在线程对象池中获取一个相应类型的poolBuffer对象,这个对象是在堆中的,也是返回的对象,然后会使用PoolThreadCache这个线程内存池,获取一块合适大小的内存。接下来根据这块内存的信息对这个poolBuffer对象进行init,比如设置内存的address,length等,用户就可以通过这个buffer对象对相应的内存进行操作。

      2)注意区别线程对象池和线程内存池。线程对象池指同一类型的对象集合,用户也只能通过对象来进行操作,比如读写数据。而线程内存池是许多内存块的集合,用户通过对象读写数据还是要定位到实际的内存地址(虚拟内存地址)。内存池就通过把对象和一块内存绑定,对象的读写操作都会反应到这块内存上。

3)内存是有限的,为了最大化内存的利用率以及提高内存分配回收的效率,netty实现了类似jemalloc内存分配的方式给对象分配内存。

4)获取一个buffer对象-->给buffer对象属性赋值(赋的值就是内存的地址、大小)

5)  比如现在需要分配一个UnpooledHeapByteBuf类型的ByteBuf对象,其初始大小为20,最大容量为100,因为是unpooled,意味着这各类型的ByteBuf是没有对象池的,需要的时候直接new一个即可

                          (1)new            UnpooledHeapByteBuf(this,initialCapacity,maxCapacity);

单纯的一个没有经过初始化(成员变量没有赋值)的ByteBuf是不能进行读写的,因为其读写方法都需要确切的读写内存地址的。对于UnpooledHeapByteBuf类型的ByteBuf,其是一个HeapByteBuf,Heap意味着这个ByteBuf的读写操作是在JVM堆上进行的,其读写内存地址需要在jvm对上进行分配,所以在初始化时要根据需要的大小创建一个Byte类型的数组Byte[]对UnpooledDirectByteBuf进行初始化,接下来对这个Buf的读写都会反应在这个字节数组中。

6)对于UnpooledDirectByteBuf类型的ByteBuf,Direct意味着这个ByteBuf的读写实现方法是以直接内存为基础进行实现的,其读写区域是在直接内存上,在初始化时构造一个DirectByteBuffer对象(nio中的ByteBuffer)赋值给UnpooledDirectByteBuf的buf属性,接下来对这个UnpooledDirectByteBuf类型对象的读写都会反应到这个ByteBuffer上。

7)对于pool类型的,

3. Netty源码_UnpooledDirectByteBuf详解

本篇文章我们讲解缓存区 ByteBuf 八大主要类型中两种,未池化直接缓冲区 UnpooledDirectByteBuf 和 未池化不安全直接缓冲区 UnpooledUnsafeDirectByteBuf 。

UnpooledDirectByteBuf 一个基于 NIO ByteBuffer 的缓冲区。
建议使用 UnpooledByteBufAllocator.directBuffer(int, int) , Unpooled.directBuffer(int) 和 Unpooled.wrappedBuffer(ByteBuffer) ;而不是虚嫌显式调用构造函数。

有四个成员属性:

通过 allocateDirect(initialCapacity) 方法创建一个新的 NIO 缓存区实例来初始化此缓存区对象。

利用现有的差亮手 NIO 缓存区创建此缓存区。

通过 NIO 缓存区 buffer 对应方法获取基本数据类型数据。

根据目标缓存区 dst 类型不同,处理的方式也不同。

你会发现这些方法都是获取此缓存区对应 NIO 缓存区 ByteBuffer 对象,调用 ByteBuffer 对象的方法,与 IO 流的交互,进行数据传输

和 get 系列方法一样, set 系列的实现也是靠 NIO 缓存区 ByteBuffer 对应方法。

剩余方法也几乎都是和 NIO 缓存区 ByteBuffer 有关,而且也不难,就不做过多介绍了。

UnpooledDirectByteBuf 主要是通过 NIO 缓存区 buffer 来存储数据。而它获取和设置数据,也都是通过 NIO 缓存区对应方法实现的。

光看介绍,和键戚 UnpooledDirectByteBuf 没有任何区别。它也是 UnpooledDirectByteBuf 的子类。

那么 UnpooledUnsafeDirectByteBuf 和 UnpooledDirectByteBuf 不同处在那里呢?

通过复习 setByteBuffer 方法,获取 NIO 缓存区 buffer 对应的直接内存地址。

通过 UnsafeByteBufUtil 对应方法,直接从内存地址获取对应基本类型数据。

通过 UnsafeByteBufUtil 对应方法,直接向内存地址设置对应基本类型数据。

只有这个类型 hasMemoryAddress() 方法才会返回 true 。

UnpooledUnsafeDirectByteBuf 就是通过直接从内存地址中获取和设置数据的方式,提高性能。

4. netty DirectByteBuffer能不能预留大小,内存泄露问题

计是网络转发不过做悔来 ,缓冲区数据没有发送出去 ,业务层的数据又不停纯盯正的wirte,所以就不断的扩容,然后 就OOM。你可以这样处理:
在业务层上做一个队列则歼缓存发送的数据,当channel.isWirteAble()时候,你在channe.writeAndFlush();默认的 应该就可以吧DirectByteBuffe

5. Netty怎样增大缓冲区

有现成的方法,你说的那种限制还根本没涉及到粘包解码的问题,而是netty底层限制了接收字节缓存区大小为1024,下面这样就行了
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(64, 1024, 65536))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new IdleStateHandler(10, 10, 0));
ch.pipeline().addLast(new HeartBeat());
ch.pipeline().addLast(new ByteStreamDecoder());
ch.pipeline().addLast(new ByteStreamEncoder());
ch.pipeline().addLast(new ServerStreamHandler());
}
});

6. Netty内存模型-PoolChunk

1概述

从netty 4开始,netty加入了内存池管理,采用内存池管理比普通的new ByteBuf性能提高了数十倍。首先介绍PoolChunk

2原理

PoolChunk主要负责内存块的分配与回收,首先来看看两个重要的术语。

上图中是一个默认大小的chunk, 由2048个page组成了一个chunk,一个page的大小为8192, chunk之上有11层节点,最后一层节点数与page数量相等。每次内存分配需要保证内存的连续性,这样才能简单的操作分配到的内存,因此这里构造了一颗完整的平衡二叉树,所有子节点的管理的内存也属于其父节伍敏森点。如果我们想获取一个8K的内存,则只需在第11层找一个可用节点即可,而如果我们需要16K的数据,则需要在第10层找一个可用节点。如果一个节点存在一个已经被分配的子节点,则该节点不能被分配,例如我们需要16K内存,这个时候id为2048的节点已经被分配,id为2049的节点未分配,就不能直接分配1024这个节点,因为这个节点下的内存只有8K了。

通过上面这个树结构,我们可以看到每次内存分配都是8K*(2^n), 比如需要24K内存时,实际上会申请到一块32K的内存。为了分配一个大小为chunkSize/(2^k)的内存段,需要在深度为k的层从左开始查找可用节点。如想分配16K的内存,chunkSize = 16M, 则k=10, 需要从第10层找一个空闲的节点分配内存。那如何快速分配到指定内存呢,netty使用memoryMap记录分配情况。

初始化中memoryMap中key是上图中节点值,value是该节点所在层数,对于节点512,其层数是9,则:

下面看看如何向PoolChunk申请一段内存:

当需要分配的内存大于pageSize时,使用allocateRun实现内存分配。否则使用allocateSubpage分配内存,主要是将一个page分割成多个element进行分配。其中针对请求的大小进行标准化处理(normCapacity是处理后的值),在分配内存是根据使用者请求的内存大小进行计算,匹配最接近的内存单元腔亩。在计算时分如下几拿汪种情况:

接下来看看allocateRun是如何实现的:

在allocateNode中遍历匹配:

如节点2048被分配出去,更新如下:

References

1. https://www.jianshu.com/p/c4bd37a3555b

2. https://www.cnblogs.com/pugongying017/p/9616333.html

3. https://blog.csdn.net/youaremoon

7. netty 性能 怎么 测试 工具

测试方法
采用 mina 和 netty 各实现一个 基于 nio 的EchoServer,测试在不同大小网络报文下的性能宴搭渣表现
测试环境
客户端-服务端:
model name: Intel(R) Core(TM) i5-2320 CPU @ 3.00GHz
cache size: 6144 KB
cpu cores: 4
jdk: 1.6.0_30-b12
network: 1000Mb
memory: -Xms256m -Xmx256m
Linux: centos 5.7, kernel 2.6.18-274.el5

测试工具:
jmeter v2.4

版本:
mina 2.0.7
netty 3.6.2.Final

配置:
mina
io-processor cpu 核数
executor cpu 核数
buffer 初始 buffer 大小,设置为 2048(2k)
netty
boss netty 默认配置 1
worker cpu 核数
executor cpu 核数

其实,从理论上来说, echo 型的应用不配置 executor 业务执行线程池会获得更好的性能和更低的消耗,但考虑在真实业务应用中,真实的业务场景处理通晌悄常枝嫌涉及各种复杂逻辑计算,缓存、数据库、外部接口访问,为避免业务执行延时阻塞 io 线程执行导致吞吐降低,通常都会分离 io 处理线程 和 业务处理线程,因此我们的测试案例中也配置了业务执行线程池考查它们线程池的调度效能。

8. Netty writeAndFlush解析

Netty事件分为入站事件与出站事件,可以通过ChannelPipline或者ChannelHandlerContext进行事件传播。通过ChannelPipline传播入站事件,它将被从ChannelPipeline的头部开始一直被传播到ChannelPipeline的尾端,出站事件则从尾端开始传递到头部。通过ChannelHandlerContext传播入站事件,它将被从下一个ChannelHandler开始直至传递到尾端,出站事件则从下一个ChannelHandler直至传递到头部。

Netty为了提高传输数据的效率,在写出数据时,会先将数据(ByteBuf)缓存到ChannelOutboundBuffer中,等到调用flush方法时才会将ChannelOutboundBuffer中的数据写入到socket缓冲区。

ChannelOutboundBuffer中有三个重要属性:

从其属性可以看出,ChannelOutboundBuffer内部是一个链表结构,里面有三个指针:

ChannelOutboundBuffer中有两个比较重要的方法,addMessage:将数据以链表形式缓存下来,addFlush:移动链表指针,将缓存的数据标记为已刷新,注意此时并没有将数据写入到socket缓冲区。接下来我们看下两个方法的实现:

我们进入其addMessage方法分析下它是怎么缓存数据的:

当第一次添加数据数,会将数据封装为Entry,此时tailEntry、unflushedEntry指针指向这个Entry,flushedEntry指针此时为null。每次添加数据都会生成新的Entry,并将tailEntry指针指向该Entry,而unflushedEntry指针则一直指向最初添加的Entry,我们通过画图展示下:

第一次添加:

第N次添加:

为了防止缓存数据过大,Netty对缓存数据的大小做了限制:

addMessage方法最后会调用incrementPendingOutboundBytes方法记录已缓存的数据大小(totalPendingSize),如果该大小超过了写缓冲区高水位阈值(默认64K),则更新不可写标志(unwritable),并传播Channel可写状态发生变化事件:fireChannelWritabilityChanged:

移动链表指针,将缓存的数据标记为已刷新,并设置每个数据节点状态为不可取消:

执行完addFlush方法后,链表图示如下:

通过ChannelHandlerContext#writeAndFlush方法来分析下Netty是如何将数据通过网络进行传输的:

ChannelHandlerContext#writeAndFlush方法最终会调用其子类AbstractChannelHandlerContext的writeAndFlush方法:

主要逻辑在write方法中:

write方法主要做了两件事,一是:找到下一个ChannelHandlerContext。二是:调用下一个ChannelHandlerContext的w riteAndFlush方法传播事件。writeAndFlush方法是一个出站事件,前面我们也讲过对于出站事件,通过ChannelHandlerContext进行事件传播,事件是从pipline链中找到当前ChannelHandlerContext的下一个ChannelHandlerContext进行传播直至头部(HeadContext),在这期间我们需要自定义编码器对传输的Java对象进行编码,转换为ByteBuf对象,最终事件会传递到HeadContext进行处理。

invokeWriteAndFlush方法主要做了两件事,一是:调用invokeWrite0方法将数据放入Netty缓冲区中(ChannelOutboundBuffer),二是:调用invokeFlush0方法将缓冲区数据通过NioSocketChannel写入到socket缓冲区。

invokeWrite0方法内部会调用ChannelOutboundHandler#write方法:

前面说过,出站事件最终会传播到HeadContext,在传播到HeadContext之前我们需要自定义编码器对Java对象进行编码,将Java对象编码为ByteBuf,关于编码器本章节暂不进行解析。我们进入HeadContext的write方法:

HeadContext#write方法中会调用AbstractChannelUnsafe#write方法:

该方法主要做了三件事情,一:对数据进行过滤转换,二:估测数据大小,三:缓存数据。我们先看下filterOutboundMessage方法:

一、对数据进行过滤转换:

filterOutboundMessage方法首先会对数据进行过滤,如果数据不是ByteBuf或者FileRegion类型,则直接抛出异常。如果数据是ByteBuf类型,则判断数据是否为直接直接内存,如果不是则转换为直接内存以提升性能。

二、估测数据大小:

三、缓存数据:

最后会调用ChannelOutboundBuffer#addMessage方法将数据缓存到链表中,关于addMessage方法可以回顾下文章中的Netty缓冲区部分。

回到AbstractChannelHandlerContext#invokeWriteAndFlush方法,方法内部在调用完invokeWrite0方法将数据放入到缓存后,会调用invokeFlush0方法,将缓存中的数据写入到socket缓冲区。invokeFlush0方法内部会调用ChannelOutboundHandler#flush方法:

flush方法最终会将事件传播到HeadContext的flush方法:

HeadContext#flush方法中会调用AbstractChannelUnsafe#flush方法:

方法主要做了两件事,一:调用ChannelOutboundBuffer#addFlush方法,移动链表指针,将缓存的数据标记为已刷新。二:调用flush0方法将缓存中数据写入到socket缓冲区。关于addFlush方法可以看文中Netty缓冲区部分,我们直接进入flush0方法:

flush0方法主要做了两件事,一:判断是否有挂起的刷新。二:调用父类flush0方法。

一、判断是否有挂起的刷新

文中提到写入数据时,当socket缓冲区没有可用空间时会设置不可写状态,并注册OP_WRITE事件,等待socket缓冲区有空闲空间时会触发forceFlush,我们进入到isFlushPending方法看下方法是如何判断的:

二、调用父类flush0方法

写入socket缓冲区的具体逻辑在AbstractNioChannel#AbstractNioUnsafe父类AbstractChannel#AbstractUnsafe中:

核心逻辑在doWrite方法中,我们进入到AbstractChannel子类NioSocketChannel的doWrite方法看下具体实现:

NioSocketChannel#doWrite方法根据nioBufferCnt大小执行不同的写逻辑,如果为0则调用AbstractNioByteChannel#doWrite方法。如果nioBufferCnt为1或者大于1,则调用NioSocketChannel不同的重载方法进行处理。注意,写数据时自旋次数默认为16,也就是说如果执行16次write后仍有数据未写完,则调用incompleteWrite方法将flush操作封装为一个任务,放入到队列中,目的是不阻塞其他任务。另外,如果调用NioSocketChannel#write方法后,返回的localWrittenBytes为0,则表示socket缓冲区空间不足,则注册OP_WRITE事件,等待有可用空间时会触发该事件,然后调用forceFlush方法继续写入数据。

9. Integer 的常量缓存池

Integer中有个静态内租帆部类IntegerCache,里面有个弊毕雹cache[],也就是Integer常量池,常量池的大小为一个字节(-128~127)

Byte,Short,Long 的缓存数拦池范围默认都是: -128 到 127。可以看出,Byte的所有值都在缓存区中,用它生成的相同值对象都是相等的。

所有整型(Byte,Short,Long)的比较规律与Integer是一样的。

10. Netty源码分析(七) PoolChunk

在分析源码之前,我们先来了解让圆一下Netty的内存管理机制。我们知道,jvm是自动管理内存的,这带来了一些好处,在分配内存的时候可以方便管理,也带来了一些问题。jvm每次分配内存的时候,都是先要去堆上申请内存空间进行分配,这就带来了很大的性能上的开销。当然,也可以使用堆外内存,Netty就用了堆外内存,但是内存的申请和释放,依然需要性能的开销。所以Netty实现了内存池来管理内存的申请和使用,提高了内存使用的效率。
PoolChunk就是Netty的内存管理的一种实现。羡虚Netty一次向系统申请16M的连续内存空间,这块内存通过PoolChunk对象包装,为了更细粒度的管理它,进一步的把这16M内存分成了2048个页(pageSize=8k)。页作为Netty内存管理的最基本的单位 ,所有的内存分配首先必须申请一块空闲页。Ps: 这里可能有一个疑问,如果申请1Byte的空间就分配一个页是不是太浪费空间,在Netty中Page还会被细化用于专门处理小于4096Byte的空间申请 那么这些Page需要通过某种数据结构跟算法管理起来。
先来看看PoolChunk有哪些属性

Netty采用完全二叉树进行管理,树中每个叶子节点表示一个Page,即树高为12,中间节点表示页节点的持有者。坦派塌有了上面的数据结构,那么页的申请跟释放就非常简单了,只需要从根节点一路遍历找到可用的节点即可。主要来看看PoolChunk是怎么分配内存的。

Netty的内存按大小分为tiny,small,normal,而类型上可以分为PoolChunk,PoolSubpage,小于4096大小的内存就被分成PoolSubpage。Netty就是这样实现了对内存的管理。
PoolChunk就分析到这里了。