《The programmer's Guide to Apache Thrift》讀書筆記

所有代碼見GitHub:Thrift Demos

第一章 Introduction to Apache Thrift

本章詳細介紹了thrift的一些特點,優點,不足,以及簡單的Java,Python,C++客戶端,服務端Demo,本次閱讀主要針對於Java相關的進行學習,C++和Python都是類似的,直接跳過。

優點:跨語言;開發效率高;提供插件序列化協議;性能可觀;靈活擴展性好

缺點:不支持自引用型數據結構;大數據量傳輸不合適;無法提供類MQ功能;對於追求極致性能可能有所欠缺

第二章 Apache Thrift Architecture

本章詳細介紹了thrift的垂直分層架構,以及每層的作用。
在這裏插入圖片描述

  • RPC Server Library Server層,基於thrift框架封裝的RPC服務,thrift提供了許多的IO模型的server
  • RPC Service Stubs Service層,根據IDL生成對應的接口,thrift已經封裝好了和server的交互,分發過程,開發者只需要重關注業務邏輯接口即可
  • User Defined Type Serialization UDT層,根據IDL生成對應的實體類,封裝了與協議層交互的序列化方法
  • The Serialization Protocol Library Protocol層,定義序列化使用的協議,封裝了與傳輸層的交互的方法
  • The Transport Library Transport層,用於端到端網絡IO,基於socket進行的封裝,再往下就是socket的底層

第三章 Moving Bytes with Transports

本章着重介紹Thrift的第一層傳輸層,在端到端進行數據傳輸的在整個Thrift中起到怎樣的作用,以及如何獨立的進行端到端讀寫,並且寫了一些memory,disk,network的讀寫Demo,如果之前做過網絡編程就很好理解,其實都是一些簡易的設備IO編程demo。

第一部分介紹了thrift如何在memory,disk之間通信,主要是使用以下兩個類:

TSimpleFileTransport //文件傳輸使用的類
TMemoryBuffer //創建內存緩存使用的類

第二部分介紹了thrift的transport接口的一些方法,分別從C++,Java,Python角度來介紹,本次着重學習Java相關的,後續同樣,不在贅述,TTransport是傳輸層所有類的公共基類,所有的傳輸方法都繼承此類,Java的TTransport類的一些抽象方法:

public abstract void open() throws TTransportException; //建立一個傳輸連接
public abstract void close(); //關閉傳輸連接
public abstract int read() throws TTransportException; //從傳輸連接讀取一定量數據
public int readAll() throws TTransportException; //一次把緩存讀取完
public void write() throws TTransportException; //寫
public void flush() throws TTransportException; //清空寫緩存
public void consumeBuffer(); //消費緩存區,直接刪除,相對於read避免了複製的開銷

第三部分介紹了使用TTransport進行網絡通信,主要的內容就是使用TTransport的子類TSocket進行通信。

第四部分主要介紹了通過TServerTransport構造一個網絡服務器,類似於rpc的服務方,然後等待客戶端的請求,TServerTransport的方法比較簡單可以直接去看源碼。

第五部分主要介紹了另外一個很重要的傳輸類,TFramedTransport,可以在傳輸的過程中指定本次傳輸的字節數,方便接收端接受,常用於在傳輸層中進行多次數據處理。

第六部分是總結,本章着重介紹了thrift的最底層-傳輸層,是thrift一切的基礎,傳輸層的所有操作由TTransport接口來定義,規範;實現了與設備無關的字節流讀寫,此外還介紹了分層傳輸的實現,分層傳輸主要就是在傳輸層再細分,從接收到消息之後再進行多次處理。

第四章 Handing Exceptions

本章着重介紹了thrift的異常處理處理模型和機制;使用thrift如何處理傳輸異常,協議異常,應用異常,返回用戶定義的異常;以及作爲開發者,如何設計出健壯的異常處理程序。

所有的可能異常都用TException來描述,對應Java中的Exception類,針對不同層的可能出現異常,分別使用TTransportException,TProtocolException,TApplicationException來捕獲

