掘金 后端 ( ) • 2024-04-27 09:33

theme: juejin highlight: atom-one-dark

一、概述

自研HTTPDNS项目启动时定的目标单核不低于5K。但实际上刚开始压测时4核8G机器极限只能压到14K,经过几个小优化极限QPS达到了34.3K。

二、优化过程

序列化工具替换

我们通过 pprof 工具进行分析,发现瓶颈在msgpack.Unmarshal, 如下图所示:

image.png

image.png

这个性能是严重低于预期的。因为本地mac本简单wrk压测(8C16G M2芯片,CPU400%) 最高可达75k+ QPS。

image.png

jetcache-go 默认是序列化方式是msgpack。而 msgpack模式是采用MsgPack编解码,如果内容>64个字节还会进行snappy压缩。

仔细想想,确实HTTPDNS的缓存仅仅是以{域名,国家,地区,运营商}作为缓存key,域名对应的IPV4、IPV6列表(IPV4、IPV6实体包括IP、过期时间、命中次数)作为缓存Value,这个场景没必要用msgpack模式。因为msgpack更适合解决Redis的大Value问题。

而且,HTTPDNS的场景本地缓存的命中率高达99%,序列化工具选择极大的影响着性能。

image.png

因此,我们对主流序列化工具统一做了benchmark测试。最终选择了字节跳动开源的sonic。(压测源码见最后)

image.png

因此,我们将jetcache-go 默认是序列化方式改成了sonic,通过pprof分析确实不存在序列化瓶颈了。极限QPS提升到了20K,但仍然低于预期。

image.png

image.png

架构调整

压测结果有了好转,pprof分析也不存在明显瓶颈了。我们突然在被压服务器观测到一个指标:HTTPDNS进程只使用了3/5的CPU。服务器上 nginx-VTS work 进程 CPU 占用达到了1.5核。

image.png

最初在HTTPDNS Server 前面架一层nginx-VTS的目的是为了采集HTTP请求相关指标。经权衡,还是去掉这一层,直接走LB的监控。

去掉这一层后,我们重新压测了一遍,极限QPS提升到了34.3K,基本符合预期了。

image.png

image.png

服务器芯片选择

前面提到,在本地mac本(8C16G M2芯片,CPU400%) 最高可达75k+ QPS。就好奇的压测了下AMD与Intel的性能区别。

image.png image.png

确实,在这个场景在AMD芯片性能优于Intel芯片架构。

三、总结

  • sonic库性能确实吊打golang标准json库和msgpack库。
  • NG-VTS目的是为了收集ng本机ERROR日志及监控。但鉴于对服务的性能影响占用达到2/5,则直接去掉,加上LB监控即可。
  • jetcache-go通用缓存框架默认的序列化工具是msgpack,可自定义改为sonic提升性能。jetcache-go本地缓存用的freecache,输入输出都是byte数组,如果能够优化为不序列化,还有很高的QPS提升空间。

四、附:序列化工具benchmark源码

package codec_go

import (
    "encoding/json"
    "reflect"
    "sync"
    "testing"

    "github.com/bytedance/sonic"
    helloworldv1 "github.com/douyu/jupiter/proto/helloworld/v1"
    jsoniter "github.com/json-iterator/go"
    "github.com/vmihailenco/msgpack/v5"
    "google.golang.org/protobuf/proto"
)

var helloReply = &helloworldv1.SayHiResponse{
    Error: 0,
    Msg:   "success",
    Data: &helloworldv1.SayHiResponse_Data{
       Name:      "testName",
       AgeNumber: 18,
    },
}

/*
encoding/json
*/
func BenchmarkDecodeStdStructMedium(b *testing.B) {
    res, _ := json.Marshal(helloReply)
    b.ReportAllocs()
    var data helloworldv1.SayHiResponse
    for i := 0; i < b.N; i++ {
       _ = json.Unmarshal(res, &data)
    }
}

func BenchmarkEncodeStdStructMedium(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
       _, _ = json.Marshal(helloReply)
    }
}

func BenchmarkDecodeSonicStructMedium(b *testing.B) {
    res, _ := sonic.Marshal(helloReply)
    b.ReportAllocs()
    var data helloworldv1.SayHiResponse
    for i := 0; i < b.N; i++ {
       _ = sonic.Unmarshal(res, &data)
    }
}

func BenchmarkEncodeSonicStructMedium(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
       _, _ = sonic.Marshal(helloReply)
    }
}

func BenchmarkDecodeMsgPackStructMedium(b *testing.B) {
    res, _ := msgpack.Marshal(helloReply)
    b.ReportAllocs()
    var data helloworldv1.SayHiResponse
    for i := 0; i < b.N; i++ {
       _ = msgpack.Unmarshal(res, &data)
    }
}

func BenchmarkEncodeMsgPackStructMedium(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
       _, _ = msgpack.Marshal(helloReply)
    }
}

func BenchmarkDecodeMsgPStructMedium(b *testing.B) {
    res, _ := msgpack.Marshal(helloReply)
    b.ReportAllocs()
    var data helloworldv1.SayHiResponse
    for i := 0; i < b.N; i++ {
       _ = msgpack.Unmarshal(res, &data)
    }
}

func BenchmarkEncodeMsgPStructMedium(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
       _, _ = msgpack.Marshal(helloReply)
    }
}

func BenchmarkDecodeJsoniterStructMedium(b *testing.B) {
    res, _ := jsoniter.Marshal(helloReply)
    b.ReportAllocs()
    var data helloworldv1.SayHiResponse
    for i := 0; i < b.N; i++ {
       _ = jsoniter.Unmarshal(res, &data)
    }
}

func BenchmarkEncodeJsoniterStructMedium(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
       _, _ = jsoniter.Marshal(helloReply)
    }
}

func BenchmarkDecodeProto(b *testing.B) {
    res, _ := proto.Marshal(helloReply)
    b.ReportAllocs()
    var data helloworldv1.SayHiResponse
    for i := 0; i < b.N; i++ {
       _ = proto.Unmarshal(res, &data)
    }
}

func BenchmarkEncodeProto(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
       _, _ = proto.Marshal(helloReply)
    }
}

func BenchmarkDecodeProtoWithReflectAndPool(b *testing.B) {
    pool := getPool[*helloworldv1.SayHiResponse]()
    res, _ := proto.Marshal(helloReply)
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
       _, _ = unmarshalWithPool[*helloworldv1.SayHiResponse](res, pool)
    }
}

var pools sync.Map

func getPool[T any]() *sync.Pool {
    var value T
    if msg, ok := any(value).(proto.Message); ok {
       msgType := reflect.TypeOf(msg).Elem()
       if pool, ok2 := pools.Load(msgType.String()); ok2 {
          return pool.(*sync.Pool)
       }

       pool := &sync.Pool{
          New: func() any {
             // Make a new one, and throw it back into T
             msgN := reflect.New(msgType).Interface().(proto.Message)
             return msgN
          },
       }
       pools.Store(msgType.String(), pool)
       return pool
    }
    return nil
}

// 反序列化,如果是pb格式,则使用proto序列化 使用sync.Pool-存在并发问题
func unmarshalWithPool[T any](body []byte, pool *sync.Pool) (value T, err error) {
    if _, ok := any(value).(proto.Message); ok { // Constrained to proto.Message
       msg := pool.Get().(proto.Message)
       err = proto.Unmarshal(body, msg)
       value = msg.(T)
       pool.Put(msg)
    } else {
       err = json.Unmarshal(body, &value)
    }
    return
}