1 引言
tomcat 与 netty 直接来对比,有些勉强。二者不管在服务定位,还是在技术架构中所处的位置,都不同。tomcat 是 HTTP 协议解决方案,而Netty是TCP(也支持UDP)协议解决方案。可以看出Netty更偏向上游,更基础一些,Tomcat更偏向应用,提供的功能更丰富,也更接近业务开发。
本文不侧重对比的全面性,只从“I/O”、“线程”两个视角分析,观察一下它们的设计理念、细节, 这也最能体现二者的特点。
2 I/O模型
从概念上,大致分4种(也有说5种的,这不是重点),分别是:阻塞模型、非阻塞模型、I/O复用模型、异步模型,都是一些帮助理解的概念,这些理论模型也不局限于网络I/O,个人理解,只要是“内核层“与“用户层”数据交互,都是适用的,这方资料很多,就不细述。
JDK早期只支持BIO阻塞模型,在JDK1.4 提供了对 NIO 非阻塞模型的支持,其后不断优化,在底层也由epoll 替换了select/poll,性能上有了大幅提升,在JDK1.7时提供了NIO2.0,新增了对异步套接字的支持,也就是AIO模型。
目前,JDK 中 NIO模型,不管是可靠性,还是性能,都很高,主流基础框架 spring boot 2.x 内嵌的tomcat 9,还是netty 4.x 都选择它作为 默认 的I/O处理方式,本文也只关注NIO相关内容。
3 tomcat
3.1 流程
以下是从外部视角,观察Tomcat 的NIO处理过程。
tomcat本身能承受的并发量,是由两个因素决定的 I/O链接数(也称为channel, 或 socket)、线程数。再就是请求平均耗时,正常应该都在100ms以内(但这个数字变化莫测,不同时间、场景,出入很大,只是个估摸值)。
QPS = 1000 / 请求平均耗时 * 线程数。
从公式看,好像跟“I/O链接数“无关啊,通常来说瓶颈在线程数,更确切是“请求平均耗时”。
当然,如果你的 最大I/O链接数 是10,就算你的线程池是10000,它也只能支持并发10的请求,也就是说 最大I/O链接数 是硬性指标。通常该值比较大,默认值 8192。
举例,如果最大I/O链接数为10000,最大线程数100,单笔请求耗时20ms,那么它的QPS理论上就是5000,也就是5000客户同时并发,100个线程在1s内轻松搞定。
如果客户的忍耐度是5s的话,再把最大I/O链接数调到30000,可以满足25000客户同时并发,仍然是100线程,在5s内搞定全部请求,当然了,它的QPS仍然是5000,这是瞬时的并发量,不可持续,否则,客户端就会有大量请求被拒,但tomcat服务本身,没有被压垮,仍然稳定。
说这么多,其实我想表达,tomcat的性能、稳定性,在它的业务场景内,其实不差。
3.2 I/O链接
主要涉及2个参数:
server.tomcat.max-connections:最大IO链接数,默认值8192,细节可参考上面描述。
server.tomcat.accept-count:内核accept队列大小,也就是内核TCP三次握手过程中“半链接”队列 + "全连接"队列的合计值,也就是BACKLOG值。悬停在内核,还没有被I/O线程 acceptor 取走。
如果较真,也可以说,tomcat 支持的最大链接数是这两个参数的合计值,客户端并发链接请求如果超过该值,就会立马收到被拒异常:Connection refused: connect。
3.3 线程
涉及2个参数:
server.tomcat.threads.max: 最大线程数,默认值200.
server.tomcat.threads.min-space: 最小线程数,默认值10
这里指的是业务线程数,至于I/O线程,因采用NIO非阻塞模型,它的数量跟CPU内核数有关。
3.4 超时
这里指的超时,是指服务端跟I/O链接相关的内容,涉及3个参数
server.tomcat.connection-timeout:
I/O线程已accept链接,等待read数据的最长时间,默认值 60000 ms
server.tomcat.keep-alive-timeout:
http请求已处理完成,并返回响应结果,keep-alive 保持I/O链接的最长时间,默认值 60000 ms
这两个参数都是tomcat服务端,为I/O链接不被耗尽的保护参数,超过限值,服务端就会主动关闭socket连接。
另外还有一个参数:
server.tomcat.max-keep-alive-requests,默认值 100
4 netty
4. 1 流程
以下是 netty 的NIO处理过程。
从图可以看出,netty 与 tomcat 在 I/O 处理部分,是很相似的,但netty在I/O 处理方面,做了增强,它有ChannelHandler链式处理逻辑,且整个过程非阻塞。对“业务”处理部分,完全由用户自己发挥。
4.2 I/O链接
netty 最引人注目的特点,可以支持大量长链接,通常在10万以上,甚至百万,理论上有多少内存,就能支持多少链接,每一个Socket链接对应一个文件句柄。
linux系统,单个进程打开的句柄是有限的,一般是1024,需要调整该参数。
同样,提供参数ChannelOption.SO_BACKLOG,来调节内核accept队列大小。
4.3 线程
Netty 本身只有I/O线程,由于全部逻辑非阻塞,线程很少, 默认为内核的2倍。
业务线程,完全由用户根据自己实际情况维护、管理。
4.4 超时
netty 服务端,具有"长链接"特性,默认不会出现连接、读写超时,但它提供了更灵活的读写状态检查机制,通过配置IdleStateHandler 可以检测链接的读写情况,以下是其构造函数。
IdleStateHandler(long readerIdleTime, long writerIdleTime, long allIdleTime, TimeUnit unit);
readerIdleTime: 表示多长时间未读。
writerIdleTime: 表示多长时间未写。
allIdleTime: 表示多长时间未读写。
unit:时间单位。
在超时时,就会触发ChannelHandler状态事件:
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent event = (IdleStateEvent)evt;
switch (event.state()) {
case READER_IDLE:
eventType = "读空闲"; break;
case WRITER_IDLE:
eventType = "写空闲"; break;
case ALL_IDLE:
eventType = "读写空闲"; break;
}
}
通过重写该事件,可以做心跳、关闭连接等处理。
另外,需要注意参数,ChannelOption.SO_TIMEOUT,ChannelOption.CONNECT_TIMEOUT_MILLIS 都是针对客户端,对服务端无效。
5 总结
(1) Netty 适用于处理大量"长链接"场合,例如:IM、消息推送等。
(2) Netty 提供了便捷的编解码、自定义协议能力,在高性能通信方面应用广泛,例如:阿里的Dubbo,RocketMQ、Hadoop的Avro等。
(3) 通过对Tomcat分析,可以看出,在性能调优方面,除常见的JVM参数外,I/O链接数、线程数是很重要的指标,尤其是线程大小,合理的设置,可以使硬件性能得到发挥,又不至于使大量线程拖慢请求响应时间,需要对I/O链接数、线程数的监控分析,来优化配置参数。