Netty實戰十四之案例研究

一、Droplr——構建移動服務前端

Bruno de Carvalho,首席架構師java

在Droplr,咱們在個人基礎設施的核心部分、從咱們的API服務器到輔助服務的各個部分都使用了Netty。linux

這是一個關於咱們如何從一個單片的、運行緩慢的LAMP(Linux、Apache Web Server、MySQL以及PHP)應用程序遷移到基於Netty實現的現代的、高性能的以及水平擴展的分佈式架構的案例研究。web

1.一、一切的原由算法

當我加入這個團隊時,咱們運行的是一個LAMP應用程序,其做爲前端頁面服務於用戶,同時還做爲API服務於客戶端應用程序,其中,也包括個人逆向工程、第三方的Windows客戶端windroplr。數據庫

後來windroplr變成了Droplr for Windows,而我則開始主要負責基礎設施的建設,而且最終獲得了一個新的挑戰:徹底從新考慮Droplr的基礎設施。後端

在那時,Droplr自己已經確立成爲了一種工做的理念,所以2.0版本的目標也是至關的標準:數組

——將單片的技術棧拆分爲多個可橫向擴展的組件瀏覽器

——添加冗餘,以免宕機緩存

——爲客戶端建立一個簡潔的API

——使其所有運行在HTTPS上

創始人Josh和Levi對我說:「要不惜一切代價,讓它飛起來」

我知道這句話意味的可不僅是變快一點或者變快不少。這意味着一個徹底數量級上的更快。並且我也知道,Netty最終將會在這樣的努力中發揮重要做用。

1.二、Droplr是怎樣工做的

Droplr擁有一個很是簡單的工做流:將一個文件拖動到應用程序的菜單欄圖標,而後Droplr將會上傳該文件。當上傳完成以後,Droplr將複製一個短URL——也就是所謂的拖樂(drop)——到剪貼板。

而在幕後,拖樂元數據將會被存儲到數據庫中(包括建立日期、名稱以及下載次數等信息),而文件自己則被存儲在Amazon S3上。

1.三、創造一個更加快速的上傳體驗

Droplr的第一個版本的上傳流程是至關地天真可愛:

(1)接收上傳

(2)上傳到S3

(3)若是是圖片,則建立縮略圖

(4)應答客戶端應用程序

更加仔細地看看這個流程,你很快便會發如今第2步和第3步上有兩個瓶頸。無論從客戶端上傳到咱們的服務器有多快,在實際的上傳完成以後,直到成功地接收到響應之間,對於拖樂的建立老是會有惱人的間隔——由於對應的文件仍然須要被上傳到S3中,併爲其生成縮略圖。

文件越大,間隔的時間也越長。對於很是大的文件來講,鏈接最終將會在等待來自服務器的響應時超時。因爲這個嚴重的問題,當時Droplr只能夠提供單個文件最大32MB的上傳能力。

有兩個大相徑庭的方案來減小上傳時間。

方案A,樂觀且看似更加簡單:

——完整地接收文件

——將文件保存到本地的文件系統,並當即返回成功到客戶端

——計劃在未來的某個時間點將其上傳到S3

在收到文件以後便返回一個短URL建立一個空想(也能夠將其稱爲隱式的契約),即該文件當即在該URL地址上可用。可是並不能保證,上傳的第二階段(實際將文件推送到S3)也將最終會成功,那麼用戶可能會獲得一個壞掉的連接,其可能已經被張貼到了Twitter或者發送給了一個重要的客戶。這是不可接受的,即便是每十萬次上傳也只會發生一次。

咱們當前的數據顯示,咱們上傳失敗率低於0.01%,絕大多數都是在上傳實際完成以前,客戶端和服務器之間的鏈接就超時了。

咱們也能夠嘗試經過在文件將最終推送到S3以前,從接收它的機器提供該文件的服務來繞開它,然而這種作法自己就是一堆麻煩:

——若是在一批文件被完整地上傳到S3以前,機器出現了故障,那麼這些文件將會永久丟失;

——也將會有跨集羣的同步問題;

——將會須要額外的複雜的邏輯來處理各類邊界狀況,繼而不斷產生更多的邊界狀況;

在思考過每種變通方案和其陷阱以後,我很快認識到,這是一個經典的九頭蛇問題——對於每一個砍下的頭,它的位置上都會再長出兩個頭

方案B,安全且複雜:

——實時地(流式地)將從客戶端上傳的數據直接管道給S3

另外一個選項須要對總體過程進行底層的控制。從本質上說,咱們必需要可以作到如下幾點。

——在接收客戶端上傳文件的同時,打開一個到S3的鏈接

——將從客戶端鏈接上收到數據管道給到S3的鏈接

——緩衝並節流這兩個連接:

——須要進行緩衝,以在客戶端到服務器,以及服務器到S3這兩個分支之間保持一條的穩定的流

——須要進行節流,以防止當服務器到S3的分支上的速度變得慢於客戶端到服務器的分支時,內存被消耗殆盡

——當出現錯誤時,須要可以在兩端進行完全的回滾

看起來概念上很簡單,可是它並非你的一般的Web服務器可以提供的能力。尤爲是當你考慮節流一個TCP鏈接時,你須要對它的套接字進行底層的訪問。

