讀古今文學網 > Netty實戰 > 第14章 案例研究,第一部分 >

第14章 案例研究,第一部分

本章主要內容

  • Droplr
  • Firebase
  • Urban Airship

在本章中,我們將介紹兩部分案例研究中的第一部分,它們是由已經在內部基礎設施中廣泛使用了Netty的公司貢獻的。我們希望這些其他人如何利用Netty框架來解決現實世界問題的例子,能夠拓展你對於Netty能夠做到什麼事情的理解。

注意 每個案例分析的作者都直接參與了他們所討論的項目。

14.1 Droplr——構建移動服務

Bruno de Carvalho,首席架構師

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

這是一個關於我們是如何從一個單片的、運行緩慢的LAMP[1]應用程序遷移到基於Netty實現的現代的、高性能的以及水平擴展的分佈式架構的案例研究。

14.1.1 這一切的起因

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

後來Windroplr變成了Droplr for Windows,而我則開始主要負責基礎設施的建設,並且最終得到了一個新的挑戰:完全重新考慮Droplr的基礎設施。

在那時,Droplr本身已經確立成為了一種工作的理念,因此2.0版本的目標也是相當的標準:

  • 將單片的技術棧拆分為多個可橫向擴展的組件;
  • 添加冗余,以避免宕機;
  • 為客戶端創建一個簡潔的API;
  • 使其全部運行在HTTPS上。

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

我知道這句話意味的可不只是變快一點或者變快很多。「要不惜一切代價」意味著一個完全數量級上的更快。而且我也知道,Netty最終將會在這樣的努力中發揮重要作用。

14.1.2 Droplr是怎樣工作的

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

就是這樣。歡暢地、實時地分享。

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

14.1.3 創造一個更加快速的上傳體驗

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

(1)接收上傳;

(2)上傳到S3;

(3)如果是圖片,則創建略縮圖;

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

更加仔細地看看這個流程,你很快便會發現在第2步和第3步上有兩個瓶頸。不管從客戶端上傳到我們的服務器有多快,在實際的上傳完成之後,直到成功地接收到響應之間,對於拖樂的創建總是會有惱人的間隔——因為對應的文件仍然需要被上傳到S3中,並為其生成略縮圖。

文件越大,間隔的時間也越長。對於非常大的文件來說,連接[2]最終將會在等待來自服務器的響應時超時。由於這個嚴重的問題,當時Droplr只可以提供單個文件最大32MB的上傳能力。

有兩種截然不同的方案來減少上傳時間。

  • 方案A,樂觀且看似更加簡單(見圖14-1):
    • 完整地接收文件;
    • 將文件保存到本地的文件系統,並立即返回成功到客戶端;
    • 計劃在將來的某個時間點將其上傳到S3。
  • 方案B,安全但複雜(見圖14-2):
    • 實時地(流式地)將從客戶端上傳的數據直接管道給S3。

圖14-1 方案A,樂觀且看似更加簡單

圖14-2 方案B,安全但複雜

1.樂觀且看似更加簡單的方案

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

我們當前的數據顯示,我們的上傳失敗率略低於0.01%(萬分之一),絕大多數都是在上傳實際完成之前,客戶端和服務器之間的連接就超時了。

我們也可以嘗試通過在文件被最終推送到S3之前,從接收它的機器提供該文件的服務來繞開它,然而這種做法本身就是一堆麻煩:

  • 如果在一批文件被完整地上傳到S3之前,機器出現了故障,那麼這些文件將會永久丟失;
  • 也將會有跨集群的同步問題(「這個拖樂所對應的文件在哪裡呢?」);
  • 將會需要額外的複雜的邏輯來處理各種邊界情況,繼而不斷產生更多的邊界情況;

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

2.安全但複雜的方案

