thrift-rpc原理

轉載自:http://blog.csdn.net/kesonyk/article/details/50924489 

RPC

RPC, 遠程過程調用(Remote Procedure Call,RPC)是一個計算機通信協議,該協議允許運行於一臺計算機的程序程調用另一臺計算機的上的程序。通俗講,RPC通過把網絡通訊抽象爲遠程的過程調用,調用遠程的過程就像調用本地的子程序一樣方便,從而屏蔽了通訊複雜性,使開發人員可以無需關注網絡編程的細節,將更多的時間和精力放在業務邏輯本身的實現上,提高工作效率。

RPC本質上是一種 Inter-process communication(IPC)——進程間通信的形式。常見的進程間通信方式如管道、共享內存是同一臺物理機上的兩個進程間的通信,而RPC就是兩個在不同物理機上的進程之間的通信。概括的說,RPC就是在一臺機器上調用另一臺機器上的方法,這種調用在遠程機器上對代碼的執行就像在本機上對代碼的執行一樣,只是遷移了一個執行環境而已。

此處輸入圖片的描述

RPC是一種C/S架構的服務模型,server端提供接口供client調用,client端向server端發送數據,server端接收client端的數據進行相關計算並將結果返回給client端。

執行一次RPC通常需要經歷以下步驟(摘自 Wikipedia):

1.The client calls the client stub. The call is a local procedure call, with parameters pushed on to the stack in the normal way.
2.The client stub packs the parameters into a message and makes a system call to send the message. Packing the parameters is called marshalling.
3.The client's local operating system sends the message from the client machine to the server machine.
4.The local operating system on the server machine passes the incoming packets to the server stub.
5.The server stub unpacks the parameters from the message. Unpacking the parameters is called unmarshalling.
6.Finally, the server stub calls the server procedure. The reply traces the same steps in the reverse direction

此處輸入圖片的描述

爲了實現上述RPC步驟,許多RPC工具被研發出來。這些RPC工具大多使用“接口描述語言” —— interface description language (IDL) 來提供跨平臺跨語言的服務調用。現在生產中用的最多的IDL是Google開源的protobuf

在日常開發中通常有兩種形式來使用RPC,一種是團隊內部完全實現上述RPC的6個步驟,自己序列化數據,然後自己利用socket或者http傳輸數據,最常見的就是遊戲開發了。另一種就是利用現成的RPC工具,這些RPC工具實現了底層的數據通信,開發人員只需要利用IDL定義實現自己的服務即可而不用關心數據是如何通信的,最常見的RPC工具是Facebook開源的Thrift RPC框架。本文將重點講解Thrift RPC。

Thrift

Thrift是一個跨語言的服務部署框架,最初由Facebook於2007年開發,2008年進入Apache開源項目。Thrift通過一箇中間語言(IDL, 接口定義語言)來定義RPC的接口和數據類型,然後通過一個編譯器生成不同語言的代碼(目前支持C++,Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, Smalltalk和OCaml),並由生成的代碼負責RPC協議層和傳輸層的實現。

Thrift實際上是實現了C/S模式,通過代碼生成工具將接口定義文件生成服務器端和客戶端代碼(可以爲不同語言),從而實現服務端和客戶端跨語言的支持。用戶在Thirft描述文件中聲明自己的服務,這些服務經過編譯後會生成相應語言的代碼文件,然後用戶實現服務(客戶端調用服務,服務器端提服務)便可以了。其中protocol(協議層, 定義數據傳輸格式,可以爲二進制或者XML等)和transport(傳輸層,定義數據傳輸方式,可以爲TCP/IP傳輸,內存共享或者文件共享等)被用作運行時庫。

Thrift的協議棧如下圖所示:

此處輸入圖片的描述

在Client和Server的最頂層都是用戶自定義的處理邏輯,也就是說用戶只需要編寫用戶邏輯,就可以完成整套的RPC調用流程。用戶邏輯的下一層是Thrift自動生成的代碼,這些代碼主要用於結構化數據的解析,發送和接收,同時服務器端的自動生成代碼中還包含了RPC請求的轉發(Client的A調用轉發到Server A函數進行處理)。

