官术网_书友最值得收藏!

1.3 AIO,大道至簡的設計與苦澀的現實

AIO是I/O模型里一個很高的層次,體現了大道至簡的軟件美學理念。與NIO相比,AIO的框架和使用方法相對簡單很多。

AIO包括兩大部分:AIO Files解決了文件的異步處理問題,AIO Sockets解決了Socket的異步處理問題。AIO的核心概念為應用發起非阻塞方式的I/O操作,在I/O操作完成時通知應用,同時,應用程序的職責很明確,比如什么時候發起I/O操作請求,在I/O操作完成時通知誰來處理。

下圖給出了AIO Sockets對讀請求的處理流程(寫請求同理),應用程序在有讀請求時就向Kenel注冊此請求,應用的線程就可以繼續執行其他操作而無須等待。與此同時,Kernel在發現有數據到達Socket以后,就將數據從內核復制到應用程序的Buffer里,在復制完成后,回調應用程序的通知接口,應用程序就可以處理此數據了。

在編寫AIO Socket程序時,我們所要掌握的最關鍵的一個類是回調接口——CompletionHandler<V,A>,它包括兩個方法:void completed(V result,A attachment);void failed(Throwable exc,attachment)。

其中,completed方法是異步請求完成時的通知接口,result是返回的結果;attachment則是應用程序捆綁在這個回調接口上的任意對象,可以記錄客戶端連接對象,或者用來保存Session會話的狀態數據,例如已讀取的字節信息等。failed方法則表明I/O事件異常,通常是不可恢復的故障。completed方法中的V與A具體是什么對象呢?這取決于調用者,比如在Asynchronous ServerSocketChannel對象中異步接收客戶端連接請求的方法簽名如下:

在上述方法中,attachment參數被作為CompletionHandler<V,A>的A參數傳遞到completed與failed回調方法中。下面的代碼創建了一個AsynchronousServerSocketChannel,并調用accept方法等待客戶端異步連接建立完成:

AioAcceptHandler這個CompletionHandler的代碼則如下:

在上述代碼中,completed方法先調用AsynchronousServerSocketChannel的accept方法,注冊下一次的異步連接請求。這個調用很重要,否則AsynchronousServerSocketChannel就不會再接收新連接的請求了;隨后調用startRead(socket)方法發起一個異步讀取數據的請求。在說明這個方法之前,我們看看AsynchronousSocketChannel的異步讀方法的簽名:

上述方法類似于之前分析的accept方法,attachment參數被作為CompletionHandler<V,A>的A參數傳遞到completed與failed回調方法里,V參數則是一個整數,用于表明此次讀到的字節總數。

下面是startRead(socket)方法的邏輯,它調用了AioReadHandler來處理讀到的數據,注意傳遞到AioReadHandler里的Attachment是此次讀到的數據——ByteBuffer,最多1024個字節:

AioReadHandler負責處理異步讀響應事件,下面是其Complete方法的源碼:

如上所示的代碼的總體邏輯類似于accept的處理邏輯,它針對每個客戶端Socket都使用了一個ByteBuffer作為Session級別的變量,用來保存客戶端發送的數據,并且通過Attachment變量傳遞到CompletionHandler的下一次讀取事件上。

AIO異步寫的操作類似于異步讀的處理,這里不做分析。從AIO的代碼來看,我們發現AIO也有類似于NIO的一面,即如果還有I/O事件要操作,則仍然需要把它們“注冊”到系統里。不同的是,在AIO框架下,客戶端收到反饋事件時,數據已經準備好了,應用程序可以直接處理,在NIO框架下則還需要應用調用底層的讀寫API完成具體的I/O操作。

AIO框架不僅僅止步于此,我們知道,在JDK的NIO模型下,多路復用的Reactor模型及多線程的Reactor模型都不是官方JDK提供的,這也大大增加了應用編程的復雜度。AIO框架則將復雜的多線程處理機制融入JDK的AIO框架中,讓我們可以輕松寫出高級又優雅的AIO程序。

從之前的NIO經驗來看,在處理很多個Socket的I/O事件時,多線程(線程池)成為必然的選擇,很直觀的推理就是CompletionHandler需要一個線程池來實現高性能并發回調機制,于是就有了AsynchronousChannelGroup對象,它內部包括一個線程池:

AsynchronousServerSocketChannel可以被綁定到某個ChannelGroup上,以便共用其線程池:

注意到ChannelGroup后面捆綁的線程池可以有多種選擇,例如固定大小的線程池、彈性擴展的線程池、緩存的線程池等,于是編程的靈活性很大。此外,如果是每個CPU核心都對應一個ChannelGroup,這就接近多線程Reactor模型的設計了。

從上面的分析來看,JDK里的AIO框架設計的確很優雅,而且很妥善地解決了JDK里NIO框架沒有考慮到的復雜問題。從誕生的那天開始,Java AIO的一切看上去都很美,但是現在,“它美麗而晴朗的天空卻被一朵烏云籠罩了”,這朵“烏云”就是Linux的AIO泥潭。

早在2003年,Linux kernel AIO項目就啟動并且制定了設計方案。2004年,IBM覺得異步狀態機的實現跟已存在的代碼不協調并且太復雜,于是做出了Retry模型,但Retry模型的阻塞問題(block point)始終無法得到解決。Oracle負責OSS的部門接管了Retry模型,后來又覺得IBM的Retry模型有很多問題,發現Retry&Exit在他們的一個產品上會有很大的性能問題,于是重起爐灶,開發了一個Syslet方案,卻以失敗告終。直到2016年5月,還有人發現在Linux 3.13內核里有AIO內存溢出的嚴重漏洞(Ubuntu Kylin 14.04 LTS版本就采用了這個內核),Docker則要求使用AIO的宿主機所安裝的版本不低于Linux 3.19(在這個版本里又有好幾處AIO代碼的修復)。

目前Linux上的AIO實現主要有兩種:Posix AIO與Kernel Native AIO,前者是以用戶態實現的,而后者是以內核態實現的,所以Kernel Native AIO的性能及前景要好于它的前輩Posix AIO。比較知名的軟件如Nginx、MySQL、InnoDB等的高版本都支持Kernel Native AIO,但基本上都只將文件傳輸到Socket中,即AIO Files的特性。Netty后來也實現了AIO,但又取消了,這個做法與Mycat的嘗試過程殊途同歸。其原因其實很簡單,Linux下AIO的實現充斥著各種Bug,并且AIO Socket還不是真正的異步I/O機制,性能的改進并不明顯和可靠;而另外一種值得重視的觀點是:AIO是為了未來的高帶寬大數據傳輸而準備的技術,還不適應當前的硬件和軟件環境。

主站蜘蛛池模板: 达拉特旗| 浠水县| 永丰县| 木兰县| 桐乡市| 芮城县| 澳门| 承德市| 奈曼旗| 宣恩县| 香河县| 左贡县| 鸡西市| 察哈| 海口市| 遂溪县| 固镇县| 虹口区| 永善县| 青海省| 顺义区| 五常市| 新余市| 南宁市| 张掖市| 阿拉尔市| 望江县| 共和县| 汉沽区| 陇川县| 望城县| 靖边县| 甘肃省| 鲜城| 桑植县| 外汇| 澳门| 永嘉县| 溆浦县| 浪卡子县| 古浪县|