此外,允許用戶在IDL使用exception關鍵字中定義異常,實現異常從客戶端傳遞到遠程服務端,但是大部分情況異常只需要在客戶端處理即可。

本章的主要作用就是簡單瞭解一下thrift的異常處理機制,和不同語言之間的對應關係,方便我們寫出更加健壯的代碼。

第五章 Serializing Data with Protocols

本章主要介紹了thrift的協議層的作用,thrift協議層主要指定了序列化的方式,並且能夠能夠做到語言無關性的序列化,比如只要使用方指定了相同的協議,Java序列化的數據,C++也同樣可以反序列化使用。thrift主要提供了三種序列化協議,分別是:

  • TBinaryProtocol 二進制的序列化協議通過將數據轉化爲二進制01比特流來存儲,是thrift的默認序列化協議,因爲大部分的語言都支持這一協議,但是JavaScript不支持,js僅支持json類型的數據。
  • TCompactProtocol 緊湊的序列化協議,顧名思義序列化之後的數據很緊湊,所以大小要比binary的更小一些,但是支持的廣泛程度不如TBinaryProtocol。
  • TJSONProtocol JSON的序列化協議,這種序列化方式的主要優點就是可讀性高,前兩者序列化之後的數據幾乎沒有可讀性。

所有協議的公共父類都是TProtocol,與TTransport類似,TProtocol的方法很多,但是看名字就能知道是什麼意思,使用的時候直接看源碼即可,這裏就不贅述了。

本章實現了一個C++寫,Java讀,Python計算的demo,以證明thrift的跨語言特性,方便起見,我只用Java演示了其中使用同一個協議的序列化,反序列化過程,效果上是一致的。

最後在協議的選擇上,作者認爲還是要根據具體的場景來看,不能一概而論,最好的方式是都去試試,大體的方向則是追求極致效率則採用TCompactProtocol,而在網絡上傳輸則大部分會選擇TJSONProtocol,解析的效率高等多方面原因。

第六章 Apache Thrift IDL

本章主要在介紹thrift的IDL(Interface Definition Languages)一些語法,以及如何在不同的語言環境下生成代碼。

  • IDL文件名:建議以英文字母開頭,以.thrift擴展名結尾
  • IDL變量命名規則:必須以字母或者下劃線開頭,後續可以使用字母,下劃線,數字,點。檢查的正則表達式爲:[a-zA-Z_][.a-zA-Z_0-9]*
  • IDL關鍵字:類似於Java關鍵字,共30個,不贅述。此外還有一些廢棄的關鍵字,以及預留字,這些關鍵字都不能在變量命名時使用,與Java語法一致。
  • IDL namespace: 語法格式:namespace 語言類型 命名空間,語言類型可以指定爲*,代表對所有的語言指定此命名空間,可以多次指定,後續會覆蓋前者,namespace在Java中對應於package。
/**
 * namespace example
**/
namespace * org.lyh
namespace java org.lyh.service
  • IDL支持typedef
/**
 * typedef example
**/
typedef i64 long
  • IDL支持基本類型:binary(java.nio.ByteBuffer), bool(boolean), byte/i8(byte), double(double), i16(short), i32(int), i64(long), string(String), void(void) 括號中爲對應的Java類型
  • IDL支持的容器類型:list(ArrayList),set(HashSet),map(HashMap),可以指定sorted_containers參數來生成有序的容器,如thrift -gen java:sorted_containers hello.thrift,即用TreeSet代替默認的HashSet
  • IDL的struct: 對應Java中class,會生成對應的構造函數,getter,setter;語義相當於Java的model只負責數據抽象,所以不支持定義函數;單獨生成文件,struct的field可以指定默認值,提供以下參數:
    在這裏插入圖片描述
