引言:php

    2011年12月,基礎架構部總經理暨搜索業務線首席架構師朱會燦以《雲計算平臺的構架,設計和實現》爲主題爲你們作了一次技術講座,對咱們的「颱風」雲計算平臺作了介紹。其中概要地介紹了雲計算平臺的底層通信機制——Poppy。如今咱們在這裏向你們對Poppy作一個更詳細的介紹。web


背景編程


    Poppy是基於Protocol Buffer的網絡通信解決方案。瀏覽器


    衆所周知,分佈式網絡程序對通信協議的靈活性、容錯性、可擴展性、安全性、性能等都有較高的要求,使得其複雜性比單機程序高不少。安全


    最原始的網絡程序每每採用本身定義協議,本身編寫打包和解包代碼的方式進行通信,繁瑣而又容易出錯,靈活性和可擴充性也很差。bash


    Protocol Buffer是Google實現的數據存儲和傳輸格式,具備效率高,編碼緊湊,使用方便,格式靈活,支持二進制數據兼容,功能強大等諸多優勢。Protocol Buffer在google內獲得了大量的應用,配套的工具以及跨語言的支持也都很成熟。所以,搜索業務線的數據存儲格式也採用Protocol Buffer。服務器


    其次,傳統網絡程序每每採用基於消息包等通信模型,而現代網絡程序普遍使用RPC的方式來下降開發的難度,好比CORBA、RMI、WCF等。網絡


    RPC是「遠程過程調用」的縮寫,經過把網絡通信抽象爲遠程的過程調用,調用遠程的過程就像調用本地的子程序同樣方便,從而屏蔽了通信複雜性,使開發人員能夠無需關注網絡編程的細節,將更多的時間和精力放在業務邏輯自己的實現上,提升工做效率。可是開源的Protocol Buffer只提供了數據格式的處理功能,並未提供RPC實現,所以咱們以Protocol Buffer爲基礎實現了RPC,就有了Poppy。Poppy的出現,進一步整合了Protocol Buffer。架構


    另外,除了RPC方式的編程接口以外,對於服務器程序來講,監控、調試、性能分析等功能也很重要。所以Poppy還提供了 Web監控、form提交、在線profiling等附加功能,爲開發和測試提供了更大的便利性。負載均衡


    在開發Poppy以前,咱們調查過Thrift等開源的RPC庫,Thrift用的數據格式與Protocol Buffer不同,沒法知足咱們的要求。咱們還調查了一些開源的基於Protocol Buffer的RPC庫,功能上也都不能知足咱們的要求。所以咱們最終選擇了自主開發。


Poppy的特性

    由於使用protobuf,客戶端和服務器端能夠單獨升級,只要協議兼容便可。這爲軟件的發佈和部署帶來了很大的靈活性。


    同時支持同步和異步的RPC調用和處理方式,同步方式簡單,異步方式略複雜可是更高效。


    內嵌http server,http server的服務端口就是poppy的rpc服務端口,用戶能夠自由擴充本身的頁面。


    web方式展示統計和狀態等監控信息,方便監控服務和診斷錯誤。


    集成了perf-tools,能夠遠程動態profiling正在運行的server。


    自動鏈接管理,無需用戶顯式處理。


    支持鏈接多個對等的無狀態同構服務器,並自動進行負載均衡。


    支持zookeeper方式的服務地址解析,並能動態響應其變化,方便動態增減服務器。


    支持可選的壓縮,不需額外寫任何代碼。


    支持protobuf的textformat以及Json兩種文本格式的訪問接口,在腳本語言甚至命令行界面都能發起調用。


    Form提交: 不須要寫程序,在瀏覽器裏填個表單就能發起調用,表單是根據proto文件的描述自動生成的。


    多語言:除了C++外,還支持Java, Python, PHP三種語言的客戶端。


    集成了可選的對統一認證/受權系統的支持,能夠識別和控制客戶端的身份,提供更高的安全性。


使用示例
    千言萬語不如例子,下面咱們以幾個簡單的例子來展現Poppy如何使用,讓你們先對Poppy有個整體的印象。
獲取和構建Poppy

    Poppy是源代碼發佈的,須要使用搜索業務線統一構建系統Blade進行構建。


第一步:定義協議

    定義協議只須要編寫一個proto文件便可。


    範例:echo_service.proto


// 定義你本身的 package,package會被映射到C++中的namespace,爲了不可能的衝突,強烈建議老是使用package。
package rpc_examples;

// 這是請求消息
message EchoRequest {
    required string user = 1;
    required string message = 2;
}
// 這是迴應消息
message EchoResponse {
    required string user = 1;
    required string message = 2;
}
// 這是服務,只包含一個方法,Service 的命名建議以 Service 爲後綴。
service EchoService {
    rpc SimpleEcho(EchoRequest) returns(EchoResponse);
}


    編譯proto文件的功能已經集成到了Blade裏,自動生成接口定義文件echo_service.pb.h和echo_service.pb.cc,不須要本身動手。注意這裏表面上EchoRequest和EchoResponse的成員徹底相同。是由於這只是例子而已,實際中請求與迴應每每有很大差別。


第二步:實現服務器

    一、必需要包含的頭文件


#include "poppy/rpc_server.h"               // 這是poppy的
#include "poppy/examples/echo_service.pb.h"   // 這是本身定義service


    其中 echo_service.pb.h是 protoc 編譯器生成的。


    二、繼承編譯生成的服務接口類,實現其各個方法:


class EchoServiceImpl : public EchoService {
private:
    // 實現服務器端的 Handler 方法
    // 方法名就是 proto 中的方法名
    // 第一個參數固定爲 controller,後面兩個參數分別是請求和迴應的消息
    // 你能夠讀取請求消息,填充迴應消息,一切都就緒後,調done->Run()就完成了對請求的處理。
    virtual void SimpleEcho(
        google::protobuf::RpcController* controller,
        const EchoRequest* request,
        EchoResponse* response,
        google::protobuf::Closure* done)
    {
        // 填充迴應消息,實際代碼每每還須要讀取請求消息。
        response->set_user(request->user());
        response->set_message(
             "simple echo from server: " + FLAGS_server_address +
             ", message: " + request->message());
        LOG(INFO) << "request: " << request->message();
        LOG(INFO) << "response: " << response->message();
        done->Run(); // 處理完成後調用 done->Run() 結束
    }
};


      注意調了done->Run()以後,全部的四個參數都再也不能訪問。只要 done 還沒 Run,就還有效。 這裏演示的是簡單的同步處理,所以若是把done轉到別的線程裏運行,就實現了異步處理。


    三、把服務對象註冊給RPC Server,並啓動服務


int main()
{
    // 定義 rpc_server 對象。
    poppy::RpcServer rpc_server;
    // 建立 service
    rpc_examples::EchoServerImpl* echo_service = new rpc_examples::EchoServerImpl();

    // 註冊給 rpc_server,註冊後,echo_service 就由 rpc_server 來負責釋放了。
    rpc_server.RegisterService(echo_service);
    // 啓動服務器
    if (!rpc_server.Start("10.6.222.21:10000")) {
        return EXIT_FAILURE;
    }

    // 運行 rpc_server,能夠被信號退出。
    return rpc_server.Run();
}


第三步:實現客戶端

    一、包含頭文件

#include "poppy/examples/echo_service.pb.h"
#include "poppy/rpc_client.h"


    其中 echo_service.pb.h是 protoc 編譯器生成的。


    二、同步調用


    同步調用就是像大多數本地函數同樣,調用者發起後,等待被調過程返回,而後繼續。


// 定義 client 對象,一個 client 程序只須要一個 client 對象。
poppy::RpcClient rpc_client;
// 定義 channel,表明通信通道,每一個服務器地址對應一個 channel。
poppy::RpcChannel rpc_channel(&rpc_client, "10.6.222.21:10000");

// 定義表明 Service 在 Client 端的表示:Stub 對象。
rpc_examples::EchoServer::Stub echo_client(&rpc_channel);

// 定義和填充調用的請求消息。
rpc_examples::EchoRequest request;
request.set_user("echo_test_user");
request.set_message("hello, poppy server!");
// 定義方法的迴應消息,會在調用返回後被填充。
rpc_examples::EchoResponse response;

// 定義  controller,表明本次調用。
poppy::RpcController rpc_controller;
// 發起調用,最後一個參數爲 NULL 即爲同步調用。
echo_client.SimpleEcho(&rpc_controller, &request, &response, NULL);


