性能优化高频面试题集锦2,彻底搞懂Reactor模型和Proactor模型
一、netty线程模型
1、传统阻塞 I/O 网络模型
2、Reactor网络模型
3、单Reactor单线程
4、单Reactor多线程
5、主从Reactor多线程
二、netty意外退出及优化
1、netty服务端意外退出问题重演
2、Java Daemon线程(守护线程)
3、netty服务端启动原理
4、NioEventLoop线程详解
5、Netty的ChannelFuture机制
6、如何防止Netty服务意外退出
7、实际项目中的优化策略
8、kill -9 pid强杀netty进程可能引发的问题
9、Java优雅退出机制
10、Java优雅退出注意点
11、Netty优雅退出机制
12、Netty优雅退出原理和源码分析
三、netty客户端连接池泄露及优化
1、Netty连接池资源泄漏问题重演
2、Netty连接池错误代码演示
3、Netty客户端运行之后抛出异常详解
4、异常原因:错用了NIO编程模式(本质上是BIO模型)
5、Netty连接池正确的创建方式代码演示
6、修改代码之后的线程模型详解
7、Bootstrap工具类的工作原理
8、并发安全和资源释放错误代码演示
9、java NIO客户端创建原理分析
10、Netty客户端创建原理分析
11、Bootstrap连接服务器原理
四、netty内存池泄露及优化
Netty内存池泄露故障复现
Netty内存池泄露错误代码片段详解
采集堆内存快照分析
问题排查详细过程
Netty内存释放深层解析(writeAndFlush方法)
Netty内存释放深层解析(read方法)
ByteBuf申请和释放场景分析
Netty内存池的性能压测对比
五、ByteBuf故障排查及优化
HTTP协议栈ByteBuf使用不当问题
HTTP协议栈ByteBuf正确使用解决方案
ByteBuf使用注意事项
java原生ByteBuffer的局限性
Netty ByteBuf工作原理分析
ByteBuf引用计数器工作原理和源码分析
六、netty发送队列积压及优化
Netty发送队列积压故障
Netty高并发故障复现
Netty高并发故障示例代码
Netty高并发故障异常信息分析
统计GC,老年代已满,发生多次Full GC分析
CPU被大量GC线程占用分析
dump内存,mat工具查看泄漏点(NioEventLoop)分析
大量WriteAndFlushTask及客户端发送信息积压分析
WriteAndFlashTask源码分析
如何防止队列积压?
Netty高水位机制原理
Netty消息积压其他因素
Netty消息发送机制
分析结论
ChannelOutboundBuffer原理和源码分析
Netty消息发送原理
七、api网关高并发性能波动及优化
Netty高并发性能波动故障
Netty高并发故障复现
故障示例代码分析
故障异常信息分析
统计GC,老年代已满,发生多次Full GC分析
CPU被大量GC线程占用分析
dump内存,mat工具查看泄漏点(ThreadPoolExecutor)分析
LinkedBlockingQueue中积压大量的char数组分析
故障原因猜测
故障根本原因
图解故障根本原因
故障解决优化方案
主动内存泄漏定位法
优化建议
八、netty Channelhandler并发安全陷阱
这样的代码安全吗?
串行执行的ChannelHandler
测试不同线程执行同一个ChannelHandler
跨链路共享ChannelHandler
共享ChannelHandler中变量的安全性
ChannelHandler并发陷阱的场景1
ChannelHandler并发陷阱的场景2
消息在ChannelPipeline中流转的原理图分析
ChannelPipeline通过链表管理ChannelHandler
九、netty ChannelHandler并发失效及优化
配置了线程池,但是业务ChannelHandler无法并发执行分析
设置客户端以100QPS的速度压测服务端,吞吐量个位数分析
原因排查,检查线程数,只有1个defaultEventExecutorGroup线程
DefaultEventExecutor源码解析
为什么无法并行执行?
并行执行优化策略1:使用EventExecutorGroup
并行执行优化策略2:使用ExecutorService
如何选择优化策略:图解优化策略1
如何选择优化策略:图解优化策略2
十、netty NioEventLoop线程夯住及优化
故障复现演示
故障排查:cpu,内存等指标都正常
故障排查:GC分析,正常
故障排查:dump线程,查看线程堆栈信息
故障原因分析
故障演示
NioEventLoop线程防夯死策略
Netty多线程最佳实践
十一、netty性能统计误区
时延毛刺问题
性能统计不一致分析
同步思维惹的祸
writeAndFlush处理流程分析
分析writeAndFlush方法,忽略的几个耗时
正确的消息发送速度性能统计策略
常见的消息发送性能统计误区
代码演示Netty关键性能指标采集策略
十二、netty事件触发策略使用不当案例
ChannelHandler调用问题
生产环境问题模拟重现
channelReadComplete方法调用
ChannelHandler职责链调用
十三、netty流量整形案例
通用流量整形功能
netty流量整形功能
流量整形示例代码
流量整形功能测试
流量整形工作原理和源码分析
并发编程在流量整形中的应用
使用流量整形的一些注意事项
一、为什么是nginx而不是apache?
1、轻量级:同样起web 服务,比apache 占用更少的内存及资源
2、静态处理:Nginx 静态处理性能比 Apache 高 3倍以上
3、抗并发:nginx 处理请求是异步非阻塞的,而apache则是阻塞型的,在高并发下nginx 能保持低资源低消耗高性能
4、高度模块化的设计,编写模块相对简单
5、社区活跃,各种高性能模块出品迅速
二、Nginx是如何做到高性能和高可扩展的?
1、事件驱动架构
2、一个主进程和若干worker进程和helper进程
3、每个worker进程以非阻塞的方式处理多个连接,这减少了上下文切换的次数
4、状态机的调度
三、Nginx运行工作进程数量优化
如何查看工作进程数?
调整worker进程数=CPU的核心或者核心数x2
调整后检查进程:ps -aux | grep nginx |grep -v grep
四、Nginx运行CPU亲和力优化
为什么要绑定 Nginx 进程到不同的CPU上?
如何分配不同的Nginx进程给不同的CPU处理?
配置案例演示
五、Nginx最大打开文件数优化
nginx报错打开文件数过多,原因是什么?
配置案例演示
六、Nginx事件处理模型
不同的操作系统会采用不同的 I/O 模型
常见的事件处理模型举例
linux下指定最佳事件处理模型
七、开启高效传输模式
nginx中的“零拷贝”
sendfile 参数详解
tcp_nopush 参数详解
开启sendfile的注意点(默认是开启的)
八、连接超时时间优化
什么是连接超时
连接超时的作用
连接超时存在的问题
设置连接超时
九、fastcgi调优
什么是CGI?
为什么选择FastCGI?
FastCGI的各大配置项详解
十、gzip调优
Gzip压缩作用
详解Gzip具体配置
十一、expires缓存调优
expires优点
expires缺点
expires具体配置
十二、防盗链
防盗链3种解决办法
设置防盗链的两种配置方案详解
相关参数解释
十三、内核参数优化
详解fs.file-max
详解net.ipv4.tcp_max_tw_buckets
详解net.ipv4.tcp_tw_recycle
......
十四、关于系统连接数的优化
详解worker_connections
worker_connections生效机制
Nginx性能优化 [ 配套教程 ] (smartan123.github.io)
Proactor 模式详解
proactor 结构模式在异步操作完成后触发服务请求的分配和分发 。
1. 举个例子吧
考虑一个需要同时处理多个请求的网络服务程序,比如,一个高效的 WEB 服务器需要并发的处理来自于不同客户端浏览器的 HTTP 请求。当一个用户希望从某个 URL 下载内容时,浏览器和 WEB 服务器建立连接并发送 HTTP 的 GET 请求。WEB 服务器顺序执行了:接收浏览器的连接事件,接受连接请求,读取请求,然后解析请求,发送指定文件给 WEB 浏览器,并关闭连接。

一种有效的 WEB 服务器实现方式是使用 reactor 模式配合相应式的事件分发模型(reactive event demultiplexing model)。当 WEB 浏览器连接 WEB 服务器时,reactor 创建并注册新的事件句柄,并关联事件的分配、分发。当 WEB 浏览器的 REQUEST 指令到达时,reactor 激活对应的事件并回调事件句柄,然后句柄完成读、解析、处理请求并发送文件给 WEB 浏览器。尽管这个模型可以很顺畅的编程实现,但是它难以支持大量用户同时请求或 / 和长连接的用户请求,因为所有的 HTTP 处理在事件分发层是顺序执行的。
一个可以更有效的实现并发 WEB 服务器的方法是使用同步多线程。在这种模型下,会针对 REQUEST 请求创建新的服务线程。每个线程同步的处理建立连接、HTTP 请求的读取、解析和文件传输操作,即服务器仅在完成操作后阻塞。尽管这是一个常见的并发模型,然而和 reactor 模式的缺陷一样,该模型存在着效率、编程复杂度、移植性低的问题。考虑到这些问题,reactor 模式和多线程模式并不是 WEB 服务器理想的解决办法,我们需要另一种可以同时处理多个服务请求的事件驱动应用模型。
2. 问题所在
事件驱动的应用,尤其是服务器,在一个分布式系统中必须异步的处理多个服务请求,并异步的完成对服务请求的处理。
比如上面提到的 WEB 服务器的例子,我们的 WEB 服务器需要能在 TCP 连接、HTTP 的 GET 请求到来时异步的接收完成事件。并且,WEB 服务器可以异步的激活 read 和 write 操作传输文件给 WEB 浏览器。当这些操作完成时,操作系统分别传递 READ 和 WRITE 完成事件给 WEB 服务器。
在激活特定的服务处理完成事件之前,应用必须分配、分发事件到指定的服务句柄。为了有效的实现这一点,我们需要保证以下几点要求:
(1)应用必须可以同时处理多个事件,而不会由于某个事件的长时间处理导致其他事件产生巨大的延迟。
(2)服务器需要最小化延迟,最大化吞吐量,同时避免 CPU 的不必要开销。
(3)服务器的设计必须尽量简化并发策略的使用。
(4)集成新的或者改进的服务,例如改变信息格式或增加服务端缓存,应该尽量减少修改和维护已有代码的代价。例如对服务器客户端的改进不应该调整通用的事件分发机制。
3. 解决方案
我们需要异步激活操作并整合来源于这些操作的异步分发的完成事件和处理这些事件的服务句柄。另外, 将针对特定应用、事件的分发和服务分配器和通用概念上的分发、分配模型解耦。对所有服务器端应用提供的服务,我们提供一个异步操作表现与之关联的异步的服务请求,并使用一个完成句柄处理包括了每个异步操作结果的完成事件。异步操作由客户端的请求激活,并由异步操作处理器进行处理。异步操作处理器可以告知何时该完成事件的操作已经处理完成。另外,异步操作处理器通知 proactor 分发和该异步操作相关的特定应用的实例化完成句柄。然后该完成句柄处理异步操作的结果。
4. 结构
proactor 模式主要由几下及部分组成:
(1) 和操作系统资源相关联的句柄:用于完成事件的生成、排队,如:网络连接、打开文件、计时、同步对象、和 IO 完成端口。

