基于netty的长连接
Netty 是什么
Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.
官方的解释最精准了,期中最吸引人的就是高性能了。但是很多人会有这样的疑问:直接用 NIO 实现的话,一定会更快吧?就像我直接手写 JDBC 虽然代码量大了点,但是一定比 iBatis 快!
但是,如果了解 Netty 后你才会发现,这个还真不一定!
利用 Netty 而不用 NIO 直接写的优势有这些:
- 高性能高扩展的架构设计,大部分情况下你只需要关注业务而不需要关注架构
Zero-Copy
技术尽量减少内存拷贝- 为 Linux 实现 Native 版 Socket
- 写同一份代码,兼容 java 1.7 的 NIO2 和 1.7 之前版本的 NIO
Pooled Buffers
大大减轻申请Buffer
和释放Buffer
的压力- ......
特性太多,大家可以去看一下《Netty in Action》这本书了解更多。
另外,Netty 源码是一本很好的教科书!大家在使用的过程中可以多看看它的源码,非常棒!
瓶颈是什么
想要做一个长链服务的话,最终的目标是什么?而它的瓶颈又是什么?
其实目标主要就两个:
- 更多的连接
- 更高的 QPS
所以,下面就针对这连个目标来说说他们的难点和注意点吧。
更多的连接
非阻塞 IO
其实无论是用 Java NIO 还是用 Netty,达到百万连接都没有任何难度。因为它们都是非阻塞的 IO,不需要为每个连接创建一个线程了。
欲知详情,可以搜索一下BIO
,NIO
,AIO
的相关知识点。
BIO是基于事件驱动思想的,用select/epoll来实现。
较之NIO而言,AIO一方面简化了程序出的编写,流的读取和写入都由操作系统来代替完成;另一方面省去了NIO中程序要遍历事件通知队列(selector)的代价。
BIO与NIO一个比较重要的不同,是我们使用BIO的时候往往会引入多线程,每个连接一个单独的线程;而NIO则是使用单线程或者只使用少量的多线程,每个连接共用一个线程。
NIO的最重要的地方是当一个连接创建后,不需要对应一个线程,这个连接会被注册到多路复用器上面,所以所有的连接只需要一个线程就可以搞定,当这个线程中的多路复用器进行轮询的时候,发现连接上有请求的话,才开启一个线程进行处理,也就是一个请求一个线程模式。
在NIO的处理方式中,当一个请求来的话,开启线程进行处理,可能会等待后端应用的资源(JDBC连接等),其实这个线程就被阻塞了,当并发上来的话,还是会有BIO一样的问题。
更高的 QPS
由于 NIO 和 Netty 都是非阻塞 IO,所以无论有多少连接,都只需要少量的线程即可。而且 QPS 不会因为连接数的增长而降低(在内存足够的前提下)。
而且 Netty 本身设计得足够好了,Netty 不是高 QPS 的瓶颈。那高 QPS 的瓶颈是什么?
是数据结构的设计!
如何优化数据结构
首先要熟悉各种数据结构的特点是必需的,但是在复杂的项目中,不是用了一个集合就可以搞定的,有时候往往是各种集合的组合使用。
既要做到高性能,还要做到一致性,还不能有死锁,这里难度真的不小…
我在这里总结的经验是,不要过早优化。优先考虑一致性,保证数据的准确,然后再去想办法优化性能。
因为一致性比性能重要得多,而且很多性能问题在量小和量大的时候,瓶颈完全会在不同的地方。 所以,我觉得最佳的做法是,编写过程中以一致性为主,性能为辅;代码完成后再去找那个 TOP1,然后去解决它!
Netty是一个基于事件的NIO框架。在Netty中,一切网络动作都是通过事件来传播并处理的,例如:Channel读、Channel写等等。回忆下Netty的流处理模型:
Boss线程(一个服务器端口对于一个)---接收到客户端连接---生成Channel---交给Work线程池(多个Work线程)来处理。
具体的Work线程---读完已接收的数据到ChannelBuffer---触发ChannelPipeline中的ChannelHandler链来处理业务逻辑。
注意:执行ChannelHandler链的整个过程是同步的,如果业务逻辑的耗时较长,会将导致Work线程长时间被占用得不到释放,从而影响了整个服务器的并发处理能力。所以,为了提高并发数,一般通过ExecutionHandler线程池来异步处理ChannelHandler链(worker线程在经过 ExecutionHandler后就结束了,它会被ChannelFactory的worker线程池所回收)。在Netty中,只需要增加一行代码
public ChannelPipeline getPipeline() { return Channels.pipeline( new DatabaseGatewayProtocolEncoder(), new DatabaseGatewayProtocolDecoder(), executionHandler, // Must be shared new DatabaseQueryingHandler()); }
例如:
ExecutionHandler executionHandler = new ExecutionHandler( new OrderedMemoryAwareThreadPoolExecutor(16, 1048576, 1048576))
对于ExecutionHandler需要的线程池模型,Netty提供了两种可选:
1) MemoryAwareThreadPoolExecutor 通过对线程池内存的使用控制,可控制Executor中待处理任务的上限(超过上限时,后续进来的任务将被阻塞),并可控制单个Channel待处理任务的上限,防止内存溢出错误;
2) OrderedMemoryAwareThreadPoolExecutor 是 MemoryAwareThreadPoolExecutor 的子类。除了MemoryAwareThreadPoolExecutor 的功能之外,它还可以保证同一Channel中处理的事件流的顺序性,这主要是控制事件在异步处理模式下可能出现的错误的事件顺序,但它并不保证同一 Channel中的事件都在一个线程中执行(通常也没必要)。
例如:
Thread X: --- Channel A (Event A1) --. .-- Channel B (Event B2) --- Channel B (Event B3) ---> \ / X / \ Thread Y: --- Channel B (Event B1) --' '-- Channel A (Event A2) --- Channel A (Event A3) --->
上图表达的意思有几个:
(1)对整个线程池而言,处理同一个Channel的事件,必须是按照顺序来处理的。例如,必须先处理完Channel A (Event A1) ,再处理Channel A (Event A2)、Channel A (Event A3)
(2)同一个Channel的多个事件,会分布到线程池的多个线程中去处理。
(3)不同Channel的事件可以同时处理(分担到多个线程),互不影响。
OrderedMemoryAwareThreadPoolExecutor 的这种事件处理有序性是有意义的,因为通常情况下,请求发送端希望服务器能够按照顺序处理自己的请求,特别是需要多次握手的应用层协议。例如:XMPP协议。
现在回到具体业务上来,我们这里的认证服务也使用了OrderedMemoryAwareThreadPoolExecutor。认证服务的其中一 个环节是使用长连接,不断处理来自另外一个服务器的认证请求。通信的数据包都很小,一般都是200个字节以内。一般情况下,处理这个过程很快,所以没有什 么问题。但是,由于认证服务需要调用第三方的接口,如果第三方接口出现延迟,将导致这个过程变慢。一旦一个事件处理不完,由于要保持事件处理的有序性,其 他事件就全部堵塞了!而短连接之所以没有问题,是因为短连接一个Channel就一个请求数据包,处理完Channel就关闭了,根本不存在顺序的问题, 所以在业务层可以迅速收到请求,只是由于同样的原因(第三方接口),处理时间会比较长。
其实,认证过程都是独立的请求数据包(单个帐号),每个请求数据包之间是没有任何关系的,保持这样的顺序没有意义!
最后的改进措施:
1、去掉OrderedMemoryAwareThreadPoolExecutor,改用MemoryAwareThreadPoolExecutor。
2、减少调用第三方接口的超时时间,让处理线程尽早回归线程池。