标签 Make 下的文章

Go函数调用链跟踪的一种实现思路

img{512x368}

这篇文章的初衷是想解答知乎上的一位知友提出的问题。没想到完成一种实现后,这个问题居然被删除了。那么既然实现了,就分享出来吧。问题的原文找不到了,问题大致是这样的:

一个程序中存在多个函数调用链都调用了函数D:

A1 -> B1 > C1 -> D

A2 -> B2 > C2 -> D

A3 -> B3 -> C3 -> D

... ...

那么,如果某次函数D被调用时出现了问题,那么怎么知道这个D是哪个函数调用链里的D呢?

有些gopher可能会说通过Delve在线调试打印函数调用栈可以知晓D的调用链,还有些gopher可能会说通过各个函数中输出的业务日志可以查明出问题的D归属的函数调用链,这些都是可行的思路。

不过当遇到这个问题时,我大脑中的第一反应却是能否像跟踪分布式服务调用链那样跟踪函数调用链呢?于是就有了本文对这种思路的一个非生产级的实现以及其演化过程。

1. 利用defer实现函数出入口的跟踪

跟踪函数调用,我们首先想到的就是跟踪函数的出入口,而完成这一任务,当仁不让的就是利用defer。对于我这样的从C语言转到Go的gopher而言,defer是我十分喜欢的Go“语法糖”,因为它可以简化代码的实现,让代码逻辑更清晰,具有更好地可读性(关于defer让代码更清晰的系统描述,可参考我的Go进阶技术专栏文章:https://www.imooc.com/read/87/article/2421)。

下面我们就来看看第一版函数跟踪实现的代码:

// github.com/bigwhite/experiments/blob/master/trace-function-call-chain/trace1/trace.go
func trace() func() {
    pc, _, _, ok := runtime.Caller(1)
    if !ok {
        panic("not found caller")
    }

    fn := runtime.FuncForPC(pc)
    name := fn.Name()

    fmt.Printf("enter: %s\n", name)
    return func() { fmt.Printf("exit: %s\n", name) }
}

// github.com/bigwhite/experiments/blob/master/trace-function-call-chain/trace1/main.go
func A1() {
    defer trace()()
    B1()
}

func B1() {
    defer trace()()
    C1()
}

func C1() {
    defer trace()()
    D()
}

func D() {
    defer trace()()
}

func main() {
    A1()
}

我们看到:以A1实现为例,当执行流来带defer语句时,首先会对defer后面的表达式进行求值。trace函数会执行,输出函数入口信息,并返回一个“打印出口信息”的匿名函数。该函数在此并不会执行,而是被注册到函数A1的defer函数栈中,待A1函数执行结束后才会被弹出执行。也就是在A1结束后,会有一条函数的出口信息被输出。

下面我们来真实运行一下上面的trace1示例(Go 1.14, macOS 10.14.6):

// github.com/bigwhite/experiments/trace-function-call-chain/trace1
$go build
$./functrace-demo
enter: main.A1
enter: main.B1
enter: main.C1
enter: main.D
exit: main.D
exit: main.C1
exit: main.B1
exit: main.A1

我们看到各个函数的出入口信息都被输出了,在单Goroutine的情况下,我们从执行顺序上能识别出D究竟是归属于哪个调用链的。

2. 添加trace开关

对函数调用链进行Trace是有一定性能损耗的,我们可能并不想在所有场合都开启trace,那么我们来给Trace添加一个“开关”,我们利用go build tags来实现这个功能特性:

// github.com/bigwhite/experiments/blob/master/trace-function-call-chain/trace2/trace.go

// +build trace

package main
... ...

// github.com/bigwhite/experiments/blob/master/trace-function-call-chain/trace2/trace_nop.go

// +build !trace

package main

func trace() func() {
    return func() {

    }
}

我们新增一个名为trace_nop.go的文件,里面包含了一个trace函数的空实现,即在trace函数与其返回的匿名函数中什么都不做。该源文件增加了一个build指示器(directive):

// +build !trace

即在关闭trace开关时,使用该文件中的trace函数。而原trace.go文件中也增加了一个build指示器:

// +build trace

即只有在打开trace开关的情况下,才会使用该源文件。

我们来对比一下在trace开关打开和关闭下的执行结果:

// github.com/bigwhite/experiments/trace-function-call-chain/trace2
// trace开关关闭
$go build
$./functrace-demo

vs.

// trace开关打开
$go build -tags trace
$./functrace-demo
enter: main.A1
enter: main.B1
enter: main.C1
enter: main.D
exit: main.D
exit: main.C1
exit: main.B1
exit: main.A1

不过这里的实现还是有一个问题的,那就是即便不开启trace开关,trace_nop.go中的trace函数也是会被编译到可执行程序中的。利用go tool compile -S查看汇编代码,trace_nop.go中的trace函数以及其返回的匿名函数都没有被inline掉。这会带来一定的运行时开销,这个问题我们先记下并留到后面解决。

3. 增加对多goroutine函数调用链的跟踪支持

前面的实现面对只有一个goroutine的时候还是可以支撑的,但当程序中并发运行多个goroutine的时候,多个函数调用链的出入口信息输出就会混杂在一起无法分辨。下面我们就来改造一下实现,增加对多goroutine函数调用链的跟踪支持。我们的方案就是在输出函数出入口信息时,带上一个在程序每次执行时能唯一区分goroutine的goroutine id:

// github.com/bigwhite/experiments/blob/master/trace-function-call-chain/trace3/trace.go
func getGID() uint64 {
    b := make([]byte, 64)
    b = b[:runtime.Stack(b, false)]
    b = bytes.TrimPrefix(b, []byte("goroutine "))
    b = b[:bytes.IndexByte(b, ' ')]
    n, _ := strconv.ParseUint(string(b), 10, 64)
    return n
}

func trace() func() {
    pc, _, _, ok := runtime.Caller(1)
    if !ok {
        panic("not found caller")
    }

    fn := runtime.FuncForPC(pc)
    name := fn.Name()

    id := getGID()
    fmt.Printf("g[%02d]: enter %s\n", id, name)
    return func() { fmt.Printf("g[%02d]: exit %s\n", id, name) }
}

main.go也改成了启动多个Goroutine:

// github.com/bigwhite/experiments/blob/master/trace-function-call-chain/trace3/main.go

func A1() {
    defer trace()()
    B1()
}

func B1() {
    defer trace()()
    C1()
}

func C1() {
    defer trace()()
    D()
}

func D() {
    defer trace()()
}

func A2() {
    defer trace()()
    B2()
}
func B2() {
    defer trace()()
    C2()
}
func C2() {
    defer trace()()
    D()
}

func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        A2()
        wg.Done()
    }()

    time.Sleep(time.Millisecond * 50)
    A1()
    wg.Wait()
}

