基于Protobuf共享字段的分包和透明零拷贝技术
https://mp.weixin.qq.com/s/isOzeuwsn_-5TUqsLcgTnQ
基于Protobuf共享字段的分包和透传零拷贝技术,你了解吗
原朱文杰贾云社区2021-11-10
简介|本文介绍了Protobuf共享字段Guard的实现,并将其应用于中控/召回场景,获得了显著的CPU/延迟收益。即使不使用Guard,希望本文的经验和思路也能给读者带来一些帮助和参考。
引言
在推荐系统中,用户级字段往往需要贯穿整个环节,如实验参数、行为序列、用户画像等。
召回/过滤/分类模块都需要用户特征。这时候,自然最好的方法就是从请求一开始就一次性获取信息,然后传递给路透社。此前,作者经常写道:
const getrecordeq oReq;//来自rpcRankReq或oRankReqoRankReq.mutable _ user _人像()-copy from(oreq . user _人像());
这种透明传输自然是有益的,例如,如果下游需要用户特征,则不必再次请求每个请求。尤其是当上游发起分包时,用户级特征的透明传输可以显著降低下游获取用户特征的RPC开销。
然而,RPC开销减少了,再得陇望蜀想一想,是否能直接省去这个CopyFrom的开销呢
我们知道,protobuf提供了Allocated/Release系列接口,通过直接转移指针所有权,消除了Copy或Swap的开销。
换句话说,如果指针所有权是借出的而不是转移的,那么共享字段是可以实现的。借用实际上是使用前转移字段指针,使用后立即收回(收回所有权防止删除)。这是经典的卫士抽象。
当然,即使不使用Guard,我相信上面的想法也足以提供一些帮助。我们可以直接使用pb的接口来实现:
const getrecordeq oReq;//来自rpcgetrecommandreq=omplaceereq=const _ castgetrecordeq(oReq);RankReq oRankReqoRankReq.set _ allocated _ user _人像(oMutableReq.mutable _ user _人像());客户。等级(oRankReq);oRankReq.release _ user _人像();
对于一些比较复杂的操作,比如我想复制一些字段,共享一些字段,修改一些字段(分包场景),我们在下面给出我们的解决方案。
设计
我们的卫士提供了两个接口,即连接和分离。接口如下。通过pb的反射机制,release和set_allocated可以相互绑定,实现Guard销毁时的回滚。
void AttachField(Message * pMessage,int iFieldId,Message * pfieldvvalue);消息*详细字段(消息*消息,整数);
AttachField:首先,将允许的字段集借给pMesage,并在Guard析构后回滚并释放它,以防止双重删除。
DetachField:先借出pMessage的释放字段,在Guard析构后回滚返回,防止内存泄漏。
回滚的顺序是FILO,也就是严格的逆序(因为release和set_allocated不是严格对称的,如果循环可能会有问题)。
因为C的结构和破坏也是菲洛(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_castAReq (oAReq).mutable_bigfield());
(二)被调分包
控制某些字段不同,而其他字段共享/相同。为了避免拷贝大字段,我们可以在拷贝前先释放这些重的字段;拷贝结束后,把重字段共享给所有的分包。使用CopyFrom好处在于,我们不需要为所有新增的字段都手动判断,只需要特殊处理重的字段即可。
//usecase:
Req oReq;
std::vectorReq 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::vectoruint32_t vecHeavyField{};//初始化为一组fieldId
SharePbFieldGuard oGuard;
std::unordered_mapuint32_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)
内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/81981.html