theme: juejin highlight: atom-one-dark
一、概述
自研HTTPDNS项目启动时定的目标单核不低于5K。但实际上刚开始压测时4核8G机器极限只能压到14K,经过几个小优化极限QPS达到了34.3K。
二、优化过程
序列化工具替换
我们通过 pprof 工具进行分析,发现瓶颈在msgpack.Unmarshal
, 如下图所示:
这个性能是严重低于预期的。因为本地mac本简单wrk压测(8C16G M2芯片,CPU400%) 最高可达75k+ QPS。
jetcache-go 默认是序列化方式是msgpack
。而 msgpack
模式是采用MsgPack编解码,如果内容>64个字节还会进行snappy压缩。
仔细想想,确实HTTPDNS的缓存仅仅是以{域名,国家,地区,运营商}作为缓存key,域名对应的IPV4、IPV6列表(IPV4、IPV6实体包括IP、过期时间、命中次数)作为缓存Value,这个场景没必要用msgpack
模式。因为msgpack
更适合解决Redis的大Value问题。
而且,HTTPDNS的场景本地缓存的命中率高达99%,序列化工具选择极大的影响着性能。
因此,我们对主流序列化工具统一做了benchmark测试。最终选择了字节跳动开源的sonic。(压测源码见最后)
因此,我们将jetcache-go 默认是序列化方式改成了sonic
,通过pprof
分析确实不存在序列化瓶颈了。极限QPS提升到了20K,但仍然低于预期。
架构调整
压测结果有了好转,pprof分析也不存在明显瓶颈了。我们突然在被压服务器观测到一个指标:HTTPDNS进程只使用了3/5的CPU。服务器上 nginx-VTS
work 进程 CPU 占用达到了1.5核。
最初在HTTPDNS Server 前面架一层nginx-VTS
的目的是为了采集HTTP请求相关指标。经权衡,还是去掉这一层,直接走LB
的监控。
去掉这一层后,我们重新压测了一遍,极限QPS提升到了34.3K,基本符合预期了。
服务器芯片选择
前面提到,在本地mac本(8C16G M2芯片,CPU400%) 最高可达75k+ QPS。就好奇的压测了下AMD与Intel的性能区别。
确实,在这个场景在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
}