在trace功能开关打开的前提下,运行上面例子:

// github.com/bigwhite/experiments/trace-function-call-chain/trace3
$go build -tags trace
$./functrace-demo
g[18]: enter main.A2
g[18]: enter main.B2
g[18]: enter main.C2
g[18]: enter main.D
g[18]: exit main.D
g[18]: exit main.C2
g[18]: exit main.B2
g[18]: exit main.A2
g[01]: enter main.A1
g[01]: enter main.B1
g[01]: enter main.C1
g[01]: enter main.D
g[01]: exit main.D
g[01]: exit main.C1
g[01]: exit main.B1
g[01]: exit main.A1

4. 让输出更美观一些

了解分布式服务调用跟踪的童鞋都知道,通过带有层次感的输出,我们可以很容易识别出某个服务在哪个环节被调用。而上面我们的Trace输出太扁平,没有层次感,不容易识别,我们这里就来美化一下输出。我们将trace.go做如下改造:

// github.com/bigwhite/experiments/trace-function-call-chain/trace4/trace.go

var mu sync.Mutex
var m = make(map[uint64]int)

func printTrace(id uint64, name, typ string, indent int) {
    indents := ""
    for i := 0; i < indent; i++ {
        indents += "\t"
    }
    fmt.Printf("g[%02d]:%s%s%s\n", id, indents, typ, name)
}

