Netty相关

NIO/BIO/AIO的区别

BIO:
一个请求一个线程,线程开销大。
伪异步:
将请求连接放入线程池,一对多。
NIO:
一个请求一个线程,但客户端发送的请求都会注册到多路复用器上,多路复用器轮询到连接有io请求时才启动一个线程进行处理。
AIO:
一个有效请求一个线程,客户端的io请求都是由os先完成后再通知服务器应用去启动线程进行处理。
BIO的stream是单向的,阻塞的,NIO的channel是双向的是面向缓冲区的,非阻塞的。
NIO特点:
事件驱动模型、单线程处理多任务、非阻塞io、io操作不再阻塞,而是返回0、基于block的传输比基于stream的传输更高效。io多路复用大大的提高了java网络应用的可伸缩性和实用性。基于Reactor线程实现。
在Reactor模式中,事件分发器等待某个事件或者可应用或可操作的状态发生,时间分发器就把这个时间传给实现注册好的时间处理函数或回调函数,有后者做实际的读写操作。如在Reactor中实现读:注册读就绪时间和相应的事件处理器,事件分发器等等待事件到来。事件到来,激活分发器,分发器调用事件对应的处理器、事件处理器完成实际的读操作,处理读到的数据,注册新的时间,然后返还控制权。

NIO的组成

Buffer:
与channel进行交互,数据从channel读入缓冲区,从缓冲区写入channel中的方法。filp():反转缓冲区,将posttion给limit,然后将position置位0。clear()清除缓冲区,将position置位0,将capacity传给limit。rewind()将position位置设置为0
DirectByteBuffer:可减少一次系统空间到用户空间的拷贝。但Buffer的创建和销毁成本很高,不可控。可以采用线程池来提高性能。直接缓冲区主要分配给那些易受基础系统的本机io操作影响的大型持久缓冲区。如果数据量比较小的中小应用情况下,可以考虑heapBuffer,有jvm管理。
channel:表示io源与目标打开的连接,双向,但不可直接访问数据,只能有buffer进行交互。
selector:可使用单独线程管理多个channel、Selector.open()方法可创建Selector。register方法向多路复用器注册通道。可以监听的时间类型:读写连接accept。注册时间后会产生一个SelectionKey。
SelectionKey: 表示SelectableChannel和Selector之间的注册关系。wakeup方法:使尚未返回的第一个选择操作立即返回,唤醒的原因是:注册了新的channel或者时间;channel关闭,取消注册;优先级更高的时间触发。

Pipe:两个线程之间的单向数据连接,数据会被写到sink通道,从source通道读取。
NIO服务端建立过程:
Selector.Open():打开一个Selector;ServerSocketChannel.open():创建服务端的channel;bind():绑定到某个端口,并配置非阻塞模式;register():注册channel和关注事件到Selector上;select()轮询拿到已经就绪的事件。

Netty的特点

高性能、异步事件驱动的NIO框架,提供了对TCP/UDP和文件传输的支持
使用更高效的Socket底层,对epoll空轮询引起的cpu飙升在内部进行了处理,避免了直接使用nio的陷阱,简化了nio的处理方式。
采用encoder、decoder支持
可使用接受、处理线程池提高连接效率,对重连和心跳检测都有支持。
可配置io线程数,tcp参数,tcp接受和发送缓冲区使用直接内存代替堆内存,通过内存池的方式循环利用bytebuf
通过引用计数器集市申请释放不再引用的对象,降低了GC频率
通过单线程串行化的方式,高效的reactor线程模型
大量使用volitale、cas和原子类、线程安全类的使用和读写锁的使用

Netty的线程模型

使用Reactor模型基于多路复用器接受并处理用户请求,内部实现了两个线程池,boss线程池和worker线程池、其中boss线程池的线程负责处理请求的accept事件,将接受到的accept事件的对应的socket封装到一个nioSocketChannel中,并交给work线程池,其中worker线程池负责处理read和write事件,由对应的handler处理。
单线程模型:所有io操作由一个线程完成,也就是多路复用、时间分发、处理都有在一个reactor线程上单独完成的。既要接受客户端的连接请求、向服务端发起连接。也要发送、读取请求或应答、响应消息。一个Nio线程同时处理成百上千的链路,性能上无法支撑,速度慢。若线程进入死循环,整个程序将不可用,对于高负载大并发的应用场景不合适。
多线程模型:有一个Nio线程acceptor只负责监听服务端,接受tcp连接请求;nio连接池负责网络io的操作,也就是消息的接受解码编码和发送;一个nio线程可以同时处理n条链路,但是一个链路只对应一个nio线程,为了避免并发操作问题的发生。
主从线程模型:acceptor线程用于绑定监听端口,接受客户端连接,将SocketChannel从住线程池的Reactor线程的多路复用器上移除,重新注册到sub线程池的线程上,用于处理io的读写等操作,从而保证mainReactor只负责认证和握手操作。

序列化协议

序列化是将对象转换为二进制编码(字节数据),主要用于网络传输、数据持久化等;而反序列化则是从网络或磁盘读取字节数组后还原成原对象,主要用于网络传输对象的解码,以便完成远程调用。
影响序列化性能的关键因素有:序列化后的码流大小、序列化的性能(cpu占用)、是否跨语言

java原生序列化

无法跨语言、序列化后码流大、序列化的性能差

xml

优点
人机可读性性好,可以指定元素的特性和名称。
缺点
序列化的数据只包含数据本身以及类的结构,不包含类型表示和程序集信息。只能系列化公共属性和字段,无法序列化方法;文件庞大,文件格式复杂

json

优点
兼容性高,数据格式简单,易于读写,序列化后数据较小,可扩展性好,兼容性好
缺点
数据描述性较差。

fastjson

接口简单易用,是java中最快的json库

thrift

不仅是序列化协议,还是一个rpc框架。
优点
序列化后体积小、速度快、支持多种语言和丰富的数据类型
缺点
跨防火墙访问时,不安全,不具可读性,不可与其他传输层协议如http协议搭配使用。无法持久化数据。

protobuf

将数据以.proto文件进行描述,通过代码生成工具可以生成对应数据结构的pojo对象和protobuf相关的属性和方法。
优点
序列化后码流小,性能高,结构化数据存储格式,通过标识字段的顺序可以实现协议的前向兼容,结构化的文档更容易管理和维护。
缺点
需要依赖于工具生成代码
试用场景
对性能要求高的RPC调用,具有良好的跨防火墙的访问属性,适合应用层对象的持久化。

Protobuf

支持的数据类型

bool/double/float/int32/int64/String/bytes/enum/message
限定符:required:必须赋值,不可为空 optional:非必须赋值可以为空 repeated:可以重复任意次数,包含0次 枚举:只能用指定的常量集中的一个值作为其值

基本规则

每个消息都必须有一个required字段,包含0或多个optional字段;repeated标识的字段可以包含0或多个数据;[1,15]之内的标识号在编码的时候都会占用一个字节,[16,2047]之内的标识号则占用两个字节,标识号一定不能重复、使用消息类型也可以将消息嵌套任意多层、可用嵌套消息类型来代替组。

消息升级原则

不要更改任意已有字段的数值标识;不能移除已经存在的required字段,optional和repeated类型的字段可以被移除,但保留标识号不可被重用。新增的字段必须是optional或repeated。因为旧版本无法读取和写入新增的required限定符的字段。

Netty的零拷贝

netty的接受和发送bytebuffer采用direct buffers,使用堆外直接内存进行socket读写,不需要进行字节缓冲区的二次拷贝。堆内存多了一次内存拷贝,jvm会将堆内存buffer拷贝一份到直接内存中,然后再写入socket中。bytebuffer由channelConfig分配,而channelConfig创建ByteBufAllocator默认使用Direct Buffer。

netty高性能的表现

心跳:
对服务端:定时清除限制会话inactive
对客户端:用来检测会话是否断开,是否重来,检测网络延迟,其中idleStateHandler类用来检测会话状态。
串行无锁化设计:
也就说是消息的处理尽可能在同一个线程内完成,期间不可切换线程,这样就避免了多线程的竞争和同步锁。表面上,串行设计对cpu利用率不高,并发不够。但是通过调整nio线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。
可靠性:
链路的有效性检测:链路空闲检测机制,读写空闲超时机制。 内存保护机制:通过内存池冲用ByteBuf
byteBuf的解码保护,优雅停机,不再接受新消息,推出前的预处理操作、资源释放操作。
安全性:支持ssl单向认证,双向认证和第三方ca认证。
高并发编程的体现:
volatile的大量、正确使用;cas和院子类的广泛使用;线程安全容器的使用;通过读写锁提升并发性能;
io通信三原则: 传输Aio、协议http、线程(主从多线程)

源码架构

image

  • NioEventLoopGroup(其实是MultithreadEventExecutorGroup) 内部维护一个类型为 EventExecutor children [], 默认大小是处理器核数 * 2, 这样就构成了一个线程池,初始化EventExecutor时NioEventLoopGroup重载newChild方法,所以children元素的实际类型为NioEventLoop。
  • 线程启动时调用SingleThreadEventExecutor的构造方法,执行NioEventLoop类的run方法,首先会调用hasTasks()方法判断当前taskQueue是否有元素。如果taskQueue中有元素,执行 selectNow() 方法,最终执行selector.selectNow(),该方法会立即返回。如果taskQueue没有元素,执行 select(oldWakenUp) 方法
  • select ( oldWakenUp) 方法解决了 Nio 中的 bug,selectCnt 用来记录selector.select方法的执行次数和标识是否执行过selector.selectNow(),若触发了epoll的空轮询bug,则会反复执行selector.select(timeoutMillis),变量selectCnt 会逐渐变大,当selectCnt 达到阈值(默认512),则执行rebuildSelector方法,进行selector重建,解决cpu占用100%的bug。
  • rebuildSelector方法先通过openSelector方法创建一个新的selector。然后将old selector的selectionKey执行cancel。最后将old selector的channel重新注册到新的selector中。rebuild后,需要重新执行方法selectNow,检查是否有已ready的selectionKey。
  • 接下来调用processSelectedKeys 方法(处理I/O任务),当selectedKeys != null时,调用processSelectedKeysOptimized方法,迭代 selectedKeys 获取就绪的 IO 事件的selectkey存放在数组selectedKeys中, 然后为每个事件都调用 processSelectedKey 来处理它,processSelectedKey 中分别处理OP_READ;OP_WRITE;OP_CONNECT事件。
  • 最后调用runAllTasks方法(非IO任务),该方法首先会调用fetchFromScheduledTaskQueue方法,把scheduledTaskQueue中已经超过延迟执行时间的任务移到taskQueue中等待被执行,然后依次从taskQueue中取任务执行,每执行64个任务,进行耗时检查,如果已执行时间超过预先设定的执行时间,则停止执行非IO任务,避免非IO任务太多,影响IO任务的执行。
  • 每个NioEventLoop对应一个线程和一个Selector,NioServerSocketChannel会主动注册到某一个NioEventLoop的Selector上,NioEventLoop负责事件轮询。
  • Outbound 事件都是请求事件, 发起者是 Channel,处理者是 unsafe,通过 Outbound 事件进行通知,传播方向是 tail到head。Inbound 事件发起者是 unsafe,事件的处理者是 Channel, 是通知事件,传播方向是从头到尾。
  • 内存管理机制,首先会预申请一大块内存Arena,Arena由许多Chunk组成,而每个Chunk默认由2048个page组成。Chunk通过AVL树的形式组织Page,每个叶子节点表示一个Page,而中间节点表示内存区域,节点自己记录它在整个Arena中的偏移地址。当区域被分配出去后,中间节点上的标记位会被标记,这样就表示这个中间节点以下的所有节点都已被分配了。大于8k的内存分配在poolChunkList中,而PoolSubpage用于分配小于8k的内存,它会把一个page分割成多段,进行内存分配。
  • ByteBuf的特点:支持自动扩容(4M),保证put方法不会抛出异常、通过内置的复合缓冲类型,实现零拷贝(zero-copy);不需要调用flip()来切换读/写模式,读取和写入索引分开;方法链;引用计数基于AtomicIntegerFieldUpdater用于内存回收;PooledByteBuf采用二叉树来实现一个内存池,集中管理内存的分配和释放,不用每次使用都新建一个缓冲区对象。UnpooledHeapByteBuf每次都会新建一个缓冲区对象。

你可能感兴趣的:(分布式)