/**
 * struct example
**/
struct Trade {
    1: required string symbol //require
    2: double price = 0 //默認值
    3: optional i32 size //optional
    4: binary bin
    5: bool boo
    6: i8 ibyte
    7: i16 ishort
    8: i32 iint
    9: long ilong //typedef
    10: double dou
    11: string str
    12: list<i32> elementList
    13: set<string> elementSet
    14: map<i64, binary> elementMap
}
  • IDL支持常量:可以使用const關鍵字定義常量,但是const不能定義在struct,service內,所有的const會單獨只生成一個Java文件,命名爲”XXXConstants.java“,其中XXX是IDL文件名
/**
 * const example
**/
const i32 MAX_TIME = 10
const i64 MAX_COUN = 100
  • IDL支持Enum,單獨生成文件
/**
 * enum example
**/
enum logic {
    AND = 1
    OR = 2
    NOT = 3
}
  • IDL的exception:在第四章已經提過,可以在IDL中聲明一種可能出現的異常,單獨生成一個文件
/**
 * exception example
**/
exception badTrade {
    1: i32 id
    2: string comment
}
  • IDL的service:語義相當於Java中的interface,實際生成的是一個class,class名字與service名字相同,class裏有兩個public的接口,Iface和AsyncIface,這兩個接口下有所有在service中定義的函數,此外還有一些thrift自己的代碼,使用的時候不需要關注。
/**
 * service example
**/
service Base {
    void a();
}
service Derived extends Base{
    i64 b(1: string str);
}
  • IDL的union:Java不支持union
  • IDL的include:對應Java中的import,可以引入其他thrift文件,如include “hello.thrift”
/**
 * include example
**/
include "hello.thrift"
  • IDL支持註釋,//, #, /**/,/**都支持
  • IDL的Annotations:這個的作用是給代碼生成器看的,目前僅支持兩個註釋, cpp.type 和 final註釋,cpp.type可以強制指明某個變量必鬚生成爲什麼類型,final可以指定final類,例子如下:
/**
 * annotation example
 * cpp.type 對Java無效
**/
struct anno {
    1: i32 (cpp.type = "long") counter
} (final = "true")

這個class會生成爲一個常量類,並且counter變量的類型被強制指定爲了long,而不會成爲默認的int

  • IDL不支持:IDL的struct不支持繼承;service支持繼承,可以用extends關鍵字繼承另一個service,但是不能重載和重寫;不支持自引用
struct A {    
		1: A a
}

但是在thrift 0.9.2之後就開始支持了

除了IDL的語法之外,本章還介紹了thrift是如何通過IDL生成代碼的,我認爲這部分不重要,書中也沒有詳細介紹,大體上類似於Java編譯成class文件的過程。

最後還介紹了thrift生成代碼的一些命令參數,摘錄一下常用的(這些都可以從-help裏看到,不用專門記)

  • -debug: 可以打印錯誤信息到stdout
  • -I path:指定thrift文件的查找路徑
  • -o path:指定生成代碼的路徑
  • -out path:指定生成代碼路徑,且不生成gen-*文件夾
  • -version:查看thrift編譯器版本號
  • -r or -recurse:如果有引用其他文件則,遞歸生成代碼
  • -help:查看thrift命令參數信息

Java代碼生成方面的一些特有參數:

  • beans: 成員變量都爲private,setter方法返回void
  • private-members: 成員變量都爲private,setter方法返回this
  • nocamel:不使用駝峯命名
  • fullcamel:使用駝峯命名
  • hashcode:生成hashcode方法
  • sorted_containers:使用sorted容器代替默認容器,如TreeSet代替HashSet
// 命令示例:將resources/test.thrift生成Java文件,生成的文件存儲在java/org/lyh/client/這個路徑下,並且不要gen-*這個文件夾
thrift -out java/org/lyh/client/ -gen java resources/test.thrift

第七章 User Defined Types