func trace() func() {
    pc, _, _, ok := runtime.Caller(1)
    if !ok {
        panic("not found caller")
    }

    id := getGID()
    fn := runtime.FuncForPC(pc)
    name := fn.Name()

    mu.Lock()
    v := m[id]
    m[id] = v + 1
    mu.Unlock()
    printTrace(id, name, "->", v+1)
    return func() {
        mu.Lock()
        v := m[id]
        m[id] = v - 1
        mu.Unlock()
        printTrace(id, name, "<-", v)
    }
}

编译运行:

// github.com/bigwhite/experiments/trace-function-call-chain/trace4
$go build -tags trace
$./functrace-demo
g[18]:  ->main.A2
g[18]:      ->main.B2
g[18]:          ->main.C2
g[18]:              ->main.D
g[18]:              <-main.D
g[18]:          <-main.C2
g[18]:      <-main.B2
g[18]:  <-main.A2
g[01]:  ->main.A1
g[01]:      ->main.B1
g[01]:          ->main.C1
g[01]:              ->main.D
g[01]:              <-main.D
g[01]:          <-main.C1
g[01]:      <-main.B1
g[01]:  <-main.A1

这回显然好看多了,也更容易定位问题了!(当多个goroutine的函数跟踪输出混在一起时,我们还可以用grep工具将特定id的goroutine的函数跟踪输出过滤出来,比如:functrace-demo|grep “01″)。

5. 利用代码生成将trace代码注入到各个函数中

在前面我们提到过上面实现的一个问题,那就是一旦将trace写死到各个函数代码中,即便在trace开关未打开的情况下,依然是有性能损耗的。并且,上面的实现存在着对业务代码的较强的“代码侵入性”。那么我们能否减少侵入,像分布式服务跟踪那样将“跟踪”的设施注入(instrumenting)到需要跟踪的函数中呢?下面我们就来尝试一下。

1) 将trace单独打包为一个module

我们首先要做的就是将trace相关的代码单独提取打包为一个module。这里我将上面的trace.go和trace_nop.go放入了一个路径为github.com/bigwhite/functrace的module中:

$tree -F -L 2 functrace
functrace
├── LICENSE
... ...
├── README.md
├── example_test.go
├── go.mod
├── go.sum
├── trace.go
└── trace_nop.go

有了这个module,你可以以“侵入式”的方式为你的代码添加函数链调用跟踪,就像上面repo中example_test.go中的那样:

// https://github.com/bigwhite/functrace/blob/main/example_test.go
import (
    "github.com/bigwhite/functrace"
)

func a() {
    defer functrace.Trace()()
    b()
}

func b() {
    defer functrace.Trace()()
    c()
}

func c() {
    defer functrace.Trace()()
    d()
}

func d() {
    defer functrace.Trace()()
}

func ExampleTrace() {
    a()
    // Output:
    // g[01]:   ->github.com/bigwhite/functrace_test.a
    // g[01]:       ->github.com/bigwhite/functrace_test.b
    // g[01]:           ->github.com/bigwhite/functrace_test.c
    // g[01]:               ->github.com/bigwhite/functrace_test.d
    // g[01]:               <-github.com/bigwhite/functrace_test.d
    // g[01]:           <-github.com/bigwhite/functrace_test.c
    // g[01]:       <-github.com/bigwhite/functrace_test.b
    // g[01]:   <-github.com/bigwhite/functrace_test.a
}

2) 增加代码注入功能

我们在github.com/bigwhite/functrace仓库中增加了一个名为gen的工具。利用该工具我们可以将functrace中的trace基础设施代码自动注入(instrumenting)到目标源文件的各个函数定义中。这个工具调用的核心算法在github.com/bigwhite/functrace/pkg/generator中:

// github.com/bigwhite/functrace/blob/main/pkg/generator/rewrite.go
func Rewrite(filename string) ([]byte, error) {
        fset := token.NewFileSet()
        oldAST, err := parser.ParseFile(fset, filename, nil, 0)
        if err != nil {
                return nil, fmt.Errorf("error parsing %s: %w", filename, err)
        }

        if !hasFuncDecl(oldAST) {
                return nil, nil
        }

        // add import declaration
        astutil.AddImport(fset, oldAST, "github.com/bigwhite/functrace")

        // inject code into each function declaration
        addDeferTraceIntoFuncDecls(oldAST)

        buf := &bytes.Buffer{}
        err = format.Node(buf, fset, oldAST)
        if err != nil {
                return nil, fmt.Errorf("error formatting new code: %w", err)
        }
        return buf.Bytes(), nil
}

我们看到这个包的Rewrite函数使用了Go项目提供的go/ast包以及Go扩展项目提供的ast(抽象语法树)操作工具包golang.org/x/tools/go/ast/astutil对目标源文件进行解析、修改并重建的。go/ast包的内容较多,其本身就具备单独写几篇文章了,这里不赘述。有兴趣的童鞋可以移步本文后面的参考资料,或查看go官方文档了解。

为了帮助大家了解如何使用gen生成带有trace的代码,我还在functrace这个repo中建立了一个demo:examples/gen-demo:

$tree examples/gen-demo
examples/gen-demo
├── Makefile
├── go.mod
├── go.sum
└── main.go

在该demo中,我们利用go generate生成带有跟踪代码的目标代码:

// https://github.com/bigwhite/functrace/blob/main/examples/gen-demo/main.go
package main

//go:generate ../../gen -w main.go

... ...

构建该demo并运行(为了方便构建,我建立了Makefile):

// Makefile
all:
    go generate
    go build -tags trace

$make
go generate
[../../gen -w main.go]
add trace for main.go ok
go build -tags trace

$./functrace-demo
g[01]:  ->main.main
g[01]:      ->main.A2
g[01]:          ->main.B2
g[01]:              ->main.C2
g[01]:                  ->main.D
g[01]:                  <-main.D
g[01]:              <-main.C2
g[01]:          <-main.B2
g[01]:      <-main.A2
g[18]:  ->main.A1
g[18]:      ->main.B1
g[18]:          ->main.C1
g[18]:              ->main.D
g[18]:              <-main.D
g[18]:          <-main.C1
g[18]:      <-main.B1
g[18]:  <-main.A1
g[01]:  <-main.main

我们看到,我们通过ast将跟踪代码注入到目标代码并运行的思路成功实现了!

6. 小结

functrace module中Trace函数的实现比较简单,目前仅是输出日志,但实际上我们可以在Trace函数中以及Trace函数返回的匿名函数中通过各种方式输出我们想要的数据,比如,像分布式服务跟踪那样,将数据发送到一个集中的后端做统一存储、分析和展示。但鉴于篇幅和需求不同,这里仅给出满足演示的实现,大家可以自行fork该repo以实现满足你们自己需求的实现。

7. 参考资料

  • https://mattermost.com/blog/instrumenting-go-code-via-ast/
  • https://developers.mattermost.com/blog/open-tracing/
  • https://blog.gopheracademy.com/code-generation-from-the-ast/
  • http://www.go2live.cn/nocate/golang-ast语法树使用教程及示例.html
  • https://www.ctolib.com/topics-80234.html
  • https://github.com/yuroyoro/goast-viewer
  • https://liudanking.com/performance/golang-%e8%8e%b7%e5%8f%96-goroutine-id-%e5%ae%8c%e5%85%a8%e6%8c%87%e5%8d%97/

本文中涉及到的示例源码可以到这里下载 https://github.com/bigwhite/experiments/tree/master/trace-function-call-chain。


“Gopher部落”知识星球开球了!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!星球首开,福利自然是少不了的!2020年年底之前,8.8折(很吉利吧^_^)加入星球,下方图片扫起来吧!

我的Go技术专栏:“改善Go语⾔编程质量的50个有效实践”上线了,欢迎大家订阅学习!

img{512x368}

我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网热卖中,欢迎小伙伴们订阅学习!

img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 微信公众号:iamtonybai
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • “Gopher部落”知识星球:https://public.zsxq.com/groups/51284458844544

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

go protobuf v1败给了gogo protobuf,那v2呢?