它同時也引入一個新的挑戰,其將最終塑造咱們的中繼架構:推遲縮略圖的建立。這也意味着,不管該平臺最終構建於哪一種技術棧之上,它都必需要不只可以提供一些基本的特性,如難以置信的性能和穩定性,並且在必要時還要可以提供操做底層(即字節級別的控制)的靈活性。

1.四、技術棧

當開始一個新的Web服務器項目時,最終你將會問本身:「好吧,這些酷小子們這段時間都在用什麼框架呢?」我也是這樣的。

選擇Netty並非一件無需動腦的事;我研究了大量的框架,並謹記我認爲的3個相當重要的要素。

(1)它必須是快速的。我可不打算用一個低性能的技術棧替換另外一個低性能的技術棧

(2)它必須可以伸縮的。無論它是有1個鏈接仍是10000個鏈接,每一個服務器實例都必需要可以保持吞吐量,而且隨着時間推移不能出現崩潰或者內存泄露

(3)它必須提供對底層數據的控制。字節級別的讀取、TCP擁塞控制等,這些都是難點。

1-基本要素:服務器和流水線

服務器基本上只是一個ServerBootstrap,其內置了NioServerSocketChannelFactory,配置了幾個常見的ChannelHandler以及在末尾的HTTP RequestController,以下代碼所示。

pipelineFactory = new ChannelPipelineFactory(){
            public ChannelPipeline getPipeline() throws Exception{
                ChannelPipeline pipeline = Channels.pipeline();
                pipeline.addLast("idleStateHandler",new IdleStateHandler(...));
                pipeline.addLast("httpServerCodec",new HttpServerCodec());
                pipeline.addLast("requestController",new RequestController(...));                return pipeline;
            }
        };

RequestController是ChannelPipeline中惟一自定義的Droplr代碼,同時也多是整個Web服務器中最複雜的部分。它的做用是處理初始請求的驗證,而且若是一切都沒問題,那麼將會把請求路由到適當的請求處理器。對於每一個已經創建的客戶端鏈接,都會建立一個新的實例,而且只要鏈接保持活動就一直存在。

請求控制器負責:

——處理負載洪峯;

——HTTP ChannelPipeline的管理

——設置請求處理的上下文

——派生新的請求處理器

——向請求處理器供給數據

——處理內部和外部的錯誤

如下代碼給出了RequestController相關部分的一個綱要

public class RequestController extends IdleStateAwareChannelUpstreamHandler{    public void channelIdle(ChannelHandlerContext ctx, IdleStateEvent e)throws Exception{        //Shut down connection to client and roll everything back
    }    public void channelConnected(ChannelHandlerContext ctx,ChannelStateEvent e)throws Exception{        if(!acquireConnectionSlot()){            //Maximum number of allowed server connections reached,
            // respond with 503 service unavailable
            // and shutdown connection
        } else {            // set up the connection's request pipeline
        }
    }    public void messageReceived(ChannelHandlerContext ctx,MessageEvent e)throws Exception{        if(isDone()){            return;
        }        if (e.getMessage() instanceof HttpRequest){
            handleHttpRequest((HttpRequest)e.getMessage());
        } else if (e.getMessage() instanceof HttpChunk){
            handleHttpChunk((HttpChunk)e.getMessage());
        }
    }

}

正如以前解釋的,你應該永遠不要在Netty的I/O線程上執行任何非CPU限定的代碼——你將會從Netty偷取寶貴的資源,並所以影響到服務器的吞吐量。

所以,HttpRequest和HttpChunk均可以經過切換到另外一個不一樣的線程,來將執行流程移交給請求服務器。當請求處理器不是CPU限定時,就會發生這樣的狀況,無論是由於它們訪問了數據庫,仍是執行了不適合本地內存或者CPU的邏輯。

當發生線程切換時,全部的代碼塊都必需要以串行的方式執行;不然,咱們就會冒風險,對於一次上傳來講,在處理完了序列號爲n的HttpChunk以後,再處理序列號n-1的HttpChunk必然會致使文件內容的損壞。爲了處理這種狀況,我建立了一個自定義的線程池執行器,其確保了全部共享了同一個通用標識符的任務都將以串行的方式執行。

我將簡短地解釋請求處理器是如何被構建的,以在RequestController和這些處理器之間的橋樑上亮起一些光芒。

2-請求處理器

請求處理器提供了Droplr的功能,它們是相似地址爲/account或者/drops這樣的URI背後的端點。它們是邏輯核心——服務器對於客戶端請求的解釋器。

請求處理器的實現也是框架實際上成爲了Droplr的API服務器的地方。

3-父接口

每一個請求處理器,無論是直接的仍是經過子類繼承,都是RequestHandler接口的實現。

其本質上,RequestHandler接口表示了一個對於請求(HttpRequest的實例)和分塊(HttpChunk的實例)的無狀態處理器。它是一個很是簡單的接口,包含了一組方法以幫助請求控制器來執行以及/或者決定如何執行它的職責。

這個接口就是RequestController對於相關動做的全部理解,經過它很是清晰和簡潔的接口,該控制器能夠和有狀態的和無狀態的、CPU限定的和非CPU限定的處理器以一種獨立的而且實現無關的方式進行交互。

4-處理器的實現