三、異步調用


    異步調用則是,調用者發起後,不等待被調過程返回,就繼續。由於能夠同時發起多個請求,所以異步模式性能高一些。不過用起來也略微麻煩一些。異步調用完成後,經過回調函數來通知調用者:


// 定義異步調用完成時的回調函數
void EchoCallback(poppy::RpcController* controller,
    rpc_examples::EchoRequest* request,
    rpc_examples::EchoResponse* response)
{
    LOG(INFO)
        << "request: " << controller->sequence_id()
        << ", message: " << request->message();

if (controller->Failed()) {
        LOG(INFO) << "failed: " << controller->ErrorText();
    } else {
        LOG(INFO) << "response: " << response->message();
    }

    // 清理異步調用分配的資源
    delete controller;
    delete request;
    delete response;
}

poppy::RpcClient rpc_client;
poppy::RpcChannel rpc_channel(&rpc_client, "10.6.222.21:10000");
rpc_examples::EchoServer::Stub echo_client(&rpc_channel);
// 異步調用時,回調運行前,request,reponse,controller 都必須一直有效。
// 所以這裏用 new 建立。調用完成後,用戶能夠回收或者釋放,done則由Poppy來釋放。rpc_examples::EchoRequest* request = new rpc_examples::EchoRequest();
request->set_user("echo_test_user");
request->set_message("hello, poppy server!");

rpc_examples::EchoResponse* response = new rpc_examples::EchoResponse();
poppy::RpcController* rpc_controller = new poppy::RpcController();
google::protobuf::Closure* done = NewClosure(&EchoCallback,
        rpc_controller, request, response);

// 無需等待,EchoCallback 就會在未來完成或者失敗時被調用。
echo_client.SimpleEcho(rpc_controller, request, response, done);


    回調函數也能夠是成員函數,具體參考 Closure test 裏的用法。


更多示例

    更多示例能夠在 poppy 下的 examples 子目錄裏找到。


編程接口

    Poppy的API都在poppy命名空間下,下面描述均省略命名空間。


RpcServer

    該類僅用於服務器端。它是服務器端的具體業務服務對象的容器,負責監聽和接收客戶端的請求,分發並調用實際的服務對象方法並將結果回送給客戶端。 server程序的實現者須要把具體Service註冊給RpcServer。


    RpcServer 從 HttpServer 派生,所以也能夠註冊 Http Handler 給它,以響應 Http 請求。


    須要注意的是,不管是RpcService仍是HttpHandler,註冊給RpcServer後,ownership就屬於RpcServer了,其生存期由RpcServer負責,你不能再去delete了。


RpcClient

    該類僅用於客戶端。一個客戶端程序只須要一個RpcClient對象,其負責全部RpcChannel對象的管理和對服務器端應答的處理。


RpcChannel

    該類僅用於客戶端。它表明通信通道,每組服務器地址對應一個RpcChannel對象,客戶端經過它向服務器端發送方法調用請求並接收結果。Poppy內部以keepalive的方式來管理活動的鏈接,支持無狀態服務器的自動負載均衡。


    客戶端要發起調用,須要先以RpcClient*爲參數構造RpcChannel。


RpcController

    該類既用於客戶端,也用於服務器端。它存儲一次RPC方法調用的上下文,包括對應的鏈接標識、該次調用的序列號以及方法執行結果。因爲Poppy是全異步的,調用的序列號是爲了便於客戶端識別服務器的某個應答包對應具體哪次方法調用。每一個活動的controller表明一次已經發出還未完成的調用。 在完成前,controller不能被用做其餘用途;調用完成後,則能夠用來發起下一次調用。


    RpcController的方法:


    Rpc調用發起以前能夠調用的方法:


    void SetTimeout(int64_t timeout); // 設置指望超時,若是不是0,覆蓋proto裏的超時設置。


    Rpc調用發起以後能夠調用的方法:


    bool Failed() const; // 返回上次調用是否失敗


    int ErrorCode() const; // 返回上次調用的錯誤碼,實際類型爲 RpcErrorCode


    std::string ErrorText() const; // 返回錯誤信息的文字描述


    服務器方能夠調用的方法:


    int64_t Time() const; // 請求接收的時間


    int64_t Timeout() const; // 客戶端指望的超時


    void SetFailed(const std::string& reason); // 主動設置爲失敗


    更詳細的介紹能夠閱讀Poppy文檔和範例。


文本協議

    除了二進制協議外,Poppy支持還以普通的HTTP協議,傳輸以JSON/protobuf文本格式定義的消息。很方便用各類腳本語言調用,甚至用 bash,調 wget/curl 都能發起 RPC 調用。

多語言

    根據須要,咱們還開發了Java,Python和PHP版的客戶端。服務器端目前還只有C++。若是有其餘需求,歡迎給咱們提。


Web界面

    Poppy不止是RPC,還提供了服務器開發的有用特性,好比Web監控。 經過同一個端口,同時提供RPC服務和Web監控服務,經過瀏覽器就能監控和調試服務。


    Poppy在Rpc的同一個端口上,提供了一個簡單的監控界面,只須要在瀏覽器輸入地址,就能進入相應頁面。


使用Web界面

    前面說到,使用Poppy的服務器只須要使用一個端口,就能同時提供了RPC和HTTP服務,默認的Http服務包含了一個簡單的監控界面。 Poppy的web監控界面以下:


<IGNORE_JS_OP>


    假設服務器ip端口爲:http://10.6.222.127:8080 ,內置可訪問列表包括:


http://10.6.222.127:8080/ 主頁,提供了全部內置可訪問頁的入口


http://10.6.222.127:8080/flags Dump 出程序全部的 Flags。


http://10.6.222.127:8080/rpc/ JSON 格式 RPC 的入口 URL 前綴,後面須要跟方法全名(包名.服務名.方法名)。


http://10.6.222.127:8080/rpc/form 經過瀏覽器以交互的方式發送 RPC 請求。


http://10.6.222.127:8080/health 狀態監控頁,server進程是否正常。若正常則返回OK。


http://10.6.222.127:8080/status 統計監控頁,提供全部統計變量的值展現,還包括當前server所使用的CPU、內存值。


http://10.6.222.127:8080/vars 導出變量頁,展現全部用戶註冊的導出變量的名字及值。


    若是程序運行在咱們的開發網上,能夠在辦公網用瀏覽器直接鏈接。可是若是是IDC上的程序,8080端口多是開放的,或者能夠經過代理訪問。


狀態監控與統計

    Poppy提供狀態的監控和統計頁面,分別對應於health page和status page。


    其中,health page 返回server端的運行是否健康。


    status page 顯示了 每個service的每一個方法的統計信息。


<IGNORE_JS_OP>


    其中, global是全局的統計。


    統計的有三項, 包括請求的數量, 請求成功的數量, 失敗的請求數量和請求的平均時延。


Form提交

    Poppy支持經過瀏覽器,以填表格的形式直接向服務器提交RPC請求,省去寫測試客戶端的麻煩,是調試利器。 使用方法以下:


    打開form列表的頁面,點擊相應的RPC方法的連接,進入form提交的頁面。


<IGNORE_JS_OP>


<IGNORE_JS_OP>


    輸入完值後,點Send Request,獲得就能獲得迴應。爲了方便複製結果,以帶縮進的文本格式顯示。這個 form 是根據 proto 中定義的 Message 自動生成的,使用者無需寫任何代碼。


<IGNORE_JS_OP>


查看Flags

    Poppy支持與Google gflags整合,在web界面上查看顯示進程的 Flags。


<IGNORE_JS_OP>


    分別以flags所在的源文件爲單位,列出其中定義的每一個flag,包括名字,類型,當前值,默認值,描述。方便在運行期間瞭解程序的配置狀況。


    紅色表示已經值被修改過,再也不是默認值。


擴充Web界面

    除了以上的頁面,Poppy還支持用戶註冊本身的頁面,目前支持三種方式:

    註冊簡單路徑處理:把特定的路徑上的請求發送到用戶註冊的函數。
    註冊前綴規則處理:把前綴的路徑的請求都發送到用戶註冊的函數。
    註冊靜態資源:對特定的前綴,返回用戶註冊的數據。
在線Profiling

    Profiling是一種很經常使用的調試技術,profiling能夠提供真實的運行狀態,找出代碼的熱點,效率瓶頸。可能不少人都用過GCC自帶的gprof,gprof須要在編譯時加上特殊的選項,在每一個函數的入口和出口插入代碼,程序運行時進行統計,程序優雅退出時輸出統計結果,再用gprof進行分析。這種方式有幾點不方便:


    額外的函數調用對程序的效率有影響,尤爲是短小的函數,可能形成結果和實際有較大差異。
    只能在程序退出後,統計程序運行的整體信息,不方便獲得某個時間段的分析結果。

    Google perftools 是一款針對 C/C++ 程序的性能分析工具,使用該工具能夠對 CPU 時間片、內存等系統資源的分配和使用進行分析。一般用以進行內存泄露檢查,以及程序耗時分析,從而優化系統性能。


    所以,Poppy集成了perf tools。能夠在不中止server運行的狀況下,動態的profiling服務器的運行情況。


    這是分析某server 30秒獲得的文本報告:


<IGNORE_JS_OP>


    Pprof也能生成圖形化的報告,能更直觀地進行分析:


<IGNORE_JS_OP>


基本結構

    本文的主要目的是向你們介紹Poppy,而非架構設計分享。所以這裏只是粗略地介紹一下。


    咱們知道,全部的RPC實現,都是把遠程過程調用封裝成本地調用的接口,Poppy也不例外:


<IGNORE_JS_OP>


RPC調用示意圖


    可是其內部同樣要向下走到底層的協議棧


<IGNORE_JS_OP>


RPC消息的傳遞


    這裏能夠注意到,RpcServer是創建在HttpServer的基礎上的,Rpc消息也是一種特殊的HTTP數據流。其內部詳細的消息流轉和調度關係如圖所示:


<IGNORE_JS_OP>


Poppy基本結構


    從圖中能夠看出,由於涉及到超時,鏈接管理,負載均衡等,Poppy客戶端遠比服務器端更復雜。由於Poppy同時支持HTTP協議的文本格式的請求和Protobuf格式的二進制請求和迴應,因此Poppy內部有兩種協議:


<IGNORE_JS_OP>


Poppy的通信協議


    Poppy的二進制協議與通常的設計不同的是,它是以HTTP協議頭爲基礎創建起來的,只是創建鏈接後的最初的採用HTTP協議,後續的消息往來直接用二進制協議,因此效率仍是比較高的。


Mock測試

    單元測試是軟件質量的重要保障方式,搜索業務線在過去的一年多中,大力推廣單元測試,使得代碼質量上了一個臺階。對網絡程序而言,由於涉及到通信的雙方,單元測試比較麻煩。所以Poppy也集成了對單元測試的支持,專門基於gmock開發了PoppyMock。經過PoppyMock,不須要起服務器,就能進行RPC測試,大大簡化了單元測試。


性能

    咱們進行了大量的測試,這裏給出一些典型的性能測試數據:

    對於很短小的消息,服務器起一個處理線程,單個單線程客戶端,同步方式調用,能夠達到9000次/s的處理速度。多客戶端,QPS最大爲9萬次。
    一樣的消息,若是是異步方式,則QPS最多能夠達到16萬次。
    一樣的消息,單個服務器,4個工做線程,多個同步調用的客戶端,QPS最大24萬次,8時則能夠達到40萬次。
    對於較大的消息(單條10k以上),單客戶端同步調用能夠達到85M/s的吞吐量。
    對於較大的消息(單條10k以上),單客戶端異步調用,或者多客戶端,最大能夠達到125M/s的吞吐量。
    在80臺服務器上,單個服務器端,4個工做線程,最多測試過32000個鏈接,QPS從峯值的24萬降低到18萬次。

    目前看來,Poppy的性能仍是比較使人滿意的,若是未來有須要,咱們會進一步優化性能。


    Protobuf的C++實現使用了較多的動態內存分配,參照其文檔推薦,咱們測試時使用了tcmalloc,效率的確有較大的提高,所以咱們也建議用戶搭配tcmalloc使用。


使用狀況

    Poppy誕生將近一年,已在以下項目中獲得應用:


    基礎架構部:的XFS,XCUBE,TBORG,MAPREDUCE項目。
    廣告平臺部:內容廣告項目。
    搜索平臺部:統一下載中心。
    搜索平臺部:網頁搜索WOB,GOB項目。
    社區搜索部:Discuz項目。

    MapReduce使用Poppy作開發,兩個月的時間就推出了Demo版,開發效率獲得的較大的提升。


    咱們也歡迎其餘項目使用Poppy。


將來     進一步優化性能。     基於用戶身份的流量控制。     優先級控制。     跨IDC的代理支持。