近期的一个项目有对结构化数据进行序列化和反序列化的需求,该项目具有performance critical属性,因此我们在选择序列化库包时是要考虑包的性能的。

github上有一个有关Go序列化方法性能比较的repo:go_serialization_benchmarks,这个repo横向比较了数十种数据序列化方法的正确性、性能、内存分配等,并给出了一个结论:推荐gogo protobuf。对于这样一个粗选的结果,我们是直接笑纳的^_^。接下来就是进一步对gogo protobuf做进一步探究。

img{512x368}

一. go protobuf v1 vs. gogo protobuf

gogo protobuf是既go protobuf官方api之外的另一个go protobuf的api实现,它兼容go官方protobuf api(更准确的说是v1版本)。gogo protobuf提供了三种代码生成方式:protoc-gen-gogofast、protoc-gen-gogofaster和protoc-gen-gogoslick。究竟选择哪一个呢?这里我也写了一些benchmark来比较,并顺便将官方go protobuf api也一并加入比较了。

我们首先安装一下gogo protobuf实现的protoc的三个插件,用于生成proto文件对应的Go包源码文件:

go get github.com/gogo/protobuf/protoc-gen-gofast
go get github.com/gogo/protobuf/protoc-gen-gogofaster

go get github.com/gogo/protobuf/protoc-gen-gogoslick

安装后,我们在$GOPATH/bin下将看到这三个文件(protoc-gen-go是go protobuf官方实现的代码生成插件):

$ls -l $GOPATH/bin|grep proto
-rwxr-xr-x   1 tonybai  staff   6252344  4 24 14:43 protoc-gen-go*
-rwxr-xr-x   1 tonybai  staff   9371384  2 28 09:35 protoc-gen-gofast*
-rwxr-xr-x   1 tonybai  staff   9376152  2 28 09:40 protoc-gen-gogofaster*
-rwxr-xr-x   1 tonybai  staff   9380728  2 28 09:40 protoc-gen-gogoslick*

为了对采用不同插件生成的数据序列化和反序列化方法进行性能基准测试,我们建立了下面repo。在repo中,每一种方法生成的代码放入独立的module中:

$tree -L 2 -F
.
├── IDL/
│   └── submit.proto
├── Makefile
├── gogoprotobuf-fast/
│   ├── go.mod
│   ├── go.sum
│   ├── submit/
│   └── submit_test.go
├── gogoprotobuf-faster/
│   ├── go.mod
│   ├── go.sum
│   ├── submit/
│   └── submit_test.go
├── gogoprotobuf-slick/
│   ├── go.mod
│   ├── go.sum
│   ├── submit/
│   └── submit_test.go
└── goprotobuf/
    ├── go.mod
    ├── go.sum
    ├── submit/
    └── submit_test.go

我们的proto文件如下:

$cat IDL/submit.proto
syntax = "proto3";

option go_package = ".;submit";

package submit;

message request {
        int64 recvtime = 1;
        string uniqueid = 2;
        string token = 3;
        string phone = 4;
        string content = 5;
        string sign = 6;
        string type = 7;
        string extend = 8;
        string version = 9;
}

我们还建立了Makefile,用于简化操作:

$cat Makefile

gen-protobuf: gen-goprotobuf gen-gogoprotobuf-fast gen-gogoprotobuf-faster gen-gogoprotobuf-slick

gen-goprotobuf:
    protoc -I ./IDL submit.proto --go_out=./goprotobuf/submit

gen-gogoprotobuf-fast:
    protoc -I ./IDL submit.proto --gofast_out=./gogoprotobuf-fast/submit

gen-gogoprotobuf-faster:
    protoc -I ./IDL submit.proto --gogofaster_out=./gogoprotobuf-faster/submit

gen-gogoprotobuf-slick:
    protoc -I ./IDL submit.proto --gogoslick_out=./gogoprotobuf-slick/submit

benchmark: goprotobuf-bench gogoprotobuf-fast-bench gogoprotobuf-faster-bench  gogoprotobuf-slick-bench

goprotobuf-bench:
    cd goprotobuf && go test -bench .