另一個選項需要對整體過程進行底層的控制。從本質上說,我們必須要能夠做到以下幾點。

  • 在接收客戶端上傳文件的同時,打開一個到S3的連接。
  • 將從客戶端連接上收到的數據管道給到S3的連接。
  • 緩衝並節流這兩個連接:
    • 需要進行緩衝,以在客戶端到服務器,以及服務器到S3這兩個分支之間保持一條的穩定的流;
    • 需要進行節流,以防止當服務器到S3的分支上的速度變得慢於客戶端到服務器的分支時,內存被消耗殆盡。
  • 當出現錯誤時,需要能夠在兩端進行徹底的回滾。

看起來概念上很簡單,但是它並不是你的通常的Web服務器能夠提供的能力。尤其是當你考慮節流一個TCP連接時,你需要對它的套接字進行底層的訪問。

它同時也引入了一個新的挑戰,其將最終塑造我們的終極架構:推遲略縮圖的創建。

這也意味著,無論該平台最終構建於哪種技術棧之上,它都必須要不僅能夠提供一些基本的特性,如難以置信的性能和穩定性,而且在必要時還要能夠提供操作底層(即字節級別的控制)的靈活性。

14.1.4 技術棧

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

選擇Netty並不是一件無需動腦的事;我研究了大量的框架,並謹記我認為的3個至關重要的要素。

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

(2)它必須能夠伸縮。不管它是有1個連接還是10 000個連接,每個服務器實例都必須要能夠保持吞吐量,並且隨著時間推移不能出現崩潰或者內存洩露。

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

要素1和要素2基本上排除了任何非編譯型的語言。我是Ruby語言的擁躉,並且熱愛Sinatra和Padrino這樣的輕量級框架,但是我知道我所追尋的性能是不可能通過這些構件塊實現的。

要素2本身就意味著:無論是什麼樣的解決方案,它都不能依賴於阻塞I/O。看到了本書這裡,你肯定已經明白為什麼非阻塞I/O是唯一的選擇了。

要素3比較繞彎兒。它意味著必須要在一個框架中找到完美的平衡,它必須在提供了對於它所接收到的數據的底層控制的同時,也支持快速的開發,並且值得信賴。這便是語言、文檔、社區以及其他的成功案例開始起作用的時候了。

在那時我有一種強烈的感覺:Netty便是我的首選武器。

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

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

代碼清單14-1 設置ChannelPipeline

pipelineFactory = new ChannelPipelineFactory {
  @Override
  public ChannelPipeline getPipeline throws Exception {
    ChannelPipeline pipeline = Channels.pipeline;
    pipeline.addLast("idleStateHandler", new IdleStateHandler(...));   ← --  IdleStateHandler 將關閉不活動的連接
    pipeline.addLast("httpServerCodec", new HttpServerCodec);   ← --  HttpServerCodec 將傳入的字節轉換為HttpRequest,並將傳出的HttpResponse 轉換為字節
    pipeline.addLast("requestController",  ← --  將RequestController添加到ChannelPipeline 中
      new RequestController(...)); 
    return pipeline;
  }
};  

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

請求控制器負責:

  • 處理負載洪峰;
  • HTTP ChannelPipeline的管理;
  • 設置請求處理的上下文;
  • 派生新的請求處理器;
  • 向請求處理器供給數據;
  • 處理內部和外部的錯誤。

代碼清單14-2給出的是RequestController相關部分的一個綱要。

代碼清單14-2 RequestController

public class RequestController
  extends IdleStateAwareChannelUpstreamHandler {

  @Override
  public void channelIdle(ChannelHandlerContext ctx,
    IdleStateEvent e) throws Exception {
    // Shut down connection to client and roll everything back.
  }

  @Override 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.
    }
  }


  @Override public void messageReceived(ChannelHandlerContext ctx,
    MessageEvent e) throws Exception {
    if (isDone) return;

    if (e.getMessage instanceof HttpRequest) {
      handleHttpRequest((HttpRequest) e.getMessage);   ← --  Droplr 的服務器請求驗證的關鍵點
    } else if (e.getMessage instanceof HttpChunk) {
      handleHttpChunk((HttpChunk)e.getMessage);  ← --  如果針對當前請求有一個活動的處理器,並且它能夠接受HttpChunk 數據,那麼它將繼續按HttpChunk 傳遞
    }
  }
}  

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