网络服务器为每个客户连接创建不同的套接字句柄。当异步连接、读、写操作执行完成时,完成事件会出现在这些句柄上。
(2)异步操作用于执行服务请求,比如异步的通过套接字句柄读写数据。当异步操作激活后,操作不需要借用回调线程的控制即可执行。因此从回调者角度看,操作的执行是异步的。

(3)完成句柄界定了由一个或多个钩子方法组成的接口,这些钩子函数抽象的代表当异步操作执行结束后,应用或服务对完成事件的处理。实例化的完成句柄继承自完成句柄,每个实例化的完成句柄实现了针对特定服务的方法。另外,实例化的完成句柄实现了继承的用于处理从外部资源传来的完成事件的钩子方法,如从远端客户端发送数据到服务端或者,或者应用内部产生的完成事件,如超时。当这些完成时间到达时,proactor 分配合适的实例化完成句柄实现的钩子方法。
在我们的例子中,WEB 服务器实现了两个实例化的完成句柄,HTTP 句柄(HTTP Handler)和 HTTP 接收器(HTTP Acceptor)来表现异步操作的完成处理。HTTP 句柄用于接收、处理、回复 HTTP 的 GET 请求。HTTP 接收器创建、连接 HTTP 句柄处理异步来自于远处客户端的顺序请求。

(4)异步操作通过异步操作处理器 (asynchronous operation processor) 执行完成动作,异步操作处理器通常由操作系统内核实现。当异步操作执行完成时,异步操作处理器委托合适的 proactor 执行后续的完成分发。
(5)proactor 即完成分发器 (completion dispatcher),用于在相应的异步操作完成后回调指定的实例化完成句柄。为了增强复用性、分离关注点,proactor 解耦了上层的完成句柄分发策略和底层的异步操作处理器提供的操作完成机制。

(6)客户端作为应用程序的实体初始化异步操作。这里客户端泛指任意可初始化异步操作的编程实体而不一定指的是 C/S 模型中的远端客户端。当服务器初始化异步操作时,它也可以扮演客户端的角色。当集活异步操作时,客户端也向异步操作处理器注册了完成句柄和 proactor。
在 WEB 服务器中,异步操作的客户端指的是服务器的控制线程。这些线程在 HTTP 接收器和 HTTP 句柄上初始化了异步的 accept 和 read/write 操作来处理来自于 WEB 服务器的 HTTP 的 GET 请求,这些请求实际上即是远端客户端。

下图是 proactor 模式的整体结构图。

5. 整体流程
(1) 为了异步式的执行服务,作为客户端的应用程序将异步操作传递给异步操作处理器以完成初始化。另外,客户端必须向异步操作处理器指明异步操作完成后回调哪个完成句柄执行后续的工作。
在 WEB 服务器的例子里,WEB 服务器的 HTTP 句柄指导操作系统通过指定的套接字句柄异步完成传来的 HTTP 的 GET 请求的读操作。为了请求这个操作,WEB 服务器必须指定应该使用哪个套接字句柄,同时指定读操作完成后应该调用哪个完成句柄执行后续的工作。
(2)客户端在异步操作处理器激活操作之后,操作和客户端可以和其他应用程序激活的异步操作一起并行的运行。
(3)当操作执行完后,异步操作处理器取回操作初始化时客户端限定的完成句柄和 proactor。然后异步操作处理器传递异步操作的结果和完成句柄给 proactor,由其完成分发。
在 WEB 服务器的例子里,如果 HTTP 句柄读取了 GET 请求,则异步操作处理报告合适的 Proactor 完成状态,如成功或失败,并返回读取的字节数等。
(4)proactor 分发完成句柄的钩子方法,并传递完成数据。
例如 WEB 服务器可能指示操作系统异步的传输大量的文件穿过网络。当操作系统成功的完成了每个异步写操作后,proactor 传递传递给完成句柄的钩子方法完成的写操作的字节数,这样下次激活时即可从对应的偏移量开始继续传输文件。

Proactor 模式详解_Ch_ty 的博客 - CSDN 博客_proactor
彻底搞懂Reactor模型和Proactor模型
在高性能的I/O设计中,有两个著名的模型:Reactor模型和Proactor模型,其中Reactor模型用于同步I/O,而Proactor模型运用于异步I/O操作。
想要了解两种模型,需要了解一些IO、同步异步的基础知识,点击查看
服务端的线程模型
无论是Reactor模型还是Proactor模型,对于支持多连接的服务器,一般可以总结为2种fd和3种事件,如下图:

2种fd
- listenfd:一般情况,只有一个。用来监听一个特定的端口(如80)。
- connfd:每个连接都有一个connfd。用来收发数据。
3种事件
- listenfd进行accept阻塞监听,创建一个connfd
- 用户态/内核态copy数据。每个connfd对应着2个应用缓冲区:readbuf和writebuf。
- 处理connfd发来的数据。业务逻辑处理,准备response到writebuf。
Reactor模型
无论是C++还是Java编写的网络框架,大多数都是基于Reactor模型进行设计和开发,Reactor模型基于事件驱动,特别适合处理海量的I/O事件。
Reactor模型中定义的三种角色:
- Reactor:负责监听和分配事件,将I/O事件分派给对应的Handler。新的事件包含连接建立就绪、读就绪、写就绪等。
- Acceptor:处理客户端新连接,并分派请求到处理器链中。
- Handler:将自身与事件绑定,执行非阻塞读/写任务,完成channel的读入,完成处理业务逻辑后,负责将结果写出channel。可用资源池来管理。
Reactor处理请求的流程:
读取操作:
- 应用程序注册读就绪事件和相关联的事件处理器
- 事件分离器等待事件的发生
- 当发生读就绪事件的时候,事件分离器调用第一步注册的事件处理器
写入操作类似于读取操作,只不过第一步注册的是写就绪事件。
1.单Reactor单线程模型
Reactor线程负责多路分离套接字,accept新连接,并分派请求到handler。Redis使用单Reactor单进程的模型。

消息处理流程:
- Reactor对象通过select监控连接事件,收到事件后通过dispatch进行转发。
- 如果是连接建立的事件,则由acceptor接受连接,并创建handler处理后续事件。
- 如果不是建立连接事件,则Reactor会分发调用Handler来响应。
- handler会完成read->业务处理->send的完整业务流程。
单Reactor单线程模型只是在代码上进行了组件的区分,但是整体操作还是单线程,不能充分利用硬件资源。handler业务处理部分没有异步。
对于一些小容量应用场景,可以使用单Reactor单线程模型。但是对于高负载、大并发的应用场景却不合适,主要原因如下:
- 即便Reactor线程的CPU负荷达到100%,也无法满足海量消息的编码、解码、读取和发送。
- 当Reactor线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重Reactor线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈。
- 一旦Reactor线程意外中断或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。
为了解决这些问题,演进出单Reactor多线程模型。
2.单Reactor多线程模型
该模型在事件处理器(Handler)部分采用了多线程(线程池)。

消息处理流程:
- Reactor对象通过Select监控客户端请求事件,收到事件后通过dispatch进行分发。
- 如果是建立连接请求事件,则由acceptor通过accept处理连接请求,然后创建一个Handler对象处理连接完成后续的各种事件。
- 如果不是建立连接事件,则Reactor会分发调用连接对应的Handler来响应。
- Handler只负责响应事件,不做具体业务处理,通过Read读取数据后,会分发给后面的Worker线程池进行业务处理。
- Worker线程池会分配独立的线程完成真正的业务处理,如何将响应结果发给Handler进行处理。
- Handler收到响应结果后通过send将响应结果返回给Client。
相对于第一种模型来说,在处理业务逻辑,也就是获取到IO的读写事件之后,交由线程池来处理,handler收到响应后通过send将响应结果返回给客户端。这样可以降低Reactor的性能开销,从而更专注的做事件分发工作了,提升整个应用的吞吐。
但是这个模型存在的问题:
- 多线程数据共享和访问比较复杂。如果子线程完成业务处理后,把结果传递给主线程Reactor进行发送,就会涉及共享数据的互斥和保护机制。
- Reactor承担所有事件的监听和响应,只在主线程中运行,可能会存在性能问题。例如并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能。
为了解决性能问题,产生了第三种主从Reactor多线程模型。
3.主从Reactor多线程模型
比起第二种模型,它是将Reactor分成两部分:
- mainReactor负责监听server socket,用来处理网络IO连接建立操作,将建立的socketChannel指定注册给subReactor。
- subReactor主要做和建立起来的socket做数据交互和事件业务处理操作。通常,subReactor个数上可与CPU个数等同。
Nginx、Swoole、Memcached和Netty都是采用这种实现。