本章主要在第六章的基礎之上對struct進行的具體實踐,User Defined Types 簡稱UDT的意思就用戶自定義類型,就是struct的使用;此外介紹瞭如何在UDT層進行序列化;對UDT進行修改之後對程序的影響,UDT的靈活性;最後介紹了序列化時候如何進行壓縮。

  1. 具體的實踐在GIthub裏。

  2. UDT層中如何進行序列化?直接看源碼
    所有的UDT生成的類裏有四個靜態內部類分別叫XXXStandardSchemeFactory,XXXStandardScheme,XXXTupleSchemeFactory,XXXTupleScheme,兩個工廠類,以及兩個具體序列化和反序列的類,StandardScheme是將字段id和字段值封裝成TField一起序列化,TupleScheme則是先將所有的id寫一個bitSet序列化,之後再寫字段值。

  // 所有UDT生成的類裏都有read和write兩個方法,首先調用scheme方法根據協議選擇一個scheme,然後根據scheme來調用對應的read,write方法,這個方法很長就不列出,邏輯也很簡單,可以自己去看看
	public void read(org.apache.thrift.protocol.TProtocol iprot) throws org.apache.thrift.TException {
    scheme(iprot).read(iprot, this);
  }

  public void write(org.apache.thrift.protocol.TProtocol oprot) throws org.apache.thrift.TException {
    scheme(oprot).write(oprot, this);
  }
  private static final org.apache.thrift.scheme.SchemeFactory STANDARD_SCHEME_FACTORY = new TradeStandardSchemeFactory();
  private static final org.apache.thrift.scheme.SchemeFactory TUPLE_SCHEME_FACTORY = new TradeTupleSchemeFactory();

	private static <S extends org.apache.thrift.scheme.IScheme> S scheme(org.apache.thrift.protocol.TProtocol proto) {
    return (org.apache.thrift.scheme.StandardScheme.class.equals(proto.getScheme()) ? STANDARD_SCHEME_FACTORY : TUPLE_SCHEME_FACTORY).getScheme();
  }
  1. UDT對一些修改的兼容性
  • rename:無影響,因爲thrift序列化和反序列化使用的是id和type,不使用name
  • add field:新增field的時候,使用一個之前沒用過的id和name,然後設置required級別爲default+ 默認值 或者 optional,這樣在序列化和反序列化的時候不會出異常
  • delete field:刪除field的時候,required級別爲required或者default + 默認值的field不能刪除,否則反序列化會出異常,然後刪除field的id不能再使用,應該保留一條備註曾經這個字段存在過
  • change field type:這種時候應該使用union來表示這個字段可能會有多種類型
  • change field required level:如果需要可以修改爲optional,這個級別非常靈活,此外最好不要修改
  • change field default value:默認值的修改需要根據業務邏輯來看,不能一概而論

雖然thrift的UDT提供了這些靈活性,但是我認爲作爲開發者,不應該依賴這些靈活性,而應該嚴格的保持序列化方和反序列化方的UDT是一致的,最好是公用一套,因爲保證這個的成本應該並不高,但是可以避免出現不可知的錯誤。

  1. 壓縮

一些語言在序列化的支持對數據進行壓縮,比如C++,Java,Python等,使用TZlibTransport在傳輸之前調用即可,在Java裏的原理是TZlibTransport內置了InflaterInputStream和DeflaterOutputStream兩個壓縮流對象。

第八章 Implementing Services

這章主要是在第六章的基礎上對service的具體實踐,包括service更加詳細的語法;使用service構造RPC的demo;service的擴展性;service的原理等。

  • service語法
    service中函數的定義語法與Java中接口函數定義相同:
    返回類型 + 函數名 + 參數列表 + 分號
    參數的語法有些不太一樣:
    在這裏插入圖片描述
    其中Requiredness和default value可以選填
  1. demo見github

  2. service的動態兼容性

  • 增加參數:新client調用舊server沒有問題(會忽略提供的多餘參數),如果給新增參數指定了default value,則舊client調用新server也沒有問題(未提供參數有默認值),Java不支持參數默認值
  • 刪除參數:舊client調用新server沒有問題(會忽略提供的多餘參數),如果刪除的參數指定了default value,則新client調用舊server也沒有問題(未提供參數有默認值),Java不支持參數默認是
  • 新增函數:舊client調用新server沒有問題,如果捕獲了not implemented exception,則新client調用舊server也沒有問題
  • 刪除函數:新client調用舊server沒有問題,如果捕獲了not implemented exception,則舊client調用新server也沒有問題