因此,HttpRequestHttpChunk都可以通過切換到另一個不同的線程,來將執行流程移交給請求處理器。當請求處理器不是CPU限定時,就會發生這樣的情況,不管是因為它們訪問了數據庫,還是執行了不適合於本地內存或者CPU的邏輯。

當發生線程切換時,所有的代碼塊都必須要以串行的方式執行;否則,我們就會冒風險,對於一次上傳來說,在處理完了序列號為nHttpChunk之後,再處理序列號為n -1的HttpChunk必然會導致文件內容的損壞。(我們可能會交錯所上傳的文件的字節佈局。)為了處理這種情況,我創建了一個自定義的線程池執行器,其確保了所有共享了同一個通用標識符的任務都將以串行的方式被執行。

從這裡開始,這些數據(請求和HttpChunk)便開始了在Netty和Droplr王國之外的冒險。

我將簡短地解釋請求處理器是如何被構建的,以在RequestController(其存在於Netty的領地)和這些處理器(存在於Droplr的領地)之間的橋樑上亮起一些光芒。誰知道呢,這也許將會幫助你架構你自己的服務器應用程序呢!

2.請求處理器

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

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

3.父接口

每個請求處理器,不管是直接的還是通過子類繼承,都是RequestHandler接口的實現。

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

  • 請求處理器是有狀態的還是無狀態的呢?它需要從某個原型克隆,還是原型本身就可以用來處理請求呢?
  • 請求處理器是CPU限定的還是非CPU限定的呢?它可以在Netty的工作線程上執行,還是需要在一個單獨的線程池中執行呢?
  • 回滾當前的變更;
  • 清理任何使用過的資源。

這個接口[3]就是RequestController對於相關動作的所有理解。通過它非常清晰和簡潔的接口,該控制器可以和有狀態的和無狀態的、CPU限定的和非CPU限定的(或者這些性質的組合)處理器以一種獨立的並且實現無關的方式進行交互。

4.處理器的實現

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

5.上傳請求處理器

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

在上傳的過程中,服務器具有雙重行為:

  • 在一邊,它充當了正在上傳文件的API客戶端的服務器;
  • 在另一邊,它充當了S3的客戶端,以推送它從API客戶端接收的數據。

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

14.1.5 性能

在服務器的初始版本完成之後,我運行了一批性能測試。結果簡直就是讓人興奮不已。在不斷地增加了難以置信的負載之後,我看到新的服務器的上傳在峰值時相比於舊版本的LAMP技術棧的快了10~12倍(完全數量級的更快),而且它能夠支撐超過1000倍的並發上傳,總共將近10k的並發上傳(而這一切都只是運行在一個單一的EC2大型實例之上)。

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

  • 它運行在一個調優的JVM中。
  • 它運行在一個高度調優的自定義技術棧中,是專為解決這個問題而創建的,而不是一個通用的Web框架。
  • 該自定義的技術棧通過Netty使用了NIO(基於選擇器的模型)構建,這意味著不同於每個客戶端一個進程的LAMP技術棧,它可以擴展到上萬甚至是幾十萬的並發連接。
  • 再也沒有以兩個單獨的,先接收一個完整的文件,然後再將其上傳到S3,的步驟所帶來的開銷了。現在文件將直接流向S3。
  • 因為服務器現在對文件進行了流式處理,所以:
    • 它再也不會花時間在I/O操作上了,即將數據寫入臨時文件,並在稍後的第二階段上傳中讀取它們;
    • 對於每個上傳也將消耗更少的內存,這意味著可以進行更多的並行上傳。
  • 略縮圖生成變成了一個異步的後處理。

14.1.6 小結——站在巨人的肩膀上

所有的這一切能夠成為可能,都得益於Netty的難以置信的精心設計的 API,以及高性能的非阻塞的I/O架構。

自2011年12月推出Droplr 2.0以來,我們在API級別的宕機時間幾乎為零。在幾個月前,由於一次既定的全棧升級(數據庫、操作系統、主要的服務器和守護進程的代碼庫升級),我們中斷了已經連續一年半安靜運行的基礎設施的100%正常運行時間,這次升級只耗費了不到1小時的時間。

