Do you understand the subcontracting and transparent zero copy technology based on Protobuf shared field?

Posted by lee20 on Wed, 10 Nov 2021 14:44:13 +0100

Introduction  |  This paper introduces the implementation of Protobuf shared field Guard, applies it to the central control / recall scenario, and obtains significant CPU / delay benefits. Even without Guard, I hope the experience and ideas of this paper can bring some help and reference to readers.

introduction

In recommendation system, user level fields often need to run through the whole link, such as experimental parameters, behavior sequence, user portrait and so on.

Recall / filtering / sorting and other modules need user characteristics. At this time, the best way is to obtain them at one time from the beginning of the request and then pass them down. Previously, the author often wrote:

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

Such transparent transmission is naturally beneficial. For example, if the downstream needs user characteristics, it is not necessary to request every request again. Especially when the upstream initiates subcontracting, transparent user level features can significantly reduce the RPC overhead of obtaining user features downstream.

However, the RPC overhead is reduced. If you think about it, can you directly save the CopyFrom overhead?

As we know, protobuf provides Allocated/Release series interfaces to eliminate the overhead of Copy or Swap by directly transferring pointer ownership.

Another way of thinking, if you do not transfer pointer ownership, but lend pointer ownership, you can realize shared fields. The so-called borrowing actually means transferring the field pointer before use, but recovering it immediately after use (recovering ownership to prevent deletion). And this is the classic Guard abstraction.

Of course, even without Guard, I believe the above idea is enough to provide some help. We can directly use pb's interface to implement:

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();

For some more complex operations, such as I want to copy some fields, share some fields and modify some fields (subcontracting scenario), we give our solutions below.

Design

Our Guard provides two interfaces: Attach and Detach. The interfaces are as follows. Through the reflection mechanism of pb, release and set_allocated can be bound to each other to roll back when Guard destructs.

void AttachField(Message* pMessage, int iFieldId, Message* pFieldValue); Message* DetachField(Message* pMessage, int iFieldId);
  • AttachField: set the field first_ Allocated is lent to pMesage. After Guard destructs, it is rolled back and released to prevent double deletion.
  • DetachField: first lend the release field of pMessage, and then roll back and return it after Guard deconstruction to prevent memory leakage.

The rollback order is FILO, that is, strictly in the opposite order (because release and set_allocated are not strictly symmetrical, there may be problems in the case of looping).

Because the construction and Deconstruction of C + + are also Filo( https://isocpp.org/wiki/faq/dtors#order -Dtors for locals), be sure to initialize Guard after pb initialization.

These two interfaces are sufficient to meet several abstractions in our business:

(1) Main dispatching transmission / subcontracting

A zero copy of a field passed from the upstream is passed to the downstream request. In this case, you can directly the Attach field.

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

(2) Transferred subcontractor

Controls that some fields are different and others are shared / the same. In order to avoid copying large fields, we can release these heavy fields before copying; After copying, share the duplicate field with all subcontractors. The advantage of using CopyFrom is that we don't need to manually judge all the new fields, but only need to deal with the duplicate fields.

//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);        }

(3) Multi field shared writing method (the following is an actual code for desensitization)

Since the operation pointers are of Message * type, you can directly use the container to store the mapping relationship between pb index and field pointers. All duplicate fields can be shared by looping.

        std::vector<uint32_t> vecHeavyField{};//Initialize to a group of 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]);            }        }

expectation

Security: set_allocated will delete the original fields during rollback. If looping may be dangerous, how to detect this situation.

Performance: is there a way to automatically bind set_allocated and release without reflection?

Repeated field support: how to deal with different reflection interfaces of repeated field?

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