文章目录
BIO
阻塞同步的通信模式:并发能力低
原理:
- 服务器通过一个Acceptor线程,负责监听客户端请求和为每个客户端创建一个新的线程进行链路处理
- 若客户端数量过多,频繁的创建和销毁线程会给服务器带来压力。可用线程池进行改良(伪异步IO)
NIO
非阻塞同步的通信模式
原理:
- 客户端和服务端之间通过Channel通信。NIO可以在Channel进行读写操作。这些操作会被注册到Selector多路复用器上。多路复用器通过一个线程不断的轮询这些Channel。找出已准备就绪的Channel进行IO操作
- 缓冲区Buffer:
1. BIO直接将数据写入读取到流Stream对象中
2. NIO的数据操作都在Buffer(实际上是一个数组)
3. 通道Channel:和流Stream不同。通道是双向的,可以进行数据的读写操作
4. 多路复用器Selector:不断的轮询注册注册在其上的通道,如果某个通道处于就绪状态,就会被selector轮询出来,然后通过selectionKey取得状态,进行后续的IO操作
NIO模型中通过SocketChannel和ServerChannel实现套接字通道通信。避免了为每个TCP链接创建一个现场
同步异步、阻塞非阻塞
- 同步异步区别:
数据拷贝阶段是否完全由操作系统处理 - 阻塞非阻塞:
针对发起IO请求操作后,是否有立刻返回一个标志信息而不让请求线程等待
AIO
非阻塞异步的通信模式
原理:
异步通道,读写都是一个Future对象。纯异步
BIO、NIO的区别?
- 线程模型不同:BIO一个连接一个线程,NIO一个请求一个线程
- BIO面向流Strem,NIO面向缓冲区Buffer
- BIO的各种操作时阻塞的,NIO的各种操作时非阻塞的
- BIO的Socket是单向的,NIO的Channel是双向的
Netty使用场景
- RocketMQ,分布式消息队列
- Dubbo,服务调用框架
- HDFS分布式文件系统
- 内部通信,数据分发,传输等都可以
Netty为何是高性能
- 线程模型:采用异步非阻塞的IO类库,基于Reactor模式实现,解决了传统同步阻塞IO模式下服务无法平滑处理客户端线性增长的问题
- 堆外内存:TCP接受和发送缓冲区采用直接内存代替堆内存,避免了内存复制,提高了IO读写性能
- 内存池化技术:支持使用内存池的方式循环利用ByteBuf,减少了对象的创建和销毁,内存池的内部实现是用一颗二叉查找树
- 队列优化:采用环形数组缓冲区,实现无锁化并发编程,代替传统的线程安全容器或锁
- 并发能力:合理使用线程安全容器、原子类等
- 降低锁竞争:关键资源的使用采用单线程串行化的方式,避免多线程并发访问带来的锁竞争和频繁切换上下文导致的额外CPU资源消耗
- 内存泄漏检测:通过引用计数器即使的释放不再被利用的对象,细粒度的内存管理降低 GC的频率,减少频繁GC带来的时延增大和CPU损耗
- 内存零拷贝:使用Direct Buffer,可以使用Zero-Copy
- 协议支持:提供对Protobuf等高性能序列化协议支持
- 使用反射技术直接操作SelectionKey,使用数组而不是java容器
Netty高可靠性
- 链路有效性检测:
长连接,使用心跳机制来保持链路检测,避免在系统空闲时因网络而断开- 读空闲超时机制:连续一段时间没有信息可读,发送心跳,检测链路。若多次检测没有读取心跳信息,可以主动关闭,重新连接
- 写空闲超时机制:连续一段时间没有信息发送,发送心跳,检测链路。若多次检测没有读取对方放回的心跳,可以主动关闭,重新连接
- 内存保护机制:
- 通过对象引用计数器对ByteBuf进行细粒度的内存申请和释放,对非法的对象引用进行检测和保护
- 可设置的内存容量上限,包括ByteBuf、线程池线程数等,避免异常请求耗光内存
- 优雅停机:当系统推出时,JVM通过注册的shutdownHook拦截推出信号量,然后执行推出操作,释放相关模块的资源占用,将缓冲区的消息处理完成或清空,将带刷新的数据持久化到磁盘和数据库中,等到资源回收和缓冲区消息处理完成之后再退出
Netty的扩展性
- 责任链模式:ChannelPipeline基于责任链模式开发,便于业务逻辑的拦截、定制和扩展
- 基于接口开发:关键的类库都提供接口或抽象类,便于用户自定义实现
- 提供大量系统参数:自行配置
Reactor模型
可以处理一个或多个输入源,并且通过service hanlder同步的将输入事件采用多路复用分发给响应的reques hander处理。
核心思想:将IO事件注册到多路复用器上,一旦有IO时间触发,将事件分发到事件处理器中,执行就绪IO事件对应的处理函数。模型中有3个重要的组件:
- 多路复用器:由操作系统提供接口
- 事件分离器:将多路复用器返回的就绪事件分发到事件处理器中
- 事件处理器:处理就绪时间处理函数
单Reactor单线程模型
单Reactor多线程模型
多Reactor多线程模型
业务线程池
在Reactor所在的线程中,进行读写操作,若读取的数据需要进行业务逻辑处理,并且这个业务逻辑需要对数据库缓存进行操作,那么就意味着进行业务逻辑处理的过程中,Reactor线程在这段时间内无法在注册了Reactor的Channel进行读写操作。多个Channel的所有读写操作变成了串行
解决思路:
创建业务线程池,把读取到的数据 提交到业务线程池中进行处理。这样,Reactor的Channel就不会被阻塞了,而Channel的所有读写操作就变成了并行
TCP沾包/拆包
拆包:一个完整的包可能会被TCP拆分成多个包进行分发
沾包:小的数据包封装成一个大的数据包发送
原因:
- 应用程序写入的字节大小大于套接字在发送缓冲区的大小,会发生拆包。小于则网卡将应用读懂次写入的数据发送到网络上,会发生沾包现象
- 待发送数据大于MISS最大报文长度,会进行拆包
- 接受数据的应用层没有及时读取缓冲区的数据,进行沾包
序列化
序列化:将对象序列化为二进制形式(字节数组),主要用于网络传输、数据持久化等
反序列化:将网络、磁盘等读取到的字节数组还原成原始对象,主要用于网络传输对象的解码
Netty的零拷贝实现
- Netty的接受呵呵发送ByteBuffer采用堆外直接内存Direct Buffer
1. 使用堆外内存直接进行socket读写,不需要进行字节缓冲区的二次拷贝;使用堆内内存会多一次内存拷贝,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才会写入socket中
2. Netty创建的ByteBuffer类型,由ChannelConfig配置的,而ChannelConfig配置的ByteBufAllocator默认创建Direct Buffer类型
3. CompositeByteBuf类,可以将多个ByteBuf合并为一个逻辑上的ByteBuf,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer
4. 通过FileRegion包装的FileChannel
5. 通过wrap方法,可以将数组、bytebuf、bytebuffer包装成一个netty bytebuf对象,进而避免了拷贝操作
原生的NIO存在Epoll Bug
- java NIO Epoll会导致selector空轮询,最终导致cpu百分百
- Netty解决方案
对selector的select操作周期进行统计,每完成一次空的select操作进行计数,若在某个周期内连续发生N此空轮询,判断触发了epoll死循环bug,此时,netty重建selector来解决。判断是否是其他线程发起的重连请求,若不是则将原socketchannel从旧的selector上取消注册,然后重新注册到新的selector上,最后将原来的selector关闭
Netty为什么要实现内存管理
IO读写是一个很频繁的操作,考虑到高效的网络传输,直接内存必然是最好的选择,但是直接内存的申请和释放都是高成本的操作,所以需要进行池化管理,多次重用。但是netty的池化技术不同于一般的对象池,连接池。byteBuf有大小,但是申请多大的直接内存进行池化又是一个大问题,太大会浪费内存,太小又会出现频繁的扩容和内存复制,所以需要一个合适的内存管理算法啊,解决高效分配内存的同时又解决内存碎片化的问题