這些服務器日復一日地堅挺著,每秒鐘處理幾百個(有時甚至是幾千個)並發請求,而同時還保持了如此低的內存和CPU使有率,以至於我們都難以相信它們實際上正在真實地做著如此大量的工作:

  • CPU使用率很少超過5%;
  • 無法準確地描述內存使用率,因為進程啟動時預分配了1 GB的內存,同時配置的JVM可以在必要時增長到2 GB,而在過去的兩年內這一次也沒有發生過。

任何人都可以通過增加機器來解決某個特定的問題,然而Netty幫助了Droplr智能地伸縮,並且保持了相當低的服務器賬單。

14.2 Firebase——實時的數據同步服務

Sara Robinson,Developer Happiness副總裁

Greg Soltis,Cloud Architecture副總裁

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

實現這種能力提出了一項艱難的技術挑戰,而Netty則是用於在Firebase內構建用於所有網絡通信的底層框架的最佳解決方案。這個案例研究概述了Firebase的架構,然後審查了Firebase使用Netty以支撐它的實時數據同步服務的3種方式:

  • 長輪詢;
  • HTTP 1.1 keep-alive和流水線化;
  • 控制SSL處理器。

14.2.1 Firebase的架構

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

圖14-3 Firebase的架構

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

因為客戶端可以通過多種方式連接到Firebase,所以保持連接代碼的模塊化很重要。Netty的Channel抽像對於Firebase集成新的傳輸來說簡直是夢幻般的構建塊。此外,流水線和處理器[6]模式使得可以簡單地把傳輸相關的細節隔離開來,並為應用程序代碼提供一個公共的消息流抽像。同樣,這也極大地簡化了添加新的協議支持所需要的工作。Firebase只通過簡單地添加幾個新的ChannelHandlerChannelPipeline中,便添加了對一種二進制傳輸的支持。對於實現客戶端和服務器之間的實時連接而言,Netty的速度、抽像的級別以及細粒度的控制都使得它成為了一個的卓絕的框架。

14.2.2 長輪詢

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

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

  • 保證消息的按順序投遞;
  • 關閉通知。

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

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

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

  • 每當客戶端有新的數據需要發送時,它都會發送一個新的請求,除非已經有了兩個請求正在被處理;
  • 每當服務器接收到來自客戶端的請求時,如果它已經有了一個來自客戶端的未完成的請求,那麼即使沒有數據,它也將立即回應第一個請求。

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

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

服務器維護了一個傳入消息分段的環形緩衝區,在它們完成之後,如果它們之前沒有不完整的消息,那麼會立即對它們進行處理。下行要簡單點,因為長輪詢傳輸響應的是HTTPGET請求,而且對於有效載荷的大小沒有相同的限制。在這種情況下,將包含一個對於每個響應都將會遞增的序列號。只要客戶端接收到了達到指定序列號的所有響應,它就可以開始處理列表中的所有消息;如果它還沒有收到,那麼它將緩衝該列表,直到它接收到了這些未完成的響應。

2.關閉通知

在長輪詢傳輸中第二個需要保證的屬性是關閉通知。在這種情況下,使得服務器意識到傳輸已經關閉,明顯要重要於使得客戶端識別到傳輸的關閉。客戶端所使用的Firebase庫將會在連接斷開時將操作放入隊列以便稍後執行,而且這些被放入隊列的操作可能也會對其他仍然連接著的客戶端造成影響。因此,知道客戶端什麼時候實際上已經斷開了是非常重要的。實現由服務器發起的關閉操作是相對簡單的,其可以通過使用一個特殊的協議級別的關閉消息響應下一個請求來實現。

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

圖14-4演示了Firebase的長輪詢傳輸是如何處理不同類型的請求的。

圖14-4 長輪詢

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

14.2.3 HTTP 1.1 keep-alive和流水線化

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

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

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

圖14-5 網絡圖

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

  • 靜態的HTML頁面——緩存的內容,可以直接返回而不需要進行處理;例子包括一個單頁面的HTTP應用程序、robots.txt和crossdomain.xml。
  • REST請求——Firebase支持傳統的GETPOSTPUTDELETEPATCH以及OPTIONS請求。
  • WebSocket——瀏覽器和Firebase服務器之間的雙向連接,擁有它自己的分幀協議。
  • 長輪詢——這些類似於HTTP的GET請求,但是應用程序的處理方式有所不同。
  • 被代理的請求——某些請求不能由接收它們的服務器處理。在這種情況下,Firebase將會把這些請求代理到集群中正確的服務器。以便最終用戶不必擔心數據存儲的具體位置。這些類似於REST請求,但是代理服務器處理它們的方式有所不同。
  • 通過SSL的原始字節——一個簡單的TCP套接字,運行Firebase自己的分幀協議,並且優化了握手過程。

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

14.2.4 控制SslHandler

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

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

Netty使得通過少量的Scala代碼便可以攔截所有的入站消息和出站消息並且統計字節數成為了可能,如代碼清單14-3所示。

代碼清單14-3 設置ChannelPipeline

case class NamespaceTag(namespace: String)

class NamespaceBandwidthHandler extends ChannelDuplexHandler {
  private var rxBytes: Long = 0
  private var txBytes: Long = 0
  private var nsStats: Option[NamespaceStats] = None

  override def channelRead(ctx: ChannelHandlerContext, msg: Object) {
    msg match {
      case buf: ByteBuf => {
        rxBytes += buf.readableBytes(   ← --  當消息傳入時,統計它的字節數
                   tryFlush(ctx)
      }
      case _ => { }
    }
    super.channelRead(ctx, msg)
  }

  override def write(ctx: ChannelHandlerContext, msg: Object,
      promise: ChannelPromise) {
    msg match {
      case buf: ByteBuf => {  ← --  當有出站消息時,同樣統計這些字節數
        txBytes += buf.readableBytes
        tryFlush(ctx)
        super.write(ctx, msg, promise)
      }
      case tag: NamespaceTag => {  ← --  如果接收到了命名空間標籤,則將這個Channel 關聯到某個賬戶,記住該賬戶,並將當前的字節計數分配給它 
        updateTag(tag.namespace, ctx)
      }
      case _ => {
        super.write(ctx, msg, promise)
      }
    }
  }

  private def tryFlush(ctx: ChannelHandlerContext) {
    nsStats match {
      case Some(stats: NamespaceStats) => {  ← --  如果已經有了該Channel 所屬的命名空間的標籤,則將字節計數分配給該賬戶,並重置計數器 
        stats.logOutgoingBytes(txBytes.toInt)
        txBytes = 0
        stats.logIncomingBytes(rxBytes.toInt)
        rxBytes = 0
      }
      case None => {
        // no-op, we don't have a namespace
      }
    }
  }

  private def updateTag(ns: String, ctx: ChannelHandlerContext) {
    val (_, isLocalNamespace) = NamespaceOwnershipManager.getOwner(ns)
    if (isLocalNamespace) {
      nsStats = NamespaceStatsListManager.get(ns)
      tryFlush(ctx)
    } else {
      // Non-local namespace, just flush the bytes
      txBytes = 0  ← -- 如果該字節計數不適用於這台機器,則忽略它並重置計數器
      rxBytes = 0
    }
  }
}  

14.2.5 Firebase小結

在Firebase的實時數據同步服務的服務器端架構中,Netty扮演了不可或缺的角色。它使得可以支持一個異構的客戶端生態系統,其中包括了各種各樣的瀏覽器,以及完全由Firebase控制的客戶端。使用Netty,Firebase可以在每個服務器上每秒鐘處理數以萬計的消息。Netty之所以非常了不起,有以下幾個原因。