和struct一樣,我認爲不建議把這種兼容性應用在實際開發中,當然如果自己很清楚應用場景的話,也是可以使用的。

  1. service的調用流程,這部分的源碼相對比較多一些,目前僅介紹主要的原理,不詳細介紹源碼。
    在這裏插入圖片描述
    如圖所示是一個service對應的生成代碼,可見其中共有六個很重要的內部類,分別是:Client,AsyncClient,Processor,AsyncProcessor,getFullName_args,getFullName_result,getFullName是我在IDL中定義的函數。
    這個六個內部類從原來的的角度來看可以減少到四個,因爲另外兩個是同樣原理的異步實現方式,相對來說代碼更復雜一些,有併發的操作。
    Client類:內部有Factory類來獲取Client對象,主要負責向服務端發起RPC請求,Client類的基類裏通過調用協議層的方法來進行進行一次RPC。
    Processor類:內部有一個getFullName類,負責對實際的服務端實現調用,Processor在服務端發揮作用,在下一章可以詳細瞭解。
    getFullName_args類:對函數參數的封裝類,thrift對我們定義的函數參數進行了默認的封裝。
    getFullName_result類:對函數返回結果的封裝類,thrift對我們定義的函數結果進行了默認的封裝。
    從service層來看的話,一次RPC的過程大概如下圖所示:
    在這裏插入圖片描述
  2. 此外還介紹了service提供的一些其他功能
    Oneway Function:只由客戶端發起調用,服務端處理邏輯,但是沒有返回的函數。這類函數可以減少一半IO,並且不用阻塞客戶端,但是出錯的時候,客戶端無法感知。
    Service Inheritance:thrift的service支持繼承,但是函數不能重寫和重載。
    Asynchronous Clients:提供了更加強大的異步客戶端。

第九章 Servers

本章很重要也很精彩,文中使用的一個詞很合適,叫culmination-高潮。

本章着重介紹瞭如何去創建一個RPC的server;thrift server的IO模型;thrift的factory模型;server event接口;service的複用

  1. 如何創建一個簡單的RPC server,這裏和第一章的demo基本類似,不再贅述。

  2. thrift server的IO模型

首先看一下Java中thrift server的類繼承結構,下面會詳細介紹每種Server class對應的IO模型,不涉及源碼實現,需要深究的時候可以去看看。
在這裏插入圖片描述
下面介紹幾種常見的IO模型。
在這裏插入圖片描述
基於連接的IO模型,也就是阻塞的IO模型,一個線程負責監聽連接,每有一個連接就創建一個線程去處理,優點是客戶端可以及時得到回覆,缺點是大量的線程創建回收負擔,當連接很多的時候,服務器可能資源不足,並且並行度受到CPU核數的限制,不能和線程數保持一致。

阻塞的IO模型問題都比較明顯,Java中的TSimpleServer和TThreadPoolServer兩種server是基於連接的IO模型實現的,TSimpleServer是採用單個線程進行連接,處理,TThreadPoolServer則是使用線程池,對每個連接分配一個線程,當連接斷開,線程池回收線程。
在這裏插入圖片描述
非阻塞的IO模型,由單個線程來處理所有的客戶端任務,優點是不需要上下文切換,資源佔用少,利用率高,缺點是在大量連接同時到達時無法及時處理,吞吐量不高,沒有充分利用CPU資源,Java中的TNonblockingServer就是使用的這種IO模型。
在這裏插入圖片描述
非阻塞的IO模型,對連接進行分組,每組交給一個線程來處理,優點是充分利用了CPU資源,缺點是分組過於侷限,可能某個連接組很空閒,某個連接組又很忙,thrift沒有提供對應的server實現。
在這裏插入圖片描述
非阻塞的IO模型,將IO線程和Processing線程分離,並且Processing使用線程池來管理,優點是吞吐量高,並且使用線程池合理管理線程,缺點是實現複雜,並且IO線程需要承擔反序列化的高壓力,可能會成爲瓶頸,這一缺點可以使用前面提到的TFramedTransport來優化,因爲TFramedTransport在頭部有指定size,所以IO線程就不要反序列來判斷數據的結尾在哪,可以直接通過TMemoryBuffer分配給task線程,讓task線程從TMemoryBuffer中讀取數據反序列化,處理邏輯,但並不能完全解決IO線程的瓶頸問題,Java中的THsHaServer是基於此模型實現的。
在這裏插入圖片描述
非阻塞的IO模型,前一種IO模型的瓶頸可能在於單個IO線程,這種IO模型在前者的基礎上將單個的IO線程替換爲線程組,分配給多個連接,來提高IO的負載,優點是吞吐量更高,缺點是消耗了更多的線程資源,實現更加複雜,且同樣存在不靈活的問題,Java中的TThreadedSelectorServer是基於這一模型實現的,是目前thrift提供的最高級的server。

