轉載:http://zheming.wang/thrift-rpcxiang-jie.html
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 vector,Java ArrayList,或腳本語言中的native array。。 set<type>: 映射為為STL set,Java HashSet,Python中的set,或PHP/Ruby中的native dictionary。 Map<type1,type2>:映射為STL map,Java HashMap,PHP 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優化。
===================================thrift對java的數據類型映射=================================
要編寫Thrift定義文件,肯定要熟悉Thrift常見的數據類型:
1.基本類型(括號內為對應的Java類型):
bool(boolean): 布爾類型(TRUE or FALSE)
byte(byte): 8位帶符號整數
i16(short): 16位帶符號整數
i32(int): 32位帶符號整數
i64(long): 64位帶符號整數
double(double): 64位浮點數
string(String): 采用UTF-8編碼的字符串
2.特殊類型(括號內為對應的Java類型):
binary(ByteBuffer):未經過編碼的字節流
3.Structs(結構):
struct定義了一個很普通的OOP對象,但是沒有繼承特性。
struct UserProfile {
1: i32 uid,
2: string name,
3: string blurb
}
如果變量有默認值,可以直接寫在定義文件里:
struct UserProfile {
1: i32 uid = 1,
2: string name = "User1",
3: string blurb
}
4.容器,除了上面提到的基本數據類型,Thrift還支持以下容器類型:
list(java.util.ArrayList):
set(java.util.HashSet):
map(java.util.HashMap):
用法如下:
struct Node {
1: i32 id,
2: string name,
3: list<i32> subNodeList,
4: map<i32,string> subNodeMap,
5: set<i32> subNodeSet
}
包含定義的其他Object:
struct SubNode {
1: i32 uid,
2: string name,
3: i32 pid
}
struct Node {
1: i32 uid,
2: string name,
3: list<subNode> subNodes
}
5.Services服務,也就是對外展現的接口:
service UserStorage {
void store(1: UserProfile user),
UserProfile retrieve(1: i32 uid)
}
===================================thrift對java的數據類型映射=================================
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調用的處理過程可以概括為:
-
TServer接收到RPC請求之后,調用TProcessor.process進行處理
-
TProcessor.process首先調用TTransport.readMessageBegin接口,讀出RPC調用的名稱和RPC調用類型。如果RPC調用類型是RPC Call,則調用TProcessor.process_fn繼續處理,對於未知的RPC調用類型,則拋出異常。
-
TProcessor.process_fn根據RPC調用名稱到自己的processMap中查找對應的RPC處理函數。如果存在對應的RPC處理函數,則調用該處理函數繼續進行請求響應。不存在則拋出異常。
a) 在這一步調用的處理函數,並不是最終的用戶邏輯。而是對用戶邏輯的一個包裝。
b) processMap是一個標准的std::map。Key為RPC名稱。Value是對應的RPC處理函數的函數指針。 processMap的初始化是在Processor初始化的時候進行的。Thrift雖然沒有提供對processMap做修改的API,但是仍可以通過繼承TProcessor來實現運行時對processMap進行修改,以達到打開或關閉某些RPC調用的目的。
- 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兩個步驟。
-
Send步驟,將用戶的調用參數作為一個整體的Struct寫入TProcotol,並發送到TServer。
-
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” :Server端Processor定義和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(案例分析)
有四種可能發生版本不匹配的情況:
-
新加的域,舊客戶端,新服務器。這種情況下,舊客戶端不發送新的域,新服務器認出該域未設置,並對過時的請求執行默認行為。
-
移除的域,舊客戶端,新服務器。這種情況下,舊客戶端發送已被移除的域,而新服務器簡單地無視它。
-
新加的域,新客戶端,舊服務器。新客戶端發送一個舊服務器不識別的域。舊服務器簡單地無視該域,像平時一樣進行處理。
-
移除的域,新客戶端,舊服務器。這是最危險的情況,因為舊服務器不太可能對丟失的域執行適當的默認行為。這種情形下,建議在新客戶端之前,先推出新服務器。
實現細節
- 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