最簡單的RequestHandler實現是AbstractRequestHandler,它表明一個子類型的層次結構的根,在到達提供了全部Droplr的功能的實際處理器以前,它將變得愈發具體。最終,它會到達有狀態的實現SimpleHandler,它在一個非I/O工做線程中執行,所以也不是CPU限定的。SimpleHandler是快速實現哪些執行讀取JSON格式的數據、訪問數據庫、而後寫出一些JSON的典型任務的端點的理想選擇。

5-上傳請求處理器

上傳請求處理器是整個Droplr API服務器的關鍵。它是對於重塑webserver模塊——服務器的框架化部分的設計的響應,也是到目前爲止整個技術棧中最複雜、最優化的代碼部分。

在上傳過程當中,服務器具備雙重行爲:

——在一邊,它充當了正在上傳文件的API客戶端的服務器

——在另外一邊,它充當了S3的客戶端,以推送它從API客戶端接收的數據

爲了充當客戶端,服務器使用了一個一樣使用Netty構建的HTTP客戶端庫。這個異步的HTTP客戶端庫暴露了一組完美匹配該服務器的需求的接口。它將開始執行一個HTTP請求,並容許在數據變得可用時再供給它,而這大大地下降了上傳請求處理器的客戶門面的複雜性。

1.5 性能

新的服務器的上傳在峯值時相比於舊版本的LAMP技術棧的快了10-20倍(徹底數量級的更快),並且他可以支撐超過1000倍的併發上傳,總共將近10k的併發上傳。

下面的這些因素促成這一點。

——它運行在一個調優的JVM中

——它運行在一個高度調優的自定義技術棧中,是專爲解決這個問題而建立的,而不是一個通用的Web框架

——該自定義的技術棧經過Netty使用了NIO構建,這意味着不一樣於每一個客戶端一個進程的LAMP技術棧,它能夠擴展到上萬甚至幾十萬的併發連接

——再也沒有以兩個單獨的,先接收一個完整的文件,而後再將其上傳到S3的步驟帶來的開銷,如今文件直接流向S3

——服務器對文件進行了流式處理,他不再會花時間在I/O操做上,即將數據寫入臨時文件,並在稍後的第二階段上傳中讀取它們,對於每一個上傳也將消耗更少的內存,這意味着能夠進行更多的並行上傳

——縮略圖生成變成了一個異步的後處理。

二、Firebase——實時的數據同步服務

實時更新是現代應用程序中用戶體驗的一個組成部分。隨着用戶期待這樣的行爲,愈來愈多的應用程序都正在實時地向用戶推送數據的變化。經過傳統的3層架構很難實現實時的數據同步,其須要開發者管理他們本身的運維、服務器以及伸縮。經過維護到客戶端的實時的、雙向的通訊,Firebase提供了一種即便的直觀體驗,容許開發人員在幾分鐘以內跨越不一樣的客戶端進行應用程序數據的同步——這一切都不須要任何的後端工做、服務器、運維或者伸縮。

實現這種能力提出了一項艱難的技術挑戰,而Netty則是用於在Firebase內構建用於全部網絡通訊的底層框架的最佳解決方案。這個案例研究概述了Firebase的架構,而後審查了Firebase使用Netty以支撐它的實時數據同步服務的3種方式:長輪詢、HTTP 1.1 keep-alive和流水線化、控制SSL處理器

2.一、Firebase的架構

Firebase容許開發者使用兩層體系結構來上線運行應用程序。開發者只須要簡單地導入Firebase庫,並編寫客戶端代碼。數據將以JSON格式暴露給開發者的代碼,而且在本地進行緩存。該庫處理了本地高速緩存和存儲在Firebase服務器上的主副本(master copy)之間的同步。對於任何數據進行的更改都將會被實時地同步到與Firebase相鏈接的潛在的數十萬個客戶端上。

Firebase的服務器接收傳入的數據更新,並將它們當即同步給全部註冊了對於更改的數據感興趣的已經鏈接的客戶端。爲了啓用狀態更改的實時通知,客戶端將會始終保持一個到Firebase的活動鏈接。該鏈接的範圍是:從基於單個Netty Channel的抽象到基於多個Channel的抽象,甚至是在客戶端正在切換傳輸類型時的多個並存的抽象。

由於客戶端能夠經過多種方式鏈接到Firebase,因此保持鏈接代碼的模塊化很重要。Netty的Channel抽象對於Firebase繼承新的傳輸來講簡直是夢幻般的構件塊,此外,流水線和處理器模式使得能夠簡單地傳輸相關的細節隔離開來,併爲應用程序代碼提供了一個公共的消息流抽象。一樣的,這也極大地簡化了添加新的協議支持所須要的工做。Firebase只經過簡單地添加幾個新的ChannelHandler到ChannelPipeline中,便添加了對一種二進制傳輸的支持。對於實現客戶端和服務器之間的實時鏈接而言,Netty的速度、抽象的級別以及細粒度的控制都使得它成爲了一個卓絕的框架。

2.二、長輪詢

Firebase同時使用了長輪詢和WebSocket傳輸。長輪詢傳輸是高度可靠的,覆蓋了全部的瀏覽器、網絡以及運營商;而基於WebSocket的傳輸、速度更快,可是因爲瀏覽器/客戶端的侷限性,並不老是可用的。開始時,Firebase將會使用長輪詢進行鏈接,而後在WebSocket可用時再升級到WebSocket。對於少數不支持WebSocket的Firebase流量,Firebase使用Netty實現了一個自定義的庫來進行長輪詢,而且通過調優具備很是高的性能和響應性。

Firebase的客戶端庫邏輯處理雙向消息流,而且會在任意一端關閉流時進行通知。雖然這在TCP或者WebSocket協議上實現起來相對簡單,可是在處理長輪詢傳輸時它仍然是一項挑戰。對於長輪詢的場景來講,下面兩個屬性必須被嚴格地保證:

——保證消息的按順序投遞

——關閉通知

1-保證消息的按順序投遞

能夠經過使得在某個指定的時刻有且只有一個未完成的請求,來實現長輪詢的按順序投遞。由於客戶端不會在它收到它的上一個請求的響應以前發出另外一個請求,因此這就保證了它以前所發出的全部消息都被接收,而且能夠安全地發送更多的請求了。一樣,在服務器端,直到客戶端收到以前的響應以前,將不會發出新的請求。所以,老是能夠安全地發送緩存在兩個請求之間的任何東西。然而,這將致使一個嚴重的缺陷。使用單一請求技術,客戶端和服務器端都將花費大量的時間來對消息進行緩衝。例如,若是客戶端有新的數據須要發送,可是這是已經有了一個未完成的請求,那麼它在發出新的請求以前,就必須得等待服務器的響應。若是這時在服務器上沒有可用的數據,則可能須要很長時間。

一個更加高性能的解決方案則是容忍更多的正在併發進行的請求。在實踐中,這能夠經過將單一請求的模式切換爲最多兩個請求的模式。這個算法包含了兩個部分:

——每當客戶端有新的數據須要發送時,它都將發送一個新的請求,除非已經有兩個請求正在被處理

——每當服務器接收到來自客戶端的請求時,若是它已經有了一個來自客戶端的未完成的請求,那麼即便沒有數據,他也將當即迴應第一個請求。

相對於單一請求的模式,這種方式提供了一個重要的改進:客戶端和服務器的緩衝時間都被限定在了最多一次的網絡往返時間裏。

固然,這種性能的增長並非沒有代價的;它致使了代碼複雜性的相應增長。該長輪詢算法也再也不保證消息的按順序投遞,可是一些來自TCP協議的理念能夠保證這些消息的按順序投遞。由客戶端發送的每一個請求都包含一個序列號,每次請求時都將會遞增。此外,每一個請求都包含了關於有效負載中的消息數量的元數據。若是一個消息跨越了多個請求,那麼在有效負載中所包含的消息的序號也會被包含在元數據中。

服務器維護了一個傳入消息分段的環形緩衝區,在它們完成以後,若是它們以前沒有不完整的消息,那麼會當即對它們進行處理,下行要簡單點,由於長輪詢傳輸響應時HTTP GET請求,並且對於有效載荷的大小沒有相同的限制。在這種狀況下,將包含一個對於每一個響應都將會遞增的序列號,只要客戶端接收到了達到指定序列號的全部響應,他就能夠開始處理列表中的全部消息;若是它沒有收到,那麼它將緩衝該列表,直到它接收到這些爲完成的響應。

2-關閉通知

在長輪詢傳輸中第二個須要保證的屬性是關閉通知。在這種狀況下,使得服務器意識到傳輸已經關閉,明顯要重要與使得客戶端識別到傳輸的關閉。客戶端所使用的Firebase庫將會在鏈接斷開時將操做放入隊列以便稍後執行,並且這些被放入隊列的操做可能也會對其它仍然鏈接着的客戶端形成影響。所以,知道客戶端何時實際上已經斷開了是很是重要的。實現由服務器發起的關閉操做是相對簡單的,其能夠經過使用一個特殊的協議級別的關閉消息響應下一個請求來實現。

實現客戶端的關閉通知是比較棘手的。雖然可使用相同的關閉通知,可是有兩種狀況可能會致使這種方式失效:用戶能夠關閉瀏覽器標籤頁,或者網絡鏈接也可能會消失。標籤頁關閉的這種狀況能夠經過iframe來處理,iframe會在頁面卸載時發送一個包含關閉消息的請求。第二種狀況則能夠經過服務器超時來處理。當心謹慎地選擇超時值大小很重要,由於服務器沒法區分慢速的網絡和斷開的客戶端。也就是說,對於服務器來講,沒法知道一個請求是被實際推遲了一分鐘,仍是該客戶端丟失了它的網絡鏈接。相對於應用程序須要多快地意識到斷開的客戶端來講,選取一個平衡了誤報所帶來的成本的合適的超時大小是很重要的。

下圖演示了Firebase的長輪詢傳輸是如何處理不一樣類型的請求的。

Netty實戰十四之案例研究

在這個圖中,每一個長輪詢請求都表明了不一樣類型的場景,最初,客戶端向服務器發送了一個輪詢(輪詢0)。一段時間以後,服務器從系統內的其它地方接收到了發送給客戶端的數據,因此它使用該數據響應了輪詢0。在該輪詢返回以後,由於客戶端目前沒有任何未完成的請求,因此客戶端有當即發送了一個新的輪詢(輪詢1)。過了一下子,客戶端須要發送數據給服務器。由於它只有一個未完成的輪詢,因此它有發送了一個新的輪詢(輪詢2),其中包含了須要被遞交的數據。根據協議,一旦在服務器同時存在兩個來自相同的客戶端的輪詢時,它將響應第一個輪詢。在這種狀況下,服務器沒有任何已經就緒的數據能夠用於該客戶端,所以它發送回了一個空響應。客戶端也維護了一個超時,並將在超時被觸發時發送第二次輪詢,即便它沒有任何額外的數據須要發送。這將系統從因爲瀏覽器超時緩慢的請求所致使的故障中隔離開來。

