字节跳动 Go RPC 框架 KiteX 性能优化实践( 三 )


string / binary 零拷贝对于有一些业务,比如视频相关的业务,会在请求或者返回中有一个很大的 Binary 二进制数据代表了处理后的视频或者图片数据,同时会有一些业务会返回很大的 String(如全文信息等) 。这种场景下,我们通过火焰图看到的热点都在数据的 copy 上,那我们就想了,我们是否可以减少这种拷贝呢?
答案是肯定的 。既然我们底层使用的 Buffer 是个链表,那么就可以很容易地在链表中间插入一个节点 。

字节跳动 Go RPC 框架 KiteX 性能优化实践

文章插图
 
我们就采用了类似的思想,当序列化的过程中遇到了 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   }   ...}


推荐阅读