  • 它很快。開發原型只需要幾天時間,並且從來不是生產瓶頸。
  • 它的抽像層次具有良好的定位。Netty提供了必要的細粒度控制,並且允許在控制流的每一步進行自定義。
  • 它支持在同一個端口上支撐多種協議。HTTP、WebSocket、長輪詢以及獨立的TCP協議。
  • 它的GitHub庫是一流的。精心編寫的Javadoc使得可以無障礙地利用它進行開發。
  • 它擁有一個非常活躍的社區。社區非常積極地修復問題,並且認真地考慮所有的反饋以及合併請求。此外,Netty團隊還提供了優秀的最新的示例代碼。Netty是一個優秀的、維護良好的框架,而且它已經成為了構建和伸縮Firebase的基礎設施的基礎要素。如果沒有Netty的速度、控制、抽像以及了不起的團隊,那麼Firebase中的實時數據同步將無從談起。

14.3 Urban Airship——構建移動服務

Erik Onnen,架構副總裁

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

值得慶幸的是,Netty非常適用於處理由隨時在線的移動設備的驚群效應所帶來的許多問題。本節將詳細地介紹Netty在伸縮移動開發人員和市場人員平台——Urban Airship時的幾個實際應用。

14.3.1 移動消息的基礎知識

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

在一個非常高的級別上,設備和推送通知行為之間的交互類似於圖14-6中所描述的那樣。

圖14-6 移動消息平台集成的高級別視圖

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

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

14.3.2 第三方遞交

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

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

接下來的幾節將提供Urban Airship是如何使用Netty來集成兩個上面所列出的服務提供商的例子。

14.3.3 使用二進制協議的例子

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

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

(2)根據Apple定義的格式[8],構造推送消息的二進製表示形式;

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

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

(5)如果有錯誤發生,則重新連接該套接字,並從步驟2繼續。

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

雖然從表面上看,該協議似乎簡單明瞭,但是想要成功地解決所有上述問題,還是有一些微妙的細節,尤其是在JVM上。

  • APNS規範規定,特定的有效載荷值需要以大端字節序進行發送(如令牌長度)。
  • 在前面的操作序列中的第3步要求兩個解決方案二選一。因為JVM不允許從一個已經關閉的套接字中讀取數據,即使在輸出緩衝區中有數據存在,所以你有兩個選項。
    • 在一次寫出操作之後,在該套接字上執行帶有超時的阻塞讀取動作。這種方式有多個缺點,具體如下。
      • 阻塞等待錯誤消息的時間長短是不確定的。錯誤可能會發生在數毫秒或者數秒之內。
      • 由於套接字對像無法在多個線程之間共享,所以在等待錯誤消息時,對套接字的寫操作必須立即阻塞。這將對吞吐量造成巨大的影響。如果在一次套接字寫操作中遞交單個消息,那麼在直到讀取超時發生之前,該套接字上都不會發出更多的消息。當你要遞交數千萬的消息時,每個消息之間都有3秒的延遲是無法接受的。
      • 依賴套接字超時是一項昂貴的操作。它將導致一個異常被拋出,以及幾個不必要的系統調用。
    • 使用異步I/O。在這個模型中,讀操作和寫操作都不會阻塞。這使得寫入者可以持續地給APNS發送消息,同時也允許操作系統在數據可供讀取時通知用戶代碼。

Netty使得可以輕鬆地解決所有的這些問題,同時提供了令人驚歎的吞吐量。

首先,讓我們看看Netty是如何簡化使用正確的字節序打包二進制APNS消息的,如代碼清單14-4所示。

代碼清單14-4 ApnsMessage實現

public final class ApnsMessage {
  private static final byte COMMAND = (byte) 1;  ← --  APNS 消息總是以一個字節大小的命令作為開始,因此該值被編碼為常量
  public ByteBuf toBuffer {
    short size = (short) (1 + // Command  ← -- 因為消息的大小不一,所以出於效率考慮,在ByteBuf創建之前將先計算它
      4 + // Identifier
      4 + // Expiry
      2 + // DT length header
      32 + //DS length
      2 + // body length header
      body.length);

    ByteBuf buf = Unpooled.buffer(size).order(ByteOrder.BIG_ENDIAN);  ← --  在創建時,ByteBuf 的大小正好,並且指定了用於APNS 的大端字節序
    buf.writeByte(COMMAND);  ← --  來自於類中其他地方維護的狀態的各種值將會被寫入到緩衝區中
    buf.writeInt(identifier);  
    buf.writeInt(expiryTime);
    buf.writeShort((short) deviceToken.length); ← --  這個類中的deviceToken字段(這裡未展示)是一個Java 的byte 
    buf.writeBytes(deviceToken);
    buf.writeShort((short) body.length);
    buf.writeBytes(body);
    return buf; ← --  當緩衝區已經就緒時,簡單地將它返回 
  }
}  

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

❶ Java數組的長度屬性值始終是一個整數。但是,APNS協議需要一個2-byte值。在這種情況下,有效負載的長度已經在其他的地方驗證過了,所以在這裡將其強制轉換為short是安全的。注意,如果沒有顯式地將ByteBuf構造為大端字節序,那麼在處理shortint類型的值時則可能會出現各種微妙的錯誤。

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

使用少量的代碼,Netty已經使得創建一個格式正確的APNS消息的過程變成小事一樁了。因為這個消息現在已經被打包進了一個ByteBuf,所以當消息準備好發送時,便可以很容易地被直接寫入連接了APNS的Channel

可以通過多重機制連接APNS,但是最基本的,是需要一個使用SslHandler和解碼器來填充ChannelPipelineChannelInitializer,如代碼清單14-5所示。

代碼清單14-5 設置ChannelPipeline

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

