转自:https://blog.csdn.net/C_J33/article/details/80737053
首先要明确要netty优化的几个主要的关注点。
1. 尽可能的复用EventLoopGroup。
这里就要涉及netty的线程模型了。netty实战的第七章里有很细致的阐释。简单说EventLoopGroup包含了指定数量(如果没有指定,默认是cpu核数的两倍,可以从源码中看到)的EvenetLoop,EvenetLoop和channel的关系是一对多,一个channel被分配给一个EventLoop,它生命周期中都会使用这个EventLoop,而EventLoop背后就是线程。见下图。
因此不需要每次都new出一个EventLoopGroup
,其本质上是线程分配,可以复用同一个EventLoopGroup
,减少资源的使用和线程的切换。特别是在服务端引导一个客户端连接的时候。如下:
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(new NioEventLoopGroup(), new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new SimpleChannelInboundHandler() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf byteBuf)
throws Exception {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class)
.group(ctx.channel().eventLoop())
.handler(new SimpleChannelInboundHandler() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf in)
throws Exception {
System.out.println("Received data");
}
});
ChannelFuture future = bootstrap.connect(new InetSocketAddress(xxx, 80));
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
// do something
}
});
}
});
bootstrap.bind(new InetSocketAddress(8080)).sync();
2. 使用EventLoop
的任务调度
在EventLoop的支持线程外使用channel,用
channel.eventLoop().execute(new Runnable() {
@Override
public void run() {
channel.writeAndFlush(data)
}
而不是直接使用channel.writeAndFlush(data)
;
前者会直接放入channel所对应的EventLoop的执行队列,而后者会导致线程的切换。
3. 减少ChannelPipline的调用长度
public class YourHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) {
// BAD (most of the times)
ctx.channel().writeAndFlush(msg);
// GOOD
ctx.writeAndFlush(msg);
}
}
前者是将msg从整个ChannelPipline中走一遍,所有的handler都要经过,而后者是从当前handler一直到pipline的尾部,调用更短。同样,为了减少pipline的长度,如果一个handler只需要使用一次,那么可以在使用过之后,将其从pipline中remove。
4. 减少ChannelHandler的创建
如果channelhandler是无状态的(即不需要保存任何状态参数),那么使用Sharable
注解,并在bootstrap时只创建一个实例,减少GC。否则每次连接都会new出handler对象。
@ChannelHandler.Shareable
public class StatelessHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) {}
}
public class MyInitializer extends ChannelInitializer {
private static final ChannelHandler INSTANCE = new StatelessHandler();
@Override
public void initChannel(Channel ch) {
ch.pipeline().addLast(INSTANCE);
}
}
同时需要注意ByteToMessageDecoder
之类的编解码器是有状态的,不能使用Sharable
注解。
5. 减少系统调用(Flush)的调用
flush操作是将消息发送出去,会引起系统调用,应该尽量减少flush操作,减少系统调用的开销。
同时也要减少write的操作, 因为这样消息会流过整个ChannelPipline。
6. 利用netty零拷贝,在IO操作时使用池化的DirectBuffer
在bootstrap配置参数的时候,使用.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)来指定一个池化的Allocator,并且使用ByteBuf buf = allocator.directBuffer();来获取Bytebuf。
PooledByteBufAllocator,netty会帮你复用(无需release,除非你后面还需要用到同一个bytebuf)而不是每次都重新分配ByteBuf。在IO操作中,分配直接内存而不是JVM的堆空间,就避免了在发送数据时,从JVM到直接内存的拷贝过程,这也就是zero copy的含义。
7. 一些配置参数的设置
ServerBootstrap启动时,通常 bossGroup 只需要设置为 1 即可,因为 ServerSocketChannel 在初始化阶段,只会注册到某一个 eventLoop 上,而这个 eventLoop 只会有一个线程在运行,所以没有必要设置为多线程。而 IO 线程,为了充分利用 CPU,同时考虑减少线上下文切换的开销,通常设置为 CPU 核数的两倍,这也是 Netty 提供的默认值。
在对于响应时间有高要求的场景,使用.childOption(ChannelOption.TCP_NODELAY, true)和.option(ChannelOption.TCP_NODELAY, true)来禁用nagle算法,不等待,立即发送。
8. 小心的使用并发编程技巧
千万不要阻塞EventLoop!包括了Thead.sleep() CountDownLatch 和一些耗时的操作等等,尽量使用netty中的各种future。如果必须尽量减少重量级的锁的的使用。
在使用volatile时,
坏的:
private volatile Selector selector;
public void method() {
selector.select();
....
selector.selectNow();
}
好的:先将volatile变量保存到方法栈中,jdk源码中大量的使用了这种技巧。
private volatile Selector selector;
public void method() {
Selector selector = this.selector;
selector.select();
....
selector.selectNow();
}