IO模型
IO操作分两步:
1)发起IO请求等待数据准备.
2)实际IO操作.
1. 阻塞与非阻塞
阻塞和非阻塞关注的是: 线程在等待调用结果(消息,返回值)时的状态.
阻塞调用是指调用结果返回之前,当前线程会被挂起。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
2.同步与异步
同步和异步关注的是:消息通信机制
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是 由调用者主动等待这个调用的结果。
而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状通知来通知调用者,或通过回调函数处理这个调用。
同步须要主动读写数据,在读写数据的过程中还是会阻塞。
异步仅仅须要I/O操作完毕的通知。并不主动读写数据,由操作系统内核完毕数据的读写。
同步IO和异步IO是针对用户应用程序和内核的交互。
Unix提供了五种IO模式,分别是:
阻塞IO
非阻塞IO
IO复用
信号驱动IO
异步IO
前四种IO模型都是同步IO操作,区别在于第一阶段,而他们的第二阶段是一样的:在数据从内核复制到应用缓冲区期间(用户空间),进程阻塞于recvfrom调用或者select()函数。 相反,异步I/O模型在这两个阶段都要处理。
阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。
同步IO和异步IO的区别就在于第二个步骤是否阻塞,如果实际的IO读写阻塞请求进程,那么就是同步IO,因此阻塞IO、非阻塞IO、IO复用、信号驱动IO都是同步IO,如果不阻塞,而是操作系统帮你做完IO操作再将结果返回给你,那么就是异步IO。
NioEventLoop
NioEventLoop中维护了一个线程和Selector,线程启动时会调用NioEventLoop的run方法,执行I/O任务(socket中accept、connect、read、write)和非I/O任务(添加到taskQueue中的任务,如register0、bind0等任务)。
两种任务的执行时间比由变量ioRatio控制,默认为50,则表示允许非IO任务执行的时间与IO任务的执行时间相等。
如果触发了epool cpu100%的bug,会发生什么?
selector.select(timeoutMillis)操作会立即返回,不会阻塞timeoutMillis,导致 currentTimeNanos 几乎不变,这种情况下,会反复执行selector.select(timeoutMillis),变量selectCnt 会逐渐变大,当selectCnt 达到阈值,则执行rebuildSelector方法,进行selector重建,解决cpu占用100%的bug。
[selector 如何修复 epoll bug]
1 对 Selector的select操作周期进行统计
2 每完成一次select操作进行一次计数
3 连续发生N(512)次空轮询则认为触发epoll bug,重新rebuild selector
服务端初始化过程
1)创建 boss线程池和work线程池。其中 boss 线程池的线程负责处理请求的 accept 事件,当接收到 accept 事件的请求时,把对应的 socket 封装到一个 NioSocketChannel 中,并交给 work线程池,其中 work 线程池负责请求的 read 和 write 事件,由对应的 Handler 处理
2)设置连接参数(SO_BACKLOG, SO_REUSEADDR)
3) 创建套接字,绑定监听地址和端口(initAndRegister)
a:创建NioServerSocketChannel :创建socket和pipeline
b:给 NioServerSocketChannel 的pipeline添加ChannelHandler 以便其注册到evetloop后调用 (init())
b:将server channel注册到eventloop (config().group().register(channel); )
c: 调用 b) 注册的handler 。将 ServerBootstrapAcceptor 注册到server channel的pipeline,以便新的连接创建后被调用(将用户指定的childhandler添加到新连接 channel的pipeline,并在 channel注册到eventloop 后被调用)。
d:向 selector 注册需要监听的网络事件(beginRead)。 selectionKey.interestOps(interestOps | SelectionKey.OP_ACCEPT);
e: 监听连接请求。
设计模式
避坑
-
在进行消息发送的时候做保护。
设置消息发送的高低水位,针对消息的平均大小、客户端并发接入数、JVM 内存大小进行计算,得出一个合理的高水位取值。服务端在推送消息时,对 Channel 的状态进行判断(channel.isWritable()),如果达到高水位之后,Channel 的状态会被 Netty 置为不可写,此时服务端不要继续发送消息,防止发送队列积压. -
消息接收防内存泄漏
Netty 的消息读取并不存在消息队列,但是如果消息解码策略不当,则可能会发生内存泄漏
2.1 对消息的最大长度做限制,当超过限制之后,抛出解码失败异常,用户可以选择忽略当前已经读取的消息,或者直接关闭链接。
2.2 Netty 的 ChannelHandler 支持串行和异步并发执行两种策略,==在将 ChannelHandler 加入到 ChannelPipeline 时,如果指定了 EventExecutorGroup,则 ChannelHandler 将由 EventExecutorGroup 中的 EventExecutor 异步执行。==这样的好处是可以实现 Netty I/O 线程与业务 ChannelHandler 逻辑执行的分离,防止 ChannelHandler 中耗时业务逻辑的执行阻塞 I/O 线程。
如果业务 ChannelHandler 中执行的业务逻辑耗时较长,消息的读取速度又比较快,很容易发生消息在 EventExecutor 中积压的问题,如果创建 EventExecutor 时没有通过 io.netty.eventexecutor.maxPendingTasks 参数指定积压的最大消息个数,则默认取值为 0x7fffffff,长时间的积压将导致内存溢出。 -
ByteBuf释放
3.1 在业务 ChannelInboundHandler 继承自 SimpleChannelInboundHandler 或者
3.2 在业务 ChannelInboundHandler 中调用 ctx.fireChannelRead(msg) 方法,让请求消息继续向后执行,直到调用到
DefaultChannelPipeline 的内部类 TailContext,由它来负责释放请求消息
4) 基于非内存池的请求 ByteBuf
如果业务使用非内存池模式覆盖 Netty 默认的内存池模式创建请求 ByteBuf
ch.config().setAllocator(UnpooledByteBufAllocator.DEFAULT)
也需要释放
5) 只要调用了 writeAndFlush 或者 flush 方法,在消息发送完成之后都会由 Netty 框架进行内存释放,业务不需要主动释放内存、】
6) 在服务端增加对客户端并发连接数的控制
服务端的流控算法如下:
获取流控阈值。
从全局上下文中获取当前的并发连接数,与流控阈值对比,如果小于流控阈值,则对当前的计数器做原子自增,允许客户端连接接入。
如果等于或者大于流控阈值,则抛出流控异常给客户端。
SSL 连接关闭时,获取上下文中的并发连接数,做原子自减。
在实现服务端流控时,需要注意如下几点:
流控的 ChannelHandler 声明为 @ChannelHandler.Sharable,这样全局创建一个流控实例,就可以在所有的 SSL 连接中共享。
通过 userEventTriggered 方法拦截 SslHandshakeCompletionEvent 和 SslCloseCompletionEvent 事件,在 SSL 握手成功和 SSL 连接关闭时更新流控计数器。
流控并不是单针对 ESTABLISHED 状态的 HTTP 连接,而是针对所有状态的连接,因为客户端关闭连接,并不意味着服务端也同时关闭了连接,只有 SslCloseCompletionEvent 事件触发时,服务端才真正的关闭了 NioSocketChannel,GC 才会回收连接关联的内存。
流控 ChannelHandler 会被多个 NioEventLoop 线程调用,因此对于相关的计数器更新等操作,要保证并发安全性,避免使用全局锁,可以通过原子类等提升性能。