消息处理流程:
- 从主线程池中随机选择一个Reactor线程作为acceptor线程,用于绑定监听端口,接收客户端连接
- acceptor线程接收客户端连接请求之后创建新的SocketChannel,将其注册到主线程池的其它Reactor线程上,由其负责接入认证、IP黑白名单过滤、握手等操作
- 步骤2完成之后,业务层的链路正式建立,将SocketChannel从主线程池的Reactor线程的多路复用器上摘除,重新注册到Sub线程池的线程上,并创建一个Handler用于处理各种连接事件
- 当有新的事件发生时,SubReactor会调用连接对应的Handler进行响应
- Handler通过Read读取数据后,会分发给后面的Worker线程池进行业务处理
- Worker线程池会分配独立的线程完成真正的业务处理,如何将响应结果发给Handler进行处理
- Handler收到响应结果后通过Send将响应结果返回给Client
总结
Reactor模型具有如下的优点:
- 响应快,不必为单个同步时间所阻塞,虽然Reactor本身依然是同步的;
- 编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销;
- 可扩展性,可以方便地通过增加Reactor实例个数来充分利用CPU资源;
- 可复用性,Reactor模型本身与具体事件处理逻辑无关,具有很高的复用性。
Proactor模型

模块关系:
- Procator Initiator负责创建Procator和Handler,并将Procator和Handler都通过Asynchronous operation processor注册到内核。
- Asynchronous operation processor负责处理注册请求,并完成IO操作。完成IO操作后会通知procator。
- procator根据不同的事件类型回调不同的handler进行业务处理。handler完成业务处理,handler也可以注册新的handler到内核进程。
消息处理流程:
读取操作:
- 应用程序初始化一个异步读取操作,然后注册相应的事件处理器,此时事件处理器不关注读取就绪事件,而是关注读取完成事件,这是区别于Reactor的关键。
- 事件分离器等待读取操作完成事件
- 在事件分离器等待读取操作完成的时候,操作系统调用内核线程完成读取操作,并将读取的内容放入用户传递过来的缓存区中。这也是区别于Reactor的一点,Proactor中,应用程序需要传递缓存区。
- 事件分离器捕获到读取完成事件后,激活应用程序注册的事件处理器,事件处理器直接从缓存区读取数据,而不需要进行实际的读取操作。
异步IO都是操作系统负责将数据读写到应用传递进来的缓冲区供应用程序操作。
Proactor中写入操作和读取操作,只不过感兴趣的事件是写入完成事件。
Proactor有如下缺点:
- 编程复杂性,由于异步操作流程的事件的初始化和事件完成在时间和空间上都是相互分离的,因此开发异步应用程序更加复杂。应用程序还可能因为反向的流控而变得更加难以Debug;
- 内存使用,缓冲区在读或写操作的时间段内必须保持住,可能造成持续的不确定性,并且每个并发操作都要求有独立的缓存,相比Reactor模型,在Socket已经准备好读或写前,是不要求开辟缓存的;
- 操作系统支持,Windows下通过IOCP实现了真正的异步 I/O,而在Linux系统下,Linux2.6才引入,并且异步I/O使用epoll实现的,所以还不完善。
因此在 Linux 下实现高并发网络编程都是以Reactor模型为主。
常见架构的进程/线程模型
Netty的线程模型
Netty采用的是主从线程模型。下面是Netty使用中很常见的一段代码。
public class Server {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childOption(ChannelOption.TCP_NODELAY, true)
.childAttr(AttributeKey.newInstance("childAttr"), "childAttrValue")
.handler(new ServerHandler())
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
}
});
ChannelFuture f = b.bind(8888).sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
对Netty示例代码进行分析:
- 定义了两个EventLoopGroup,其中bossGroup对应的就是主线程池,只接收客户端的连接(注册,初始化逻辑),具体的工作由workerGroup这个从线程池来完成。可以理解为老板负责招揽接待,员工负责任务完成。线程池和线程组是一个概念,所以名称里有group。之后就采用ServerBootstrap启动类,传入这两个主从线程组。
- 客户端和服务器建立连接后,NIO会在两者之间建立Channel,所以启动类调用channel方法就是为了指定建立什么类型的通道。这里指定的是NioServerSocketChannel这个通道类。
- 启动类还调用了handler()和childHandler()方法,这两个方法中提及的handler是一个处理类的概念,他负责处理连接后的一个个通道的相应处理。handler()指定的处理类是主线程池中对通道的处理类,childHandler()方法指定的是从线程池中对通道的处理类。
- 执行ServerBootstrap的bind方法进行绑定端口的同时也执行了sync()方法进行同步阻塞调用。
- 关闭通道采用Channel的closeFuture()方法关闭。
- 最终优雅地关闭两个线程组,执行shutdownGracefully()方法完成关闭线程组。
如果需要在客户端连接前的请求进行handler处理,则需要配置handler();如果是处理客户端连接之后的handler,则需要配置在childHandler()。option和childOption也是一样的道理。
boss线程池作用:
- 接收客户端的连接,初始化Channel参数。
- 将链路状态变更时间通知给ChannelPipeline。
worker线程池作用:
- 异步读取通信对端的数据报,发送读事件到ChannelPipeline。
- 异步发送消息到通信对端,调用ChannelPipeline的消息发送接口。
- 执行系统调用Task。
- 执行定时任务Task。
通过配置boss和worker线程池的线程个数以及是否共享线程池等方式,Netty的线程模型可以在以上三种Reactor模型之间进行切换。
tomcat的线程模型
Tomcat支持四种接收请求的处理方式:BIO、NIO、APR和AIO
- NIO 同步非阻塞,比传统BIO能更好的支持大并发,tomcat 8.0 后默认采用该模型。 使用方法(配置server.xml):<Connector port="8080" protocol="HTTP/1.1"/> 改为 protocol="org.apache.coyote.http11.Http11NioProtocol"
- BIO 阻塞式IO,tomcat7之前默认,采用传统的java IO进行操作,该模型下每个请求都会创建一个线程,适用于并发量小的场景。 使用方法(配置server.xml):protocol =" org.apache.coyote.http11.Http11Protocol"
- APR tomcat 以JNI形式调用http服务器的核心动态链接库来处理文件读取或网络传输操作,需要编译安装APR库。 使用方法(配置server.xml):protocol ="org.apache.coyote.http11.Http11AprProtocol"
- AIO 异步非阻塞 (NIO2),tomcat8.0后支持。多用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。 使用方法(配置server.xml):protocol ="org.apache.coyote.http11.Http11Nio2Protocol"
Nginx的进程模型
Nginx采用的是多进程(单线程)&多路IO复用模型。
工作模型:
- Nginx在启动后,会有一个master进程和多个相互独立的worker进程。
- 接收来自外界的信号,向所有worker进程发送信号,每个进程都有可能来处理这个连接。
- master进程能监控worker进程的运行状态,当worker进程退出后(异常情况下),会自动启动新的worker进程。
Redis的线程模型
点击查看笔者之前的博客,快速搞懂Redis的线程模型
Swoole的进程模型
点击查看笔者之前的博客,Swoole引擎原理的快速入门干货
彻底搞懂 Java 的网络 IO
IO 是 Input/Output 的缩写。Unix 网络编程中有五种 IO 模型:
-
blocking IO(阻塞 IO)
-
nonblocking IO(非阻塞 IO)
-
IO multiplexing(多路复用 IO)
-
signal driven IO(信号驱动 IO)
-
asynchronous IO(异步 IO)
背景
-
java.io 包基于流模型实现,提供 File 抽象、输入输出流等 IO 的功能。交互方式是同步、阻塞的方式,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞。
java.io 包的好处是代码比较简单、直观,缺点则是 IO 效率和扩展性存在局限性,容易成为应用性能的瓶颈。
java.net 下面提供的部分网络 API,比如 Socket、ServerSocket、HttpURLConnection 也时常被归类到同步阻塞 IO 类库,因为网络通信同样是 IO 行为。
-
在 Java 1.4 中引入了 NIO 框架 (java.nio 包),提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用 IO 程序,同时提供更接近操作系统底层的高性能数据操作方式。
-
在 Java7 中,NIO 有了进一步的改进,也就是 NIO2,引入了异步非阻塞 IO 方式,也被称为 AIO (Asynchronous IO),异步 IO 操作基于事件和回调机制。
基本概念
在学习 Java 的 IO 流之前,需要了解同步异步、阻塞非阻塞的基本概念。
同步与异步
同步和异步是针对应用程序和内核的交互而言的。
-
同步指的是用户进程触发 IO 操作并等待或者轮询的去查看 IO 操作是否就绪。例如:自己上街买衣服,自己亲自干这件事,别的事干不了。
-
异步指的是用户进程触发 IO 操作以后便开始做其他的事情,而当 IO 操作已经完成的时候会得到 IO 完成的通知。例如:告诉朋友自己合适衣服的尺寸、颜色、款式,委托朋友去买,然后自己可以去干别的事。同时,你还需要告诉朋友你家衣柜在哪,方便朋友买完之后,直接将衣服放到你的衣柜。(使用异步 I/O 时,Java 将 I/O 读写委托给 OS 处理,需要将数据缓冲区地址和大小传给 OS)。
阻塞与非阻塞
阻塞和非阻塞是针对进程在访问数据的时候,根据 IO 操作的就绪状态来采取的不同方式。
-
阻塞指的是当试图对该文件描述符进行读写时,如果当时没有东西可读,或暂时不可写,程序就进入等待状态,直到有东西可读或可写为止。去地铁站充值,发现这个时候充值员碰巧不在,然后我们就在原地等待,一直等到充值员回来为止。
-
非阻塞指的是如果没有东西可读,或不可写,读写函数马上返回,而不会等待。在银行里办业务时,领取一张小票,之后我们可以玩手机,或与别人聊聊天,当轮到我们时,银行的喇叭会通知,这时候我们就可以去办业务了。
注意,这里办业务的时候,还是需要我们也参与其中的。这和异步是完全不同的,是很多网上都在误导人的地方,后面会澄清这块误解。
I/O 模型分类
应用程序向操作系统发出 IO 请求:应用程序发出 IO 请求给操作系统内核,操作系统内核需要等待数据就绪,这里的数据可能来自别的应用程序或者网络。一般来说,一个 IO 分为两个阶段:
-
等待数据:数据可能来自其他应用程序或者网络,如果没有数据,应用程序就阻塞等待。
-
拷贝数据:将就绪的数据拷贝到应用程序工作区。
在 Linux 系统中,操作系统的 IO 操作是一个系统调用 recvfrom (),即一个系统调用 recvfrom 包含两步,等待数据就绪和拷贝数据。
同步阻塞 IO
在此种方式下,用户进程在发起一个 IO 操作以后,必须等待 IO 操作的完成,只有当 IO 操作完成之后,用户进程才能运行。JAVA 传统的 BIO 属于此种方式。