2.三、HTTP 1.1 keep-alive和流水線化

經過HTTP 1.1 keep-alive特性,能夠在同一個鏈接上發送多個請求到服務器。這使得HTTP流水線化——能夠發送新的請求而沒必要等待來自服務器的響應,成爲了可能。實現對於HTTP流水線化以及keep-alive特性的支持一般是直截了當的,可是當混入了長輪詢以後,他就明顯變得更加複雜起來。

若是一個長輪詢請求緊跟着一個REST(表徵狀態轉移)請求,那麼將有一些注意事項須要被考慮在內,以確保瀏覽器可以正確工做。一個Channel可能會混和異步消息(長輪詢請求)和同步消息(REST請求)。當一個Channel上出現了一個同步請求時,Firebase必須按照順序同步響應該Channel中全部以前的請求。例如,若是有一個未完成的長輪詢請求,那麼在處理該REST請求以前,須要使用一個空操做對該長輪詢傳輸進行響應。

下圖說明了Netty是如何讓Firebase在一個套接字上響應多個請求的。

Netty實戰十四之案例研究

若是瀏覽器有多個打開的鏈接,而且正在使用長輪詢,那麼它將重用這些鏈接來處理來自這兩個打開的標籤頁的消息。對於長輪詢請求來講,這是很困難的,而且還須要妥善地管理一個HTTP請求隊列。長輪詢請求能夠被中斷,可是被處理的請求卻不能。Netty使服務於多種類型的請求很輕鬆。

——靜態的HTML頁面:緩存的內容,能夠直接返回而不須要進行處理;例子包括一個單頁面的HTTP應用程序、robots.txt和crossdomain.xml

——REST請求:Firebase支持傳統的GET、POST、PUT、DELETE以及OPTIONS請求

——WebSocket:瀏覽器和Firebase服務器之間的雙向連接,擁有它本身的分幀協議

——長輪詢:這些相似於HTTP的GET請求,可是應用程序的處理方式有所不一樣。

——被代理的請求:某些請求不能由接收它們的服務器處理。在這種狀況下,Firebase將會把這些請求代理到集羣中正確的服務器。以便最終用戶沒必要擔憂數據存儲的具體位置。這些相似於REST請求,可是代理服務器處理它們的方式有所不一樣。

——經過SSL的原始字節:一個簡單的TCP套接字,運行Firebase本身的分幀協議,而且優化了握手過程。

Firebase使用Netty來設置好它的ChannelPipeline以解析傳入的請求,並隨後適當地從新配置ChannelPipeline剩餘的其它部分。在某些狀況下,如WebSocket和原始字節,一旦某個特定類型的請求被分配給某個Channel以後,它就會在它的整個生命週期內保持一致。在其餘狀況下,如各類HTTP請求,該分配則必須以每一個消息爲基礎進行賦值。同一個Channel能夠處理REST請求、長輪詢請求以及被代理的請求。

2.四、控制SslHandler

Netty的SslHandler類是Firebase如何使用Netty來對它的網絡通訊進行細粒度控制的一個例子。當傳統的Web技術棧使用Apache或者Nginx之類的HTTP服務器來將請求傳遞給應用程序時,傳入的SSL請求在被應用程序的代碼接收到的時候就已經被解碼了。在多租戶的架構體系中,很難將部分的加密流量分配給使用了某個特定服務的應用程序的租戶。這很複雜,由於事實上多個應用程序可能使用了相同的加密Channel來和Firebase通訊(例如,用戶可能在不一樣的標籤頁中打開了兩個Firebase應用程序)。爲了解決這個問題,Firebase須要在SSL請求被解碼以前對它們擁有足夠的控制來處理它們。

Firebase基於帶寬向客戶進行收費。然而,對於某個消息來講,在SSL解密被執行以前,要收取費用的帳戶一般是不知道的,由於它被包含在加密了的有效負載中。Netty使得Firebase能夠在ChannelPipeline中的多個位置對流量進行攔截,所以對於字節數的統計能夠從字節剛被從套接字讀取出來時便當即開始。在消息被解密而且被Firebase的服務器端邏輯處理以後,字節計數即可以被分配給對應的帳戶。在構建這項功能時,Netty在協議棧的每一層上,都提供了對於處理網絡通訊的控制,而且也使得很是精確的計費、限流以及速率限制成爲了可能,全部的這一切都對業務具備顯著的影響。

Netty使得經過少許的Scala代碼即可以攔截全部的入站消息和出站消息而且統計字節數成爲了可能。

2.五、Firebase小結

在Firebase的實時數據同步服務的服務器端架構中,Netty扮演了不可或缺的角色。它使得能夠支持一個異構的客戶端生態系統,其中包括了各類各樣的瀏覽器,以及徹底由Firebase控制的客戶端。使用Netty,Firebase能夠在每一個服務器上每秒鐘處理數以萬計的消息。Netty之因此很是了不得,有如下幾個緣由:

——他很快:開發原型只須要幾天時間,而且歷來不是生成瓶頸

