有热心的网友加我微信,时不时问我一些技术的或者学习技术的问题。有时候我回微信的时候都是半夜了。但是我很乐意解答他们的问题。因为这些年轻人都是很有上进心的,所以在我心里他们就是很优秀的,我愿意多和努力的人交朋友。我原来拿老公高中时复读过一年来开过玩笑。他却很平和而骄傲的回复说:“我是为了等你。” 眼里有一种赚翻了的表情。虽然我很感激我婆婆给了个好老公,但是生气的一点是婆婆从小说我老公脑子笨。我总跟老公说:“就是因为妈从小这么说你,你才从原本应该是天之骄子沦为一个苦逼程序员的。”但是毕业十年,他一直很努力,也很顾家。所以在我眼里他很优秀。有网友问我是怎样成长为技术大牛的。我回复说:“额~,你可不可以不问这么笼统的问题,都不知道怎么回答你。”过了几天他又问我:“你有没有看过的东西当时理解了然后又忘了。”我当时只是告诉他忘了就多看几遍,反复的看。但是他问的这个问题我一直都没有忘,我一直在想自己是怎么做的。因为通过第一个问题到第二个问题,可以看到他是很努力的在思考我说的话,对于认真的人,咱们也得认真。
本文有两条主线:一条是学习方法,怎样学过就能记住。另一条是实际项目中的IO处理问题,包括BIO,NIO,AIO,netty。旨在用学习具体知识的具体流程体现学习方法的形成过程。
对于技术的东西,我是这么过目不忘的(上篇文章已经提过,但是LZ觉得有必要强化一下,另外还加了一点):
☆ 积极的做项目
☆ 做过的东西反复琢磨
☆ 看完一本书后要再次梳理项目
☆ 手别懒,看到例子没一眼看明白就要动手实践
☆ 记录项目中潜在的问题每隔一段时间回来想一想
☆ 工匠精神:代码当成艺术品反复优化
☆ 沉住气把项目做深
☆ 好书要每隔一段时间就再看一遍
我在好几篇博客中都提到一个离线数据的小项目。因为这个项目我只有我一个人开发维护,所以我可以随时将可以优化并且放到线上,很有成就感。我做的是视频和音频部分,音频和音频的专辑是另一个小伙子做的,他当初把我的代码拷贝过去,因为数据量比我这边小很多,一直也没出什么问题,直到有一天他拿着代码来问我一个问题,我很无奈的说:“你可不可以拷贝一个新一点儿的版本,我都改的完全不是这个样子了。”
离线数据里有一个万一实时消息发送有问题,手动补发消息的接口,我直接用socket同步阻塞的方式(BIO)接收浏览器作为客户端,一直也没出什么问题,但是做过的东西要反复琢磨,所以我又分别使用了NIO,AIO,netty进行了尝试性改造。出于信息安全的考虑,本文不附带源码,简单例子可参考<实战Java高并发程序设计>第五章和<netty in action>。
由于这个手动服务一次也就是一两个人调用,比较改造后的性能,顶多看看浏览器里的请求响应时间,误差很大。对于这个最简单的RPC请求没什么可说的,最终都实现了,但是使用BIO,NIO,AIO由于返回响应我直接write到socket或者buffer里,没用什么工具,结果显示有浏览器兼容问题。netty由于内置了http协议的实现,不存在此问题。这里只比较原理的区别。
对于一个服务器端业务的开发者来说,如果这四者选,肯定要选netty。netty的jar包1MB多点,要嵌入手机啥的就算了,这是通信用的,放在客户端基本也没啥应用场景。
☆ netty使用简单,预置多种编解码器,支持多种主流协议。就像刚才说的,直接使用java api,我需要自己将read到的GET xxxx HTTP1.1 XXXXX这些数据自己解析出感兴趣的数据,返回客户端还要自己封装一个浏览器可读的响应。
☆ netty可定制,通过channelhander对通信框架进行灵活扩展。
☆ netty性能好,社区活跃,版本迭代周期短。
首先简单提一下概念。
BIO:同步阻塞式IO,服务器端与客户端三次握手简历连接后一个链路建立一个线程进行面向流的通信。这曾是jdk1.4前的唯一选择。在任何一端出现网络性能问题时都影响另一端,无法满足高并发高性能的需求。
NIO:同步非阻塞IO,以块的方式处理数据。采用多路复用Reactor模式。JDK1.4时引入。
AIO:异步非阻塞IO,基于unix事件驱动,不需要多路复用器对注册通道进行轮询,采用Proactor设计模式。JDK1.7时引入。
Netty是实现了NIO的一个流行框架,JBoss的。Apache的同类产品叫Mina。阿里云分布式文件系统TFS里用的就是Mina。我目前的项目中使用了netty作为底层实现的有阿里云的dubbo和ElasticSearch。我之所有要研究这个东西也是因为我要实现自己的搜索引擎先要调研已存在的产品。我之前项目中用过Solr,个人比较倾向于Solr。但是大家做了很多性能比较,ElasticSearch的并发能力确实要比Solr,究其原因,就是因为Solr底层还是用的Servlet容器,而ElasticSearch底层用的是Netty。
有人问:单看实现原理,显然AIO要比NIO高级,为什么Netty底层用NIO? Netty也曾经做过一些AIO的尝试性版本,但是性能效果不理想。AIO理念很好,但是有赖于底层操作系统的支持,操作系统目前的实现并没听上去那么有吸引力,是一匹刚孕育出来的黑马。其实AIO的性能上不去,也很好感性的理解。NIO相当于餐做好了自己去取,AIO相当于送餐上门。要吃饭的人是百万,千万级的,送餐员也就几百人。所以一般要吃到饭,是自己去取快呢,还是等着送的更快?目前的外卖流程不是很完善,所以时间上没想的那么靠谱,但是有优化空间,这就是AIO。
关于NIO的内部实现,大家可以参考我写的IO和socket编程。里面没有提到操作系统方面对于多路复用技术的三种常用机制:select,poll和epoll。三个的作用都是指示内核等待多个事件中的任何一个发生的时候或一定时间滞后被唤醒。区别是select函数文件描述符的数量有限制,poll函数没限制,epoll函数用一个描述符来管理多个描述符。
关于文件描述符,也就是句柄,想起一件事:好几年前用Solr做高并发压测的时候,我用了一台内存所剩很小的服务器做测试,出现了NIO超出句柄数错误。而查看了系统的最大句柄数设置,设置的很大,不可能用完。换了一台好点的服务器就没了这个问题。那是因为内存不够的时候,每次都要打开磁盘文件的数据进行搜索,而内存大点儿都走内存中的缓存了。内存数据也有句柄,但是访问磁盘速度慢,资源长时间不释放,新的请求又过来了,才是句柄超出的原因。
关于epoll,不得不提臭名昭著的epoll bug。它会导致Selector空轮询,最终导致CPU 100%。
关于多路复用,多唠叨两句。多路指的是多个网络连接,复用是复用同一个线程。目前解决各种并发问题的最大利器就是缓存。我用过Memecached和Redis。Memcached采用的是多线程,非阻塞IO复用的网络模型。Redis采用的是单线程的IO复用模型。既然单线程会严重影响整体吞吐量,因为CPU计算过程中,整个IO调用都是被阻塞的。那么为什么实际上大家普遍表示Redis的性能要优于Memcached?多线程模型可以发挥多核作用,但是引入了缓存一致性和锁的问题,带来了性能损耗。单线程可以将速度优势发挥到最大。想用多核,可以多开几个进程的嘛。话说nginx也是这样多进程单线程的模型。Netty可以定制,可以单线程IO复用,多线程IO复用,主从多线程IO复用。
Netty逻辑架构(从底层向上介绍)
1> Reactor通信调度层,由一系列辅助类组成,包括Reactor线程NioEventLoop以及其父类,NioSocketChannel以及父类,ByteBuffer及衍生Buffer,Unsafe及内部子类。
2> 职责链ChannelPipeLine,它负责调度事件在职责链中的传播,支持动态的编排职责链,职责链可以选择性的拦截自己关心的事件,对其它IO操作和事件忽略。
3> 业务逻辑编排层。
Netty实现上优势点:
☆ 零拷贝,又叫内存零拷贝,指CPU不需要为数据在内存之间的拷贝消耗资源。通常指计算机在网络上发送文件时,不需要将文件内容拷贝到用户空间而直接在内核空间中传输到网络的方式。
Netty的ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果大家自己动手写过NIO和AIO的程序,就会知道我们接触的网络传输数据是直接和ByteBuffer打交道的。在JAVA的API中,一个ByteBuffer读完数据后要flip一下,将当前操作位置设置为0.<Netty In Action>里详细介绍了Netty的ByteBuf怎样将这个缓冲区分成一段一段的,还可以压缩,将读的数据滑向一侧。而堆外内存的零拷贝,如果有JVM基础也很好理解。Java内存模型里提到每个线程都有自己的高速工作内存空间,而不是直接访问主内存。想不用工作内存,直接主内存可见,就要用volatile关键字修饰。所以堆内内存,走JVM就存在这个拷贝开销。
Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,而不用将几个小Buffer通过内存拷贝合并成一个大的Buffer.
Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。其实这个功能不是Netty特有的,Linux的sendfile函数和Java NIO的FileChannel的transferTo方法都实现了零拷贝,而Netty也通过FileRegion包装了NIO的transferTo方法。
☆ 内存池,刚才提到堆外内存可以实现零拷贝,那为啥java要设置自己的工作内存呢?快啊。堆外直接内存的分配和回收是一个非常 耗时的操作,有C或者C++基础的应该都能理解。刚才提到的ByteBuf就是采用内存池的,用来对缓存区复用。
基本一个比较底层的框架都会有自己特有的内存池技术。比如Memcached使用预分配的内存池方式,使用slab和带下不同的Chunk来管理内存。这样避免了上面提到的内存分配和回收的开销问题。
☆ 无锁化的串行设计。我在自己的博客中提到自己做项目会把业务划分的很清楚,尽量少的采用线程间通信。这就是一种无锁化的串行设计思想。共享资源的并发访问处理不当,会带来严重的锁竞争,最终会导致性能下降。
☆ 高效的并发编程。有些人在面试时很吃亏(老公,当你看到这篇文章的时候,说的就是你)。CPU直接操作磁盘型的。没有内存,很多东西临场发挥一时加载不上来。说是吃亏究其原因是总结的少,没建立好强大的索引。而好的程序员是体现在细节中的,为什么一说原理怎么看怎么觉得Memcached比Redis先进。而Mina的实现原理上也没有多少劣于Netty的地方。就是因为后者的开发者更为勤奋。比如CAS操作要比加锁性能更好,但是开发维护上要繁琐很多。
不论电影,电视,武侠小说还是实际生活中, 智商高的人最终会被情商高的人打败。精诚所至金石为开是真理。爱一个人会打开心灵的通道,不着一字便可沟通。我在你心底,你就会用我的方式去思考。技术的书,技术的文章,代码后面都隐藏着高人。爱一种技术,多看多想,技术能力和境界都能得到升华。