1. 程式人生 > 其它 >基於Protobuf共享欄位的分包和透傳零拷貝技術

基於Protobuf共享欄位的分包和透傳零拷貝技術

https://mp.weixin.qq.com/s/isOzeuwsn_-5TUqsLcgTnQ

基於Protobuf共享欄位的分包和透傳零拷貝技術,你瞭解嗎?

導語|本文通過介紹實現Protobuf共享欄位Guard,並將其應用於中控/召回場景,並獲得了顯著CPU/時延收益。即使不使用Guard,希望本文的經驗和思路也能為讀者帶來一些幫助和參考。

引言

在推薦系統中,使用者級的欄位常常需要貫穿整條鏈路,例如,實驗引數,行為序列,使用者畫像等等。

召回/過濾/排序等模組都需要使用者特徵,此時最好的方法自然是從請求開始時一次性獲取,然後一路透傳下去。此前筆者的寫法常常是:

const GetRecommendReq & oReq;//from rpcRankReq oRankReq;oRankReq.mutable_user_portrait()->CopyFrom(oReq.user_portrait());

這樣的透傳自然有好處,例如,下游如果需要使用者特徵,不需要再每個請求去請求一次。尤其是上游發起分包時,透傳使用者級別特徵能夠顯著減少下游獲取使用者特徵的RPC開銷。

然而,RPC開銷減少了,再得隴望蜀想一想,是否能直接省去這個CopyFrom的開銷呢

我們知道,protobuf提供了Allocated/Release系列介面,通過直接轉移指標所有權的方式消除Copy或Swap的開銷。

換個思路,如果不是轉移指標所有權,而是借出指標所有權,就能夠實現共享欄位了。所謂借,其實就是在使用前把欄位指標轉移,但在使用結束後立刻收回(收回所有權以防被delete)。而這正是經典的Guard抽象。

當然,即使不使用Guard,相信上面這個思路已經足夠提供一些幫助了。我們可以直接使用pb的介面實現:

const GetRecommendReq & oReq;//from rpcGetRecommendReq & oMutableReq =  const_cast<GetRecommendReq &>(oReq);RankReq oRankReq;oRankReq.set_allocated_user_portrait(oMutableReq.mutable_user_portrait());
Client.Rank(oRankReq);oRankReq.release_user_portrait();

對於一些更復雜的操作,例如我想要拷貝部分欄位,共享部分欄位,修改部分欄位(分包的場景),我們在下文給出了我們的解決方案。

設計

我們的Guard提供了兩個介面,分別是Attach和Detach,介面如下。實現通過pb的反射機制,使得release和set_allocated能夠相互繫結,實現Guard析構時回滾。

void AttachField(Message* pMessage, int iFieldId, Message* pFieldValue); Message* DetachField(Message* pMessage, int iFieldId);
  • AttachField:先把欄位set_allocted借給pMesage,Guard析構後回滾釋放,以防雙重delete。

  • DetachField:先把pMessage的欄位release借出,Guard析構後回滾歸還,以防記憶體洩漏。

回滾的順序是FILO,也就是嚴格按照相反的順序(因為release和set_allocated並非嚴格對稱,如果在成環的情況下可能會有問題)。

由於C++的構造和析構也是FILO(https://isocpp.org/wiki/faq/dtors#order-dtors-for-locals),一定要在pb初始化後再初始化Guard

這兩個介面已經足夠滿足在我們的業務中存在的幾種抽象:

(一)主調透傳/分包

把上游傳遞的某個欄位,零拷貝傳入下游的請求。此時直接Attach欄位即可。

//usecase:        const AReq & oAReq;        BReq oBReq;        SharePbFieldGuard guard;        guard.AttachField(&oBReq, BReq::BigFieldId, const_cast<AReq &>(oAReq).mutable_bigfield());

(二)被調分包

控制某些欄位不同,而其他欄位共享/相同。為了避免拷貝大欄位,我們可以在拷貝前先釋放這些重的欄位;拷貝結束後,把重欄位共享給所有的分包。使用CopyFrom好處在於,我們不需要為所有新增的欄位都手動判斷,只需要特殊處理重的欄位即可。

//usecase:        Req & oReq;        std::vector<Req> vecMultiReq(n);        SharePbFieldGuard guard;        auto* pField = guard.DetachField(&oReq, Req::BigFieldId);        for(auto && oSingleReq: multiReq)        {            oSingleReq.CopyFrom(oReq);            oSingleReq.set_field(...);            guard.AttachField(&oSingleReq, Req::BigFieldId, pField);}

(三)多欄位共享寫法(以下是一段脫敏的實際程式碼)

由於操作的指標都是Message*型別,可以直接用容器儲存pb index到欄位指標的對映關係。通過迴圈即可共享所有重欄位。

        std::vector<uint32_t> vecHeavyField{};//初始化為一組fieldId        SharePbFieldGuard oGuard;        std::unordered_map<uint32_t, ::google::protobuf::Message*> mapIndex2Message;        for(auto uField: vecHeavyField)        {            mapIndex2Message[uField] = oGuard.DetachField(&oReq, uField);        }        for (auto && oSingleReq: vecReq)        {            oSingleReq.CopyFrom(oReq);            //shared filed            for(auto uField: vecHeavyField)            {                oGuard.AttachField(&oSingleRecallReq, uField, mapIndex2Message[uField]);            }        }

展望

安全性:因為回滾時set_allocated會delete掉原本的欄位,假如成環可能會很危險,如何偵測這種情況。

效能:是否存在不使用反射,就能自動繫結set_allocated和release的方法?

Repeated欄位支援:怎樣處理Repeatd欄位不同的反射介面?

(https://developers.google.com/protocol-buffers/docs/reference/cpp/google.protobuf.message#repeated-field-getters)