Java I/O API之性能分析 (下)(轉(zhuǎn))
四、注冊(cè)與處理過(guò)程詳解
接下來(lái)我們要分析Connection的register()方法。前面我們總是說(shuō)用Selector注冊(cè)的連接,其實(shí)這是一種簡(jiǎn)化的說(shuō)法。實(shí)際上,用Selector注冊(cè)的是一個(gè)java.nio.channels.SocketChannel對(duì)象,但只針對(duì)特定的I/O操作。注冊(cè)之后,有一個(gè)java.nio.channels.SelectionKey被返回。這個(gè)選擇鍵可以通過(guò)attach()方法關(guān)聯(lián)到任意對(duì)象。為了通過(guò)鍵獲得連接,這里把Connection對(duì)象關(guān)聯(lián)到鍵。這樣,我們就可以從Selector間接地獲得一個(gè)Connection。
public void register(Selector selector)
throws IOException
{
key = socketChannel.register(selector,
SelectionKey.OP_READ);
key.attach(this);
}
回過(guò)頭來(lái)看ConnectionSelector。select()方法的返回值表示有多少連接已經(jīng)做好了I/O操作的準(zhǔn)備。如果返回值是0,則返回;否則,調(diào)用selectedKeys()獲得鍵的集合(Set),從這些鍵獲得以前關(guān)聯(lián)的Connection對(duì)象,然后調(diào)用其readRequest()或writeResponse()方法,具體調(diào)用哪一個(gè)方法由連接被注冊(cè)為讀取操作還是寫入操作決定。
現(xiàn)在再來(lái)看Connection類。Connection類代表著連接,處理所有協(xié)議有關(guān)的細(xì)節(jié)。在構(gòu)造函數(shù)中,通過(guò)參數(shù)傳入的SocketChannel被設(shè)置成非阻塞模式,這對(duì)于
服務(wù)器來(lái)說(shuō)是很重要的。另外,構(gòu)造函數(shù)還設(shè)置了一些默認(rèn)值,分配了緩沖區(qū)requestLineBuffer。由于分配直接緩沖區(qū)代價(jià)稍高,且這里的每一個(gè)連接都用一個(gè)新的緩沖區(qū),因此這里使用java.nio.ByteBuffer.allocate()而不是ByteBuffer.allocateDirect()。如果重用緩沖區(qū),直接緩沖區(qū)可能具有更高的效率。
public Connection(SocketChannel socketChannel)
throws
IOException {
this.socketChannel =
socketChannel;
...
socketChannel.configureBlocking(false);
requestLineBuffer
= ByteBuffer.allocate(512);
...
}
完成所有初始化工作且SocketChannel做好了讀取準(zhǔn)備之后,ConnectionSelector調(diào)用了readRequest()方法,利用socketChannel.read(requestLineBuffer)方法把所有可用的數(shù)據(jù)讀入緩沖區(qū)。如果不能讀取完整的行,則返回發(fā)出調(diào)用的ConnectionSelector,允許另一個(gè)連接進(jìn)入處理過(guò)程;反之,如果成功地讀取了整個(gè)行,接下來(lái)應(yīng)該做的是象在Httpd中一樣解析請(qǐng)求。如果當(dāng)前的請(qǐng)求合法,程序?yàn)檎?qǐng)求目標(biāo)文件創(chuàng)建一個(gè)java.nio.Channels.FileChannel,并調(diào)用prepareForResponse()方法。
private void prepareForResponse() throws IOException
{
StringBuffer responseLine = new
StringBuffer(128);
...
responseLineBuffer =
ByteBuffer.wrap(
responseLine.toString().getBytes("ASCII")
);
key.interestOps(SelectionKey.OP_WRITE);
key.selector().wakeup();
}
prepareForResponse()方法構(gòu)造出緩沖區(qū)responseLine以及(如果必要的話)應(yīng)答頭或錯(cuò)誤信息,并把這些數(shù)據(jù)寫入responseLineBuffer。這個(gè)ByteBuffer是一個(gè)byte數(shù)組的簡(jiǎn)單的封裝器。生成待輸出的數(shù)據(jù)之后,我們還要通知ConnectionSelector:從現(xiàn)在開始不再讀取數(shù)據(jù),而是要寫入數(shù)據(jù)了。這個(gè)通知通過(guò)調(diào)用選擇鍵的interestedOps(SelectionKey.OP_WRITE)方法完成。為了保證選擇器能夠迅速認(rèn)識(shí)到連接操作狀態(tài)的變化,接著還要調(diào)用wakeup()方法。接下來(lái)ConnectionSelector調(diào)用連接的writeResponse()方法。首先,responseLineBuffer被寫入到Socket管道。如果緩沖區(qū)的內(nèi)容全部被寫入,而且還有被請(qǐng)求的文件需要發(fā)送,接著調(diào)用前面打開的FileChannel的transferTo()方法。transferTo()方法通常能夠高效地把數(shù)據(jù)從文件傳輸?shù)焦艿?,但?shí)際的傳輸效率依賴于底層的操作系統(tǒng)。任何時(shí)候,被傳輸?shù)臄?shù)據(jù)量至多相當(dāng)于在無(wú)阻塞的情況下可寫入目標(biāo)管道的數(shù)據(jù)量。為安全和確保各個(gè)連接之間的公平起見,這里把上限設(shè)置成64
KB。
如果所有數(shù)據(jù)都已經(jīng)傳輸完畢,close()執(zhí)行清理工作。取消Connection的注冊(cè)是這里的主要任務(wù),具體通過(guò)調(diào)用鍵的cancel()方法完成。
public void close() {
...
if (key != null)
key.cancel();
...
}
這個(gè)新的方案性能如何呢?答案是肯定的。從原理上看,一個(gè)Acceptor和一個(gè)ConnectionSelector足以支持任意數(shù)量的打開的連接。因此,新的實(shí)現(xiàn)方案在可伸縮性方面占有優(yōu)勢(shì)。但是,由于兩個(gè)線程必須通過(guò)同步的queue()方法通信,它們可能互相阻塞對(duì)方。解決這個(gè)問(wèn)題有兩種途徑:
?改進(jìn)實(shí)現(xiàn)隊(duì)列的方法
?采用多個(gè)Acceptor/ConnectionSelector對(duì)
與Httpd相比,NIOHttpd的一個(gè)缺點(diǎn)是,對(duì)于每一個(gè)請(qǐng)求,就有一個(gè)新的帶緩沖的Connection對(duì)象被創(chuàng)建。這就導(dǎo)致了垃圾收集器產(chǎn)生的額外的CPU占用,這部分附加代價(jià)的具體程度又與VM的類型有關(guān)。然而,Sun不厭其煩地強(qiáng)調(diào)說(shuō),有了Hotspot,短期生存的對(duì)象不再成為問(wèn)題。
五、可伸縮性的定量分析和比較
在可伸縮性方面,NIOHttpd到底比Httpd好多少?下面我們來(lái)看看具體的數(shù)字。首先要聲明的是,這里的數(shù)字具有大量的推測(cè)成分,一些重要的環(huán)境因素,例如線程同步、上下文切換、換頁(yè)、硬盤速度和緩沖等,都沒有考慮到。首先評(píng)估處理r個(gè)并發(fā)的請(qǐng)求需要多少時(shí)間,假設(shè)被請(qǐng)求的文件大小是s字節(jié),客戶端的帶寬是b字節(jié)/秒。對(duì)于Httpd,這個(gè)時(shí)間顯然直接依賴于線程的數(shù)量t,因?yàn)橥粫r(shí)刻只能處理t個(gè)請(qǐng)求。所以Httpd的處理時(shí)間可以從公式一得到,其中c是執(zhí)行請(qǐng)求分析之類操作的開銷常量,這個(gè)值對(duì)于每一個(gè)請(qǐng)求來(lái)說(shuō)都是一樣的。另外,這里假定從磁盤讀取數(shù)據(jù)的速度總是快于寫入Socket的速度,服務(wù)器帶寬總是大于客戶機(jī)帶寬之和,且CPU未滿載。因此,服務(wù)器端的帶寬、緩沖和硬盤速度等因素都不必在該公式中考慮。
然而,NIOHttpd的處理時(shí)間不再依賴于t。對(duì)于NIOHttpd,傳輸時(shí)間l在很大程度上依賴于客戶端的帶寬b、文件大小s以及前面提到的常數(shù)c。由此可以得出公式二,從該公式可以得到NIOHttpd的最小傳輸時(shí)間。
注意公式三的比值d,它度量了NIOHttpd和Httpd的性能對(duì)比關(guān)系。
進(jìn)一步的分析表明,如果s、b、t和c是常數(shù),r
趨向無(wú)窮時(shí)d的增長(zhǎng)趨向于一個(gè)極限,從公式四可以方便地計(jì)算出這個(gè)極限。
因此,除了線程的數(shù)量和常量性的開銷,連接的時(shí)長(zhǎng)s/b對(duì)d具有極端重要的影響。連接持續(xù)的時(shí)間越長(zhǎng),d值越小,NIOHttpd對(duì)比Httpd的優(yōu)勢(shì)也就越高。表一顯示出,當(dāng)c=10ms,t=100,s=1mb,b=8kb/s時(shí),NIOHttpd要比Httpd快126倍。如果連接持續(xù)了很長(zhǎng)一段時(shí)間,NIOHttpd表現(xiàn)出巨大的優(yōu)勢(shì)。當(dāng)連接時(shí)間較短時(shí),例如在100
Mb的局域網(wǎng)內(nèi),如果文件較大,NIOHttpd表現(xiàn)出10%的優(yōu)勢(shì);如果文件較小,優(yōu)勢(shì)不明顯。[@more@]