gogoprotobuf-fast-bench:
    cd gogoprotobuf-fast && go test -bench .

gogoprotobuf-faster-bench:
    cd gogoprotobuf-faster && go test -bench .

gogoprotobuf-slick-bench:
    cd gogoprotobuf-slick && go test -bench .

针对每一种方法,我们建立一个benchmark test。benchmark test代码都是一样的,我们以gogoprotobuf-fast为例:

// submit_test.go

package protobufbench

import (
    "fmt"
    "os"
    "testing"

    "github.com/bigwhite/protobufbench_gogoprotofast/submit"
    "github.com/gogo/protobuf/proto"
)

var request = submit.Request{
    Recvtime: 170123456,
    Uniqueid: "a1b2c3d4e5f6g7h8i9",
    Token:    "xxxx-1111-yyyy-2222-zzzz-3333",
    Phone:    "13900010002",
    Content:  "Customizing the fields of the messages to be the fields that you actually want to use removes the need to copy between the structs you use and structs you use to serialize. gogoprotobuf also offers more serialization formats and generation of tests and even more methods.",
    Sign:     "tonybaiXZYDFDS",
    Type:     "submit",
    Extend:   "",
    Version:  "v1.0.0",
}

var requestToUnMarshal []byte

func init() {
    var err error
    requestToUnMarshal, err = proto.Marshal(&request)
    if err != nil {
        fmt.Printf("marshal err:%s\n", err)
        os.Exit(1)
    }
}

func BenchmarkMarshal(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        _, _ = proto.Marshal(&request)
    }
}
func BenchmarkUnmarshal(b *testing.B) {
    b.ReportAllocs()
    var request submit.Request
    for i := 0; i < b.N; i++ {
        _ = proto.Unmarshal(requestToUnMarshal, &request)

    }
}

func BenchmarkMarshalInParalell(b *testing.B) {
    b.ReportAllocs()

    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            _, _ = proto.Marshal(&request)
        }
    })
}
func BenchmarkUnmarshalParalell(b *testing.B) {
    b.ReportAllocs()
    var request submit.Request
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            _ = proto.Unmarshal(requestToUnMarshal, &request)
        }
    })
}

我们看到,对每种方法生成的代码,我们都会进行顺序和并行的marshal和unmarshal基准测试。

我们首先分别使用不同方式生成对应的go代码:

$make gen-protobuf
protoc -I ./IDL submit.proto --go_out=./goprotobuf/submit
protoc -I ./IDL submit.proto --gofast_out=./gogoprotobuf-fast/submit
protoc -I ./IDL submit.proto --gogofaster_out=./gogoprotobuf-faster/submit
protoc -I ./IDL submit.proto --gogoslick_out=./gogoprotobuf-slick/submit

然后运行基准测试(使用macos上的go 1.14):

$make benchmark
cd goprotobuf && go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/protobufbench_goproto
BenchmarkMarshal-8                  2437068           483 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshal-8                2262229           529 ns/op         400 B/op           7 allocs/op
BenchmarkMarshalInParalell-8        7592120           162 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshalParalell-8        5306744           225 ns/op         400 B/op           7 allocs/op
PASS
ok      github.com/bigwhite/protobufbench_goproto    6.239s
cd gogoprotobuf-fast && go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/protobufbench_gogoprotofast
BenchmarkMarshal-8                  7186828           164 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshal-8                4706794           251 ns/op         400 B/op           7 allocs/op
BenchmarkMarshalInParalell-8       15107896            83.0 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshalParalell-8        6258507           179 ns/op         400 B/op           7 allocs/op
PASS
ok      github.com/bigwhite/protobufbench_gogoprotofast    5.449s
cd gogoprotobuf-faster && go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/protobufbench_gogoprotofaster
BenchmarkMarshal-8                  7036842           166 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshal-8                4666698           256 ns/op         400 B/op           7 allocs/op
BenchmarkMarshalInParalell-8       15444961            83.2 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshalParalell-8        6936337           202 ns/op         400 B/op           7 allocs/op
PASS
ok      github.com/bigwhite/protobufbench_gogoprotofaster    5.750s
cd gogoprotobuf-slick && go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/protobufbench_gogoprotoslick
BenchmarkMarshal-8                  6529311           176 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshal-8                4737463           252 ns/op         400 B/op           7 allocs/op
BenchmarkMarshalInParalell-8       15700746            81.8 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshalParalell-8        6528390           202 ns/op         400 B/op           7 allocs/op
PASS
ok      github.com/bigwhite/protobufbench_gogoprotoslick    5.668s

