netty常见知识点

 Java读源码之Netty深入剖析解析netty各大组件细节/百万级性能调优/设计模式实际运用

前言:这是一门对Java开发人员非常重要的课程,源码的学习方式是不可逃避的。Netty也是大型互联网公司面试必备的问题,如果没有分布式开发经验,在面试时提出自己阅读过Netty源码,并能清晰表达的话。这部分内容会是很重要的加分项

netty

Netty 源码阅读的思考------耗时业务到底该如何处理,处理耗时业务的第一种方式-------handler 种加入线程池

nettty里面的任务分为定时任务(优先级队列)和普通任务(多输入单输出队列),定时任务队列会加载到普通任务队列里面执行,每隔64个会判断一下是否大于超时时间,定时任务队列里面维护着一个截止时间,截止时间到了,这个定时任务就会被拿出来放到普通任务队列里面执行。

Selector BUG出现的原因

若Selector的轮询结果为空,也没有wakeup或新消息处理,则发生空轮询,CPU使用率100%,

Netty的解决办法

  • 对Selector的select操作周期进行统计,每完成一次空的select操作进行一次计数,超过512次,则重建Selector

  • 没有阻塞就立马返回了,对应代码中处理时间小于超时时间,这个就叫JDK空轮询BUG

  • 若在某个周期内连续发生N次空轮询,则触发了epoll死循环bug。

  • ,判断是否是其他线程发起的重建请求,若不是则将原SocketChannel从旧的Selector上去除注册,重新注册到新的Selector上,并将原来的Selector关闭。

select方法执行原理

如果这个时候有新任务的话或用户唤醒的话,就会中断阻塞

Handlerhandler和child

在服务端的ServerBootstrap中增加了一个方法childHandler,它的目的是添加handler,用来监听已经连接的客户端的Channel的动作和状态。handler在初始化时就会执行,而childHandler会在客户端成功connect后才执行,这是两者的区别。

netty中handler的执行顺序

Handler与Servlet中的filter很像,通过Handler可以完成通讯报文的解码编码、拦截指定的报文、统一对日志错误进行处理、统一对请求进行计数、控制Handler执行与否。一句话,没有它做不到的只有你想不到的。

Netty中的所有handler都实现自ChannelHandler接口。按照输出输出来分,分为ChannelInboundHandler、ChannelOutboundHandler两大类。ChannelInboundHandler对从客户端发往服务器的报文进行处理,一般用来执行解码、读取客户端数据、进行业务处理等;ChannelOutboundHandler对从服务器发往客户端的报文进行处理,一般用来进行编码、发送报文到客户端。

Netty中,可以注册多个handler。ChannelInboundHandler按照注册的先后顺序执行;ChannelOutboundHandler按照注册的先后顺序逆序执行,

1服务的socket在哪里初始化?在哪里accept连接?

总结: EventLoopGroup bossGroup = new NioEventLoopGroup()发生了以下事情:

1、 为NioEventLoopGroup创建数量为:处理器个数 x 2的,类型为NioEventLoop的实例。 每个NioEventLoop实例 都持有一个线程,以及一个类型为LinkedBlockingQueue的任务队列

2、线程的执行逻辑由NioEventLoop实现
3、每个NioEventLoop实例都持有一个selector,并对selector进行优化ServerBootstrap外观,NioServerSocketChannel创建,初始化,注册selector,绑定端口,接受新连接

参考文章

服务器接受客户端过程(Netty 接受请求过程源码分析 (基于4.1.23))

  1. 服务器轮询 Accept 事件,获取事件后调用 unsafe 的 read 方法,这个 unsafe 是 ServerSocket 的内部类,该方法内部由2部分组成。
  2. doReadMessages 用于创建 NioSocketChannel 对象,该对象包装 JDK 的 Nio Channel 客户端。该方法会像创建 ServerSocketChanel 类似创建相关的 pipeline , unsafe,config。
  3. 随后执行 执行 pipeline.fireChannelRead 方法,并将自己绑定到一个 chooser 选择器选择的 workerGroup 中的一个 EventLoop。并且注册一个0,表示注册成功,但并没有注册读(1)事件.
  4. 在注册的同时,调用用户程序中设置的 ChannelInitializer handler,向管道中添加一个自定义的处理器,随后立即删除这个 ChannelInitializer handler,为什么呢?因为初始化好了,不再需要。
  5. 其中再调用管道的 channelActive 方法中,会将曾经注册过的 Nio 事件改成读事件,开始真正的读监听。到此完成所有客户端连接的读前准备。