Class IO模型 Accept Threads IO Threads Processing Threads notes
TSimpleServer Blocking 1 1 1 最樸素的單線程阻塞IO模型
TThreadPoolServer Blocking 1 1 1+ 使用了線程池的阻塞IO模型
TNonblockingServer Nonblocking 1 1 1 非阻塞的單線程IO模型(Selector/Channel)
THsHaServer Nonblocking 1 1 1+ IO與Processing分離的,單線程IO的,非阻塞的多線程IO模型
TThreadedSelectorServer Nonblocking 1 1+ 1+ IO與Processing分離的,多線程IO的,非阻塞的多線程IO模型

總體來說,不存在最好的IO模型,選擇要根據業務場景,多線程並不一定好,單線程也不一定差,契合業務場景的纔是最好的。

  1. 工廠模型

thrift的TServer定義了靜態內部類Args,Args繼承自抽象類AbstractServerArgs。AbstractServerArgs採用了建造者模式,向TServer提供各種工廠,通過工廠模型來構建一個完整的server。

工廠屬性 作用 備註
ProcessorFactory 處理器工廠,用於產生對應的業務處理器對象
TransportFactory 傳輸層工廠,用於產生對應的傳輸層對象
ProtocolFactory 協議層工廠,用於產生對應的協議層對象
InputTransportFactory 傳輸層輸入工廠,用於產生對應的傳輸層對象 傳輸層可以爲輸入和輸出分別指定不同的工廠
OutputTransportFactory 傳輸層輸出工廠,用於產生對應的傳輸層對象
InputProtocolFactory 協議層輸入工廠,用於產生對應的協議層對象 協議層可以爲輸入和輸出分別指定不同的工廠
OutputProtocolFactory 協議層輸出工廠,用於產生對應的協議層對象

這就是thrift的工廠模型,通過爲Args指定這些工廠,然後構建server,thrift在每層提供了很多的工廠,如果這些工廠不能滿足業務需求,也完全可以繼承工廠基類來自定義工廠。

  1. 服務器事件接口

thrift的爲server的setServerEventHandler方法可以指定一個server event Handler,參數是TServerEventHandler接口,接口裏有四個方法來留給開發者在需要的時候實現需要的邏輯,如下表所示:

方法 調用時機
void preServe(); 在server啓動之前調用,僅調用一次
ServerContext createContext(TProtocol input, TProtocol output); 在client的一次連接之後立刻調用
void processContext(ServerContext serverContext, TTransport inputTransport, TTransport outputTransport); 在一次RPC的processor和接口邏輯之間調用
void deleteContext( ServerContext serverContext, TProtocol input, TProtocol output); 在client的一次連接斷開之後立刻調用

通過這些開放的接口方法能夠實現更多我們需要的邏輯。

  1. server 和 service

前面我們都是演示一個service對應一個server的情況,thrift也提供了在一個server中處理多個service的方法,只需要使用TMultiplexedProcessor即可

但是客戶端和服務端都必須使用多路複用的協議,在Java中是TMultiplexedProtocol

參考文獻:《The programmer’s Guide to Apache Thrift》

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