協議棧的其他模塊都是Thrift的運行時模塊:

  • 底層IO模塊,負責實際的數據傳輸,包括Socket,文件,或者壓縮數據流等。

  • TTransport負責以字節流方式發送和接收Message,是底層IO模塊在Thrift框架中的實現,每一個底層IO模塊都會有一個對應TTransport來負責Thrift的字節流(Byte Stream)數據在該IO模塊上的傳輸。例如TSocket對應Socket傳輸,TFileTransport對應文件傳輸。

  • TProtocol主要負責結構化數據組裝成Message,或者從Message結構中讀出結構化數據。TProtocol將一個有類型的數據轉化爲字節流以交給TTransport進行傳輸,或者從TTransport中讀取一定長度的字節數據轉化爲特定類型的數據。如int32會被TBinaryProtocol Encode爲一個四字節的字節數據,或者TBinaryProtocol從TTransport中取出四個字節的數據Decode爲int32。

  • TServer負責接收Client的請求,並將請求轉發到Processor進行處理。TServer主要任務就是高效的接收Client的請求,特別是在高併發請求的情況下快速完成請求。

  • Processor(或者TProcessor)負責對Client的請求做出響應,包括RPC請求轉發,調用參數解析和用戶邏輯調用,返回值寫回等處理步驟。Processor是服務器端從Thrift框架轉入用戶邏輯的關鍵流程。Processor同時也負責向Message結構中寫入數據或者讀出數據。

Thrift的模塊設計非常好,在每一個層次都可以根據自己的需要選擇合適的實現方式。同時也應該注意到Thrift目前的特性並不是在所有的程序語言中都支持。例如C++實現中有TDenseProtocol沒有TTupleProtocol,而Java實現中有TTupleProtocol沒有TDenseProtocol。

利用Thrift用戶只需要做三件事:

(1). 利用IDL定義數據結構及服務
(2). 利用代碼生成工具將(1)中的IDL編譯成對應語言(如C++JAVA),編譯後得到基本的框架代碼
(3). (2)中框架代碼基礎上完成完整代碼(純C++代碼、JAVA代碼等)

爲了實現上述RPC協議棧,Thrift定義了一套IDL,封裝了server相關類, processor相關類,transport相關類,protocol相關類以及併發和時鐘管理方面的庫。下文將一一介紹。

數據類型

Thrift類型系統的目標是使編程者能使用完全在Thrift中定義的類型,而不論他們使用的是哪種編程語言。Thrift類型系統沒有引入任何特殊的動態類型或包裝器對象,也不要求開發者編寫任何對象序列化或傳輸的代碼。Thrift IDL文件在邏輯上,是開發者對他們的數據結構進行註解的一種方法,該方法告訴代碼生成器怎樣在語言之間安全傳輸對象,所需的額外信息量最小。

  • Base Types(基本類型)
bool 布爾值,真或假
byte 有符號字節
i16  16位有符號整數
i32  32位有符號整數
i64  64位有符號整數
double 64位浮點數
string 與編碼無關的文本或二進制字符串

許多語言中都沒有無符號整數類型,且無法防止某些語言(如Python)的開發者把一個負值賦給一個整型變量,這會導致程序無法預料的行爲。從設計角度講,無符號整型鮮少用於數學目的,實際中更長用作關鍵詞或標識符。這種情況下,符號是無關緊要的,可用有符號整型代替。

  • Structs(結構體)

Thrift結構體定義了一個用在多種語言之間的通用對象。定義一個Thrift結構體的基本語法與C結構體定義非常相似。域可由一個整型域標識符(在該結構體的作用域內是唯一的),以及可選的默認值來標註。

struct Phone {
 1: i32 id,
 2: string number,
 3: PhoneType type
}
  • enum(枚舉)
enum Operation {
   ADD = 1,
   SUBTRACT = 2,
   MULTIPLY = 3,
   DIVIDE = 4
 }
  • Containers(容器)

Thrift容器是強類型的,映射爲通用編程語言中最常使用的容器。使用C++模板類來標註。有三種可用類型:

list<type>:映射爲STL vectorJava ArrayList,或腳本語言中的native array。。
set<type>: 映射爲爲STL setJava HashSetPython中的set,或PHP/Ruby中的native dictionary
Map<type1,type2>:映射爲STL mapJava HashMapPHP associative array,或Python/Ruby dictionary

在目標語言中,定義將產生有read和write兩種方法的類型,使用Thrift TProtocol對象對對象進行序列化和傳輸。

  • Exceptions(異常)

異常在語法和功能上都與結構體相同,唯一的區別是它們使用exception關鍵詞,而非struct關鍵詞進行聲明。 生成的對象繼承自各目標編程語言中適當的異常基類,以便與任何給定語言中的本地異常處理無縫地整合。

exception InvalidOperation {
  1: i32 whatOp,
  2: string why
}
  • Services(服務)

使用Thrift類型定義服務。對一個服務的定義在語法上等同於在面向對象編程中定義一個接口(或一個純虛抽象類)。Thrift編譯器生成實現該接口的客戶與服務器存根。服務的定義如下:

service <name> {
<returntype> <name>(<arguments>)
[throws (<exceptions>)]
...
}

一個例子:

service StringCache {
void set(1:i32 key, 2:string value),
string get(1:i32 key) throws (1:KeyNotFound knf),
void delete(1:i32 key)
}

注意: 除其他所有定義的Thrift類型外,void也是一個有效的函數返回類型。void函數可添加一個async修飾符,產生的代碼不等待服務器的應答。 一個純void函數會向客戶端返回一個應答,保證服務器一側操作已完成。應用開發者應小心,僅當方法調用失敗是可以接受的,或傳輸層已知可靠的情況下,才使用async優化。

TServer

Thrift核心庫提供一個TServer抽象類。

此處輸入圖片的描述

此處輸入圖片的描述

TServer在Thrift框架中的主要任務是接收Client的請求,並轉到某個TProcessor上進行請求處理。針對不同的訪問規模,Thrift提供了不同的TServer模型。Thrift目前支持的Server模型包括:

1.  TSimpleServer:使用阻塞IO的單線程服務器,主要用於調試
2.  TThreadedServer:使用阻塞IO的多線程服務器。每一個請求都在一個線程裏處理,併發訪問情況下會有很多線程同時在運行。
3.  TThreadPoolServer:使用阻塞IO的多線程服務器,使用線程池管理處理線程。
4.  TNonBlockingServer:使用非阻塞IO的多線程服務器,使用少量線程既可以完成大併發量的請求響應,必須使用TFramedTransport

Thrift 使用 libevent 作爲服務的事件驅動器, libevent 其實就是 epoll更高級的封裝而已(在linux下是epoll)。處理大量更新的話,主要是在TThreadedServer和TNonblockingServer中進行選擇。TNonblockingServer能夠使用少量線程處理大量併發連接,但是延遲較高;TThreadedServer的延遲較低。實際中,TThreadedServer的吞吐量可能會比TNonblockingServer高,但是TThreadedServer的CPU佔用要比TNonblockingServer高很多。

TServer的Benchmark可以參考: https://github.com/m1ch1/mapkeeper/wiki/TThreadedServer-vs.-TNonblockingServer

TServer對象通常如下工作:

1 使用TServerTransport獲得一個TTransport
2 使用TTransportFactory,可選地將原始傳輸轉換爲一個適合的應用傳輸(典型的是使用TBufferedTransportFactory
3 使用TProtocolFactory,爲TTransport創建一個輸入和輸出
4 調用TProcessor對象的process()方法

恰當地分離各個層次,這樣服務器代碼無需瞭解任何正在使用的傳輸、編碼或者應用。服務器在連接處理、線程等方面封裝邏輯,而processor處理RPC。唯一由應用開發者編寫的代碼存在於Thrift定義文件和接口實現裏。 Facebook已部署了多種TServer實現,包括單線程的TSimpleServer,每個連接一個線程的TThreadedServer,以及線程池的TThreadPoolServer。 TProcessor接口在設計上具有非常高的普遍性。不要求一個TServer使用一個生成的TProcessor對象。應用開發者可以很容易地編寫在TProtocol對象上操作的任何類型的服務器(例如,一個服務器可以簡單地將一個特定的對象類型流化,而沒有任何實際的RPC方法調用)。

Thrift中定義一個server的方法如下:

  TSimpleServer server(
    boost::make_shared<CalculatorProcessor>(boost::make_shared<CalculatorHandler>()),
    boost::make_shared<TServerSocket>(9090),
    boost::make_shared<TBufferedTransportFactory>(),
    boost::make_shared<TBinaryProtocolFactory>());

  TThreadedServer server(
    boost::make_shared<CalculatorProcessorFactory>(boost::make_shared<CalculatorCloneFactory>()),
    boost::make_shared<TServerSocket>(9090), //port
    boost::make_shared<TBufferedTransportFactory>(),
    boost::make_shared<TBinaryProtocolFactory>());


  const int workerCount = 4;//線程池容量
  boost::shared_ptr<ThreadManager> threadManager = ThreadManager::newSimpleThreadManager(workerCount);
  threadManager->threadFactory(boost::make_shared<PlatformThreadFactory>());
  threadManager->start();
  TThreadPoolServer server(
    boost::make_shared<CalculatorProcessorFactory>(boost::make_shared<CalculatorCloneFactory>()),
    boost::make_shared<TServerSocket>(9090),
    boost::make_shared<TBufferedTransportFactory>(),
    boost::make_shared<TBinaryProtocolFactory>(),
    threadManager);

  TNonBlockingServer server(
    boost::make_shared<CalculatorProcessorFactory>(boost::make_shared<CalculatorCloneFactory>()),
    boost::make_shared<TServerSocket>(9090),
    boost::make_shared<TFramedTransportFactory>(),
    boost::make_shared<TBinaryProtocolFactory>(),
    threadManager);

  server.serve();//啓動server

TTransport

Thrift最底層的傳輸可以使用Socket,File和Zip來實現,Memory傳輸在Thrift之前的版本里有支持,Thrift 0.8裏面就不再支持了。TTransport是與底層數據傳輸緊密相關的傳輸層。每一種支持的底層傳輸方式都存在一個與之對應的TTransport。在TTransport這一層,數據是按字節流(Byte Stream)方式處理的,即傳輸層看到的是一個又一個的字節,並把這些字節按照順序發送和接收。TTransport並不瞭解它所傳輸的數據是什麼類型,實際上傳輸層也不關心數據是什麼類型,只需要按照字節方式對數據進行發送和接收即可。數據類型的解析在TProtocol這一層完成。

TTransport具體的有以下幾個類:

TSocket:使用阻塞的TCP Socket進行數據傳輸,也是最常見的模式
THttpTransport:採用Http傳輸協議進行數據傳輸
TFileTransport:文件(日誌)傳輸類,允許client將文件傳給server,允許server將收到的數據寫到文件中
TZlibTransport:與其他的TTransport配合使用,壓縮後對數據進行傳輸,或者將收到的數據解壓

下面幾個類主要是對上面幾個類地裝飾(採用了裝飾模式),以提高傳輸效率。
TBufferedTransport:對某個Transport對象操作的數據進行buffer,即從buffer中讀取數據進行傳輸,或者將數據直接寫入buffer
TFramedTransport:同TBufferedTransport類似,也會對相關數據進行buffer,同時,它支持定長數據發送和接收(按塊的大小,進行傳輸)。
TMemoryBuffer:從一個緩衝區中讀寫數據

此處輸入圖片的描述

Thrift實現中,一個關鍵的設計選擇就是將傳輸層從代碼生成層解耦。從根本上,生成的Thrift代碼只需要知道如何讀和寫數據。數據的源和目的地無關緊要,可以是一個socket,一段共享內存,或本地磁盤上的一個文件。TTransport(Thrift transport)接口支持以下方法:

open    Opens the tranpsort
close    Closes the tranport
isOpen  Indicates whether the transport is open
read    Reads from the transport
write   Writes to the transport
flush   Forces any pending writes

除以上的TTransport接口外,還有一個TServerTransport接口,用來接收或創建原始傳輸對象。它的接口如下:

open   Opens the transport
listen   Begins listening for connections
accept  Returns a new client transport
close   Closes the transport

TProtocol

TProtocol的主要任務是把TTransport中的字節流轉化爲數據流(Data Stream),在TProtocol這一層就會出現具有數據類型的數據,如整型,浮點數,字符串,結構體等。TProtocol中數據雖然有了數據類型,但是TProtocol只會按照指定類型將數據讀出和寫入,而對於數據的真正用途,需要在Thrift自動生成的Server和Client中裏處理。

Thrift 可以讓用戶選擇客戶端與服務端之間傳輸通信協議的類別,在傳輸協議上總體劃分爲文本 (text) 和二進制 (binary) 傳輸協議,爲節約帶寬,提高傳輸效率,一般情況下使用二進制類型的傳輸協議爲多數。常用協議有以下幾種:

TBinaryProtocol: 二進制格式
TCompactProtocol: 高效率的、密集的二進制編碼格式
TJSONProtocol: 使用 JSON 的數據編碼協議進行數據傳輸
TSimpleJSONProtocol: 提供JSON只寫協議, 生成的文件很容易通過腳本語言解析。
TDebugProtocol: 使用易懂的可讀的文本格式,以便於debug

TCompactProtocol 高效的編碼方式,使用了類似於ProtocolBuffer的Variable-Length Quantity (VLQ) 編碼方式,主要思路是對整數採用可變長度,同時儘量利用沒有使用Bit。對於一個int32並不保證一定是4個字節編碼,實際中可能是1個字節,也可能是5個字節,但最多是五個字節。TCompactProtocol並不保證一定是最優的,但多數情況下都會比TBinaryProtocol性能要更好。

此處輸入圖片的描述

TProtocol接口非常直接,它根本上支持兩件事: 1) 雙向有序的消息傳遞; 2) 基本類型、容器及結構體的編碼。

writeMessageBegin(name, type, seq)
writeMessageEnd()
writeStructBegin(name)
writeStructEnd()
writeFieldBegin(name, type, id)
writeFieldEnd()
writeFieldStop()
writeMapBegin(ktype, vtype, size)
writeMapEnd()
writeListBegin(etype, size)
writeListEnd()
writeSetBegin(etype, size)
writeSetEnd()
writeBool(bool)
writeByte(byte)
writeI16(i16)
writeI32(i32)
writeI64(i64)
writeDouble(double)
writeString(string)
name, type, seq = readMessageBegin()
readMessageEnd()
name = readStructBegin()
readStructEnd()
name, type, id = readFieldBegin()
readFieldEnd()
k, v, size = readMapBegin()
readMapEnd()
etype, size = readListBegin()
readListEnd()
etype, size = readSetBegin()
readSetEnd()
bool = readBool()
byte = readByte()
i16 = readI16()
i32 = readI32()
i64 = readI64()
double = readDouble()
string = readString()

注意到每個write函數有且僅有一個相應的read方法。WriteFieldStop()異常是一個特殊的方法,標誌一個結構的結束。讀一個結構的過程是readFieldBegin()直到遇到stop域,然後readStructEnd()。生成的代碼依靠這個調用順序,來確保一個協議編碼器所寫的每一件事,都可被一個相應的協議解碼器讀取。 這組功能在設計上更加註重健壯性,而非必要性。例如,writeStructEnd()不是嚴格必需的,因爲一個結構體的結束可用stop域表示。

TProcessor/Processor

Processor是由Thrift生成的TProcessor的子類,主要對TServer中一次請求的 InputProtocol和OutputTProtocol進行操作,也就是從InputProtocol中讀出Client的請求數據,向OutputProtcol中寫入用戶邏輯的返回值。Processor是TServer從Thrift框架轉到用戶邏輯的關鍵流程。同時TProcessor.process是一個非常關鍵的處理函數,因爲Client所有的RPC調用都會經過該函數處理並轉發。

Thrift在生成Processor的時候,會遵守一些命名規則,可以參考 Thrift Generator部分的介紹。

TProcessor對於一次RPC調用的處理過程可以概括爲:

  1. TServer接收到RPC請求之後,調用TProcessor.process進行處理

  2. TProcessor.process首先調用TTransport.readMessageBegin接口,讀出RPC調用的名稱和RPC調用類型。如果RPC調用類型是RPC Call,則調用TProcessor.process_fn繼續處理,對於未知的RPC調用類型,則拋出異常。

  3. TProcessor.process_fn根據RPC調用名稱到自己的processMap中查找對應的RPC處理函數。如果存在對應的RPC處理函數,則調用該處理函數繼續進行請求響應。不存在則拋出異常。

a) 在這一步調用的處理函數,並不是最終的用戶邏輯。而是對用戶邏輯的一個包裝。

b) processMap是一個標準的std::map。Key爲RPC名稱。Value是對應的RPC處理函數的函數指針。 processMap的初始化是在Processor初始化的時候進行的。Thrift雖然沒有提供對processMap做修改的API,但是仍可以通過繼承TProcessor來實現運行時對processMap進行修改,以達到打開或關閉某些RPC調用的目的。

  1. RPC處理函數是RPC請求處理的最後一個步驟,它主要完成以下三個步驟:

a) 調用RPC請求參數的解析類,從TProtocol中讀入數據完成參數解析。不管RPC調用的參數有多少個,Thrift都會將參數放到一個Struct中去。Thrift會檢查讀出參數的字段ID和字段類型是否與要求的參數匹配。對於不符合要求的參數都會跳過。這樣,RPC接口發生變化之後,舊的處理函數在不做修改的情況,可以通過跳過不認識的參數,來繼續提供服務。進而在RPC框架中提供了接口的多Version支持。

b) 參數解析完成之後,調用用戶邏輯,完成真正的請求響應。

c) 用戶邏輯的返回值使用返回值打包類進行打包,寫入TProtocol。

ThriftClient

瞭解了上述提到的TProtocol,TTransport,參數解析類和返回值打包類的概念,Thrift的Client就會變得非常容易理解。

ThriftClient跟TProcessor一樣都主要操作InputProtocol和OutputProtocol,不同的是ThritClient將RPC調用分爲Send和receive兩個步驟。

  1. Send步驟,將用戶的調用參數作爲一個整體的Struct寫入TProcotol,併發送到TServer。

  2. Send結束之後,ThriftClient便立刻進入Receive狀態等待TServer的響應。對於TServer返回的響應,使用返回值解析類進行返回值解析,完成RPC調用。

Thrift RPC Version

Thrift的RPC接口支持不同Version之間的兼容性。需要注意的是:

1.  不要修改已經存在數據的字段編號
2.  新加的字段必須是optional的。以保證新生成的代碼可以序列舊的Message。同時儘量爲新加的字段添加默認值。
3.  Required字段不能被刪除。可以字段前加上"OBSOLETE_"來提醒後續用戶該字段已經不再使用,同時字段編號不能複用。
4.  修改默認值對Version控制沒有影響。因爲默認值不會被傳輸,而是由數據的接受者來決定。

Thrift Generator

Thrift自動生成代碼的代碼框架被直接HardCode到了代碼生成器裏,因此對生成代碼的結構進行修改需要重新編譯Thrift,並不是十分方便。如果Thrift將代碼結構保存到一個模板文件裏,修改生成代碼就會相對容易一些。

自動生成的代碼就會遵守一定的命名規則。Thrift中幾種主要的命名規則爲:

1.  IDLName + _types.h :用戶自定義數據類型頭文件
2.  IDLName + _constants.h :用戶自定義的枚舉和常量數據類型頭文件
3.  ServiceName + .h ServerProcessor定義和Client定義頭文件
4.  ServericeName + _ + RPC名稱 + _args :服務器端RPC參數解析類
5.  ServericeName + _ + RPC名稱 + _result :服務器端RPC返回值打包類
6.  ServericeName + _ + RPC名稱 + _pargs :客戶端RPC參數打包類
7.  ServericeName + _ + RPC名稱 + _presult :客戶端RPC返回值解析類
8.  process_ + RPC名稱:服務器端RPC調用處理函數
9.  send_ + RPC名稱:客戶端發送RPC請求的方法
10. recv_ + RPC名稱:客戶端接收RPC返回的方法

客戶端和服務器的參數解析和返回值解析雖然針對的是同樣的數據結構,但是Thrift並沒有使用同一個類來完成任務,而是將客戶端和服務器的解析類分開。

當RPC調用參數含有相同信息,並需要進行相同操作的時候,對參數解析類的集中管理就會變得非常有必要了。比如在一些用Thrift實現訪問控制的系統中,每一個RPC調用都會加一個參數token作爲訪問憑證,並在每一個用戶函數裏進行權限檢查。使用統一的參數解析類接口的話,就可以將分散的權限檢查集中到一塊進行處理。Thrift中有衆多的解析類,這些解析類的接口類似,但是卻沒有一個共有的基類,對參數的集中管理造成了一定的困難。如果Thrift爲解析類建立一個基類,並把解析類指針放到一個Map中,這樣參數就可以進行集中管理,不僅可以進一步減小自動生成代碼的體積,也滿足了對參數進行統一管理的需求。

版本化(Versioning)

Thrift面對版本化和數據定義的改變是健壯的。將階段性的改變推出到已部署的服務中的能力至關重要。系統必須能夠支持從日誌文件中讀取舊數據,以及過時的客戶(服務器)向新的服務器(客戶)發送的請求。

  • Field Identifiers(域標識符)

Thrift的版本化通過域標識符實現。Thrift中,一個結構體的每一個成員的域頭都用一個唯一的域標識符編碼。域標識符和類型說明符結合起來,唯一地標誌該域。Thrift定義語言支持域標識符的自動分配,但最好始終顯式地指定域標識符。標識符的指定如下所示:

struct Example {
    1:i32 number=10,
    2:i64 bigNumber,
    3:double decimals,
    4:string name="thrifty"
}

爲避免手動和自動分配的標識符之間的衝突,省略了標識符的域所賦的標識符從-1開始遞減,本語言對正的標識符僅支持手動賦值。

函數參數列表裏能夠、並且應當指定域標識符。事實上,參數列表不僅在後端表現爲結構,實際上在編譯器前端也表現爲與結構體同樣的代碼。這允許我們對方法參數進行版本安全的修改。

service StringCache {
    void set(1:i32 key, 2:string value),
    string get(1:i32 key) throws (1:KeyNotFound knf),
    void delete(1:i32 key)
}

可認爲結構體是一個字典,標識符是關鍵字,而值是強類型的有名字的域。 域標識符在內部使用i16的Thrift類型。然而要注意,TProtocol抽象能以任何格式編碼標識符。

  • Isset

如果遇到了一個預料之外的域,它可被安全地忽視並丟棄。當一個預期的域找不到時,必須有某些方法告訴開發者該域不在。這是通過定義的對象內部的一個isset結構實現的。(Isset功能在PHP裏默認爲null,Python裏爲None,Ruby裏爲nil)。 各個Thrift結構內部的isset對象爲各個域包含一個布爾值,表示該域在結構中是否存在。接收一個結構時,應當在直接對其進行操作之前,先檢查一個域是否已設置(being set)。

class Example {
public:
    Example() :
    number(10),
    bigNumber(0),
    decimals(0),
    name("thrifty") {}

    int32_t number;
    int64_t bigNumber;
    double decimals;
    std::string name;

    struct __isset {
        __isset() : number(false), bigNumber(false), decimals(false), name(false) {};
        bool number;
        bool bigNumber;
        bool decimals;
        bool name;
    } __isset;
...
}
  • Case Analysis(案例分析)

有四種可能發生版本不匹配的情況:

  1. 新加的域,舊客戶端,新服務器。這種情況下,舊客戶端不發送新的域,新服務器認出該域未設置,並對過時的請求執行默認行爲。

  2. 移除的域,舊客戶端,新服務器。這種情況下,舊客戶端發送已被移除的域,而新服務器簡單地無視它。

  3. 新加的域,新客戶端,舊服務器。新客戶端發送一箇舊服務器不識別的域。舊服務器簡單地無視該域,像平時一樣進行處理。

  4. 移除的域,新客戶端,舊服務器。這是最危險的情況,因爲舊服務器不太可能對丟失的域執行適當的默認行爲。這種情形下,建議在新客戶端之前,先推出新服務器。

實現細節

  • Target Languages(目標語言)

Thrift當前支持五種目標語言:C++,Java,Python,Ruby和PHP。在Facebook,用C++部署的服務器占主導地位。用PHP實現的Thrift服務也已被嵌入Apache web服務器,從後端透明地接入到許多使用THttpClient實現TTransport接口的前端結構。

  • Servers and Multithreading(服務器和多線程)

爲處理來自多個客戶機的同時的請求,Thrift服務要求基本的多線程。對Thrift服務器邏輯的Python和Java實現來說,隨語言發佈的標準線程庫提供了足夠的支持。對C++實現來說,不存在標準的多線程運行時庫。具體說來,不存在健壯的、輕量的和可移植的線程管理器及定時器類。爲此,Thrift實現了自己的庫,如下所述。

  • ThreadManager

ThreadManager創建一池工作者線程,一旦有空閒的工作者線程,應用就可以調度任務來執行。ThreadManager並未實現動態線程池大小的調整,但提供了原語,以便應用能基於負載添加和移除線程。Thrift把複雜的API抽象留給特定應用,提供原語以制定所期望的政策,並對當前狀態進行採樣。

  • TimerManager

TimerManager允許應用在未來某個時間點調度Runnable對象以執行。它具體的工作是允許應用定期對ThreadManager的負載進行抽樣,並根據應用的方針使線程池大小發生改變。TimerManager也能用於生成任意數量的定時器或告警事件。 TimerManager的默認實現,使用了單個線程來處理過期的Runnable對象。因此,如果一個定時器操作需要做大量工作,尤其是如果它需要阻塞I/O,則應當在一個單獨的線程中完成。

  • Nonblocking Operation(非阻塞操作)

儘管Thrift傳輸接口更直接地映射到一個阻塞I/O模型,然而Thrift基於libevent和TFramedTransport,用C++實現了一個高性能的TNonBlockingServer。這是通過使用狀態機,把所有I/O移動到一個嚴密的事件循環中來實現的。實質上,事件循環將成幀的請求讀入TMemoryBuffer對象。一旦全部請求ready,它們會被分發給TProcessor對象,該對象能直接讀取內存中的數據。

  • Compiler(編譯器)

Thrift編譯器是使用C++實現的。儘管若用另一種語言來實現,代碼行數可能會少,但使用C++能夠強制語言結構的顯示定義,使代碼對新的開發者來說更容易接近。 代碼生成使用兩遍pass完成。第一遍只看include文件和類型定義。這一階段,並不檢查類型定義,因爲它們可能依賴於include文件。第一次pass,所有包含的文件按順序被掃描一遍。一旦解析了include樹,第二遍pass過所有文件,將類型定義插入語法樹,如果有任何未定義的類型,則引發一個error。然後,根據語法樹生成程序。 由於固有的複雜性以及潛在的循環依賴性,Thrift顯式地禁止前向聲明。兩個Thrift結構不能各自包含對方的一個實例。

  • TFileTransport

TFileTransport通過將來的數據及數據長度成幀,並將它寫到磁盤上,來對Thrift的請求/結構作日誌。使用一個成幀的磁盤上格式,允許了更好的錯誤檢查,並有助於處理有限數目的離散事件。TFileWriterTransport使用一個交換內存中緩衝區的系統,來確保作大量數據的日誌時的高性能。一個Thrift日誌文件被分裂成某一特定大小的塊,被記入日誌的信息不允許跨越塊的邊界。如果有一個可能跨越塊邊界的消息,則添加填塞直到塊的結束,並且消息的第一個字節與下一個塊的開始對齊。將文件劃分成塊,使從文件的一個特定點讀取及解釋數據成爲可能。

Facebook的Thrift服務

Facebook中已經大量使用了Thrift,包括搜索、日誌、手機、廣告和開發者平臺。下面討論兩種具體的使用。

  • Search(搜索)

Facebook搜索服務使用Thrift作爲底層協議和傳輸層。多語言的代碼生成很適合搜索,因爲可以用高效的服務器端語言(C++)進行應用開發,並且Facebook基於PHP的web應用可以使用Thrift PHP庫調用搜索服務。Thrift使搜索團隊能夠利用各個語言的長處,快速地開發代碼。

  • Logging(日誌)

使用Thrift TFileTransport功能進行結構化的日誌。可認爲各服務函數定義以及它的參數是一個結構化的日誌入口,由函數名識別。

Thrift vs ProtocolBuffer

與ProtocolBuffer不同,Thrift不僅提供了跨語言的數據序列化和反序列化機制,更提供了跨語言的RPC實現。在Thrift的框架裏,用戶只需要實現用戶邏輯即可完成從客戶端到服務器的RPC調用。由於Thrift良好的模塊設計,用戶也可以非常方便的根據自己的需要選擇合適的模塊。例如RPC的Server既可以使用單線程的SimpleServer,也可以使用更高性能的多線程NonblockingServer。

Thrift和ProtocolBuffer解決的一個主要問題是結構化數據的序列化和反序列化。與ProtocolBuffer不同,Thrift在結構化數據之前加入了一個MessageHeader,並使用MessageHeader來完成RPC調用。在MessageBoy上,Thrift和ProtocolBuffer的結構大致相同,每一個數據字段都由Meta信息和數據信息兩部分組成。Meta的內容會隨着數據信息的不同而發生變化,例如在表示String類型的數據時,Meta信息中會包含一個字長信息,而表示int32類型的數據,並不需要這種Meta信息。但一般都會包含字段類型(Byte,i32,String…)和字段編號兩個Meta信息。

Thrift不僅支持的程序語言比ProtocolBuffer多,而且支持的數據結構也比ProtocolBuffer要多。Thrift不僅支持Byte,i32,String等基本數據類型,更是支持List,Map,Set和Struct等複雜數據類型,但是Thrift在數據序列化和反序列化上的性能要比ProtocolBuffer稍微差一些。

Ref


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章