——它的抽象層次具備良好的定位:Netty提供了必要的細粒度控制,而且容許在控制流的每一步進行自定義。

——它支持在同一個端口上支撐多種協議:HTTP、WebSocket、長輪詢以及獨立的TCP協議

——它的GitHub庫是一流的:精心編寫的javadoc使得能夠無障礙地利用它進行開發

三、Urban Airship——構建移動服務

隨着智能手機的使用之前所未有的速度在全球範圍內不斷增加,涌現了大量的服務提供商,以協助開發者和市場人員提供使人驚歎不已的終端用戶體驗。不一樣於它們的功能手機前輩,智能手機渴求IP鏈接,並經過多個渠道(3G、4G、WiFi、WIMAX以及藍牙)來尋求鏈接。隨着愈來愈多的這些設備經過基於IP的協議鏈接到公共網絡,對於後端服務提供商來講,伸縮性、延遲以及吞吐量方面的挑戰變得愈來愈艱鉅了。

值得慶幸的是,Netty很是適用於處理由隨時在線的移動設備的驚羣效應所帶來的許多問題。

3.一、移動消息的基礎知識

雖然市場人員長期以來都使用SMS來做爲一種觸達移動設備的通道,可是最近一種被稱爲推送通知的功能正在迅速地成爲向智能手機發送消息的首選機制。推送通知一般使用較爲便宜的數據通道,每條消息的價格只是SMS費用的一小部分。推送通知的吞吐量一般都比SMS高2-3個數量級,因此它成爲了突發新聞的理想通道。最重要的是,推送通知爲用戶提供了設備驅動的對推送通道的控制。若是一個用戶不喜歡某個應用程序的通知消息,那麼用戶能夠禁用該應用程序的通知,或者乾脆刪除該應用程序。

在一個很是高的級別上,設備和推送通知行爲的交互相似於下圖所述。

Netty實戰十四之案例研究

在高級別上,當應用程序開發人員想要發送推送通知給某臺設備時,開發人員必需要考慮存儲有關設備及其應用程序安裝的信息。一般,應用程序的安裝都將會執行代碼以檢索一個平臺相關的標識符,而且將該標識符上報給一個持久化該標識符的中心化服務。稍後,應用程序安裝以外的邏輯將會發起一個請求以向該設備投遞一條消息。

一旦一個應用程序的安裝已經將它的標識符註冊到了後端服務,那麼推送消息的遞交就能夠反過來採起兩種方式。在第一種方式中,使用應用程序維護一條到後端服務的直接鏈接,消息能夠被直接遞交給應用程序自己。第二種方式更加常見,在這種方式中,應用程序將依賴第三方表明該後端服務來將消息遞交給應用程序。在Urban Airship,這兩種遞交推送通知的方式都有使用,並且也都大量地使用了Netty。

3.二、第三方遞交

在第三方推送遞交的狀況下,每一個推送通知平臺都爲開發者提供了一個不一樣的API,來將消息遞交給應用程序安裝。這些API有着不一樣的協議(基於二進制的或者基於文本的)、身份驗證(OAuth、X.509等)以及能力。對於集成它們而且達到最佳吞吐量,每種方式都有着其各自不一樣的挑戰。

儘管事實上每一個這些提供商的根本目的都是嚮應用程序遞交通知消息,可是它們各自又都採起了不一樣的方式,這對系統集成商形成了重大的影響。例如,蘋果公司的Apple推送通知服務(APNS)定義了一個嚴格的二進制協議;而其餘的提供商則將它們的服務構建在了某種形式的HTTP之上,全部的這些微妙變化都影響了如何以最佳的方式達到最大的吞吐量。值得慶幸的是,Netty是一個靈活得使人驚奇的工具,它爲消除不一樣協議之間的差別提供了極大的幫助。

3.三、使用二進制協議的例子

蘋果公司的APNS是一個具備特定的網絡字節序的有效載荷的二進制協議。發送一個APNS通知將涉及下面的事件序列:

(1)經過SSLv3鏈接將TCP套接字鏈接到APNS服務器,並用X.509證書進行身份認證;

(2)根據Apple定義的格式,構造推送消息的二進制表示形式

(3)將消息寫出到套接字

(4)若是你已經準備好了肯定任何和已經發送消息相關的錯誤代碼,則從套接字中讀取

(5)若是有錯誤發生,則從新鏈接該套接字,並從步驟2繼續。

做爲格式化二進制消息的一部分,消息的生產者須要生成一個對於APNS系統透明的標識符。一旦消息無效(如不正確的格式、大小或者設備信息),那麼該標識符將會在步驟4的錯誤響應消息中返回給客戶端。

雖然從表面上看,該協議彷佛簡單明瞭,可是想要成功地解決全部上述問題,仍是有一些微妙的細節,尤爲是在JVM上。

——APNS規範規定,特定的有效載荷值須要以大端字節序進行發送(如令牌長度)。

——在前面的操做序列中的第3步要求兩個解決方案二選一。由於JVM不容許從一個已經關閉的套接字中讀取數據,即便在輸出緩衝區中有數據存在,因此你有兩個選項。

——在一次寫出操做以後,在該套接字上執行帶有超時的阻塞讀取動做。這種方式有多個缺點:

阻塞等待錯誤消息的時間長短是不肯定的,錯誤可能會發生在數毫秒或者數秒以內 因爲套接字對象沒法在多個線程之間共享,因此在等待錯誤消息時,對套接字的寫操做必須當即阻塞。這將對吞吐量形成巨大的影響。若是在一次套接字寫操做中遞交單個消息,那麼在直到讀取超時發生以前,該套接字上都不會發出更多的消息。當你要遞交數千萬的消失時,每一個消息之間都有3秒的延遲是沒法接受的。

依賴套接字超時是一項昂貴的操做。它將致使一個異常被拋出,以及幾個沒必要要的系統調用。

——使用異步I/O。在這個模型中,讀操做和寫操做都不會阻塞。這使得寫入者能夠持續地給APNS發送消息,同時也容許操做系統在數據可供讀取時通知用戶代碼。

Netty使得能夠輕鬆地解決全部的這些問題,同時提供了使人驚歎的吞吐量。首先,讓咱們看看Netty是如何簡化使用正確的字節序打包二進制APNS消息的,以下代碼。

public final class ApnsMessage {    //APNS消息老是以一個字節大小的命令做爲開始,所以該值被編碼爲常量
    private static final byte COMMAND = (byte)1;    public ByteBuf toBuffer(){        //由於消息的大小不一,因此出於效率考慮,在ByteBuf建立以前將先計算它
        short size = (short)(1 +//Command
            4 + //Identifier
            4 + //Expiry
            2 + //DT length header
            32 + //DS length
            2 + //body length header
            body.length);        //在建立時,ByteBuf的大小正好,而且指定了用於APNS的大端字節序
        ByteBuf buf = Unpooled.buffer(size).order(ByteOrder.BIG_ENDIAN);
        buf.writeByte(COMMAND);        //來自於類中其它地方維護的狀態的各類值將會被寫入到緩衝區中
        buf.writeInt(identifier);
        buf.writeInt(expiryTime);        //這個類中的deviceToken字段是一個Java的byte[]
        buf.writeShort((short)deviceToken.length);
        buf.writeBytes(deviceToken);
        buf.writeShort((short)body.length);
        buf.writeBytes(body);        //當緩衝區已經就緒時,簡單地將它返回
        return buf;
    }
}

關於該實現的一些重要說明以下。

——Java數組的長度屬性值始終是一個整數,可是,APNS協議須要一個2-byte值。在這種狀況下,有效負載的長度已經在其餘的地方驗證過了,因此在這裏將其強制轉換爲short是安全的。注意,若是沒有顯式地將ByteBuf構造爲大端字節序,那麼在處理short和int類型的值時則可能會出現各類微妙的錯誤。

——不一樣於標準的java.nio.ByteBuffer,沒有必要翻轉緩衝區,也沒有必要關心它的位置:Netty的ByteBuf將會自動管理用於讀取和寫入的位置。

使用少許的代碼,Netty已經使得建立一個格式正確的APNS消息的過程變成小事一樁了。由於這個消息如今已經被打包進了一個ByteBuf,因此當消息準備好發送時,即可以很容易地被直接寫入鏈接了APNS的Channel。

能夠經過多重機制鏈接APNS,可是最基本的,是須要一個使用SslHandler和解碼器來填充ChannelPipeline的ChannelInitializer,以下代碼所示。

public class ApnsClientPipelineInitializer extends ChannelInitializer<Channel>{
    private final SSLEngine clientEngine;

    public ApnsClientPipelineInitializer(SSLEngine clientEngine) {        //一個X.509認證的請求須要一個javax.net.ssl.SSLEngine類的實例
        this.clientEngine = clientEngine;
    }    @Override
    protected void initChannel(Channel channel) throws Exception {        final ChannelPipeline pipeline = channel.pipeline();        //構造一個Netty的SslHandler
        final SslHandler handler = new SslHandler(clientEngine);        //APNS將嘗試在鏈接後不久從新協商SSL,須要容許從新協商
        //handler.setEnableRenegotiation(true);
        pipeline.addLast("ssl",handler);        //這個類擴展了Netty的ByteToMessageDecoder,而且處理了APNS返回一個錯誤代碼並斷開鏈接的狀況
        pipeline.addLast("decoder",new ApnsResponseDecoder());
    }
}

值得注意的是,Netty使得協商結合了異步I/O的X.509認證的鏈接變得多麼的容易。在Urban Airship早起的沒有使用Netty的原型APNS的代碼中,協商一個異步的X.509認證的鏈接須要80多行代碼和一個線程池,而這隻僅僅是爲了創建鏈接。Netty隱藏了全部的複雜性,包括SSL握手、身份驗證、最重要的將明文的字節加密爲密文,以及使用SSL所帶來的密鑰的從新協商。這些JDK中異常無聊的、容易出錯的而且缺少文檔的API都被隱藏在了3行Netty代碼以後。

在Urban Airship,在全部和衆多的包括APNS以及Google的GCM的第三方推送通知服務的鏈接中,Netty都扮演了重要的角色。在每種狀況下,Netty都足夠靈活,容許顯式地控制從更高級別的HTTP的鏈接行爲到基本的套接字級別的配置(如TCP keep-alive以及套接字緩衝區大小)的集成如何生效。

3.四、直接面向設備的遞交