  public ApnsClientPipelineFactory(SSLEngine engine) {   ← --  一個X.509 認證的請求需要一個javax.net.ssl.SSLEngine 類的實例
    this.clientEngine = engine;  
  }

  @Override
  public void initChannel(Channel channel) throws Exception {
    final ChannelPipeline pipeline = channel.pipeline;
    final SslHandler handler = new SslHandler(clientEngine);  ← --  構造一個Netty的SslHandler
    handler.setEnableRenegotiation(true);  ← --   APNS 將嘗試在連接後不久重新協商SSL,需要允許重新協商
    pipeline.addLast("ssl", handler);
    pipeline.addLast("decoder", new ApnsResponseDecoder);  ← --   這個類擴展了Netty 的ByteToMessageDecoder,並且處理了APNS 返回一個錯誤代碼並斷開連接的情況 
  }
}  

值得注意的是,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以及套接字緩衝區大小)的集成如何生效。

14.3.4 直接面向設備的遞交

上一節提供了Urban Airship如何與第三方集成以進行消息遞交的內部細節。在談及圖14-6時,需要注意的是,將消息遞交到設備有兩種方式。除了通過第三方來遞交消息之外,Urban Airship還有直接作為消息遞交通道的經驗。在作為這種角色時,單個設備將直接連接Urban Airship的基礎設施,繞過第三方提供商。這種方式也帶來了一組截然不同的挑戰。

  • 由移動設備發出的套接字連接往往是短暫的。根據不同的條件,移動設備將頻繁地在不同類型的網絡之間進行切換。對於移動服務的後端提供商來說,設備將不斷地重新連接,並將感受到短暫而又頻繁的連接週期。
  • 跨平台的連接性是不規則的。從網絡的角度來看,平板設備的連接性往往表現得和移動電話不一樣,而對比於台式計算機,移動電話的連接性的表現又不一樣。
  • 移動電話向後端服務提供商更新的頻率一定會增加。移動電話越來越多地被應用於日常任務中,不僅產生了大量常規的網絡流量,而且也為後端服務提供商提供了大量的分析數據。
  • 電池和帶寬不能被忽略。不同於傳統的桌面環境,移動電話通常使用有限的數據流量包。服務提供商必須要尊重最終用戶只有有限的電池使用時間,而且他們使用昂貴的、速率有限的(蜂窩移動數據網絡)帶寬這一事實。濫用兩者之一都通常會導致應用被卸載,這對於移動開發人員來說可能是最壞的結果了。
  • 基礎設施的所有方面都需要大規模的伸縮。隨著移動設備普及程度的不斷增加,更多的應用程序安裝量將會導致更多的到移動服務的基礎設施的連接。由於移動設備的龐大規模和增長,這個列表中的每一個前面提到的元素都將變得愈加複雜。

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

  • 移動運營商的多樣性可以對移動設備的連接性造成巨大的影響;
  • 許多運營商都不允許TCP的keep-alive特性,因此許多運營商都會積極地剔除空閒的TCP會話;
  • UDP不是一個可行的向移動設備發送消息的通道,因為許多的運營商都禁止它;
  • SSLv3所帶來的開銷對於短暫的連接來說是巨大的痛苦。

