騰訊與閱文技術合作 微服務框架Tars再添PHP

騰訊開源發表於2017-11-02

樑晨(Ted),任職閱文集團技術中心,負責起點中文網的WEB後臺開發工作。曾負責騰訊上海企業產品部營銷QQWeb後臺開發、QQ公眾號Web後臺開發,對大型網站技術架構,有自己的經驗和見解。騰訊開源專案TSF2.0框架開發者,騰訊開源元件Tars-PHP開發者,也曾是騰訊公司多個PHP擴充套件元件的開發者與維護者。

引言

TARS作為由騰訊公司開源的優秀RPC框架與服務部署運維解決方案,被閱文集團引入了實際實踐中,同時閱文集團對TARS在PHP語言層面進行了能力的補全,令TARS如虎添翼。TARS-PHP的解決方案兼具簡單高效、介面維護方便容易擴充套件、程式碼自動生成,以及整合定址、服務發現、監控、上報等功能。經歷了閱文集團線上業務的考驗與洗禮,充分證明了該解決方案的優勢。

專案地址:github.com/Tencent/Tar…

"PHP是世界上最好的語言"

眾所周知,在PHP誕生之初,就是WEB站點的開發而生。但是一直以來,都無法擺脫弱型別、指令碼語言的效能之殤的帽子。隨著網際網路行業的不斷髮展,以及使用者需求和基礎架構的不斷變化,PHP語言本身也一直在發展。無論是SWOOLE的出現,還是PHP7對效能的提升,都豐富和助力了PHP本身的應用。

相信大家在開發中也會發現,作為經常處在WEB中間層的PHP,其實有很多的痛點。既要接收前端的HTTP請求,又要呼叫各式各樣的後臺服務與儲存服務,常常成為一個站點的效能瓶頸。其中HTTP協議的過分冗餘以及上層封裝帶來的損耗,就是一個比較突出的問題。

開發者不但要應對使用同步的HTTP的呼叫庫所帶來的吞吐量的下降,還要忍受HTTP協議本身,以及JSON、XML協議在資訊傳輸上的低效率。為了解決這一問題,一套在TCP協議層的,使用簡單的二進位制協議。才能保證業務用更少的傳輸頻寬,承載更多的傳輸內容,從而提高吞吐量和WEB服務伺服能力。

同時,在實際開發的層面上,PHP邏輯層與後臺服務之間通訊協議的維護成本較高。同時,後臺服務側新增或修改介面欄位,往往呼叫側也要配合修改,很多時候無法保證介面的完全相容而引發線上的運營問題。因此,這種二進位制協議又要做到介面方便維護,同時又容易擴充套件。

除此之外,從開發效率上而言,原本的開發中總是包含大量的重複的,但又不得不去做的工作內容。因為每一次新協議的開發,程式碼很難複用,JSON和XML也並不允許你共用部分資料。同時一個很現實的問題是,不同HTTP介面的提供方,往往會視自己的心情和習慣來定義介面。

一個常見的例子就是對返回碼的定義,有些人叫ret,有些人叫code,還有些人就叫r,簡直是無所不包。因此這類重複無趣的開發工作,給呼叫方的開發同學帶來了極大的生理和心理負擔。基於這種需求,一種服務端和客戶端都能夠根據協議和介面自動生成呼叫程式碼,保證聯調通暢的解決方案必不可少。

再者,呼叫方對後端服務的發現和呼叫的上報與監控,也是一個老生常談的問題。後端服務如何被發現,後端的介面如何被發現,這都是呼叫方真真切切想知道的。同時,呼叫方非常有必要對後端服務的呼叫情況進行上報到中央伺服器,中央伺服器再根據收集上來的資訊,對後端服務的負載進行動態的調整,保證服務的高可用。要實現這樣的需求,必須引入一種整合了監控、主控定址、上報通道、負載均衡功能的解決方案。

Tars作為騰訊公司的優秀RPC框架與服務部署運維解決方案,可以滿足上述的所有需求。通過引入Tars-PHP的全套解決方案,開發者既可以使用二進位制的Tars協議,大大壓縮了服務請求的流量。同時也能夠藉助Tars協議解析的PHP擴充套件,提高了打包解包的效能進而提升了單程式的任務處理能力。再次,自動生成程式碼的工具也能夠提升開發者的效率。

Tars-PHP解決方案

Tars-PHP的開源方案,首先從二進位制的協議說起:

二進位制協議

HTTP協議可能是在應用層上使用最為廣泛的協議了。現有HTTP的版本主要是1.0和1.1版本。它在TCP協議的基礎上做了十分簡潔的應用層協議封裝,純文字的內容,以及Header和Body的區分。都使得這種協議的使用和理解十分的方便。但是不可避免的,使用和閱讀的簡單意味著資訊的冗餘,為了傳輸少量的內容,往往需要耗費大量的流量。

另外兩個比較熟知的協議,就是JSON和XML了,這兩位在API互動常用的協議中不分上下,可讀性強、容易理解、語言客戶端支援豐富、協議表述能力突出,都是兩者的優勢所在。先看看同樣一段資訊,兩者需要的資料量。

假定有一所學校,一個學生,如果用JSON標識的話,如下所示:

{
    "school":
    {
        "student":{
            "name":"ted",
            "age":18,
            "degree":"master"
        }
    }
}複製程式碼

很簡單的結構,共需要65個字元來表述。而如果換成XML:

<school>
        <student>
            <name>ted</name>
            <age>18</age>
            <degree>master</degree>
        </student>
</school>複製程式碼

則一共需要92個字元。從資訊學的角度而言,資訊熵明顯就是太低了。所以為了實現通訊的更高效能和更少頻寬的使用,二進位制協議的引入勢在必行。

Tars協議作為一個二進位制的協議,相比於上述兩個協議的優勢不言自明。從上文中的JSON和XML中發現其靈活性,也就是沒有指定欄位的型別。但是不可避免的,這種靈活帶來了效能的大損失。因此Tars定義了八種基本的資料型別,通過對不同的資料型別進行編碼優化:

bool、byte、short、int、long、float、double 、string複製程式碼

而同時為了滿足業務需求,擴充套件出了struct(包含任意欄位)、vector(陣列)、map(key-value結構)這三種可以巢狀資料,豐富協議表現力的複雜型別。

按照上文的表現結構,幾個struct就可以完成。
首先是student結構體:

struct student {
    0 required string name; // tag為0,type為string,實際資料為ted,共5個位元組
    1 required byte age; // tag為1,type為short,實際資料為18, 共2個位元組
    2 required string degree; // tag為2,type為string,實際資料為master,共7個位元組
}複製程式碼

從註釋中可以看到,三個欄位需要的位元組數為14,再加上結構體的開始和結構體結束的標識共2個位元組,一共只需要16個位元組而已。相比之下,這僅僅是JSON的1/4,是XML協議標識同樣資訊的1/5,高下立判. 巧妙地用協議強約定換傳輸可讀性,這就是高資訊熵的二進位制協議的訣竅。

為了使得PHP能夠充分與Tars結合,必須使其具備作為客戶端和作為服務端兩個方面的能力。

Tars-PHP的客戶端

作為客戶端而言,要能夠滿足快速開發的需求,也要能夠與PHP現有的常見使用方式相結合,同時還要給出遠端呼叫的例項。基於這些需求,客戶端方案中實現瞭如下的特性:

  • 實現了用TUP協議進行打包解包、編碼解碼的PHP擴充套件及相應的測試用例;
  • 實現了從Tars協議檔案生成對應PHP類檔案的tars2php工具;
  • 實現了包含網路庫的二次封裝,以及遠端呼叫的程式碼示例;

作為客戶端實現的最核心一步,就是對TUP協議的支援。TUP協議是在Tars協議的上層,通過固定的資料結構封裝一些收發包必須的資訊,如返回值、輸入輸出引數、包本身的狀態、包計數等,來給非Tars原生客戶端與Tars服務端進行通訊的協議。Tars-PHP在支援TUP協議的方案中,選擇了使用PHP擴充套件作為實現方式。

PHP語言本身被詬病最多的,就是針對CPU密集型的運算的低效率。由於並不十分高效的ZEND虛擬機器、鬆散的資料結構和弱型別的存在,使得打包、解包這類CPU密集型的效率低下。因此,PHP擴充套件應運而生。通過引入高效能的C/C++類庫和一些原生的C/C++實現,使得PHP在效能處理方面迎頭趕上。這也就是以擴充套件的方式實現打包解包主邏輯的初衷。

首先來看看PHP5x語言的結構:

最底層的Server API用來PHP與Webserver通訊,這個主要是之前與APACHE配合需要使用的。在其左上的PHPCORE層,是為了提供最基本的檔案和網路操作的能力。而右上的ZEND,則是用來把PHP的指令碼語言編譯成機器碼的工具。最上面就是擴充套件層了,這層會充分利用ZEND的API和PHPCORE的能力,直接寫出ZEND能夠高效執行和理解的程式碼,省去了PHP指令碼編譯為機器碼的過程,從而大大的提高執行的效率。

如果要設計這個擴充套件,必須要將上文中Tars的資料結構通過C語言的方式加以表達,同時設計出基於這套資料結構的編碼器與解碼器。另一個需要考慮的方面是,必須要使得在PHP層面儘可能的簡單、易用,這就對擴充套件的設計提出了比較高的挑戰。一方面要兼顧效能,另一方面,要將Tars協議中的Struct,進行了PHP中的Class的表達:

從圖中可以清晰的看到,結構體SimpleStruct被分解成了三個部分:

  • TAG部分
  • 成員變數部分
  • 變數描述的fields

TAG部分至關重要,這部分用來代表Struct中每個元素的TAG值。這也是實際進行TUP編碼和解碼的時候,二進位制包裡面最終包含的內容。為什麼要有TAG?這是因為相比於JSON裡面對欄位的文字性質的描述,TAG本身更節省空間。

第二部分則是類的成員變數,這部分成員變數和Tars協議的Struct中的變數一一對應。這是為了承載對應變數的實際值而存在的。藉此才能對真正的資料進行打包和解包。

為了在TAG和變數之間搭起一座橋樑,就有了第三部分:Fields部分。這部分是TAG與其對應的變數屬性的一個對映。包含了變數的名稱、變數是否必填以及變數的型別。通過這些資訊,一方面實現了對Tars協議的二進位制編碼,也實現瞭解碼時候的對映。可謂一舉兩得。

那麼經過複雜的擴充套件設計與實現,有必要將擴充套件實現的打包解包效能和原生PHP實現的打包解包效能進行比對。從下面的表格中可以非常明顯的看出擴充套件實現擁有效能上面的絕對優勢:

方式/100次 tars複雜度 打包時間(ms) 打包耗時倍數 解包時間(ms) 解包耗時倍數
擴充套件 簡單 0.69 1 1.18 1
php原生 簡單 11.25 16 16.28 13
擴充套件 複雜 1.17 1 1.55 1
php原生 複雜 14.5 12 15.1 10

從這個表格中可以非常清晰的看到,無論是簡單的Tars協議,還是複雜的Tars協議,使用擴充套件進行打包解包都比原生PHP的效能提高十倍以上。當遇到複雜的業務邏輯,需要呼叫大量的使用Tars協議的後臺服務的時候,這種效率的提升會讓服務的吞吐量上一個數量級。

開發者在完成擴充套件的編譯工作之後,就可以非常方便的使用TUP協議進行打包,解包與編碼解碼的工作了。

// 針對基本型別的打包和解包的方法,輸出二進位制buf
$buf = \TASAPI::put*($name, $value);
$value = \TUPAPI::get*($name, $buf);

// 針對Struct,傳輸物件,返回結果的時候,以陣列的方式返回,其元素與類的成員變數一一對應
$buf = \TUPAPI::putStruct($name, $clazz);
$result = \TUPAPI::getStruct($name, $clazz, $buf);

// 針對Vector,傳入完成pushBack的Vector
$buf = \TUPAPI::putVector($name, TARS_Vector $clazz);
$value = \TUPAPI::getVector($name, TARS_Vector $clazz, $buf);

// 針對Map,傳入完成pushBack的Map
$buf = \TUPAPI::putMap($name, TARS_Map $clazz);
$value = \TUPAPI::getMap($name, TARS_Map $clazz, $buf);

// 需要將上述打好包的資料放在一起用來編碼
$inbuf_arr[$name] = $buf;
// 進行tup協議的編碼,返回結果可以用來傳輸、持久化
$reqBuffer = \TUPAPI::encode(
                         $iVersion=3,
                         $iRequestId,
                         $servantName,
                         $funcName,
                         $cPacketType=0,
                         $iMessageType=0,
                         $iTimeout,
                         $context=[],
                         $statuses=[],
                         $inbuf_arr);
// 進行tup協議的解碼
$ret = \TUPAPI::decode($respBuffer);
$code = $ret['code'];
$msg = $ret['msg'];
$buf = $ret['sBuffer'];複製程式碼

為了方便開發者擴充套件使用中經常遇到的無法找到具體函式和引數的問題,同時提供了tars-ide-helper:

以PHPSTORM為例,只需要匯入到相應的INCLUDE路徑中,就可以實現自動提示了:

除了打包解包的能力,Tars-PHP同時也提供了網路收發的能力,網路收發主要實現了以下幾個點:

  • TarsAssistant.php檔案:通過COMPOSER載入,底層內建SOCKET原生網路層收發包實現;
  • 根據Interface自動生成PHP的Class,與TarsAssistant無縫結合
  • 提供Exception等容錯處理;

一旦完成了程式碼的自動生成之後,使用者即可通過如下程式碼,方便的進行遠端Tars服務呼叫:

    require_once "./vendor/autoload.php";

    $ip = "";// taf服務ip
    $port = 0;// taf服務埠
    $servant = new App\Server\Servant\servant($ip,$port);

    $in1 = "test";
    $ss1 = new SimpleStruct();
    $ss1->id = 1;
    $ss1->count = 2;
    $ss1->page = 3;

    try {
        $intVal = $servant->singleParam($in1,$ss1,$out1);
    }
    catch(phptars\TarsException $e) {
        // 錯誤處理
    }複製程式碼

Tars-PHP的服務端

除了建設Tars-PHP作為客戶端的能力之外,服務端的能力同樣是必不可少的。為了能夠滿足不同業務場景下的需求,Tars-PHP在服務端主要會關注兩類服務。

第一類是HTTP的服務,會以SWOOLE2.0為網路收發的基礎,實現一套高效能、簡潔好用的面向WEB服務的框架。這套框架會支援基本的 路由、中介軟體、MVC架構等常見的WEB框架特性。同時也會整合Redis、Mysql、Http、Multicall、Tars等常見的客戶端,方便WEB服務再去呼叫後臺服務。更重要的是,接入到Tars平臺中,使得服務可監控,可重啟,享受Tars運維平臺帶來的一站式便利。現在框架的第一個版本已經實現,並在閱文集團內部上線使用,測試成熟後,會及時進行開源。

第二類則是TCP的服務,同樣底層依賴於SWOOLE2.0,但是協議從HTTP換成了對TUP和Tars的支援。框架實現上而言,會與JAVA、C++的服務端保持一致,底層整合網路能力,使用者只需關心服務名稱以及介面引數和自己的業務處理邏輯而已。當然,這個服務肯定也是要與Tars運維平臺相結合的。現在框架對TUP協議支援的第一個版本已經完成,後續也會在完成Tars協議的底層支援之後,在業務上進行使用和驗證。

業務實踐

閱文集團在進行後臺服務治理與改造的過程中,使用了Tars-PHP的解決方案。一方面,所有WEB後臺與後臺服務的介面,全部從原有的HTTP介面,切換為了基於Tars協議的TCP網路傳輸。依賴於Tars-PHP的自動程式碼生成,開發效率提升巨大,保證了專案的順利按時上線。同時,這套基於PHP擴充套件的方案,也保證了程式碼執行效率的高效,單個請求的處理時間,相比於原有的HTTP介面呼叫,得到了顯著的縮短。

另一方面,由於使用的WEB後臺服務是常駐記憶體的,基於SWOOLE的實現。所以在釋出、啟動、監控等方面與原有PHP中固有的Apache和PHP-FPM的方式都不相同。因此,正如上文中所說,服務接入Tars平臺,享受其監控、保活、日誌等一系列的功能,會大大提高服務本身的運維和擴容的便利性。如今在其線上服務中,超過十個服務已經切入並穩定執行了接入到Tars平臺的HTTP服務。這些服務的釋出、擴容和運維完全依賴Tars平臺,十分便利。

除去對Tars平臺運維的使用,閱文WEB後臺側同樣在服務發現上,有一套方案。

對於遠端服務的地址管理,最差的方案就是將其寫入本地檔案中。這種方案無法應對快速縮擴容以及伺服器下線的需求,會給後續的運維帶來很大的工作量。

稍微好一些的方案是本地儲存虛擬IP,那麼每次只需要調整虛擬IP,就可以實現服務地址的自動對映和變化。但是這意味著對要呼叫的每一個後臺服務,都需要儲存其對應的虛擬IP、HOST資訊、介面資訊等一系列的資訊,同樣維護成本很高。

而更加通用的方案,則是提供服務的統一配置中心,每次需要呼叫後臺服務的時候,就從配置中心根據唯一的標識拉取出服務最新的地址。這樣一方面能夠做到縮擴容對業務的無感知,另一方面配置中心也能夠通過服務的定址情況,給每個客戶端分配最適合它的服務機器地址,比如機房或者SET就近分配等等。本地的服務只需要提供兩個能力,第一個是能夠呼叫定期的定址服務,並存入本機的儲存中,保證定址的速度。第二個則是能夠接收配置中心下發的命令,更新特定服務的地址。能做到這兩點,就能夠實現高效的定址和可靠的定址了。

在實際使用中,結合實際業務情況,一方面每分鐘向主控請求一次服務的地址,通過輪詢的方式獲取一個可用的服務地址,再放入本地的高速共享記憶體,方便在這一分鐘之內重複的讀取。另一方面在每次服務呼叫的時候,都自動在底層整合對服務呼叫情況的耗時、成功率的上報。在雙管齊下的作用之下,對遠端服務的呼叫不再像過去那樣難以維護、難以開發、難以監控,而是清晰可見高效的被管理。

結語

從開發效率上而言,使用Tars-PHP擺脫了過分冗餘的業務程式碼,以自動生成的方式提高程式碼開發自動化程度。

從效能方面而言,Tars-PHP方案通過引入擴充套件,做到了效能的大幅度提升,讓效能不再成為PHP“之殤”。

從易用性而言,通過提供TarsAssistant的網路收發元件,使得收發包無需單獨實現。後面也會引入更高效能的Swoole作為socket收發的利器,進一步提高網路效能。

後續,Tars-PHP的SERVER側方案也會盡快開源,從而能夠提供一套包含客戶端與服務端的完整解決方案。這一整套的WEB後臺的Tars-PHP開發體系,能夠真正做到了高效能、高效率與高可用。而閱文集團也會繼續與騰訊在Tars-PHP技術方案上深度合作與實踐。歡迎開發者試用!

相關文章