除了經過第三方來遞交消息以外,Urban Airship還有直接做爲消息遞交通道的經驗。在做爲這種角色時,單個設備將直接鏈接Urban Airship的基礎設施,繞過第三方提供商。這種方式也帶來了一組大相徑庭的挑戰。

——由移動設備發出的套接字鏈接每每是短暫的。根據不一樣的條件,移動設備將頻繁地在不一樣類型的網絡之間進行切換,對於移動服務的後端提供商來講,設備將不斷地從新鏈接,並將感覺到短暫而又頻繁的鏈接週期。

——跨平臺的鏈接性是不規則的。從網絡的角度來講,平板設備的鏈接性每每表現得和移動電話不同,而對比於臺式計算機,移動電話的鏈接性的表現又不同。

——移動電話向後端服務提供商更新的頻率必定會增長。移動電話愈來愈多地被應用於平常任務中,不只產生了大量常規的網絡流量,並且也爲後端服務提供商提供了大量的分析數據。

——電池和帶寬不能被忽略。不一樣於傳統的桌面環境,移動電話一般使用有線的數據流量包。服務提供商必需要尊重最終用戶只有有限的電池使用時間,並且他們使用昂貴的、速率有限的(蜂窩移動數據網絡)帶寬這一事實。濫用二者之一都一般會致使應用被卸載,這對於移動開發人員來講多是最壞的結果。

——基礎設施的全部方面都須要大規模的伸縮。隨着移動設備普及程度的不斷增長,更多的應用程序安裝量將會致使更多的到移動服務的基礎設施的鏈接。因爲移動設備的龐大規模和增加,這個列表中的每個前面提到的元素都將變得越發複雜。

隨着時間的推移,Urban Airship從移動設備的不斷增加中學到了幾點關鍵的經驗教訓:

——移動運營商的多樣性能夠對移動設備的鏈接性形成巨大的影響;

——許多運營商都不容許TCP的keep-alive特性,所以許多運營商都會積極地剔除空閒的TCP會話;

——UDP不是一個可行的向移動設備發送消息的通道,由於許多的運營商都禁止它。

——SSLv3所帶來的開銷對於短暫的鏈接來講是巨大的痛苦。

鑑於移動增加的挑戰,以及Urban Airship的經驗教訓,Netty對於實現一個移動消息平臺來講簡直就是天做之合。

3.五、Netty擅長管理大量的併發鏈接

Netty使得能夠輕鬆地在JVM平臺上支持異步I/O。由於Netty運行在JVM之上,而且由於JVM在Linux上將最終使用Linux的epoll方面的設施來管理套接字文件描述符中所感興趣的事件(interest),因此Netty使得開發者可以輕鬆地接受大量打開的套接字——每個Linux進程將近一百萬的TCP鏈接,從而適應快速增加的移動設備的規模。有了這樣的伸縮能力,服務提供商即可以在保持低成本的同時,容許大量的設備鏈接到物理服務器上的一個單獨的進程。

在受控的測試以及優化了配置選項以使用少許的內存的條件下,一個基於Netty的服務得以容納略少於100萬的鏈接。在這種狀況下,這個限制從根本上來講是因爲linux內核強制硬編碼了每一個進程限制100萬個文件句柄。若是JVM自己沒有持有大量的套接字以及用於JAR文件的文件描述符,那麼該服務器可能本可以處理更多的鏈接,而全部的這一切都在一個4GB大小的堆上。利用這種效能,Urban Airship成功地維持了超過2000萬的到它的基礎設施的持久化的TCP套接字鏈接以進行消息遞交,全部的這一切都只使用了少許的服務器。

值得注意的是,雖然在實踐中,一個單一的基於Netty的服務便能處理將近1百萬的入站TCP套接字鏈接,可是這樣作並不必定就是務實的或者明智的。如同分佈式計算中的全部陷阱同樣,主機將會失敗、進程將須要從新啓動而且將會發生不可預期的行爲。因爲這些現實的問題,適當的容量規劃意味着須要考慮到單個進程失敗的後果。

3.六、Urban Airship小結——跨越防火牆邊界

(1)內部的RPC框架

Netty一直都是Urban Airship內部的RPC框架的核心,其一直都在不斷進化。今天,這個框架每秒鐘能夠處理數以十萬計的請求,而且擁有至關低的延遲以及傑出的吞吐量。幾乎每一個Urban Airship發出的API請求都經由了多個後端服務處理,而Netty正是全部這些服務的核心。

(2)負載和性能測試

Netty在Urban Airship已經被用於幾個不一樣的負載測試框架和性能測試框架。例如,在測試前面所描述的設備消息服務時,爲了模擬數百萬的設備鏈接,Netty和一個Redis實例相結合使用,以最小的客戶端足跡(負載)測試了端到端的消息吞吐量。

(3)同步協議的異步客戶端

對於一些內部的使用場景,Urban Airship一直都在嘗試使用Netty來爲典型的同步協議建立異步的客戶端,包括如Apache Kafka以及Memcached這樣的服務。Netty的靈活性使得咱們可以很容易地打造自然異步的客戶端,而且可以在真正的異步或同步的實現之間來回地切換,而不須要更改任何的上游代碼。

總而言之,Netty一直都是Urban Airship服務的基石。其做者和社區都是及其出色的,併成爲任何須要在JVM上進行網絡通訊的應用程序,創造了一個真正意義上的一流框架。

相關文章
相關標籤/搜索