在我的macpro(4核8线程)上,我们看到两点结论:

  • 官方go protobuf实现生成的代码性能的确弱于gogo protobuf生成的代码,在顺序测试中,差距还较大;

  • 针对我预置的proto文件中数据格式,gogo protobuf的三种生成方法产生的代码的性能差异并不大,选择protoc-gen-gofast生成的代码在性能上即可满足。

二. go protobuf v2

今年三月份初,Go官方发布了protobuf的新API版本,这个版本与原go protobuf并不兼容。新版API旨在使protobuf的类型系统与go类型系统充分融合,提供反射功能和自定义消息实现。那么该版本生成的序列/反序列化代码在性能上有提升吗?我们将其加入我们的benchmark。

我们先下载go protobuf v2的代码生成插件(注意:由于go protobuf v1和go protobuf v2的插件名称相同,需要先备份好原先已经安装的protoc-gen-go):

$  go get google.golang.org/protobuf/cmd/protoc-gen-go
go: found google.golang.org/protobuf/cmd/protoc-gen-go in google.golang.org/protobuf v1.21.0

然后将新安装的插件名称改为protoc-gen-gov2,这样$GOPATH/bin下的插件文件列表如下:

$ls -l $GOPATH/bin/|grep proto
-rwxr-xr-x   1 tonybai  staff   6252344  4 24 14:43 protoc-gen-go*
-rwxr-xr-x   1 tonybai  staff   9371384  2 28 09:35 protoc-gen-gofast*
-rwxr-xr-x   1 tonybai  staff   9376152  2 28 09:40 protoc-gen-gogofaster*
-rwxr-xr-x   1 tonybai  staff   9380728  2 28 09:40 protoc-gen-gogoslick*
-rwxr-xr-x   1 tonybai  staff   8716064  4 24 14:56 protoc-gen-gov2*

在Makefile中增加针对go protobuf v2的代码生成和Benchmark target:

gen-goprotobufv2:
        protoc -I ./IDL submit.proto --gov2_out=./goprotobufv2/submit

goprotobufv2-bench:
        cd goprotobufv2 && go test -bench .

由于go protobuf v2与v1版本不兼容,因此也无法与gogo protobuf兼容,我们需要修改一下go protobuf v2对应的submit_test.go,将导入的“github.com/gogo/protobuf/proto”包换为“google.golang.org/protobuf/proto”

重新生成代码:

$make gen-protobuf
protoc -I ./IDL submit.proto --go_out=./goprotobuf/submit
protoc -I ./IDL submit.proto --gov2_out=./goprotobufv2/submit
protoc -I ./IDL submit.proto --gofast_out=./gogoprotobuf-fast/submit
protoc -I ./IDL submit.proto --gogofaster_out=./gogoprotobuf-faster/submit
protoc -I ./IDL submit.proto --gogoslick_out=./gogoprotobuf-slick/submit

运行benchmark:

$make benchmark
cd goprotobuf && go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/protobufbench_goproto
BenchmarkMarshal-8                  2420620           485 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshal-8                2186240           538 ns/op         400 B/op           7 allocs/op
BenchmarkMarshalInParalell-8        7334412           162 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshalParalell-8        4537429           222 ns/op         400 B/op           7 allocs/op
PASS
ok      github.com/bigwhite/protobufbench_goproto    6.052s
cd goprotobufv2 && go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/protobufbench_goprotov2
BenchmarkMarshal-8                  2404473           506 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshal-8                1901947           626 ns/op         400 B/op           7 allocs/op
BenchmarkMarshalInParalell-8        6629139           171 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshalParalell-8       panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x11d4956]

