有熱心的網友加我微信,時不時問我一些技術的或者學習技術的問題。有時候我回微信的時候都是半夜了。但是我很樂意解答他們的問題。因為這些年輕人都是很有上進心的,所以在我心里他們就是很優秀的,我願意多和努力的人交朋友。我原來拿老公高中時復讀過一年來開過玩笑。他卻很平和而驕傲的回復說:“我是為了等你。” 眼里有一種賺翻了的表情。雖然我很感激我婆婆給了個好老公,但是生氣的一點是婆婆從小說我老公腦子笨。我總跟老公說:“就是因為媽從小這么說你,你才從原本應該是天之驕子淪為一個苦逼程序員的。”但是畢業十年,他一直很努力,也很顧家。所以在我眼里他很優秀。有網友問我是怎樣成長為技術大牛的。我回復說:“額~,你可不可以不問這么籠統的問題,都不知道怎么回答你。”過了幾天他又問我:“你有沒有看過的東西當時理解了然后又忘了。”我當時只是告訴他忘了就多看幾遍,反復的看。但是他問的這個問題我一直都沒有忘,我一直在想自己是怎么做的。因為通過第一個問題到第二個問題,可以看到他是很努力的在思考我說的話,對於認真的人,咱們也得認真。
本文有兩條主線:一條是學習方法,怎樣學過就能記住。另一條是實際項目中的IO處理問題,包括BIO,NIO,AIO,netty。旨在用學習具體知識的具體流程體現學習方法的形成過程。
本文首發於靜兒1986的博客,原文地址是http://www.cnblogs.com/xiexj/p/6874654.html。
對於技術的東西,我是這么過目不忘的(上篇文章已經提過,但是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操作要比加鎖性能更好,但是開發維護上要繁瑣很多。
不論電影,電視,武俠小說還是實際生活中, 智商高的人最終會被情商高的人打敗。精誠所至金石為開是真理。愛一個人會打開心靈的通道,不着一字便可溝通。我在你心底,你就會用我的方式去思考。技術的書,技術的文章,代碼后面都隱藏着高人。愛一種技術,多看多想,技術能力和境界都能得到升華。