Apache Thrift 爬坑行

jopen 9年前發布 | 16K 次閱讀 Apache Thrift

什么是 Thrift

也不知道誰規定的, 當寫一篇技術分享博客的時候, 第一個大標題必須是"什么是XXX".

</blockquote>

The Apache Thrift software framework, for scalable cross-language services development, combines a software stack with a code generation engine to build services that work efficiently and seamlessly between C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, OCaml and Delphi and other languages.

什么是爬坑行

就是趟應用一個新技術時遇到的各種坑. Common pitfalls 用英文的話.

時空座標

既然是 爬坑, 那就具有一定的攻擊性, 所以, 鎖定座標很重要.

軟件版本

  1. thrift 0.9.2
  2. Mac OSX 10.9.X ~ 10.10.2
  3. </ol>

    時間區間

    2013.11.28 ~ 2015.2.3


    那么前戲結束, 爬坑正式開始

    1. 安裝

    沒錯, 坑從安裝就開始了. 而且這個坑不小.

    THRIFT-2229

    這個 Issue 說的是無法在 Mac 下編譯 Thrift, 目前是 close 了, 但仍在困擾著一些人.

    這里的 Issue 本身就是坑, 其實這些討論更具誤導性, 主要問題在于, 由于 Thrfit 這樣的跨語言項目的復雜性, 其"編譯安裝"系統其實是有別于一般軟件的, 大家都沒有理解 Thrift 的架構, 而編譯失敗后, 又認定是這個項目不成熟.

    我覺得這一點在相當大的程度上, 妨礙了大家對 Apache Thrift 的第一印象.

    要相信, Apache 也是在很努力的維護 Thrift 這個項目的! (大概8:20發

    </blockquote>

    首先, 你要理解 Thrift

    1. Thrift compiler 核心編譯器: 將 thrift 接口定義文件, 編譯成 Target 語言的源代碼
    2. Thrift libraries : 編譯出的 Target 語言代碼, 在 Traget 語言中執行的支持庫
    3. Target : 指的是 Thrift 支持的各種語言
    4. </ol>

      Thrfit 的編譯系統

      它是這么工作的:

      1. 首先編譯用 c++ 寫的 Thrift compiler
      2. 查找是否具有各個語言的運行環境, 比如, 是否有 ruby 環境, 如果有, 則編譯 ruby 版的 Library.
      3. </ol>

        Issue-2229 的實際情況是?

        1. 當年 10.9 的時候, Apple 用 clang 前端徹底替換了 gcc, 導致 compiler 無法被正確編譯. 后來, Apache 奮力工作后, 到0.9.2, 這個 bug 被修復了.
        2. OSX 自帶了 Ruby / Python / Java 等環境, Thrift編譯系統發現系統中存在這些工具鏈, 就嘗試編譯了相關的 Library, 然后因為 Apple 自帶的版本比較老舊, 編譯就失敗了.
        3. </ol>

          其實, 這些 Library 大部分是不需要自己編譯的

          1. 用--without-java這樣的編譯選項, 略過相關 Library 的編譯就可以獲得 Success 的結果了.
          2. 對于腳本語言, 直接使用 release 包里面的就可以了.
          3. 有些是以源代碼形式 release 的運行環境, 比如 Cocoa.
          4. 有些庫是以各種語言自帶的包管理工具發布的, 比如 maven / gem / npm 等.
          5. </ol>

            注意: 不要混淆 編譯器支持的語言 比如: 即便用 Linux 環境編譯的 Thrift, 其 Compiler 也是具有編譯出 Cocoa 代碼的能力的. (是的, 就是這么跳脫!)

            </blockquote>

            Protobuf 做得怎么樣?

            Protobuf 面臨相似的問題, 不過要簡單許多: 只有3種語言, 運行環境沒RPC.

            Protobuf 不愧為 Google 主導的項目, 在 Mac 上編譯, 測試, 不會有任何問題.

            當然, 很多 Protobuf 的第三方語言支持, 做得都不怎么樣. 比如, 之前我吐槽過的 iOS + Protobuf.

            2. 編寫接口文件

            當在編寫接口文件的時候, 會遇到第二個大坑:

            前向聲明在哪里?

            </blockquote>

            以下代碼會導致 Thrift compiler 報錯:

            struct UserInfo {
                1: optional string name,
                2: optional ProfileInfo profile,
            }

            struct ProfileInfo { 1: optional list<BlogInfo> createdBlogs, // Compile error }

            struct BlogInfo { 1: optional string title, 2: optional string content, }</pre>

            Compile error這行里, 會報"使用了未定義的 BlogInfo"錯誤, 因為, BlogInfo 的定義晚于 ProfileInfo 定義.

            也就是說, 這里我們需要"前向聲明"一下 BlogInfo. 那么, 問題來了:

            Thrift 不支持前向聲明

            </blockquote>

            在上述例子里, 我們只需要把 BlogInfo 放在 ProfileInfo 之前, 即可解決這個問題.

            不過, 如果這里的 BlogInfo 定義更復雜一點:

            struct BlogInfo {
                1: optional string title,
                2: optional string content,
                3: optional UserInfo author,
            }

            這是, BlogInfo / UserInfo / ProfileInfo 三者之間相互引用, 構成了循環. 該怎么辦?

            恭喜您獲得了 <<發現 Thrift 的死穴>> 成就.

            </blockquote>

            不僅僅是 Thrift 的 Compiler 不支持, 目前 Compiler 生成的代碼里, 有些也是不能夠被前向聲明的. 舉個例子: 如果是 C 語言, 那么在出現結構體嵌套時, 使用的是直接嵌套, 而不是指針. 這樣, 即便 Thrift 的 Compiler 支持了前向聲明, 那這種寫法在 C 語言編譯的時候, 也是非法的: 結構體不能直接嵌套. (這里只栗子, C實現我沒看過, 不過ObjC是如此的)

            這就意味著, 如果要添加這個特性, 連帶靜態語言的 Runtime Library 也都要大改一番了.

            往好處設想:

            This is a feature, it's by design, not a bug. (post @ 8:20

            </blockquote>

            也許是為了提升效率, 畢竟如果消息體很小, 從操作系統的內存管理機制來看, 反復 malloc 小塊數據可能導致消息解析/組裝性能嚴重下降. 如果想要提供這個特性, 則有必要在 Runtime Library 中添加配套的內存池技術, 才能獲得出色的性能. 而就目前 Thrift 那些 Runtime 的現狀而言, 能 compile 就謝天謝地了, 再添加這么復雜的特性屬于強人所難. (等等! 我是不是說得太多了...

            Protobuf 做得怎么樣?

            1. Protobuf 不需要前向聲明, 可以遞歸嵌套.
            2. 據第三方測試: Protobuf 的解析速度, 比 Thrift 更快.
            3. </ol>

              PB 完勝, 收工~

              </blockquote>

              沒有前向聲明的 Work around

              在我的技術方案里, 規避方法是將同一個對象在表示時分層:

              1. Base : 只包含該對象相關的 Thrift 基礎類型, list 之類也算基礎類型.
              2. Info : 包含該對象的 Base. 還有相關對象的 Base, 比如, BlogInfo 里 包含UserBase author.
              3. Detail : 類似 Info, 可以包含 Base 或 Info Level 的 struct. 與 Info 的區別是, Info 一般用于返回列表(多個對象)時使用, Detail 是獲取單個對象時使用, 比如 getUserDetail時.
              4. Result : 有的接口返回XyzDetail或list<XyzInfo>的 形式都無法滿足需求, 那就專門創建一個對應該接口的 AbcdResult 結構.
              5. </ol>

                應用了這套方法后, 基本滿足了我的應用場景下的 API.

                我時常也為定義這樣愚蠢的DTO而感到苦惱, 但是, 考慮到不需要自己實現通信模塊, 還有那如絲般順滑的調用體驗, 想想心里還有點小激動吶~

                </blockquote>

                3. Client編程

                在使用 Thrift 生成的 RPC 代碼的時候, 你會感受到:

                經典 RPC 純爺們兒, 就 TM 不向 RESTful 低頭!

                </blockquote>

                • 眾: 那你倒是支持異步啊,
                • Thrift: 有人說Callback-Hell眾口難調啊.
                • 眾: 那咱支持個 Future, 沒 Callback 了吧?
                • Thrift: 整那些個模式逼格太高, 再說也不好用啊.
                • 眾: 那咱支持個設超時? 這個不過分吧???
                • Thrift: 不需要, 咱們就是干白兒利落脆, 就是快
                • 眾: 那萬一超時了怪誰? 阻塞了 UI, 用戶體驗不能絲般順滑了該怪誰?
                • Thrift: 那...那...那都是時辰的錯!
                • 時辰: ...對不起
                • </ul>

                  (其實我只用了 Cocoa 的Client Library, 大概并不客觀)

                  沒有異步的iOS Work around

                  首先定義一個 block 類型:

                  typedef void(^UIUpdatingBlock)(); 

                  創建一個 Utils 類, 在其中增加靜態方法: (生成的 service object 名為 AccountServiceClient )

                  + (void)invokeAccountService:(InvokingAccountServiceBlock)block;
                  {
                      static dispatch_once_t onceToken;
                      static dispatch_queue_t _thriftRpcQueue;
                      dispatch_once(&onceToken, ^{
                          _thriftRpcQueue = dispatch_queue_create("com.myapp.thriftq", NULL);
                      });

                  dispatch_async(_thriftRpcQueue, ^{
                      THTTPClient * transport;
                      TBinaryProtocol * protocol;
                      AccountServiceClient * client;
                  
                      @try {
                          transport = [[THTTPClient alloc] initWithURL: [NSURL URLWithString:@"http://server.url/"]];
                          protocol = [[TBinaryProtocol alloc] initWithTransport:transport strictRead:YES strictWrite:YES];
                  
                          /* get the service object */
                          client = [[AccountServiceClient alloc] initWithProtocol:protocol];
                  
                          UIUpdatingBlock b2 = block(client);
                          dispatch_async(dispatch_get_main_queue(), b2);
                      }
                      @catch (NSException *ex)
                      {
                          NSLog(@"connection problem: %@", ex);
                      }
                      @finally
                      {
                          client = nil;
                          protocol = nil;
                          transport = nil;
                      }
                  });
                  

                  }</pre>

                  這樣在調用的地方: (調用的 getUserDetail)

                     [CHXUtils invokeAccountService:^UIUpdatingBlock(AccountServiceClient *client) {

                      // 調用 service object, 如果需要 Catch Exception, 也在這里. 
                      self.user = [client getUserDetail:_userId];
                  
                      return ^void(void) {
                          // 更新 UI 的地方
                          // ...
                      };
                  }];</pre><br />
                  

                  我相信更有經驗的 ObjC 選手會利用 protocol 技術創建更合理的設計, 我這里只是做了一個必要的封裝:

                  畢竟, 我們不能在 Main Thread 里調用 service object 的方法, 這會阻塞 UI 的.

                  </blockquote>

                  (寫到這里, 我才意識到, 我從來沒試過在 Main Thread 里調用 service obejct, 我猜 iOS 會狂飆異常吧)

                  2015.2.9補充:

                  1. (經別人提醒想到)在更新 UI 時, 操作的指針應該是設置了__weak屬性的, 否則, 在調用過程中如果 UI 已被退出了, 這里的 block 引用將導致其無法正確釋放內存.
                  2. dispatch_once 的部分還是在 UI Thread 上, 分配內存就有可能導致卡頓, 應該移動到其他位置.
                  3. </ol>

                    4. 性能

                    由于, 我并沒有機會公平的測試當 Thrift 流量大了以后, 服務器性能的占用情況.(這里無法公平的原因是, 我自己用 go 語言配 Protobuf, 用 ruby 配 Thrift, 所以沒有絕對公平的比較環境)

                    但是, 在使用 Thrift 的過程中:

                    用 Wireshark 發現, 每次請求似乎并非一次通信

                    </blockquote>

                    這是一種奇怪的協議實現, 出現在用 Ruby 的 Runtime Library + TCP Transport 環境下.

                    如果你對性能優化敏感的話, 你大概已經意識到這里的問題了.

                    如果是 HTTP Transport, 倒是明確的一次 Request, 一次 Response. 但問題是, 真正在內部網絡下, 追求性能時, 大概應該是 TCP 大顯神通的地方, 現在卻..

                    其實, 這一點還是屬于存疑狀態, 畢竟是號稱高性能的RPC. 也許是我的打開方式不對.

                    綜上

                    Apache Thrift 作為在 非死book 實際使用的 RPC 系統, 其可用性還是值得肯定的, 拋去對小白用戶不友好, 和沒有前向聲明的兩個大坑之外, 其他的小坑對生產力的破壞并不明顯, 總體而言, 如果你不是 RESTful 鐵桿粉, 而且想要給 Client 的程序員提供一個相對舒適的編程環境的話, Thrift 還是你值得信賴的選擇.

                    同時, 如果你的業務邏輯相對復雜, 特別是對象之間關系相對復雜時, 使用 Protobuf 會節省你在 Data Transfer Object 定義上所消耗的時間和精力, 相應的, 你要付出一部分實現消息通信的精力, 換取比 Thrift 更好的性能.

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!