阅读 143

Netty笔记(下)

Netty.common
要点1:
Netty的io线程又被称为事件循环组(eventLoopGroup),事件循环指的是不断轮询监听底层网络事件,并转交给channelHandleContext进行处理.每个事件循环对象都对应一条线程.

要点2:
Netty拓展了原本jdk的AbstractExecutorService接口 原本AbstractExecutorService接口只能返回jdk.future不支持设置监听器,也不允许从外部直接设置结果.netty拓展了一个promise可以满足上面的条件. Netty作为一个全异步网络通信框架,能够支持以回调的方式处理结果非常关键.

要点3:
DefaultPromise有一个特殊点,不能用事件循环线程调用get()方法,会在checkDeadLock中抛出异常.这是在暗示事件循环线程才是应该为promise设置结果的线程吗

要点4:
Netty定义了一种事件循环模型,内部有一条核心线程会轮询任务队列和定时任务队列,并执行任务,外部线程或者事件循环线程本身可以往任务队列中插入任务,而任务的执行都交由事件循环线程.任务队列使用mpsc队列实现最合适.

要点5:
AbstractScheduledEventExecutor内部存储定时任务的队列是一个非线程安全的队列.从子类来看,设置定时任务时会先检测当前线程是否是事件循环线程,如果是的话直接加入到定时任务队列中,如果是外部线程,会将插入到定时任务队列的动作作为一个任务,并存入到线程安全的阻塞队列,当事件循环线程开始执行普通任务时,再转移到定时任务队列,也就是实际上只有事件循环线程本身会访问定时任务队列.

要点6:
为什么要使用事件循环模型.netty基于reactor模型实现,用尽量少的线程处理网络io,配合jdk.selector实现非阻塞io,但是在等待选择器事件的过程中,线程还是处于空闲状态,为了进一步提高利用率,在等待过程中可以检测任务队列中是否有需要执行的任务.而任务可以由外部线程和本线程插入.这样可以进一步提升线程的利用率.但是需要注意一点,这种处理网络io的线程不适合执行一些cpu密集型的任务,如果任务耗时过长,线程对网络io的响应会变慢,吞吐量就会下降.最合适的方式就是将一些cpu密集的任务转移到用户自定义的业务线程中执行.

要点7:
Netty有关线程优化的一系列组件职责: FastThreadLocalRunnable: Runnable的一个包装类,因为在netty中存在大量依赖本地线程变量进行优化的类(比如PoolThreadLocalCache 在内存分配时有提到),使用本地线程变量就会存在资源泄露的风险.这个包装类会在runnable.run执行完后清理所有本地线程变量.

FastThreadLocalThread: 对比thread,只是在内部增加了一个InternalThreadLocalMap的成员变量.

InternalThreadLocalMap:存储线程私有变量,内部使用一个可扩容数组实现.

FastThreadLocal: 相当于是线程私有变量的包装类,当某个线程创建线程私有变量后会将变量用FastThreadLocal包裹,并插入到线程关联的InternalThreadLocalMap中. 在InternalThreadLocalMap中数组的第一个元素存储的是一个位图对象,记录了数组中哪些slot存储了私有变量,便于快速清理.

要点8:
Netty中存在一个Constant接口用于表示常量,每个常量被创建时会存储在常量池中,常量池的设计比较简单,直接使用了concurrentHashMap
要点9:
其实HashedWheelTimer的定时线程也可以看作一个事件循环组,模式和SingleThreadEventExecutor非常接近,通过一个mpsc队列接收外部多线程传入的新任务,定时线程则作为单消费者从队列中收集任务,设置到bucket中,因为只有单线程会访问bucket[]结构,所以也是无锁化.(容器操作尽量交给一个线程处理) 与jdk的ScheduledThreadPoolExecutor相比除了存储结构从二叉堆变成hash桶外还有一个关键点就是ScheduledThreadPoolExecutor访问二叉堆时需要加锁.

要点10:
Recycle 先简要介绍下recycle的作用 Recycle实际上是一种特化的对象池. 对象池是什么?对象池可以是跨线程的,也可以是单线程使用的.单线程的实现就可以用简单的数组,回收就插入数组,复用就从数组中获取元素. 对于跨线程对象池,任何线程都可以往内部插入元素,并且任何线程都可以从中取出元素.最简单的实现方式就是使用并发队列.

而recycle回收机制的设计基于netty特殊的线程模型. 由一条核心线程发散出去(也就是事件循环线程,当然可以用事件循环组.但是事件循环线程之间不应该相互影响,理想他们不应该有任何的锁竞争)当遇到一些cpu密集型任务时,使用者往往会将相关对象从事件循环线程传递到业务线程,并在业务线程触发回收方法,设计的目标应该是事件循环线程可以使用从业务线程回收的对象.或者事件循环线程使用自己回收的对象. 整理一下要实现的目标

  1. 事件循环线程可以使用本线程回收的对象
  2. 当事件循环线程将对象传播到业务线程时,在业务线程完成回收后,事件循环线程应该可以再复用该对象.
  3. 业务线程应当可以回收多个事件循环线程传播过来的对象,并且他们的存储容器应该是彼此隔离的,这样可以避免事件循环线程组之间的锁竞争.

如何实现目标: 首先每个创建对象的线程会对应stack对象,内部对应一个普通数组,针对本线程的回收对象/复用对象.都是直接访问这个数组.因为只有单线程操作数组,所以没有线程竞争.这里就满足了第一个目标 当a线程传播对象到b后,b线程发现不是在匹配的线程回收对象,会在FastThreadLocal<Map<Stack<?>, WeakOrderQueue>>结构中存储a线程与对象容器的映射关系.这样就实现了第三个目标 当a线程发现stack内的数组没有元素可用时,就会找到WeakOrderQueue并尝试转移对象到数组中,而b线程回收a线程对象时就往WeakOrderQueue中填充对象 简单来将WeakOrderQueue内部有一个数组,同时有一个readIndex/一个writerIndex代表2个读写指针,readIndex实际上只有a线程会使用,writeIndex是唯一的竞争点,冲突范围就非常小.(实际上看了代码内部有一个link结构,line.next也是一个竞争点,推测tail.lazySet(writeIndex + 1)可以刷新内存屏障)这样就实现了第二个目标

有一些实现细节需要注意 当b线程回收了a线程创建的对象后,对象有一个stack引用需要被置空,这个对象不应该阻止stack被gc回收.WeakOrderQueue内部有一个thread弱引用,同理queue不应该阻止线程被回收.FastThreadLocal<Map<Stack<?>, WeakOrderQueue>>中stack也是用弱引用包裹,b线程不应该阻止a线程的stack被回收

Netty.transport
要点1:
Netty可以通过Bootstrap/ServerBootstrap 快速启动,不同的连接逻辑由不同的channel实现。比如一般使用的都是NioSocketChannel/NioServerSocketChannel 引导程序内置了EventLoopGroup事件循环组,以及channelFactory封装了产生channel的逻辑。 还有一个channelHandler,内部定义了 如何处理从channel中收到的数据流。 也就是在使用引导程序前需要配置事件循环组,channel工厂,以及channel处理器。

引导程序的作用就是内部会自动完成从创建通信用的channel到将channel注册到事件循环线程的动作。这里的事件循环内部就包含了网络io数据流的接收/发送。 启动引导程序的入口是bind()方法

  1. 创建channel
  2. 装配channel
  3. 绑定到事件循环组上
  4. 将channel绑定到地址上

绑定地址操作会放到事件循环组中执行,因为它也是监听selector

Bootstrap针对客户端启动进行引导 Bootstrap.connect()逻辑

  1. 创建channel
  2. 装配channel
  3. 绑定到事件循环组
  4. 通过事件循环组执行连接逻辑

如何实现绑定以及连接到对端的逻辑有channel类自行定义 在Bootsrap.init() 中会将之前设置到内部的channelHandler追加到channel上 Channel本身关联一个pipeline对象,上面关联一组handler用于处理收到/发送的数据流,像编解码器都是实现了handler接口

ServerBootstrap 对应服务端引导程序 它有相比父类多了一个childHandler 为什么需要这个对象 实际上是这样的 拿nioSocketchannel/nioSocketServerChannel来说 服务端接收到的是channel,而不是数据流,通过这个channel可以向连接上的客户端发送数据,childHandler就是用来装配连接上的channel。 所以它的事件循环组也有2套 一套专门负责接收客户端的连接,另一套负责向客户端传输数据。 同时2套的负载也是不一样 所以需要的线程数也不一样(boss线程,worker线程)

要点2:
ChannelGroup 可以使用一个channelgroup统一管理一组channel,比如向group中所有channel发送消息flush等

要点3:
Channel可以拆解为3个核心部分

  1. eventLoop 代表这个channel绑定在了哪个事件循环上
  2. ChannelPipeline 该对象就是一组channelHandler,用于处理发送/接收到的消息,比如编解码器就是channelHandler
  3. Unsafe 一些底层操作都是通过该对象来完成的,具体的实现跟channel类型有关

DefaultChannelPipeline相关组件职责:
DefaultChannelPipeline:
实际上就是一个AbstractChannelHandlerContext链表.处理收到/发送的数据流.它有2个特殊的AbstractChannelHandlerContext(TailContext,HeadContext),执行一些缺省工作,比如将writer最终交由unsafe来执行

AbstractChannelHandlerContext:
首先该对象存在几个状态
INIT:对象刚刚初始化 (本对象可以脱离handler先创建)
ADD_PENDING:代表该handler设置到pipeline上 但是还没有绑定到eventloop上,是不起作用的
ADD_COMPLETE:已经注册到eventloop上,handler被pipeline认可 (因为channel绑定到eventloop是一个异步操作,而在此期间是可以操作channel相关的pipeline的,比如增加handler)
REMOVE_COMPLETE:handler从pipeline中移除\

它的所有方法都有一个固定的模式(这些方法名都以fireXXX开头),先判断本context是否指定了executor,如果指定了使用该executor执行handler任务,如果handler未设置则传递到下一个context继续处理事件(一般我们在handler的实现中也会将事件传递下去)如果没有指定默认使用channel.eventloop有的话使用自定义的业务线程,netty很多组件的设计都是基于这个线程模型,比如recycle。

DefaultChannelHandlerContext:
在AbstractChannelHandlerContext的基础上包含了handler属性

HeadContext:
作为一个特殊的context执行一些兜底工作,比如bind,connect,write,read(这些方法交由HeadContext.unsafe来执行),默认情况下该对象会被设置到pipeline的头部.

TailContext:
针对接收到的数据进行兜底处理,默认是NOOP

AbstractUnsafe:
Unsafe的骨架类,针对bind,connect,write,read,close..定义了一套模板方法,而核心实现逻辑则取决于channel的实现类. 它还包含了将channel注册到eventloop上的逻辑 并且针对write操作,还有一个ChannelOutboundBuffer对象用于管理所有本channel写出的数据,因为底层网络io想要最大化提升性能,就是采用批发送.这个缓存区就是将每次write的数据先存放到一个地方并在合适的时机统一刷盘,而当底层网络缓存区满的时候,就会设置高水位拒绝新的write请求 注意所有unsafe的操作都在eventloop线程执行.(操纵unsafe的是headContext,而该context没有自定义executor)unsafe操作就是各种跟jdkchannel交互,属于io密集型

AbstractNioChannel:
内部包含jdk.nio.channel,以及SelectionKey等特化于jdk.nio的类.并且指定了内部的unsafe是nioUnsafe类型,eventloop是NioEventLoop类型

AbstractNioUnsafe:
基本都是nio选择器的那一套操作,选择器是关联在NioEventLoop上的,Nio的操作套路跟netty本身没有太大关系就不展开了

AbstractNioByteChannel:
NioSocketChannel的父类,代表read到的是数据流. 在read过程中如果发现对端channel已经关闭,也就是半开状态,已经在4次挥手了.那么关闭本channel的input流,还是允许向外传输数据 在往该channel写入数据前,都会强制使用directByteBuf填装数据

RecvByteBufAllocator:
在AbstractNioByteChannel读取数据时,该对象是用来控制每次读取的大小的,一般使用的是AdaptiveRecvByteBufAllocator,会根据本次读取到的数据调整下次readByteBuf的大小.主要是为了避免buffer内存的浪费

NioSocketChannel:
实现bind,connect都是直接跟jdkchannel打交道,在将数据写入到网络io缓冲区的逻辑中可以看到也是有一个自动调节功能.

AbstractNioMessageChannel:
对应nio服务端channel,接收到的是客户端的channel对象

NioServerSocketChannel:
不支持write方法,也就是nioServerChannel不支持写入操作

NioEventLoop:
事件循环组内部包含一个selector,首先要使用一个channel前,需要先注册到eventloop上,对应NioEventLoop就是将channel注册到选择器上,当调用channel.read(),就是往selector上注册读事件.当外部线程调用事件循环对外开放的api时,会将执行逻辑包装成task存入到任务队列中,之后再由事件循环线程取出任务并执行. ioRate=100代表在处理完所有准备好的selectionKey后,需要处理完任务队列中所有任务才能进行下一次select.其他值代表处理任务队列的时间与处理selectionKey的时间的占比

SelectedSelectionKeySet:
netty优化了selector中存储selectionKey的容器

SelectedSelectionKeySetSelector:
适配jdk.selector与SelectedSelectionKeySet

要点4:
ChannelInitializer作为一个被@Sharable注解修饰的handler可以设置到多个pipeline上,每当某个channel成功注册到pipeline后,该对象会针对register事件对channel进行装配.

要点5:
ChannelOutboundBuffer,当调用channel.write时,数据不会立即写入到底层网络io缓冲区,而是尽可能批量写入,这些数据会先留存在容器中(ByteBuf),而ChannelOutboundBuffer对他们进行统一管理,当执行flush时,会取出ByteBuf并转换成ByteBuffer写入到底层缓冲区(这里提现了0拷贝,ByteBuffer实际上和ByteBuf共用一个内存块只是换了个包装).一个ChannelOutboundBuffer只对应一个channel,这样就只有一个事件循环线程访问ByteBuf链表不需要加锁(ByteBuf链表就是待刷盘的数据块).当囤积的数据过多时,一旦超过高水位,会拒绝之后的write请求.必须等到之后数据刷盘,水位下降并低于低水位时,才可以继续调用write

几种事件类型 只列举了几个比较重要的
handlerAdd:当channel注册到eventloop后,再往pipeline上添加channelHandler会触发.或者在注册到eventloop前设置到pipeline的handler,会在注册后触发.
channelRegistered: 代表channel被注册到了事件循环组上(实际上就是将selectionKey注册到selector上)
channelUnregistered:将selectionKey从selector注销,同时销毁pipeline
channelActive:针对服务端channel代表绑定到了本地,针对客户端channel代表连接到了服务器,以及服务端接收到的channel在注册到eventloop时就会触发active(headContext感知到channel被激活后会自动触发read,会往selector上注册read事件)
channelInactive:当检测到channel断开时触发,可以自定义handler进行重连
channelRead:在read事件准备完成后,每当读取到一块数据/一个客户端channel就触发一次read
channelReadComplete:某次read事件对应的所有数据都读取完后触发,此时会检查config.autoRead.如果为true继续注册read事件
channelWritabilityChanged:描述当前囤积的数量量情况(比如已经囤积了很多未发送的数据,此时不应该继续接收write调用.或者囤积的数据发送完毕允许继续调用write.这些限流功能需要用户自己实现)\

文章分类
后端
文章标签