同步非阻塞 IO
在此种方式下,用户进程发起一个 IO 操作以后边可返回做其它事情,但是用户进程需要时不时的询问 IO 操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的 CPU 资源浪费。JAVA 的 NIO 就属于同步非阻塞 IO。

多路复用 IO
IO multiplexing 这个词可能有点陌生,但如果换成 select,epoll,大概就都能明白了,有时也称这种 IO 方式为事件驱动 IO。select/epoll 的好处就在于单个 process 就可以同时处理多个网络连接的 IO。它的基本原理就是 select/epoll 这个 function 会不断的轮询所负责的所有 socket,当某个 socket 有数据到达了,就通知用户进程。
有关 epoll 的详细解释,欢迎点击查看
多路复用中,通过 select 函数,可以同时监听多个 IO 请求的内核操作,只要有任意一个 IO 的内核操作就绪,都可以通知 select 函数返回,再进行系统调用 recvfrom () 完成 IO 操作。
这个过程应用程序就可以同时监听多个 IO 请求,这比起基于多线程阻塞式 IO 要先进得多,因为服务器只需要少数线程就可以进行大量的客户端通信。
上面描述的 select 函数,是 NIO 下的 selector 的成员函数。

信号驱动式 IO 模型
在 unix 系统中,应用程序发起 IO 请求时,可以给 IO 请求注册一个信号函数,请求立即返回,操作系统底层则处于等待状态(等待数据就绪),直到数据就绪,然后通过信号通知主调程序,主调程序才去调用系统函数 recvfrom () 完成 IO 操作。
信号驱动也是一种非阻塞式的 IO 模型,比起上面的非阻塞式 IO 模型,信号驱动式 IO 模型不需要轮询检查底层 IO 数据是否就绪,而是被动接收信号,然后再调用 recvfrom 执行 IO 操作。
比起多路复用 IO 模型来说,信号驱动 IO 模型针对的是一个 IO 的完成过程, 而多路复用 IO 模型针对的是多个 IO 同时进行时候的场景。 信号驱动式 IO 模型用下图表示,

