【架構】Twitter高性能RPC框架Finagle介紹

Twitter的RPC框架Finagle簡介

Finagle是Twitter基於Netty開發的支持容錯的、協議無關的RPC框架,該框架支撐了Twitter的核心服務。來自Twitter的軟件工程師Jeff Smick撰文詳細描述了該框架的工做原理和使用方式html

在Jeff Smick的博客文章中,介紹了Twitter的架構演進歷程。Twitter面向服務的架構是由一個龐大的Ruby on Rails應用轉化而來的。爲了適應這種架構的變化,須要有一個高性能的、支持容錯的、協議無關且異步的RPC框架。在面向服務的架構之中,服務會將大多數的時間花費在等待上游服務的響應上,所以使用異步的庫可以讓服務併發地處理請求,從而充分發揮硬件的潛能。Finagle構建在Netty之上,並非直接在原生NIO之上構建的,這是由於Netty已經解決了許多Twitter所遇到的問題並提供了乾淨整潔的API。ios

Twitter構建在多個開源協議之上,包括HTTP、Thrift、Memcached、MySQL以及Redis。所以,網絡棧須要足夠靈活,以保證能與這些協議進行交流而且還要具備足夠的可擴展性以支持添加新的協議。Netty自己沒有與任何特定的協議綁定,對其添加協議支持很是簡單,只需建立對應的事件處理器(event handler)便可。這種可擴展性產生了衆多社區驅動的協議實現,包括SPDY、PostrgreSQL、WebSockets、IRC以及AWS。git

Netty的鏈接管理以及協議無關性爲Finagle奠基了很好的基礎,不過Twitter的有些需求是Netty沒有原生支持的,由於這些需求都是「較高層次的」。好比,客戶端須要鏈接到服務器集羣而且要進行負載均衡。全部的服務均須要導出指標信息(如請求率、延遲等),這些指標爲調試服務的行爲提供了有價值的內部信息。在面向服務架構中,一個請求可能會通過數十個服務,因此若是沒有跟蹤框架的話,調試性能問題幾乎是不可能的。爲了解決這些問題,Twitter構建了Finagle。簡而言之,Finagle依賴於Netty的IO多路複用技術(multiplexing),並在Netty面向鏈接的模型之上提供了面向事務(transaction-oriented)的框架。github

Finagle的工做原理

Finagle強調模塊化的理念,它會將獨立的組件組合在一塊兒。每一個組件能夠根據配置進行替換。好比,全部的跟蹤器(tracer)都實現了相同的接口,這樣的話,就能夠建立跟蹤器將追蹤數據存儲到本地文件、保持在內存中並暴露爲讀取端點或者將其寫入到網絡之中。編程

在Finagle棧的底部是Transport,它表明了對象的流,這種流能夠異步地讀取和寫入。Transport實現爲Netty的ChannelHandler,並插入到ChannelPipeline的最後。當Finagle識別到服務已經準備好讀取數據時,Netty會從線路中讀取數據並使其穿過ChannelPipeline,這些數據會被codec解析,而後發送到Finagle的Transport。從這裏開始,Finagle將數據發送到本身的棧之中。json

對於客戶端的鏈接,Finagle維持了一個Transport的池,經過它來平衡負載。根據所提供的鏈接池語義,Finagle能夠向Netty請求一個新的鏈接,也能夠重用空閒的鏈接。當請求新的鏈接時,會基於客戶端的codec建立一個Netty ChannelPipeline。一些額外的ChannelHandler會添加到ChannelPipeline之中,以完成統計(stats)、日誌以及SSL的功能。若是全部的鏈接都處於忙碌的狀態,那麼請求將會按照所配置的排隊策略進行排隊等候。api

在服務端,Netty經過所提供的ChannelPipelineFactory來管理codec、統計、超時以及日誌等功能。在服務端ChannelPipeline中,最後一個ChannelHandler是Finagle橋(bridge)。這個橋會等待新進入的鏈接併爲每一個鏈接建立新的Transport。Transport在傳遞給服務器實現以前會包裝一個新的channel。而後,會從ChannelPipeline之中讀取信息,併發送到所實現的服務器實例中。安全

  1. Finagle客戶端位於Finagle Transport之上,這個Transport爲用戶抽象了Netty;
  2. Netty ChannelPipeline包含了全部的ChannelHandler實現,這些實現完成實際的工做;
  3. 對於每個鏈接都會建立Finagle服務器,而且會爲其提供一個Transport來進行讀取和寫入;
  4. ChannelHandler實現了協議的編碼/解碼邏輯、鏈接級別的統計以及SSL處理。

橋接NettyFinagle

Finagle客戶端使用ChannelConnector來橋接Finagle與Netty。ChannelConnector是一個函數,接受SocketAddress並返回Future Transport。當請求新的Netty鏈接時,Finagle使用ChannelConnector來請求一個新的Channel,並使用該Channel建立Transport。鏈接會異步創建,若是鏈接成功的話,會使用新創建的Transport來填充Future,若是沒法創建鏈接的話,就會產生失敗。Finagle客戶端會基於這個Transport分發請求。服務器

Finagle服務器會經過Listener綁定一個接口和端口。當新的鏈接建立時,Listener建立一個Transport並將其傳入一個給定的函數。這樣,Transport會傳給Dispatcher,它會根據指定的策略未來自Transport的請求分發給Service。網絡

Finagle的抽象

Finagle的核心概念是一個簡單的函數(在這裏函數式編程很關鍵),這個函數會從Request生成包含Response的Future:

type Service[Req, Rep] = Req => Future[Rep]

Future是一個容器,用來保存異步操做的結果,這樣的操做包括網絡RPC、超時或磁盤的I/O操做。Future要麼是空——此時還沒有有可用的結果,要麼成功——生成者已經完成操做並將結果填充到了Future之中,要麼失敗——生產者發生了失敗,Future中包含告終果異常。

這種簡單性可以促成很強大的結構。在客戶端和服務器端,Service表明着相同的API。服務器端實現Service接口,這個服務器能夠用來進行具體的測試,Finagle也能夠將其在某個網絡接口上導出。客戶端能夠獲得Service的實現,這個實現能夠是虛擬的,也能夠是遠程服務器的具體實現。

好比說,咱們能夠經過實現Service建立一個簡單的HTTP Server,它接受HttpReq並返回表明最終響應的Future[HttpRep]:

val s: Service[HttpReq, HttpRep] = new Service[HttpReq, HttpRep] {
def apply(req: HttpReq): Future[HttpRep] =
	Future.value(HttpRep(Status.OK, req.body)) 
}

Http.serve(":80", s)

這個樣例在全部接口的80端口上暴露該服務器,而且經過twitter.com的80端口進行使用。可是,咱們也能夠選擇不暴露服務器而是直接使用它:

server(HttpReq("/")) map { rep => transformResponse(rep) }

在這裏,客戶端代碼的行爲方式是同樣的,可是並不須要網絡鏈接,這就使得客戶端和服務器的測試變得很簡單直接。

客戶端和服務器提供的都是應用特定的功能,但一般也會須要一些與應用自己無關的功能,舉例來講認證、超時、統計等等。Filter爲實現應用無關的特性提供了抽象。

Filter接受一個請求以及要進行組合的Service:

type Filter[Req, Rep] = (Req, Service[Req, Rep]) => Future[Rep]

在應用到Service以前,Filter能夠造成鏈:

recordHandletime andThen
traceRequest andThen
collectJvmStats
andThen myService

這樣的話,就可以很容易地進行邏輯抽象和關注點分離。Finagle在內部大量使用了Filter,Filter有助於促進模塊化和可重用性。

Filter還能夠修改請求和響應的數據及類型。下圖展示了一個請求穿過過濾器鏈到達Service以及響應反向穿出的過程:

對請求失敗的管理

在擴展性的架構中,失敗是常見的事情,硬件故障、網絡阻塞以及網絡鏈接失敗都會致使問題的產生。對於支持高吞吐量和低延遲的庫來講,若是它不能處理失敗的話,那這樣庫是沒有什麼意義的。爲了獲取更好的失敗管理功能,Finagle在吞吐量和延遲上作了必定的犧牲。

Finagle可使用主機集羣實現負載的平衡,客戶端在本地會跟蹤它所知道的每一個主機。它是經過計數發送到某個主機上的未完成請求作到這一點的。這樣的話,Finagle就能將新的請求發送給最低負載的主機,同時也就能得到最低的延遲。

若是發生了失敗的請求,Finagle會關閉到失敗主機的鏈接,並將其從負載均衡器中移除。在後臺,Finagle會不斷地嘗試從新鏈接,若是Finagle可以從新創建鏈接的話,就會再次將其添加到負載均衡器之中。

服務的組合

Finagle將服務做爲函數的理念可以編寫出簡單且具備表述性的代碼。例如,某個用戶對其時間線(timeline)的請求會涉及到多個服務,核心包括認證服務、時間線服務以及Tweet服務。它們之間的關係能夠很簡潔地進行表述:

val timelineSvc = Thrift.newIface[TimelineService](...) // #1  
val tweetSvc = Thrift.newIface[TweetService](...) 
val authSvc = Thrift.newIface[AuthService](...)   
val authFilter = Filter.mk[Req, AuthReq, Res, Res] { (req, svc) => // #2
           authSvc.authenticate(req) flatMap svc(_) 
}   
val apiService = Service.mk[AuthReq, Res] { req =>   
   timelineSvc(req.userId) flatMap {tl =>     
	val tweets = tl map tweetSvc.getById(_)     
	Future.collect(tweets) map tweetsToJson(_) }     
	}    
} //#3  
Http.serve(":80", authFilter andThen apiService) // #4   
// #1 建立每一個服務的客戶端 
// #2 建立過濾器來認證傳入的請求
// #3 建立服務,將已認證的時間線請求轉換爲json響應  
// #4 使用認證過濾器和服務,在80端口啓動新的HTTP服務器

在上面的例子中,建立了時間線服務、Tweet服務以及認證服務的客戶端,建立了一個Filter來認證原始的請求,最後,實現了服務,將其與認證過濾器組合起來,並暴露在80端口上。

當收到請求時,認證過濾器會嘗試進行認證,若是失敗的話就會當即返回並不會影響到核心服務。認證成功後,AuthReq將會發送到API服務上。服務會使用附帶的userId藉助時間線服務查找該用戶的時間線,這裏會返回一個tweet id的列表,而後對其進行遍歷獲取關聯的tweet,最終請求的tweet列表會收集起來並做爲JSON返回。在這裏咱們將併發的事情所有留給了Finagle處理,並不用關心線程池以及競態條件的問題,代碼整潔清晰而且很安全。

以上介紹了Finagle的基本功能以及簡單的用法。Finagle支撐了Tweet巨大的網絡傳輸增加,同時還下降了延遲以及對硬件的需求。目前,Finagle與Netty社區積極合做,在完善產品的同時,也爲社區作出了貢獻。Finagle內部會更加模塊化,從而爲升級到Netty 4鋪平道路。

關於Finagle的更多文檔和樣例,能夠參考其站點

 

參考資料:

http://www.infoq.com/cn/news/2014/05/twitter-finagle-intro

有沒有人能對twitter的finagle和國內的dubbo作個對比? :http://www.zhihu.com/question/31440152

月PV破150億:Tumblr架構揭密:http://os.51cto.com/art/201202/317899.htm

Finagle 介紹:http://wiki.jikexueyuan.com/project/scala/finagle.html

相關文章
相關標籤/搜索