鑒於移動增長的挑戰,以及Urban Airship的經驗教訓,Netty對於實現一個移動消息平台來說簡直就是天作之合,原因將在以下各節強調。

14.3.5 Netty擅長管理大量的並發連接

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

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

值得注意的是,雖然在實踐中,一個單一的基於Netty的服務便能夠處理將近1百萬的入站TCP套接字連接,但是這樣做並不一定就是務實的或者明智的。如同分佈式計算中的所有陷阱一樣,主機將會失敗、進程將需要重新啟動並且將會發生不可預期的行為。由於這些現實的問題,適當的容量規劃意味著需要考慮到單個進程失敗的後果。

14.3.6 Urban Airship小結——跨越防火牆邊界

我們已經演示了兩個在Urban Airship內部網絡中每天都會使用Netty的場景。Netty適合這些用途,並且工作得非常出色,但在Urban Airship內部的許多其他的組件中也有它作為腳手架存在的身影。

1.內部的RPC框架

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

2.負載和性能測試

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

3.同步協議的異步客戶端

對於一些內部的使用場景,Urban Airship一直都在嘗試使用Netty來為典型的同步協議創建異步的客戶端,包括如Apache Kafka(http://kafka.apache.org/)以及Memcached(http://memcached.org/)這樣的服務。Netty的靈活性使得我們能夠很容易地打造天然異步的客戶端,並且能夠在真正的異步或同步的實現之間來回地切換,而不需要更改任何的上游代碼。

總而言之,Netty一直都是Urban Airship服務的基石。其作者和社區都是極其出色的,並為任何需要在JVM上進行網絡通信的應用程序,創造了一個真正意義上的一流框架。

14.4 小結

本章旨在揭示真實世界中的Netty的使用場景,以及它是如何幫助這些公司解決了重大的網絡通信問題的。值得注意的是,在所有的場景下,Netty都不僅是被作為一個代碼框架而使用,而且還是開發和架構最佳實踐的重要組成部分。

在下一章中,我們將介紹由Facebook和Twitter所貢獻的案例研究,描述兩個開源項目,這兩個項目是從基於Netty的最初被開發用來滿足內部需求的項目演化而來的。


[1] 一個典型的應用程序技術棧的首字母縮寫;由Linux、Apache Web Server、MySQL以及PHP的首字母組成。

[2] 指客戶端和服務器之間的連接。——譯者注

[3] 指RequestHandler。——譯者注

[4] 你可以在https://github.com/brunodecarvalho/http-client找到這個HTTP客戶端庫。

[5] 上一個腳注中提到的這個HTTP客戶端庫已經廢棄,推薦AsyncHttpClient(https://github.com/AsyncHttpClient/ async-http-client)和Akka-HTTP(https://github.com/akka/akka-http),它們都實現了相同的功能。——譯者注

[6] 指ChannelPipelineChannelHandler。——譯者注

[7] 某些移動操作系統允許一種被稱為本地推送的推送通知,可能不會遵循這種做法。

[8] 有關APNS的信息,參考http://docs.aws.amazon.com/sns/latest/dg/mobile-push-apns.html和http://bit.ly/189mmpG。

[9] 即調用ByteBuffer的flip方法。——譯者注

[10] 注意,在這種情況下物理服務器的區別。儘管虛擬化提供了許多的好處,但是領先的雲計算提供商仍然未能支持到單個虛擬主機超過200 000~300 000的並發TCP連接。當連接達到或者超過這種規模時,建議使用裸機(bare metal)服務器,並且密切關注網絡接口卡(Network Interface Card,NIC)提供商。