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


Twitter的RPC框架Finagle簡介

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

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

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

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

Finagle的工作原理

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

在Finagle棧的底部是Transport,它代表了對象的流,這種流可以異步地讀取和寫入。Transport實現為Netty的ChannelHandler,並插入到ChannelPipeline的最后。當Finagle識別到服務已經准備好讀取數據時,Netty會從線路中讀取數據並使其穿過ChannelPipeline,這些數據會被codec解析,然后發送到Finagle的Transport。從這里開始,Finagle將數據發送到自己的棧之中。

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

在服務端,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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM