string / binary 零拷贝对于有一些业务,比如视频相关的业务,会在请求或者返回中有一个很大的 Binary 二进制数据代表了处理后的视频或者图片数据,同时会有一些业务会返回很大的 String(如全文信息等) 。这种场景下,我们通过火焰图看到的热点都在数据的 copy 上,那我们就想了,我们是否可以减少这种拷贝呢?
答案是肯定的 。既然我们底层使用的 Buffer 是个链表,那么就可以很容易地在链表中间插入一个节点 。
文章插图
我们就采用了类似的思想,当序列化的过程中遇到了 string 或者 binary 的时候,将这个节点的 buffer 分成两段,在中间原地插入用户的 string / binary 对应的 buffer,这样可以避免大的 string / binary 的拷贝了 。
这里再介绍一下,如果我们直接用 []byte(string) 去转换一个 string 到 []byte 的话实际上是会发生一次拷贝的,原因是 Go 的设计中 string 是 immutable 的但是 []byte 是 mutable 的,所以这么转换的时候会拷贝一次;如果要不拷贝转换的话,就需要用到 unsafe 了:
func StringToSliceByte(s string) []byte { l := len(s) return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{ Data: (*(*reflect.StringHeader)(unsafe.Pointer(&s))).Data, Len: l, Cap: l, }))}
这段代码的意思是,先把 string 的地址拿到,再拼装上一个 slice byte 的 header,这样就可以不拷贝数据而将 string 转换成 []byte 了,不过要注意这样生成的 []byte 不可写,否则行为未定义 。预计算线上存在某些服务有大包传输的场景,这种场景下会引入不小的序列化 / 反序列化开销 。一般大包都是容器类型的大小非常大导致的,如果能够提前计算出 buffer,一些 O(n) 的操作就能降到 O(1),减少了函数调用次数,在大包场景下也大量减少了内存分配的次数,带来的收益是可观的 。
基本类型如果容器元素为基本类型(bool, byte, i16, i32, i64, double)的话,由于基本类型大小固定,在序列化时是可以提前计算出总的大小,并且一次性分配足够的 buffer,O(n) 的 malloc 操作次数可以降到 O(1),从而大量减少了 malloc 的次数,同理在反序列化时可以减少 next 的操作次数 。
struct 字段重排上面的优化只能针对容器元素类型为基本类型的有效,那么对于元素类型为 struct 的是否也能优化呢?答案是肯定的 。
沿用上面的思路,假如 struct 中如果存在基本类型的 field,也可以预先计算出这些 field 的大小,在序列化时为这些 field 提前分配 buffer,写的时候也把这些 field 顺序统一放到前面写,这样也能在一定程度上减少 malloc 的次数 。
一次性计算上面提到的是基本类型的优化,如果在序列化时,先遍历一遍 request 所有 field,便可以计算得到整个 request 的大小,提前分配好 buffer,在序列化和反序列时直接操作 buffer,这样对于非基本类型也能有优化效果 。
定义新的 codec 接口:
type thriftMsgFastCodec interface { BLength() int // count length of whole req/resp FastWrite(buf []byte) int FastRead(buf []byte) (int, error)}
在 Marshal 和 Unmarshal 接口中做相应改造:func (c thriftCodec) Marshal(ctx context.Context, message remote.Message, out remote.ByteBuffer) error { ... if msg, ok := data.(thriftMsgFastCodec); ok { msgBeginLen := bthrift.Binary.MessageBeginLength(methodName, thrift.TMessageType(msgType), int32(seqID)) msgEndLen := bthrift.Binary.MessageEndLength() buf, err := out.Malloc(msgBeginLen + msg.BLength() + msgEndLen)// malloc once if err != nil { return perrors.NewProtocolErrorWithMsg(fmt.Sprintf("thrift marshal, Malloc failed: %s", err.Error())) } offset := bthrift.Binary.WriteMessageBegin(buf, methodName, thrift.TMessageType(msgType), int32(seqID)) offset += msg.FastWrite(buf[offset:]) bthrift.Binary.WriteMessageEnd(buf[offset:]) return nil } ...}func (c thriftCodec) Unmarshal(ctx context.Context, message remote.Message, in remote.ByteBuffer) error { ... data := message.Data()if msg, ok := data.(thriftMsgFastCodec); ok && message.PayloadLen() != 0 { msgBeginLen := bthrift.Binary.MessageBeginLength(methodName, msgType, seqID) buf, err := tProt.next(message.PayloadLen() - msgBeginLen - bthrift.Binary.MessageEndLength()) // next once if err != nil { return remote.NewTransError(remote.PROTOCOL_ERROR, err.Error()) } _, err = msg.FastRead(buf) if err != nil { return remote.NewTransError(remote.PROTOCOL_ERROR, err.Error()) } err = tProt.ReadMessageEnd() if err != nil { return remote.NewTransError(remote.PROTOCOL_ERROR, err.Error()) } tProt.Recycle() return err } ...}
推荐阅读
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 字节跳动官方出品的免费图标库,超好用还能自定义修改
- API怎么选?比较SOAP,REST,GraphQL和RPC
- 为啥需要RPC,而不是简单的HTTP?
- Rhino 字节跳动全链路压测的实践
- 从RPC到服务化框架
- YARN 在字节跳动的优化与实践
- 胎心115
- 聊聊从RPC到服务治理框架
- 主流RPC框架通讯协议实现原理与源码解析
- SpringBoot中使用dubbo实现RPC调用