簡介
由於瀏覽器禁止跨域的XMLHTTP調用,所有的Ajax網站都必須有一個服務端代理來從外部域比如Flickr或者Digg來抓去內容。對用戶端Javascript代碼來說,一個XMLHttp的調用將請求傳遞給宿主在相同域裡的服務端代理,然後由代理來從外部伺服器上下載內容,並回傳給用戶端。通常,所有從外部伺服器擷取內容的Ajax網站都採用這種代理方案,除了一些罕見的使用JSONP的人。當網站上的許多組件正在從外部域下載內容時,這樣的代理將會被大量地調用。所以,當代理開始被百萬次地調用時,它將變成一個可擴充的問題。另外,一個頁面整體的負載平衡很大程度上依賴於當代理向頁面提供內容時它的效能。這篇文章,我們來看看我們如何能將傳統的Ajax代理變得更快,非同步,持續提供內容流,從而使其更具擴充性。
Ajax代理 進行時
當你訪問Pageflakes.com時,你可以看到這樣的代理正在工作。你將看到許多不同的內容,例如天氣預報、flickr圖片、YouTuBe的視頻以及RSS等,像一個個組件一樣從許多不同的外部域中被載入。所有這些載入都需要使用一個內容代理。該內容代理在最近一個月為差不多42.3百萬的URL提供過服務。使它既快又有擴充性,是對我們相當大的挑戰!有時內容代理需要提供MB資料的服務,這更加提高了這些挑戰的高度。因為這樣的代理會被大量的調用,如果我們能夠為每個調用平均節約大概100ms,我們每個月就可以為下載、上傳、處理的時間節約大概4.23百萬秒。這大概1175人時都被全世界百萬人在瀏覽器前等待下載內容而浪費掉了。
這樣的內容代理將外部伺服器的URL作為一個查詢參數。它從這些URL下載內容,然後將這些內容作為響應回傳給伺服器。
圖片:內容代理像一個中間人一樣在瀏覽器和外部域之間工作。
上面的時間軸顯示了,一個請求如何到達伺服器,然後伺服器向外部伺服器發出請求,下載響應,並將它傳輸給用戶端。從代理到瀏覽器的響應箭頭比從外部伺服器到代理的箭頭更長,這是因為通常一個Proxy 伺服器的宿主環境比使用者的互連網串連有更快的下載速度。
一個基本的代理
這種內容代理也同樣存在與我的開源Ajax網站——Dropthings.com中。你可以去CodePlex看它的代碼是如何?這樣一個代理的。
下面是一個非常簡單,同步,沒有流的阻塞代理。
儘管它顯示了通常的原則,但它沒有接近一個真實的代理,因為:
(1) 他是一個同步代理,因此沒有可擴充性。每一個對該Web方法的調用,都導致Asp.net線程處於等待狀態,直到對外部URL的調用完成。
(2) 它是非流式的。它第一次從伺服器上下載整個內容,將內容儲存在一個字串中,然後向瀏覽器更新整個內容。如果你點擊一個MSDN Feed URL,它將從伺服器下載220KB的巨大的RSS XML,將它儲存到一個220KB的長字串中(總得來說,是.net內建String類型的雙倍大小,並且都是Unicode字元),然後將這220KB寫到asp.net響應對象(Response)的緩衝區(buffer)中,並將另外的220KB的UTF8的位元組數組儲存在記憶體中。然後,那220KB將被傳遞到IIS,以便可以傳輸到瀏覽器。
(3) 它沒有在服務端存產生一個正確的回應標頭來緩衝響應。它也沒有從源檔案中提供重要的頭部,例如Content-Type。
(4) 如果一個外部URL提供對內容的GZIP壓縮,它解壓內容到一個字串來表示,因此它浪費了伺服器的記憶體。
(5) 它沒有在服務端緩衝內容。因此,重複對相同外部URL的調用也將從外部URL重新下載資料,因此浪費了你服務端的頻寬。
我們需要一個非同步streaming proxy當它從外部網域服務器下載後傳輸內容到瀏覽器。因此,它將從外部伺服器下載位元組流到一個小塊兒中,並且直接將其傳輸到瀏覽器。結果是,在調用Web Service之後瀏覽器將看到一個持續的位元組傳輸。當內容已經完全從伺服器上下載下來後,將沒有延遲。
一個更好的代理
之前,我展示了複雜的基於流代理的代碼,讓我們討論一個改進式的方案。讓我們建立一個比上面更好的內容代理。上面的代理是一個同步並且非流式,但沒有其他問題的代理。我們將構建一個命名為Regular.ashx的HTTP Handler,它將把URL作為查詢參數。它也將緩衝作為一個查詢參數,該查詢參數將用來產生一個正確的回應標頭來在瀏覽器上緩衝內容。因此,它將一次又一次減少瀏覽器重複下載相同內容的時間。
上面的代理主要增強了兩點功能:
l 它允許服務端緩衝內容。在一段時間內來自不同瀏覽器的相同URL請求,在服務端將不會再次下載,而是從服務端緩衝中擷取資料。
l 它產生一個正確的輸出回應標頭,可以讓內容緩衝到瀏覽器端。
l 它沒有在記憶體中解壓下載的內容。它保持原始位元組流的完整。它節省了記憶體。
l 它用一種無緩衝的形式發送資料,這意味著asp.net的Response對象沒有緩衝響應,因此節約了記憶體。
然而,這是一個“阻塞”式的代理。
更好的代理——基於流
我們需要構建一個基於流的非同步代理程式來提供更好的效能。下面的圖闡述了這是為什麼:
圖片:持續的流式代理
就像你所看到的,當伺服器下載內容的時候,資料從伺服器被發送回瀏覽器,服務端下載的延時被消除。因此,如果伺服器花300ms來從一個外部資源下載內容,然後花700ms將它發送回伺服器,你就可以在伺服器和瀏覽器之間節約300ms的網路延遲(因為非同步就會邊下載邊傳輸資料流)。這種方案在當外部伺服器提供的內容非常慢並且需要花相當長的一段時間來提供內容時效果非常好。外部網站越慢,採用這種持續流式代理,你節約的時間就越多。當你的網站很遠時,這比起阻塞式的方案在效能上是一個很大的提升。
這種持續式的代理方案是:
l 用一個特殊的線程(讀取器線程)來從外部伺服器讀取一個8KB的位元組塊,目的是使其不阻塞。
l 將讀取到的Block Storage在一個被稱為管線流的記憶體隊列裡面。
l 從該隊列裡將塊寫到Asp.net的Response對象。
l 如果隊列完成,使其處於等待狀態直到更多的位元組被讀取器線程下載。
這種管線流需要是安全執行緒的,並且它需要支援“阻塞式”的讀取。“阻塞式”的讀取,這意味著如果一個線程嘗試讀取一個塊兒並且流是空的,將暫停該線程直到另一個線程在流上寫完東西。一旦在流上的一個寫操作發生了,它將恢複讀線程並且允許它繼續讀。我從CodeProjectarticle by James Kolpack那裡獲得了管線流的代碼,並且測試了並確信它有很高的效能,支援儲存位元組塊而不是單個位元組,支援等待逾時等等。
我將普通的代理(阻塞、同步、下載完所有之後才傳輸資料)和流式代理(從外部伺服器到瀏覽器持續傳輸資料)做了一些對比。兩個代理下載MSDN資源並且傳輸到瀏覽器。顯示在下面的時間是從瀏覽器發出請求到代理然後回傳整個響應到用戶端。
這不是一個非常科學的圖片,並且響應的時間還取決於從瀏覽器到Proxy 伺服器之間串連的速度和從Proxy 伺服器到外部伺服器的速度。但,它顯示了大部分時間裡,流式代理比通常的代理表現出更好的效能。
構建流式代理
構建一個效能好於普通流式代理並不是那麼簡單。我嘗試了三種方式,最終找出了最佳組合可以表現出比普通代理更好的效能。
該流式代理使用HttpWebRequest和HttpWebResponse來從一個外部伺服器下載資料。它們被用來對如何讀取資料取得更多得控制,更具體地說,是讀取WebClient不提供的位元組塊兒。另外,這裡有某些對構建一個代理需要的快速、可擴充HttpWebRequest的最佳化。
DownloadData方法從輸出資料流(串連到外部伺服器)下載資料,然後將其發送給asp.net的Response流。
這裡,我曾嘗試了三種不同的方案。一個是現在被注釋掉的,叫做TransmitDataAsyncOptimized,是最好的解決方案。我將解釋這三種方案。DownloadData方法的目的是在發送資料之前準備asp.net的輸出資料流。然後,它使用這三種方案裡的其中一種發送資料,並緩衝下載的位元組到記憶體流中。
第一種方案是從串連到外部伺服器的輸出資料流中讀取8192位元組,然後直接寫到響應(TransmitDataAsyncOptimized)。
這裡,readStream是從HttpWebResponse.GetResponseStream調用返回的輸出資料流。它從外部伺服器下載。responseBuffer僅僅是用來在記憶體中儲存整個響應一個記憶體流,目的是我們能夠緩衝它。
這個方案甚至比一個通常的代理更慢。在做了一些代碼層級的效能分析之後,看起來寫入OutputStream花費了相當長得時間,因為IIS嘗試發送資料到瀏覽器。所以,這裡會有網路延遲和傳輸資料的延遲。從頻繁調用累積的網路延時到OutputStream.Write加起來顯著地延遲了整個操作。
第二個方案是嘗試多線程。一個從asp.net線程建立的新線程,持續地從Socket讀取,甚至不用等待Response.OutputStream發送位元組到瀏覽器端。主asp.net線程等待直到所有的位元組被收集完成,然後直接傳輸它們到response。
這裡,讀取是在PipeStream上進行的,而不是從asp.net線程的socket上。這裡有一個新線程被催生,它將資料寫入PipeStream就像它從外部網站下載位元組一下。結果,我們有asp.net線程持續地將資料寫入OutputStream,並且有另外一個線程從外部伺服器不斷地下載資料。接下來的代碼從外部伺服器下載資料,然後儲存到PipeStream中。
這個方案的問題是,仍然有很多Response.OutputStream.Write的調用發送。外部伺服器發送各種不同位元組數的內容,有時3592位元組,有時8192位元組,並且有時僅僅501位元組。它完全依賴於從你的伺服器到外部伺服器的串連有多快。通常,微軟的伺服器都是非常快速的,當你調用_ResponseStream.Read從MSDN讀取資源的時候,你總是能獲得8192(緩衝區的最大容量)的位元組,但,當你串連到一個非可靠的伺服器,例如在澳大利亞,你將無法在每次讀取調用時都獲得8192個位元組的資料。所以你將以超過你對Response.OutputStream.Write預期調用次數而結束。所以,一個更好的最終方案是介紹另一個緩衝區,它將儲存將寫到asp.netResponse的位元組,並且一旦8192個位元組已經準備好傳輸時它將自己清空緩衝區到Response.OutputStream。在這中間緩衝區將確保總是有8192個位元組被發送到Response.OutputStream。
上面的方法確保一次僅有8192位元組被寫入asp.netResponse.Stream中。以這種方式,寫的次數是總位元組數/8192。
用非同步httphandler構建流式代理
現在,我們正基於流來傳輸位元組,我們需要讓這個代理“非同步”讓它不把持著asp.net的主線程太長時間。變成非同步,這意味著一旦asp.net線程向外部伺服器發出一個調用,它就會被釋放。當外部伺服器調用完成並且位元組都下載完成,它將從asp.net搶佔一個線程然後完成執行。
當代理不是非同步時候,它會使得asp.net線程很繁忙,直到整個的串連以及下載操作完成。如果外部伺服器響應很慢,那它就沒有必要持有asp.net線程太長時間。結果如果代理正對一個很慢的伺服器發起太多的請求,asp.net線程不久就將消耗殆盡,並且你的伺服器將停止回應任何新請求。
構建一個非同步代理程式的第一步是實現IhttpAsyncHandler然後將ProcessRequest方法分成兩部分:BeginProcessRequest和EndProcessRequest。Begin方法將對HttpWebRequest.BeginGetResponse發出一個調用,然後線程返回到asp.net的線程池。
當BeginProcessRequest調用完成並且外部伺服器已經開始向我們發送響應資料,asp.net會調用EndProcessRequest方法。該方法從外部伺服器下載資料,然後發送回瀏覽器端。
現在你已經擁有了它——一個快速的,可擴充的,持續流式的代理,它總是會比通常的代理有更好的效能。
如果,你正在考慮寫HttpHelper,AsyncState,以及SyncResult類,這裡有一些現成的類,下面是這些協助類的代碼:
原始碼:
http://download.csdn.net/detail/yanghua_kobe/3702484