总的来说就是:接受连接----->创建一个新的NioSocketChannel----------->注册到一个 worker EventLoop 上--------> 注册selecot Read 事件。

  NioEventLoop实例创建时,同时会创建一个Selector(通过SelectorProvider.open()),即每个NioEventLoop都持有一个selector实例,由此可见,NioEventLoopGroup的线程池容量,就是线程的个数,也是Bootstrap中持有的Selector的数量。每个NioEventLoop实例内部Thread负责select多个Channel的IO事件(NIO Selector.select),如果某个Channel有事件发生,则在内部线程中直接使用此Channel的Unsafe实例进行底层实际的IO操作。简单而言,就是让每个NioEventLoop管理一组Channel。

    

    对于ServerBootstrap而言,创建2个NioEventLoopGroup,其中“bossGroup”为Acceptor 线程池,这个线程池只需要一个线程(大于1,事实上没有意义),它主要是负责accept客户端链接,并创建SocketChannel,此后从“workerGroup”线程池(reactor)中以轮询的方式(next)取出一个NioEventLoop实例,并将此Channel注册到NioEventLoop的selector上,此后将由此selector负责监测Channel上的读写事件(并由此NioEventLoop线程负责执行)。由此可见,对于ServerBootstrap而言,bossGroup中的一个线程的selector只关注SelectionKey.OPT_ACCEPT事件,将接收到的客户端Channel绑定到workerGroup中的一个线程,全局而言ServerSocketChannel和SocketChannel并没有公用一个Selector,ServerSocketChannel单独使用一个selector(线程),而众多SocketChannel将会被依次绑定到workerGroup中的每个Selector;这在高并发环境中非常有效,每个Selector响应事件的及时性更强,如果Selector异常(比如典型的空轮询的BUG,即Select方法唤醒后缺得到一个空的key列表,而死循环下去,CPU空转至100%,这是由于Epoll的BUG引起),只需要rebuild当前Selector即可(Nettyselect唤醒后,如果没有获取到事件keys列表且这种空唤醒的次数达到阀值),影响的Channel数量比较有限。同时这也是为什么开发者不能在IO线程中使用阻塞方法(比如wait)的原因,同一个Selector下的所有Channel公用一个线程,这种阻塞其实会导致当前线程下其他Channel(包括当前Channel)中后续事件的select无法进行,因为一个Selector中的所有selectionKey都会在此线程中依次执行。

2 默认情况下,Netty服务端启动多少线程?何时启动?Netty如何解决JDK空轮询bug?Netty如何保证异步串行无锁化?

吃透高并发线程模型

深入理解Netty无锁化串行设计,精心设计的reactor线程模型将
榨干你的cpu,打满你的网卡,让你的应用程序性能爆表

3 Netty在哪里检测有新连接接入的?新连接是怎样注册到NioEventLoop线程的?

总结: EventLoopGroup bossGroup = new NioEventLoopGroup()发生了以下事情: 1、 为NioEventLoopGroup创建数量为:处理器个数 x 2的,类型为NioEventLoop的实例。 每个NioEventLoop实例 都持有一个线程,以及一个类型为LinkedBlockingQueue的任务队列 2、线程的执行逻辑由NioEventLoop实现 3、每个NioEventLoop实例都持有一个selector,并对selector进行优化。



作者:未名枯草
链接:https://www.jianshu.com/p/4e836460512c
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

boos reactor线程,监测新连接,创建NioSocketChannel,IO线
程分配,selector注册事件

4 Netty是如何判断ChannelHandler类型的?对于ChannelHandler的添加应遵循什么顺序?用户手动触发事件传播,不同触发方式的区别?

明晰事件传播机制脉络

大动脉pipeline,处理器channelHandler,inbound、outbound
事件传播,异常传播

ChannelHandler有2种:ChannelInboundHandler和ChannelOutboundHandler,从字面意思上说,inbound即为入站,outbound为出站;对于Netty通道而言,从Socket通道中read数据进入Netty Handler的方向为inbound,从Netty handler向Socket通道write数据的方向为outbound。上述例子,我们也看到,Encoder为outbound,Decoder为inbound。简而言之,Socket中read到数据后,将会从tail到head依次执行链表中的inbound handler实例的相关方法(比如channelRead,channelReadComplete方法);当开发者通过channelHandlerContext向Socket写入数据时,将会从head到tail方向依次执行链表中的outbound handler实例的相关方法(比如write)。pipeline中的Head Handler是一个outbound实例,它处于outbound方向的最后一站,由此可见它必须处理那些“bind”、“connect”、“read”(非read数据,而是向Channel注册感兴趣的事件,在channel注册成功后执行)操作,以及使用unsafe操作Socket写入实际数据;Tail Handler是一个inbound实例,它是Socket read到数据后,第一个交付的inbound,不过从源码来看,tail似乎并没有做什么实质性的操作。

5 Netty内存类别有哪些?如何减少多线程内存分配之间的竞争?不同大小的内存是如何进行分配的?零拷贝

攻破内存分配机制

堆外内存,堆内,由于堆外内存创建较慢,引入内存池概念,采用引用计数方式,

Netty中,ByteBuf根据其内存分配机制,有2种实现:HeapByteBuf和DirectByteBuf(直接内存分配),这一点和NIO一样。Netty为了提升buffer的使用效率,减少因分配内存和内存管理带来的性能开支,又将ByteBuf分为:Pooled和Unpooled两类,即是否将ByteBuf基于对象池。最终ByteBuf子类为:PooledHeapByteBuf,PooledDirectByteBuf,UnpooledDirectByteBuf,UnpooledHeapByteBuf。不过和NIO ByteBuffer不同的时,ByteBuf并没有提供视图类,比如CharBuffer、IntBuffer等。

    HeapByteBuf:同NIO种的HeapByteBuffer,即堆内存buffer,内存的创建和回收速度较快,缺点是内存Copy,即当HeapByteBuf的数据需要与Socket(或者磁盘)进行数据交换时,需要将数据copy到相应的驱动器缓冲区中,效率稍低。

    DirectByteBuf:直接内存,或者说堆外内存,有操作系统分配,所以分配和销毁的速度稍慢,一般适用于“使用周期较长、可循环使用”的场景,优点是进行跨介质数据交换时无需数据Copy,速度稍高一些。

    通常Socket数据直接操作端使用DirectByteBuf,上层业务处理阶段(比如编解码)可以使用HeapByteBuf。

    ByteBuffer还有一个比较不友好的设计:容量不可变;即一个ByteBuffer在创建时需要指定容量(ByteBuffer.allocate(capacity)),而且将会立即申请此capacity大小的字节数组空间,此后capacity不可调整。ByteBuf则稍有不同,它在创建时指定一个maxCapacity(最大容量),不过并不会立即申请此容量大小的数组空间,当首次write数据时,才会初始一定容量的空间,此后空间动态调整直到达到maxCapacity;如果ByteBuf中已分配(或者write需要)的容量小于4M时,首先分配64个字节,此后以double的方式扩容(128,512,1024...)直到4M为止,当容量达到4M后,当空间不足时每次递增4M(而不是double);以4M为分界(为什么是4M?),采取2中扩容手段,不仅能够提高扩容效率(<4M),而且可以避免内存盲目消耗(> 4M)。

    我们会发现ByteBuf继承了一个ReferenceCounted接口,上述我们谈到的ByteBuff的子类都实现了AbstractReferenceCountedByteBuf,为什么Netty要与“引用计数器”产生关系?JVM中使用“计数器”(一种GC算法)标记对象是否“不可达”进而收回,Netty也使用了这种手段来对ByteBuf的引用进行计数,如果一个ByteBuf不被任何对象引用,那么它就需要被“回收”,这种“回收”可能是放回对象池(比如Pooled ByteBuf)或者被销毁,对于Heap类型的ByteBuf则直接将底层数组置为null,对于direct类型则直接调用本地方法释放外部内存(unsafe.freeMemory)。Netty采用“计数器”来追踪ByteBuf的生命周期,一是对Pooled ByteBuf的支撑,二是能够尽快的“发现”那些可以回收的ByteBuf(非Pooled),以便提成ByteBuf的分配和销毁的效率。(参见:“引用计数器”

Zero-copy: 就是在操作数据时, 不需要将数据 buffer 从一个内存区域拷贝到另一个内存区域. 因为少了一次内存的拷贝, 因此 CPU 的效率就得到的提升

通过 CompositeByteBuf 实现(零拷贝)

假设我们有一份协议数据, 它由头部和消息体组成, 而头部和消息体是分别存放在两个 ByteBuf 中的, 即:

ByteBuf header = ...
ByteBuf body = ...

我们在代码处理中, 通常希望将 header 和 body 合并为一个 ByteBuf, 方便处理, 那么通常的做法是:

ByteBuf allBuf = Unpooled.buffer(header.readableBytes() + body.readableBytes());
allBuf.writeBytes(header);
allBuf.writeBytes(body);

可以看到, 我们将 header 和 body 都拷贝到了新的 allBuf 中了, 这无形中增加了两次额外的数据拷贝操作了.

那么有没有更加高效优雅的方式实现相同的目的呢? 我们来看一下 CompositeByteBuf 是如何实现这样的需求的吧.

ByteBuf header = ...
ByteBuf body = ...

CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer();
compositeByteBuf.addComponents(true, header, body);

6解码器抽象的解码过程是什么样的?
Netty里面有哪些拆箱即用的解码器?
如何把对象变成字节流,最终写到Socket底层?

掌握编解码原理

编解码顶层抽象,定长解码器,行解码器,分隔符解码器,基于
长度域解码器全面分析,编码抽象,writeAndFlush深入分析

Netty的高性能之道

1.Netty心跳

(1)定义:心跳其实就是一个简单的请求,

  • 对于服务端:会定时清除闲置会话inactive(netty5)channelclose(netty3)

  • 对于客户端:用来检测会话是否断开,是否重来,检测网络延迟!

(2)idleStateHandler类 用来检测会话状态

参考优秀博客netty实践

猜你喜欢

转载自blog.csdn.net/zhousenshan/article/details/82318885