异步 IO
在此种模式下,将整个 IO 操作(包括等待数据就绪,复制数据到应用程序工作空间)全都交给操作系统完成。数据就绪后操作系统将数据拷贝进应用程序运行空间之后,操作系统再通知应用程序,这个过程中应用程序不需要阻塞。

I/O 模型对比
举个现实生活中的例子:
如果你想吃一份卤肉饭,
-
同步阻塞:你到饭馆点餐,然后在那儿等着,还要一直喊:好了没啊!
-
同步非阻塞:在饭馆点完餐,就去遛狗了。不过遛一会儿,就回饭馆喊一声:好了没啊!
-
多路复用:遛狗的时候,接到饭馆电话,说饭做好了,让您亲自去拿。
-
异步非阻塞:饭馆打电话说,我们知道您的位置,一会儿给你送过来,安心遛狗就可以了。

澄清很多人的误区
网络上一些热度很高的博客给初学者造成了很多的误解,所以这里做一个澄清。
阻塞、非阻塞、多路 IO 复用,都是同步 IO,异步必定是非阻塞的,所以不存在异步阻塞和异步非阻塞的说法。真正的异步 IO 需要 CPU 的深度参与。换句话说,只有用户线程在操作 IO 的时候根本不去考虑 IO 的执行,全部都交给 CPU 去完成,而只需要等待一个完成信号的时候,才是真正的异步 IO。所以,fork 子线程去轮询、死循环或者使用 select、poll、epoll,都不是异步。
BIO
在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞。
传统的服务器端同步阻塞 I/O 处理(也就是 BIO,Blocking I/O)的经典编程模型。
public class IOServer { public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(8000);
// (1) 接收新连接线程 new Thread(() -> { while (true) { try { // (1) 阻塞方法获取新的连接 Socket socket = serverSocket.accept();
// (2) 每一个新的连接都创建一个线程,负责读取数据 new Thread(() -> { try { int len; byte[] data = new byte[1024]; InputStream inputStream = socket.getInputStream(); // (3) 按字节流方式读取数据 while ((len = inputStream.read(data)) != -1) { System.out.println(new String(data, 0, len)); } } catch (IOException e) { } }).start();
} catch (IOException e) { }
} }).start(); }}
这是一个经典的每连接每线程的模型,之所以使用多线程,主要原因在于 socket.accept ()、socket.read ()、socket.write () 三个主要函数都是同步阻塞的,当一个连接在处理 I/O 的时候,系统是阻塞的。
这个模型严重依赖于线程,但线程是很” 贵” 的资源,主要表现在:
-
线程的创建和销毁成本很高,在 Linux 这样的操作系统中,线程本质上就是一个进程。创建和销毁都是重量级的系统函数。
-
线程本身占用较大内存,像 Java 的线程栈,一般至少分配 512K~1M 的空间,如果系统中的线程数过千,占用的内存将非常惊人。
-
线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统 load 偏高、CPU sy 使用率特别高(超过 20% 以上),导致系统几乎陷入不可用的状态。
-
容易造成锯齿状的系统负载。因为系统负载是用活动线程数或 CPU 核心数,一旦线程数量高而且外部网络环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大量阻塞线程从而使系统负载压力过大。
所以,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。随着移动端应用的兴起和各种网络游戏的盛行,百万级长连接日趋普遍,此时,NIO 技术应运而生。
NIO
基于事件驱动思想,采用 reactor(反应器)模式。当发起 IO 请求时,应用程序是非阻塞的。当 SOCKET 有流可读或写的时候,由操作系统通知应用程序,应用程序再将流读取到缓冲区或者写入系统。
有关 NIO 的详解,点击博主之前的博客,进行学习。
AIO
同样基于事件驱动的思想,通常采用 Proactor (前摄器模式) 实现。在进行 I/O 操作时,直接调用 API 的 read 或 write,这两种方法均为异步。对于读操作,操作系统将数据读到缓冲区,并通知应用程序,对于写操作,操作系统将 write 方法传递的流写入并主动通知应用程序。它节省了 NIO 中 select 函数遍历事件通知队列的代价 (红黑树遍历)。
增加的新的类如下:
-
AsynchronousChannel:支持异步通道,包括服务端 AsynchronousServerSocketChannel 和普通 AsynchronousSocketChannel 等实现。
-
CompletionHandler:用户处理器。定义了一个用户处理就绪事件的接口,由用户自己实现,异步 io 的数据就绪后回调该处理器消费或处理数据。
-
AsynchronousChannelGroup:一个用于资源共享的异步通道集合。处理 IO 事件和分配给 CompletionHandler
另外,主要在 java.nio.channels 包下增加了下面四个异步通道:
-
AsynchronousSocketChannel
-
AsynchronousServerSocketChannel
-
AsynchronousFileChannel
-
AsynchronousDatagramChannel
AIO 的实施需充分调用 OS 参与,IO 需要操作系统支持、并发也同样需要操作系统的支持,所以性能方面不同操作系统差异会比较明显。因此在实际中 AIO 使用不是很广泛。
代码就放弃展示了,毕竟没使用过,而且有 netty 的广泛使用,AIO 并没有太多使用的地方
Netty 使用 NIO 放弃使用 AIO 的原因
关于 AIO,有个很热门的话题,就是 Netty 并没有使用 AIO,只使用了 NIO。
至于原因,先看下作者原话:
-
Not faster than NIO (epoll) on unix systems (which is true)
-
There is no daragram suppport
-
Unnecessary threading model (too much abstraction without usage)
扩展一下如下:
-
Netty 不看重 Windows 上的使用(这也不只是 netty 这一个开源框架的事)。在 Linux2.6 之后系统上,AIO 的底层实现仍使用 EPOLL,由于实现方式的不成熟,因此在性能上没有明显的优势,而且被 JDK 封装了一层不容易深度优化
-
Netty 整体架构是 reactor 模型,而 AIO 是 proactor 模型,混合在一起会非常混乱,把 AIO 也改造成 reactor 模型看起来是把 epoll 绕个弯又绕回来
-
AIO 有个重要的缺点是接收数据需要预先分配缓存,而 NIO 只需要在接收时才分配缓存,所以对连接数量非常大但流量小的情况,造成了大量的内存浪费。
BIO/NIO/AIO 适用场景
-
BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,但程序直观简单易理解。
-
NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4 开始支持。
-
AIO 方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK7 开始支持。

浙公网安备 33010602011771号