goroutine 196 [running]:
google.golang.org/protobuf/internal/impl.(*messageState).protoUnwrap(0xc00007e210, 0xc000010360, 0xc00008ce01)
    /Users/tonybai/Go/pkg/mod/google.golang.org/protobuf@v1.21.0/internal/impl/message_reflect_gen.go:27 +0x26
google.golang.org/protobuf/internal/impl.(*messageState).Interface(0xc00007e210, 0xc00007e210, 0xc00012c000)
    /Users/tonybai/Go/pkg/mod/google.golang.org/protobuf@v1.21.0/internal/impl/message_reflect_gen.go:24 +0x2b
google.golang.org/protobuf/proto.UnmarshalOptions.unmarshal(0x0, 0x12acc00, 0xc000010360, 0xc00012c000, 0x177, 0x177, 0x12b23e0, 0xc00007e210, 0xc000200001, 0x0, ...)
    /Users/tonybai/Go/pkg/mod/google.golang.org/protobuf@v1.21.0/proto/decode.go:71 +0x2c5
google.golang.org/protobuf/proto.Unmarshal(0xc00012c000, 0x177, 0x177, 0x12ac180, 0xc00007e210, 0x0, 0x0)
    /Users/tonybai/Go/pkg/mod/google.golang.org/protobuf@v1.21.0/proto/decode.go:48 +0x89
github.com/bigwhite/protobufbench_goprotov2.BenchmarkUnmarshalParalell.func1(0xc0004a8000)
    /Users/tonybai/test/go/protobuf/goprotobufv2/submit_test.go:65 +0x6a
testing.(*B).RunParallel.func1(0xc0000161b0, 0xc0000161a8, 0xc0000161a0, 0xc00010c700, 0xc00004a000)
    /Users/tonybai/.bin/go1.14/src/testing/benchmark.go:763 +0x99
created by testing.(*B).RunParallel
    /Users/tonybai/.bin/go1.14/src/testing/benchmark.go:756 +0x192
exit status 2
FAIL    github.com/bigwhite/protobufbench_goprotov2    4.878s
make: *** [goprotobufv2-bench] Error 1

我们看到go protobuf v2并未完成所有benchmark test,在运行并行unmarshal测试中panic了。目前go protobuf v2官方并未在github开通issue,因此尚不知道哪里去提issue。于是回到test代码,再仔细看一下submit_test.go中 BenchmarkUnmarshalParalell的代码:

func BenchmarkUnmarshalParalell(b *testing.B) {
        b.ReportAllocs()
        var request submit.Request
        b.RunParallel(func(pb *testing.PB) {
                for pb.Next() {
                        _ = proto.Unmarshal(requestToUnMarshal, &request)
                }
        })
}

这里存在一个“问题”,那就是多goroutine会共享一个request。但在其他几个测试中同样的代码并未引发panic。我修改一下代码,将其放入for循环中:

func BenchmarkUnmarshalParalell(b *testing.B) {
        b.ReportAllocs()
        b.RunParallel(func(pb *testing.PB) {
                for pb.Next() {
                        var request submit.Request
                        _ = proto.Unmarshal(requestToUnMarshal, &request)
                }
        })
}

再运行go protobuf v2的benchmark:

$go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/protobufbench_goprotov2
BenchmarkMarshal-8                  2348630           509 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshal-8                1913904           627 ns/op         400 B/op           7 allocs/op
BenchmarkMarshalInParalell-8        7133936           175 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshalParalell-8        4841752           232 ns/op         576 B/op           8 allocs/op
PASS
ok      github.com/bigwhite/protobufbench_goprotov2    6.355s

看来的确是这个问题。

从Benchmark结果来看,即便是与go protobuf v1相比,go protobuf v2生成的代码性能也要逊色一些,更不要说与gogo protobuf相比了。

三. 小结

从性能角度考虑,如果要使用go protobuf api,首选gogo protobuf。

如果从功能角度考虑,显然go protobuf v2在成熟稳定了以后,会成为Go语言功能上最为强大的protobuf API。

本文涉及源码可以在这里下载。


我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网上线了,感谢小伙伴们学习支持!

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

微博:https://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats