两种操作类型
- CPU计算/业务处理
- IO操作与等待/网络、磁盘、数据库
netty定义
Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server. 作者:Trustin Lee 韩国人 Line公司 2004年开发 本质:网络应用程序框架 实现:异步、事件驱动 特性:高性能、可维护、快速开发 用途:开发服务器和客户端
和JDK NIO的对比
Netty做的更多:
支持常用应用层协议;
解决传输问题:粘包、半包现象;
支持流量整形;
完善的断连、Idle异常处理等。
API更友好更强大:
例如bytebuffer -> netty bytebuf
threadlocal -> fastthreadlocal
隔离变化、屏蔽细节
隔离jdk nio实现细节: nio -> nio2(aio)
Netty竞品
apache mina(同一作者,推荐Netty)
sun grizzly(用得少、文档少、更新少)
apple swift nio、ACE(其他语言,不考虑)
Cindy(生命周期不长)
tomcat、jetty(还没有独立出来)
服务器或者客户端可以创建多少个连接
以 linux 为例:
一个连接是由:客户端【IP + PORT】+ 服务器 【IP(固定的) + PORT(固定的)】 四个元素决定的,所以支持多少,要从两个角度分析:
(1)对于单个客户端(IP地址固定)而言:
理论值:单个客户端连接到一个服务器最多连接数取决于本地可用端口数(因为其他3个元素固定了): 65535(报文中端口占用字节数是16,所以最大端口数65535)- 1024(保留端口,不给用) 约 64K。
实际值:取决于以下三个方面:
1)TCP层:ip_local_port_range (参考/proc/sys/net/ipv4/ip_local_port_range),可调整,最大65535-1024
2)系统限制:最大文件句柄数(参考/etc/security/limits.conf),可调整,最大21亿
3)资源限制:内存等资源有限,例如连接本身占用资源,Netty本身的socket相关的对象也占用jvm,需要根据机器做测试。
(2)对于服务器而言:
理论值: 最大连接数 = 客户端数量(IP地址数量) * 单个客户端的最多连接数(约64K),不考虑资源限制,最多21亿,实际以资源限制为准, 100万连接就要占用3G以上了。
实际值: 受限于三个方面:
1)TCP层: IPv4使用32位(4字节)地址,因此地址空间中只有4,294,967,296(约43亿),所以乘以单个客户端最大64K, 数量惊人。
1)系统限制:同上,最大21亿
2)资源限制:同上
总结:
(1)对于客户端,6万多点,对于服务器,100万到1000万,再多,内存就要30G以上了,所以网上经常说百万连接。当然,如果你机器内存1G不到的话,那也搞不了了。
(2)单纯看连接多少意义不是很大,因为连接是为了做事情,光能连上很多,但是占用资源过大导致基本已经不能动弹的话,意义就不大了,不过这个问题本身有趣。
扩展:
(1)查看ip_local_port_range方法:
[root@netty ~]# cat /proc/sys/net/ipv4/ip_local_port_range
16384 61000
(2)服务器支持1200万连接的案例:
mrotaru.wordpress.com/2013/06/20/…
(3) 最大文件句柄数的最大值受另外一个参数控制:
[root@netty ~]# cat /proc/sys/fs/nr_open
1048576
设置超过的话:
[root@netty ~]# ulimit -Hn 9000000
- bash: ulimit: open files: cannot modify limit: Operation not permitted
所以如果想改的更大,这需要修改这个参数(默认百万:1048576(1024 * 1024)):
sysctl -w fs.nr_open=100000000
那这参数最大值,可以到多少呢?2147483584 (即7FFFFFC0,也就是在MAXINT(2147483647)基础上按64字节对齐)
(4)linux系统下,一个socket连接一般占用3K,所以100万连接至少需要3G,而1000万就要30G了,所以可以看下(2)引用连接文章里的 内存大小,很大。
版本发布
2004年6月 Netty2发布
2008年10月Netty3发布
2013年7月Netty4发布
2013年12月发布Netty 5.0.0 alpha1
2015年11月废弃5.0.0(废弃原因: 复杂、没有证明明显性能优势、维护不过来,主要维护3.10.X、4.0.X、4.1.X)
典型使用项目
数据库: Cassandra
大数据处理:Spark、Hadoop
MQ: RocketMQ
搜索:Elasticsearch
框架:gRPC、apache dubbo、Spring5
分布式协调器: ZooKeeper
阻塞、非阻塞和同步、异步
阻塞、非阻塞和同步、异步其实针对的对象是不一样的。 阻塞、非阻塞说的是调用者; 同步、异步说的是被调用者。 同步请求,A调用B,B的处理是同步的,在处理完之前他不会通知A,只有处理完之后才会明确的通知A。 异步请求,A调用B,B的处理是异步的,B在接到请求后先告诉A我已经接到请求了,然后异步去处理,处理完之后通过回调等方式再通知A。 所以说,同步和异步最大的区别就是被调用方的执行方式和返回时机。同步指的是被调用方做完事情之后再返回,异步指的是被调用方先返回,然后再做事情,做完之后再想办法通知调用方。 阻塞请求,A调用B,A一直等着B的返回,别的事情什么也不干。 非阻塞请求,A调用B,A不用一直等着B的返回,先去忙别的事情了。 所以说, 阻塞和非阻塞最大的区别就是在被调用方返回结果之前的这段时间内,调用方是否一直等待。阻塞指的是调用方一直等待别的事情什么都不做。非阻塞指的是调用方先去忙别的事情。
什么是经典的三种I/O模式?
生活场景:
当我们去饭店吃饭时:
- 食堂排队打饭模式:在窗口排队,打好才去;
- 点单、被叫模式:等待被叫,好了自己去端;
- 包厢模式:点单后菜直接被端上桌。
类比
饭店 -> 服务器
饭菜 -> 数据
饭菜好了 -> 数据就绪
端菜/送菜 -> 数据读取
排队打饭模式类似于BIO(阻塞I/O),出现在before jdk1.4;
点单、等待被叫模式类似于NIO(非阻塞I/O),出现在from jdk1.4;2002年,Java.nio包;
包厢模式类似于AIO(异步I/O),出现在from jdk1.7,2011年;
阻塞与非阻塞
- 菜没好,要不要死等 ->(类比)
- 数据就绪前要不要等待?
- 阻塞:没有数据传过来时,读请求会阻塞直到有数据,缓冲区满时,写操作也会阻塞。
- 非阻塞:遇到这些情况,都是直接返回。
同步与异步
- 菜好了,谁端 ->(类比) 数据就绪后,数据操作谁完成?
- 数据就绪后需要自己去读是同步;
- 数据就绪后直接读好再回调给程序是异步。
总结:
阻塞、非阻塞和同步、异步其实针对的对象是不一样的:
阻塞、非阻塞说的是调用者;
同步、异步说的是被调用者。
同步请求,A调用B,B的处理是同步的,在处理完之前他不会通知A,只有处理完之后才会明确的通知A;
异步请求,A调用B,B的处理是异步的,B在接到请求后先告诉A我已经接到请求了,然后异步去处理,处理完之后通过回调等方式再通知A。
所以说,同步和异步最大的区别就是被调用方的执行方式和返回时机。同步指的是被调用方做完事情之后再返回,异步指的是被调用方先返回,然后再做事情,做完之后再想办法通知调用方。
阻塞请求,A调用B,A一直等着B的返回,别的事情什么也不干。
非阻塞请求,A调用B,A不用一直等着B的返回,先去忙别的事情了;
所以说, 阻塞和非阻塞最大的区别就是在被调用方返回结果之前的这段时间内,调用方是否一直等待。阻塞指的是调用方一直等待别的事情什么都不做。非阻塞指的是调用方先去忙别的事情。
bio 阻塞同步模式
nio 非阻塞同步模式
aio 非阻塞异步模式
Netty对三种模式的支持
为什么Netty仅仅支持NIO了?
为什么不建议阻塞I/O?
连接数高的场景下: 阻塞 -> 耗资源、效率低下
为什么删掉已经做好的AIO支持?
- windows实现成熟,但是很少用来做服务器;
- Linux常用来做服务器,但是AIO实现不够成熟;
- Linux下AIO相比较NIO的性能提升不明显。
为什么Netty有多种NIO实现?
通用的COMMOM NIO在Linux下也是使用Epoll实现,为什么要单独实现? 根因:实现的更好:
- 首先,Netty暴露了更多的可控参数,例如: jdk的 NIO默认实现是水平触发; Netty是边缘出发(默认)和水平触发可切换;
- Netty实现的垃圾回收更少、性能更好
水平触发&&边缘触发
水平触发:点单后,菜(数据)做好了,服务员端上来问吃不吃(读),你不吃或者吃不完,她过会还会端过来问你吃不吃,提醒你,还没吃完,可以继续吃,反反复复。
边缘触发: 服务员端上菜后,你一次没有吃完,好了,等你想吃剩下的时候,也别吃了,除非再点菜,才能吃到刚没吃完的。
边缘触发是指消息到来的时刻进行消费,如果一次到达的消息超过了一次消费的最大值,剩余的消息不会被继续消费,要消费这一部分消息要么等到下一次消息的到来,要么在这次消费之后主动触发消费剩余消息。至于水平触发,则是以是否有剩余消息为标准,有剩余,就一直主动消费直到无消息。
TODO2:水平触发和边缘触发,适用场景是什么?
边缘触发相当于高速模式,理论上效率更高,但是复杂度也高,所以现在大多应用(Redis等)还是默认水平触发,如果追求要更好的性能、同时有信心编码好,可以尝试使用边缘触发,例如nginx。
另外,还有点注意的是边缘触发只支持非阻塞模式。
TODO1:三种模式中的第二种,点单等待被叫是否有些小问题?NIO中还是要自己用selector去查询事件是否就绪的,没有事件就绪时查询会阻塞在那里,这个是否和“点单等待被叫”有些不同?
阻塞的主体不同,这个selector你可以理解成点单的吧台,你去点单了,给你个号码,等于注册了一个事件,然后吧台记录下来了,等菜好了,就喊这个号码了。selector本身的阻塞不是我们读写数据时候的阻塞,是他在等待事件就绪的阻塞,比如菜也没好,吧台就等着,和我们数据阻塞不是一个主体。
看到后面回头重温捡漏一遍,当被监控的文件有可读写事件发生时,epoll_wait()会通知处理程序去读写,如果这次没有把数据一次性全部读写完的话,水平触发:那么下次调用 epoll_wait()时通知你上次没读写完,如果一直不处理它会一直通知你;边缘触发:下次调用 epoll_wait()的时候不会通知你,也就是只通知一次,知道该文件上出现第二次可读写事件才会通知,效率比水平触发要高,结合老师给楼上举例吃饭的生活场景就好理解多了。
NIO一定优于BIO吗?
BIO代码简单; 特定场景:连接数少,并发度低,NIO性能不输NIO。
Netty怎么切换I/O模式
例如对于ServerSocketChannel类型:(工厂模式 + 泛型 + 反射实现IO模式切换)
public B channel(Class<? extends C> channelClass) {
if (channelClass == null) {
throw new NullPointerException("channelClass");
}
//工厂模式
return channelFactory(new ReflectiveChannelFactory<C>(channelClass));
}
public class ReflectiveChannelFactory<T extends Channel> implements ChannelFactory<T> {
private final Class<? extends T> clazz;
public ReflectiveChannelFactory(Class<? extends T> clazz) {
if (clazz == null) {
throw new NullPointerException("clazz");
}
this.clazz = clazz;
}
//泛型T代表不同的Channel
@Override
public T newChannel() {
try {
//反射创建不同的Channel
return clazz.getConstructor().newInstance();
} catch (Throwable t) {
throw new ChannelException("Unable to create Channel from class " + clazz, t);
}
}
}
public B channel(Class<? extends C> channelClass) {
if (channelClass == null) {
throw new NullPointerException("channelClass");
}
//工厂模式
return channelFactory(new ReflectiveChannelFactory<C>(channelClass));
}
public class ReflectiveChannelFactory<T extends Channel> implements ChannelFactory<T> {
private final Class<? extends T> clazz;
public ReflectiveChannelFactory(Class<? extends T> clazz) {
if (clazz == null) {
throw new NullPointerException("clazz");
}
this.clazz = clazz;
}
//泛型T代表不同的Channel
@Override
public T newChannel() {
try {
//反射创建不同的Channel
return clazz.getConstructor().newInstance();
} catch (Throwable t) {
throw new ChannelException("Unable to create Channel from class " + clazz, t);
}
}
}
什么是Reactor以及三种版本
生活场景类比: 饭店规模变化
一个人包揽所有:迎宾、点菜、做饭、上菜、送客等;
多招几个伙计:大家一起做上面的事情;
进一步分工:搞一个或者多个人专门迎宾;
饭店伙计 -> 线程
迎宾工作 -> 接入连接
点菜 -> 请求
做菜 -> 业务处理
上菜 -> 响应
送客 -> 断连
- 一个人包揽所有:迎宾、点菜、做饭、上菜、送客等等 -> Reactor单线程
- 多招几个伙计,大家一起做上面的事情 -> Reactor多线程模式
- 进一步分工,搞一个或者专门几专门做迎宾 -> 主从Reactor多线程模式
三种IO模型对应的开发模型:
BIO: Thread-Per-Connection
NIO: Reactor
AIO: Proactor
Reactor是一种开发模式,核心流程是是:
注册感兴趣的实现 -> 扫描是否有感兴趣的事件发生 -> 事件发生后作出相应的处理
Thread-Per-Connection模式
阻塞型,对于每一个连接都会有一个线程处理。
即:一个线程处理一件事情。
Reactor模式之:单线程
所有的事情包括接收连接、处理读写操作、注册事件、扫描事件等等都是一个线程在处理。
即:eventLoopGroup(1)后面设置1.
Reactor模式之:多线程
把decode、compute、encode三个耗时的操作放在线程池worker threads里面处理.
即:eventLoopGroup()不设置,如果不设置线程数据,就会根据cpu去设置最优的线程数。
Reactor模式之:主从多线程
把acceptor事件单独注册到另外一个reactor中。
reactor的主从多线程模式,搞几个迎宾来处理接入的连接。
如何在Netty中使用Reactor模式
上面提到的“前台招揽顾客”对应这里的bossGroup
netty非主从多线程模式按照下面的写法有什么问题?分bossGroup和workerGroup,把bossGroup的线程数设为1
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
NioEventLoopGroup()参数是1还是其他数字是控制是否多线程,而是否分2组是区别是否采用主从,这种写法还是主从,只不过是主是单线程,从是多线程。非主从只有一个group。
非主从模式下的bossGroup的线程数是否和端口数量有关,如果服务端只开放一个端口供客户端链接,那么bossGroup设置为多线程是否多余?
简单来看,可以按照端口来对应。这种场景下,可以显示设置为1。
从单纯性能角度来看,独立接受连接的reactor性能是如何得到提升的?
关键点不是性能提升问题,因为做事情的总人数没变,只是从无分工变成了有分工,假设不独立出来单独接受连接,等于所有的事都是一样优先级的,那在繁忙的时候,接受连接的处理可能就会延迟很久,导致连接超时,很明显连接比一次业务处理更重要,所以才把最重要的事独立处理,例如大饭店都是有迎宾的。另外,反过来说,目标不是性能提升,因为分工了,只是说问题少了。
针对NIO使用reactor开发模式:注册事件、扫描事件、处理事件。如果是AIO话,其中的扫描事件,是不是就不用自己扫描了,而是只要注册事件然后被动的等待通知就行了?
NIO也不需要自己去扫描事件。
AIO的优点在于,不需要自己去读数据了,NIO还要自己去读,但是这两个都是事件驱动的,在linux下实现也都是epoll。
解析Netty对Reactor模式支持的常见疑问
AbstractBootstrap.java
//开始register,将serversocketchannel绑定到bossgroup
ChannelFuture regFuture = config().group().register(channel);
ServerBootstrap.java
将socketchannel绑定到另外一个workergroup
@Override
@SuppressWarnings("unchecked")
public void channelRead(ChannelHandlerContext ctx, Object msg) {
final Channel child = (Channel) msg;
child.pipeline().addLast(childHandler);
setChannelOptions(child, childOptions, logger);
for (Entry<AttributeKey<?>, Object> e: childAttrs) {
child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
}
try {
childGroup.register(child).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
forceClose(child, future.cause());
}
}
});
} catch (Throwable t) {
forceClose(child, t);
}
}
为什么说Netty的main reactor大多并不能用到一个线程组、只能线程组里面的一个?
io.netty.bootstrap.AbstractBootstrap#doBind()方法里面初始化bossgroup,doBind()方法绑定一个地址和端口,把服务器给启动起来。对于服务器来说,我们一般只会绑定一个地址和一个端口,所以我们只会调用一次线程组的线程。
Netty给Channel分配NIO event loop的规则是什么?
io.netty.channel.MultithreadEventLoopGroup#register(io.netty.channel.Channel)
@Override
public ChannelFuture register(Channel channel) {
return next().register(channel);
}
//根据待绑定的executor是否是2的幂次方,做出不同的选择
io.netty.util.concurrent.DefaultEventExecutorChooserFactory#newChooser
@Override
public EventExecutorChooser newChooser(EventExecutor[] executors) {
if (isPowerOfTwo(executors.length)) {
return new PowerOfTwoEventExecutorChooser(executors);
} else {
return new GenericEventExecutorChooser(executors);
}
}
总共有两种实现:
//递增、取模,取正值,不然可能是负数
io.netty.util.concurrent.DefaultEventExecutorChooserFactory.GenericEventExecutorChooser#next
@Override
public EventExecutor next() {
return executors[Math.abs(idx.getAndIncrement() % executors.length)];
}
//executors总数必须是2的次方幂才会用,&效率比较高
io.netty.util.concurrent.DefaultEventExecutorChooserFactory.PowerOfTwoEventExecutorChooser#next
@Override
public EventExecutor next() {
return executors[idx.getAndIncrement() & executors.length - 1];
}
通用模式的NIO实现多路复用器是怎么跨平台的?
io.netty.channel.nio.NioEventLoopGroup#NioEventLoopGroup(int, java.util.concurrent.Executor)
public NioEventLoopGroup(int nThreads, Executor executor) {
this(nThreads, executor, SelectorProvider.provider());
}
java.nio.channels.spi.SelectorProvider#provider
public static SelectorProvider provider() {
synchronized (lock) {
if (provider != null)
return provider;
return AccessController.doPrivileged(
new PrivilegedAction<SelectorProvider>() {
public SelectorProvider run() {
........
provider = sun.nio.ch.DefaultSelectorProvider.create();
return provider;
}
});
}
}
public static SelectorProvider create() {
return new KQueueSelectorProvider();
}
可以发现,mac平台使用的是KQueueSelectorProvider,即每个平台的代码不一样。
什么是粘包和半包?
假设客户端发送两条消息:123和456。服务端收到的消息格式可能是123456,这就是粘包;假如收到的格式是12、34、56三个消息,这就是半包。
为什么TCP应用中会出现粘包和半包现象?
粘包的原因:
- 发送方每次写入消息 < 套接字缓冲区大小
- 接收方读取套接字缓冲区数据不够及时
半包的原因:
发送方写入数据 > 套接字缓冲大小
发送的数据大于协议的MTU(maximum transmission unit,最大传输单元),必须拆包
从其他角度来看:
收发:
一个发送可能被多次接收,多个发送可能被一次接收
传输:
一个发送可能占用多个传输包,多个发送可能公用一个传输包
根本原因:
TCP是流式协议,消息无边界。
备注:
UDP像邮寄的包裹,虽然一次运输多个,但是每个包括都有“界限”,一个一个签收,所以无粘包、半包问题。
解决粘包和半包问题的几种常用方法
方法:
找出消息的边界
Netty对三种常用封帧方式的支持
解读Netty处理粘包、半包的源码
解码核心工作流程
解码入口方法:io.netty.handler.codec.ByteToMessageDecoder#channelRead
//参数msg就是传入的数据
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
CodecOutputList out = CodecOutputList.newInstance();
try {
//要解析的数据首先转化为data
ByteBuf data = (ByteBuf) msg;
first = cumulation == null;
//如果是第一笔数据,就赋值给cumulation数据积累器,也就是在解码前或者解码后都是一个数据积累的过程
if (first) {
cumulation = data;
} else {//如果不是第一笔数据,就追加到cumulation,采用的是策略模式
cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
}
callDecode(ctx, cumulation, out);
} catch (DecoderException e) {
throw e;
} catch (Exception e) {
throw new DecoderException(e);
} finally {
if (cumulation != null && !cumulation.isReadable()) {
numReads = 0;
cumulation.release();
cumulation = null;
} else if (++ numReads >= discardAfterReads) {
// We did enough reads already try to discard some bytes so we not risk to see a OOME.
// See https://github.com/netty/netty/issues/4275
numReads = 0;
discardSomeReadBytes();
}
int size = out.size();
decodeWasNull = !out.insertSinceRecycled();
fireChannelRead(ctx, out, size);
out.recycle();
}
} else {
ctx.fireChannelRead(msg);
}
}
参数msg就是传入的数据,要解析的数据首先转化为data. 如果是第一笔数据,就赋值给cumulation数据积累器,也就是在解码前或者解码后都是一个数据积累的过程;如果不是第一笔数据,就追加到cumulation,采用的是策略模式。 接着调用callDecode解码。
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
try {
//参数in就是数据积累器的数据
while (in.isReadable()) {
int outSize = out.size();
if (outSize > 0) {
fireChannelRead(ctx, out, outSize);
out.clear();
// Check if this handler was removed before continuing with decoding.
// If it was removed, it is not safe to continue to operate on the buffer.
//
// See:
// - https://github.com/netty/netty/issues/4635
if (ctx.isRemoved()) {
break;
}
outSize = 0;
}
int oldInputLength = in.readableBytes();
decodeRemovalReentryProtection(ctx, in, out);
// Check if this handler was removed before continuing the loop.
// If it was removed, it is not safe to continue to operate on the buffer.
//
// See https://github.com/netty/netty/issues/1664
if (ctx.isRemoved()) {
break;
}
if (outSize == out.size()) {
if (oldInputLength == in.readableBytes()) {
break;
} else {
continue;
}
}
if (oldInputLength == in.readableBytes()) {
throw new DecoderException(
StringUtil.simpleClassName(getClass()) +
".decode() did not read anything but decoded a message.");
}
if (isSingleDecode()) {
break;
}
}
} catch (DecoderException e) {
throw e;
} catch (Exception cause) {
throw new DecoderException(cause);
}
}
参数in就是数据积累器的数据,也就是我们收到的数据,第一次outSize大小肯定为0,所以跳过if语句,然后会调用decodeRemovalReentryProtection()方法。该方法作用是:在decode中时,是不能执行handler remove清理操作的,在decode完之后需要清理数据。
final void decodeRemovalReentryProtection(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
throws Exception {
decodeState = STATE_CALLING_CHILD_DECODE;
try {
decode(ctx, in, out);
} finally {
boolean removePending = decodeState == STATE_HANDLER_REMOVED_PENDING;
decodeState = STATE_INIT;
if (removePending) {
handlerRemoved(ctx);
}
}
}
内部会调用decode方法,该方法采用的是模版方法。
protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;
可以查询子类FixedLengthFrameDecoder,此时就完成一次数据解析的流程。 通过下面的代码就可以发现,处理了粘包和半包的问题。 在编码数据前,先对数据进行积累Cumulator。解码的时候先判断是否小于固定的长度,小的话就是包不全也就不去解码数据,等于的话就直接解(没问题),大于的话就解对应长度的,多的那部分还是在积累器里面,等下次用。
protected Object decode(
@SuppressWarnings("UnusedParameters") ChannelHandlerContext ctx, ByteBuf in) throws Exception {
if (in.readableBytes() < frameLength) {//处理半包的问题
return null;
} else {//处理粘包的问题,多余的数据还会存储在accumation中
return in.readRetainedSlice(frameLength);
}
}
解码中两种数据积累器cumulator的区别
MERGE_CUMULATOR
using memory copies
public static final Cumulator MERGE_CUMULATOR = new Cumulator() {
@Override
public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
final ByteBuf buffer;
//如果容量不够就扩容
if (cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes()
|| cumulation.refCnt() > 1 || cumulation.isReadOnly()) {
// Expand cumulation (by replace it) when either there is not more room in the buffer
// or if the refCnt is greater then 1 which may happen when the user use slice().retain() or
// duplicate().retain() or if its read-only.
//
// See:
// - https://github.com/netty/netty/issues/2327
// - https://github.com/netty/netty/issues/1764
buffer = expandCumulation(alloc, cumulation, in.readableBytes());
} else {
buffer = cumulation;
}
//如果容量足够的话,就直接把数据追加进去
buffer.writeBytes(in);
in.release();
return buffer;
}
};
COMPOSITE_CUMULATOR
- Cumulate {@link ByteBuf}s by add them to a {@link CompositeByteBuf} and so do no memory copy whenever possible.* Be aware that {@link CompositeByteBuf} use a more complex indexing implementation so depending on your use-case* and the decoder implementation this may be slower then just use the {@link #MERGE_CUMULATOR}. 不是真正的复制,而是提供一个逻辑的视图,
public static final Cumulator COMPOSITE_CUMULATOR = new Cumulator() {
@Override
public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
ByteBuf buffer;
if (cumulation.refCnt() > 1) {
// Expand cumulation (by replace it) when the refCnt is greater then 1 which may happen when the user
// use slice().retain() or duplicate().retain().
//
// See:
// - https://github.com/netty/netty/issues/2327
// - https://github.com/netty/netty/issues/1764
buffer = expandCumulation(alloc, cumulation, in.readableBytes());
buffer.writeBytes(in);
in.release();
} else {
CompositeByteBuf composite;
//创建compositeByteBuf
if (cumulation instanceof CompositeByteBuf) {
composite = (CompositeByteBuf) cumulation;
} else {
composite = alloc.compositeBuffer(Integer.MAX_VALUE);
composite.addComponent(true, cumulation);
}
//避免内存复制
composite.addComponent(true, in);
buffer = composite;
}
return buffer;
}
};
MERGE_CUMULATOR是通过copy实现,实际操作的是ByteBuf;COMPOSITE_CUMULATOR操作的CompositeByteBuf可以看做是对ByteBuf的封装,其维护了一个ByteBuf的List列表,每次cumulate操作其实把当前的ByteBuf放到List中。我认为这两种cumulate的性能侧重点不同,merge方式提前copy,那么读取时会更快,反之,使用composite的方式在读取时需要遍历List,读取数据时更慢。 我觉得使用哪种数据累计器主要看decode实现,如果decode的实现包含很多随机读(比如读取第4-8个字节这种),这种情况肯定是基于复制的Cumulator更好,因为复制方式是连续内存,随机读时间复杂度为O(1), 而组合方式是基于数组实现,由于不确定目标内容在数组的位置,所以 需要遍历数组,效率偏低。
数据收集器有两种方式,一种内存复制,一种组合方式,这两种方式netty选择内存复制作为默认方式?
因为组合的方式没有经过充分的证明:证明在所有场景下肯定比内存复制的性能要好(毕竟组合方式的指针维护复杂些,如果解码是把组合的直接能拆出来就可以用,那明显会好,例如ssl hanlder里面就有显示设置为组合方式的例子,但是大多不是如此用,仅有的测试也只是表明好一点点而已),所以自然默认就还是用原始的那种方式(也就是所有人一上来就会想到的通用的内存复制的方式),而不是直接切换到后来加的组合方式,另外提供了setCumulator方法让我们有切换的选择。
总结下:不是说1+1不等于2,而是说大家都这么觉得,但是没有人去有力的证明,所以保持怀疑的态度,就保守起见了,没有改默认的。
三种解码器的常见额外控制参数有哪些
为什么需要“二次”解码?
假设我们把解决半包粘包问题的常用三种解码器叫做一次解码器:
在实际项目中,除了可选的压缩解压缩之外,还需要一层解码,因为一次解码的结果是字节,需要和项目中所使用的对象做转化,方便使用,这层解码器可以称为“二次解码器”,相应的,对应的编码器是为了将Java对象转化为字节流方便存储或者运输。
常用的“二次”编解码方式?
1.一次解码是封装成帧,用那三种编码器去读数据,decoder传进来的原始数据--》最终是字节
2.二次是把字节转成其它的东西,比如对象
一次解码器:io.netty.handler.codec.ByteToMessageDecoder
io.netty.buffer.ByteBuf(原始数据流) -> io.netty.buffer.ByteBuf(用户数据)
二次解码器:io.netty.handler.codec.MessageToMessageDecoder
io.netty.buffer.ByteBuf(原始数据流) -> java Object
常见的“二次”编解码方式?
Java序列化(缺点:占用空间大,只有Java能用) Marshaling XML(可读性好,但是占用空间大) JSON(可读性好,占用空间适中) MessagePack(占用空间比json小,可读性比json差) ProtoBuf(性能最好,可能性最差) Hessian thrift and so on
是不是也可以一步到位?合并一次解码器(解决粘包和半包)和二次解码器(解决可操作问题)?
可以,但是不建议,原因是:
- 没有分层,不够清晰;
- 耦合性高,不容易置换方案。
选择编解码方式的要点
- 空间:编码后占用空间,需要比较不同的数据的大小
- 时间:编解码速度:需要比较不同的数据大小情况
- 是否追求可读性
- 多语言的支持
女朋友不断问你“你爱不爱我”,这就是一种keepalive,用来确认你还有没有活路。
为什么需要keepalive?
生活场景: 假设你正在和女朋友打电话,电话通了之后,你们正在甜言蜜语,但是说着说着对方就不说话了(可能是挂机、网络故障、外出等等)
- 这个时候你会一直握着电话等么?
不会 - 如果不会,那你会怎么做?
会再确认问一下“你还在吗?”。如果对方没有回复,那就直接挂机。
这套机制就是keepalive
类比服务器应用:
怎么设计keepalive?以TCP keepalive为例
TCP keepalive 核心参数:
sysctl -a | grep tcp_keepalive
//问题出现概率小 -> 没有必要频繁
net.ipv4.tcp_keepalive_time = 7200
net.ipv4.tcp_keepalive_intvl = 75
判断需“谨慎” -> 不能武断
net.ipv4.tcp_keepalive_probes = 9
当启用(默认关闭)keepalive 时,TCP 在连接没有数据通过的7200秒后发送 keepalive 消息,当探测没有确认时,按75秒的重试频率重发,一直发 9 个探测包都没有确认,就认定连接失效。
所以总耗时一般为:2 小时 11 分钟 (7200 秒 + 75 秒* 9 次)
为什么还需要应用层keepalive?
1.tcp层关注的是有没有活着,应用层关注的是能不能用
2.tcp默认是关的,而且在传输途中可能没了,所以应用层再做一层保险
3.tcp的参数是系统参数,如果是自己的应用在用就没事。如果别人也在用,你改了他也要变,就变化很大了
- 协议分层,各层关注点不同: 传输层关注是否“通”,应用层关注是否可服务。 类比前面的打电话的例子,电话能通,不代表有人接;服务器连接在,但是不一定可以服务(例如服务不过来等)。
- TCP 层的 keepalive 默认关闭,且经过路由等中转设备 keepalive 包可能会被丢弃。
- TCP 层的 keepalive 时间太长:
默认 > 2 小时,虽然可改,但属于系统参数,改动影响所有应用
HTTP 属于应用层协议,但是常常听到名词“ HTTP Keep-Alive ”指的是对长连接和短连接的选择: - Connection : Keep-Alive 长连接(HTTP/1.1 默认长连接,不需要带这个 header)
- Connection : Close 短连接
Idle监测是什么?
生活场景: 假设你正在和女朋友打电话,电话通了之后,你们正在甜言蜜语,但是说着说着对方就不说话了(可能是挂机、网络故障、外出等等)
- 你会立马发问你还在吗?
不会
一般你会稍微等待一定的时间,在这个时间内看看对方还会不会说话(Idle 检测),如果还不说,认定对方存在问题(Idle),于是开始发问“你还在么?”(keepalive),或者问都不问干脆直接挂机(关闭连接)。
Idle 监测,只是负责诊断,诊断后,做出不同的行为,决定 Idle 监测的最终用途: - 发送 keepalive :一般用来配合 keepalive ,减少 keepalive 消息。
Keepalive 设计演进:
V1 定时 keepalive 消息;
V2 空闲监测 + 判定为 Idle 时才发keepalive。
V1:keepalive 消息与服务器正常消息交换完全不关联,定时就发送;
V2:有其他数据传输的时候,不发送 keepalive ,无数据传输超过一定时间,判定为 Idle,再发 keepalive 。 - 直接关闭连接:
- 快速释放损坏的、恶意的、很久不用的连接,让系统时刻保持最好的状态。
- 简单粗暴,客户端可能需要重连。
实际应用中:结合起来使用。按需 keepalive ,保证不会空闲,如果空闲,关闭连接。
如何在Netty中开启TCP keepalive和Idle监测?
开启keepalive:
- Server 端开启 TCP keepalive
bootstrap.childOption(ChannelOption.SO_KEEPALIVE,true)
bootstrap.childOption(NioChannelOption.of(StandardSocketOptions.SO_KEEPALIVE), true)
提示:.option(ChannelOption.SO_KEEPALIVE,true) 存在但是无效
开启不同的 Idle Check:
- ch.pipeline().addLast(“idleCheckHandler", new IdleStateHandler(0, 20, 0, TimeUnit.SECONDS));
io.netty.handler.timeout.IdleStateHandler#IdleStateHandler(int, int, int)
http长连接、http短连接,还有tcp这个连接,区别在哪里?
一个永远牵着手,一个一会牵手一会断,http是基于tcp的,http的长连接是说它下面用的tcp连接一直连着,所有请求都复用这个传输通道,而http短连接,就是比如一个请求就建一个tcp连接,处理完就断了。实际上,一般我们现在经常用的是比如保持几分钟的长连接。
tcp的keepalive和http的keep-alive看清楚了,中间有个杠。
tcp的keepalive是在ESTABLISH状态的时候,双方如何检测连接的可用行。
而http的keep-alive说的是如何避免进行重复的TCP三次握手和四次挥手的环节(选择长连接还是短的)。
长连接:指在一个连接上可以连续发送多个数据包,在连接保持期间,如果没有数据包发送,需要双方发链路检测包.
短连接:指通讯双方有数据交互时,就建立一个连接,数据发送完成后,则断开此连接,即每次连接只完成一项业务的发送。
idle检测:就是看对方有没有反应。作用是省的你每次都去问人家
idle:就是对方不回复你,对方就是idle状态
去询问的动作叫keepalive
netty开启keepalive
可以在创建ServerBootstrap的group后通过额外参数(childOption)来指定开启
开启idle检测可以在handler下的初始化Channel那里开启,记得那个addlast不,就是加在这个里面
源码解读Netty对TCP keepalive和三种Idle监测的支持
设置TCP keepalive是怎么生效的?
io.netty.bootstrap.ServerBootstrap#childOption
-> io.netty.bootstrap.ServerBootstrap#init
-> io.netty.bootstrap.ServerBootstrap.ServerBootstrapAcceptor#ServerBootstrapAcceptor
-> io.netty.bootstrap.ServerBootstrap.ServerBootstrapAcceptor#channelRead
-> io.netty.bootstrap.AbstractBootstrap#setChannelOptions
->io.netty.bootstrap.AbstractBootstrap#setChannelOption
-> io.netty.channel.socket.nio.NioSocketChannel.NioSocketChannelConfig#setOption
两种设置keepalive的方式有什么区别?
childOption(ChannelOption.SO_KEEPALIVE, true)
用的是io.netty.channel.socket.nio.NioChannelOption#setOption
static <T> boolean setOption(Channel jdkChannel, NioChannelOption<T> option, T value) {
java.nio.channels.NetworkChannel channel = (java.nio.channels.NetworkChannel) jdkChannel;
if (!channel.supportedOptions().contains(option.option)) {
return false;
}
if (channel instanceof ServerSocketChannel && option.option == java.net.StandardSocketOptions.IP_TOS) {
// Skip IP_TOS as a workaround for a JDK bug:
// See http://mail.openjdk.java.net/pipermail/nio-dev/2018-August/005365.html
return false;
}
try {
//java.nio.channels.NetworkChannel: JDK调用
channel.setOption(option.option, value);
return true;
} catch (IOException e) {
throw new ChannelException(e);
}
}
childOption(NioChannelOption.SO_KEEPALIVE, true)
io.netty.channel.socket.DefaultSocketChannelConfig#setOption
public <T> boolean setOption(ChannelOption<T> option, T value) {
validate(option, value);
if (option == SO_RCVBUF) {
setReceiveBufferSize((Integer) value);
} else if (option == SO_SNDBUF) {
setSendBufferSize((Integer) value);
} else if (option == TCP_NODELAY) {
setTcpNoDelay((Boolean) value);
} else if (option == SO_KEEPALIVE) {
setKeepAlive((Boolean) value);
} else if (option == SO_REUSEADDR) {
setReuseAddress((Boolean) value);
} else if (option == SO_LINGER) {
setSoLinger((Integer) value);
} else if (option == IP_TOS) {
setTrafficClass((Integer) value);
} else if (option == ALLOW_HALF_CLOSURE) {
setAllowHalfClosure((Boolean) value);
} else {
return super.setOption(option, value);
}
return true;
}
Idle监测类包io.netty.handler.timeout的功能浏览
三种类型的Idle
public enum IdleState {
/**
* No data was received for a while.
*/
READER_IDLE,
/**
* No data was sent for a while.
*/
WRITER_IDLE,
/**
* No data was either received or sent for a while.
*/
ALL_IDLE
}
Idle的六个状态
public class IdleStateEvent {
public static final IdleStateEvent FIRST_READER_IDLE_STATE_EVENT = new IdleStateEvent(IdleState.READER_IDLE, true);
public static final IdleStateEvent READER_IDLE_STATE_EVENT = new IdleStateEvent(IdleState.READER_IDLE, false);
public static final IdleStateEvent FIRST_WRITER_IDLE_STATE_EVENT = new IdleStateEvent(IdleState.WRITER_IDLE, true);
public static final IdleStateEvent WRITER_IDLE_STATE_EVENT = new IdleStateEvent(IdleState.WRITER_IDLE, false);
public static final IdleStateEvent FIRST_ALL_IDLE_STATE_EVENT = new IdleStateEvent(IdleState.ALL_IDLE, true);
public static final IdleStateEvent ALL_IDLE_STATE_EVENT = new IdleStateEvent(IdleState.ALL_IDLE, false);
}
读Idle监测的原理
io.netty.handler.timeout.IdleStateHandler.ReaderIdleTimeoutTask#run
@Override
protected void run(ChannelHandlerContext ctx) {
long nextDelay = readerIdleTimeNanos;
if (!reading) {
//计算是否idle的关键
nextDelay -= ticksInNanos() - lastReadTime;
}
if (nextDelay <= 0) {
//空闲了
// Reader is idle - set a new timeout and notify the callback.
readerIdleTimeout = schedule(ctx, this, readerIdleTimeNanos, TimeUnit.NANOSECONDS);
boolean first = firstReaderIdleEvent;
//firstReaderIdleEvent下个读来之前,第一次idle之后,可能触发多次,都属于非第一次idle.
firstReaderIdleEvent = false;
try {
IdleStateEvent event = newIdleStateEvent(IdleState.READER_IDLE, first);
channelIdle(ctx, event);
} catch (Throwable t) {
ctx.fireExceptionCaught(t);
}
} else {
//重新建一个监测task,用nextdelay时间
// Read occurred before the timeout - set a new timeout with shorter delay.
readerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
}
}
}
写Idle监测原理和参数observeOutput用途?
io.netty.handler.timeout.IdleStateHandler.WriterIdleTimeoutTask#run
protected void run(ChannelHandlerContext ctx) {
long lastWriteTime = IdleStateHandler.this.lastWriteTime;
long nextDelay = writerIdleTimeNanos - (ticksInNanos() - lastWriteTime);
if (nextDelay <= 0) {
// Writer is idle - set a new timeout and notify the callback.
writerIdleTimeout = schedule(ctx, this, writerIdleTimeNanos, TimeUnit.NANOSECONDS);
boolean first = firstWriterIdleEvent;
firstWriterIdleEvent = false;
try {
if (hasOutputChanged(ctx, first)) {
return;
}
IdleStateEvent event = newIdleStateEvent(IdleState.WRITER_IDLE, first);
channelIdle(ctx, event);
} catch (Throwable t) {
ctx.fireExceptionCaught(t);
}
} else {
// Write occurred before the timeout - set a new timeout with shorter delay.
writerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
}
}
}
observeOutput
//正常情况下,false,即写空闲的判断中的写是指写成功,但是实际上,有可能遇到几种情况:
//(1)写了,但是缓存区满了,写不出去;(2)写了一个大“数据”,写确实在“动”,但是没有完成。
//所以这个参数,判断是否有“写的意图”,而不是判断“是否写成功”。
keepalive其实本质也是用来检测链路是否正常,而idle检测看起来也是用来检测链路是否正常;tcp的keepalive是在7200s之后如果没有数据就进行发送,那会不会在之前idle就已经被触发然后断掉连接呢?
会的,如果你设置idle监测的时间低于你的keepalive时间,然后没数据,就会监测到空闲后就关闭连接,就是你说的情况,所以一般把空闲监测的时间设置的比keepalive大,这样就好了。当然这里说的都是应用层keepalive.你说的那个tcp层的keepalive对应用层的idle监测并没用,idle监测是应用层在做,它看的是应用层有没有数据,而不是tcp层的,另外idle监测主要是保护系统的,优化系统的,所以连接正常也可能触发的。
netty使用IdleStateHandler类做idle监测
1、对于读idle监测,IdleStateHandler的子类ReadTimeoutHandler会触发ReadTimeoutException异常,自定义的handler感知到这个异常可以做一些处理操作。
2、对于写idle监测,
a、如果observeOutput为false, 只有写入缓冲区成功 才算有写操作。
b、如果observeOutput为true,只要有写的意图 就算 有写操作,写意图包括:①写了,但缓冲区满了,没写成功 ②写了一个大数据,写确实在动,但没有完成。
如果有个场景:调用write方法写入了一个4个G的文件(一时半会写不完,假如每次只能写1k到缓冲区)
问题一:这对应的是b.②情况么?
问题二:如果observeOutput为false, 是要把4G都写入完缓冲区才算有写操作?
1、如果observeOutput为false,虽然发送的是4个G数据,只要有n字节(n>0)写入成功 都算有写操作.
2、如果发送的是4个G的数据,判断在一段时间内有没有全部把4G写完,是WriteTimeoutHandler干的事情。
问题1:对的;
问题2:对的。另外,这么大的文件,应该用chunk的方式那写了,否则容易OOM的。
内存使用技巧的目标
- 内存占用少(空间)
- 应用速度快(时间) 对Java而言,减少Full GC的STW(stop the world)时间
Netty 内存使用技巧 - 减少对像本身大小
- 用基本类型就不要用包装类,包装类明显占用的空间更大,之前有说到,包装类除了值,还带了一些信息,比如hash之些的对象头,还有引用地址
- 应该定义成类变量的不要定义为实例变量:一些类定义的属性变量,就直接在这个类里面定义,不要跑到引用的地方去一个一个的重新定义。 一个类 -> 一个类变量; 一个实例 -> 一个实例变量; 一个类 -> 多个实例; 实例越多,浪费越多。
- Netty 中结合前两者:io.netty.channel.ChannelOutboundBuffer#incrementPendingOutboundBytes(long, boolean),统计待写的请求的字节数
AtomicLong -> volatile long + static AtomicLongFieldUpdater
Netty 内存使用技巧 - 对分配内存进行预估
- 对于已经可以预知固定 size 的 HashMap避免扩容,可以提前计算好初始size或者直接使用 com.google.common.collect.Maps#newHashMapWithExpectedSize
public static <K, V> HashMap<K, V> newHashMapWithExpectedSize(int expectedSize) {
return new HashMap(capacity(expectedSize));
}
static int capacity(int expectedSize) {
if (expectedSize < 3) {
CollectPreconditions.checkNonnegative(expectedSize, "expectedSize");
return expectedSize + 1;
} else {
return expectedSize < 1073741824 ? (int)((float)expectedSize / 0.75F + 1.0F) : 2147483647;
}
}
- Netty 根据接受到的数据动态调整(guess)下个要分配的 Buffer 的大小。可参考 io.netty.channel.AdaptiveRecvByteBufAllocator
/**
* 接受数据buffer的容量会尽可能的足够大以接受数据
* 也尽可能的小以不会浪费它的空间
* @param actualReadBytes
*/
private void record(int actualReadBytes) {
//尝试是否可以减小分配的空间仍然能满足需求:
//尝试方法:当前实际读取的size是否小于或等于打算缩小的尺寸
if (actualReadBytes <= SIZE_TABLE[max(0, index - INDEX_DECREMENT - 1)]) {
//decreaseNow: 连续2次尝试减小都可以
if (decreaseNow) {
//减小
index = max(index - INDEX_DECREMENT, minIndex);
nextReceiveBufferSize = SIZE_TABLE[index];
decreaseNow = false;
} else {
decreaseNow = true;
}
//判断是否实际读取的数据大于等于预估的,如果是,尝试扩容
} else if (actualReadBytes >= nextReceiveBufferSize) {
index = min(index + INDEX_INCREMENT, maxIndex);
nextReceiveBufferSize = SIZE_TABLE[index];
decreaseNow = false;
}
}
Netty 内存使用技巧 - Zero-Copy
- 使用逻辑组合,代替实际复制。例如 CompositeByteBuf: io.netty.handler.codec.ByteToMessageDecoder#COMPOSITE_CUMULATOR
public static final Cumulator COMPOSITE_CUMULATOR = new Cumulator() {
@Override
public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
ByteBuf buffer;
try {
if (cumulation.refCnt() > 1) {
// Expand cumulation (by replace it) when the refCnt is greater then 1 which may happen when the
// user use slice().retain() or duplicate().retain().
//
// See:
// - https://github.com/netty/netty/issues/2327
// - https://github.com/netty/netty/issues/1764
buffer = expandCumulation(alloc, cumulation, in.readableBytes());
buffer.writeBytes(in);
} else {
CompositeByteBuf composite;
//创建composite bytebuf,如果已经创建过,就不用了
if (cumulation instanceof CompositeByteBuf) {
composite = (CompositeByteBuf) cumulation;
} else {
composite = alloc.compositeBuffer(Integer.MAX_VALUE);
composite.addComponent(true, cumulation);
}
//避免内存复制
composite.addComponent(true, in);
in = null;
buffer = composite;
}
return buffer;
} finally {
if (in != null) {
// We must release if the ownership was not transferred as otherwise it may produce a leak if
// writeBytes(...) throw for whatever release (for example because of OutOfMemoryError).
in.release();
}
}
}
};
- 使用包装,代替实际复制。
byte[] bytes = data.getBytes();
ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes); - 调用 JDK 的 Zero-Copy 接口。
Netty 中也通过在 DefaultFileRegion 中包装了 NIO 的 FileChannel.transferTo() 方法实现了零拷贝: io.netty.channel.DefaultFileRegion#transferTo
public long transferTo(WritableByteChannel target, long position) throws IOException {
long count = this.count - position;
if (count < 0 || position < 0) {
throw new IllegalArgumentException(
"position out of range: " + position +
" (expected: 0 - " + (this.count - 1) + ')');
}
if (count == 0) {
return 0L;
}
if (refCnt() == 0) {
throw new IllegalReferenceCountException(0);
}
// Call open to make sure fc is initialized. This is a no-oop if we called it before.
open();
long written = file.transferTo(this.position + position, count, target);
if (written > 0) {
transferred += written;
} else if (written == 0) {
// If the amount of written data is 0 we need to check if the requested count is bigger then the
// actual file itself as it may have been truncated on disk.
//
// See https://github.com/netty/netty/issues/8868
validate(this, position);
}
return written;
}
Netty 内存使用技巧 - 堆外内存
- 堆外内存生活场景:
夏日,小区周边的烧烤店铺,人满为患坐不下,店家常常怎么办?
解决思路:店铺门口摆很多桌子招待客人。
店内 -> JVM 内部 -> 堆(heap) + 非堆(non heap)
店外 -> JVM 外部 -> 堆外(off heap)
优点: - 更广阔的“空间 ”,缓解店铺内压力 -> 破除堆空间限制,减轻 GC 压力
- 减少“冗余”细节(假设烧烤过程为了气氛在室外进行:烤好直接上桌:vs 烤好还要进店内)-> 避免复制
缺点: - 需要搬桌子 -> 创建速度稍慢
- 受城管管、风险大 -> 堆外内存受操作系统管理 Netty 内存使用技巧 - 内存池
- 内存池生活场景:
点菜单的演进:
一张纸:一桌客人一张纸
点菜平板:循环使用 - 为什么引入对象池:
- 创建对象开销大
- 对象高频率创建且可复用
- 支持并发又能保护系统
- 维护、共享有限的资源
- 如何实现对象池?
- 开源实现:Apache Commons Pool
- Netty 轻量级对象池实现 io.netty.util.Recycler
源码解读Netty内存使用
怎么从堆外内存切换堆内使用?以UnpooledByteBufAllocator为例
//切换到unpooled的方式之一
.childOption(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT)
//切换到pool的方式之一
.childOption(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT)
堆外内存的分配本质?内存池实现?
io.netty.buffer.PooledDirectByteBuf
private static final Recycler<PooledDirectByteBuf> RECYCLER = new Recycler<PooledDirectByteBuf>() {
@Override
protected PooledDirectByteBuf newObject(Handle<PooledDirectByteBuf> handle) {
return new PooledDirectByteBuf(handle, 0);
}
};
//从“池”里借一个用
static PooledDirectByteBuf newInstance(int maxCapacity) {
PooledDirectByteBuf buf = RECYCLER.get();
buf.reuse(maxCapacity);
return buf;
}
io.netty.util.Recycler
public final T get() {
if (maxCapacityPerThread == 0) {
//表明没有开启池化
return newObject((Handle<T>) NOOP_HANDLE);
}
Stack<T> stack = threadLocal.get();
DefaultHandle<T> handle = stack.pop();
//试图从“池”中取出一个,没有就新建一个
if (handle == null) {
handle = stack.newHandle();
handle.value = newObject(handle);
}
return (T) handle.value;
}
io.netty.buffer.PooledByteBuf
//归还对象到“池”里去,pipeline的tail会调用
@Override
protected final void deallocate() {
if (handle >= 0) {
final long handle = this.handle;
this.handle = -1;
memory = null;
chunk.arena.free(chunk, tmpNioBuf, handle, maxLength, cache);
tmpNioBuf = null;
chunk = null;
recycle();
}
}
内存池/非内存池的默认选择及切换方式?
以io.netty.allocator.type为准,没有的话,安卓平台用非池化实现,其他用池化实现
io.netty.channel.DefaultChannelConfig#allocator
//默认bytebuf分配器
private volatile ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;
io.netty.buffer.ByteBufAllocator
ByteBufAllocator DEFAULT = ByteBufUtil.DEFAULT_ALLOCATOR;
io.netty.buffer.ByteBufUtil
static final ByteBufAllocator DEFAULT_ALLOCATOR;
static {
//以io.netty.allocator.type为准,没有的话,安卓平台用非池化实现,其他用池化实现
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 {
//io.netty.allocator.type设置的不是"unpooled"或者"pooled",就用池化实现。
alloc = PooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: pooled (unknown: {})", allocType);
}
DEFAULT_ALLOCATOR = alloc;
THREAD_LOCAL_BUFFER_SIZE = SystemPropertyUtil.getInt("io.netty.threadLocalDirectBufferSize", 0);
logger.debug("-Dio.netty.threadLocalDirectBufferSize: {}", THREAD_LOCAL_BUFFER_SIZE);
MAX_CHAR_BUFFER_SIZE = SystemPropertyUtil.getInt("io.netty.maxThreadLocalCharBufferSize", 16 * 1024);
logger.debug("-Dio.netty.maxThreadLocalCharBufferSize: {}", MAX_CHAR_BUFFER_SIZE);
}
怎么从堆外内存切换堆内使用?
io.netty.util.internal.PlatformDependent
// We should always prefer direct buffers by default if we can use a Cleaner to release direct buffers.
//使用堆外内存两个条件:1 有cleaner方法去释放堆外内存; 2 io.netty.noPreferDirect 不能设置为true
DIRECT_BUFFER_PREFERRED = CLEANER != NOOP
&& !SystemPropertyUtil.getBoolean("io.netty.noPreferDirect", false);
if (logger.isDebugEnabled()) {
logger.debug("-Dio.netty.noPreferDirect: {}", !DIRECT_BUFFER_PREFERRED);
}
- 方法 1:参数设置 io.netty.noPreferDirect = true;
- 方法 2:传入构造参数false ServerBootstrap serverBootStrap = new ServerBootstrap(); UnpooledByteBufAllocator unpooledByteBufAllocator = new UnpooledByteBufAllocator(false); serverBootStrap.childOption(ChannelOption.ALLOCATOR, unpooledByteBufAllocator)
堆外内存的分配本质?
io.netty.buffer.UnpooledDirectByteBuf#allocateDirect
/**
* Allocate a new direct {@link ByteBuffer} with the given initialCapacity.
*/
protected ByteBuffer allocateDirect(int initialCapacity) {
//此处调用JDK的allocateDirect来分配堆外内存
return ByteBuffer.allocateDirect(initialCapacity);
}
堆外内存时也可以使用内存池吗?如果可以那是不是对堆内的引用做的池化?
是的,因为堆外还是堆内相当于图书馆本身从哪进书,池化和非池化相当于书的使用模式,一个是借还模式,一个是买卖模式,池化的也只能是引用,引用指向堆外内存。
堆外内存和内存池,分别什么场景下使用的?还有这些优化是会提升哪方面的效率的吗?
堆外内存,主要是想存更多的对象,同时也减少对象都放堆里带来的GC压力,内存池主要是为了复用对象,直接减少对象产生; 两个是不同:有时候能用堆外内存,但是不见得能用对象/内存池(假设对象不能复用); 相同点在于都可以减少GC压力,减少STW时间,间接带来处理的平滑度。
channel、eventloop与eventloopgroup的关系
eventloop:为连接服务的执行器,说白了,就是一个死循环(loop)轮询、处理channel上发生的事件(event)。一个channel只会绑定到一个eventloop,但是一个eventloop一般服务于多个channel。 eventloopgroup: 假设就一个eventloop服务于所有channel,肯定会有瓶颈,所以搞一个组,相当于多线程了。
我一个bossgroup有8个线程, 在启动过程中, 初始化了8个NioEventGroup作为executor,注册serversocketchannel时采用power(2)方式选择了其中1个, 然后绑定了serversocketchannel, 问题是其他7个NioEventGroup是不是没有用?如果有用,用来干嘛?
是的,没有用,判断有没有用的标准,就是看绑定的端口多少,像服务器,一般都绑定一个地址启动,比如8080,所以其他的的用不到,但是你绑定2个端口启动就会用到了,比如一个绑定8080,一个绑定443.
pipeline是什么?
pipeline其实就像工厂的生产线,上面有很多道工序(channel handler),然后这个生产线开始就是接受原始数据,最终把产品加工,处理完,打包出去就结束了,本身还是很简单的,扩展性就在于这些工序可以随意定制,我们基于netty开发实际上就是写这些工序,然后有序组织起来。