如何将google/pprof集成在已有服务中?

编程

上周接到领导给的研发需求,写一个监控服务 monitor server,用于分析 etcd 中注册的服务的状态。项目中的大多数服务都已经引入了 pprof 库,想要查看这些服务的 /debug/pprof 只需要走一层代理即可,这里用官方 httputil 库中的 httputil.NewSingleHostReverseProxy

func proxy(w http.ResponseWriter, r *http.Request) {

_ = r.ParseForm()

URL := r.Form.Get("url")

profile := r.Form.Get("profile")

target, _ := url.Parse("http://" + URL + "/debug/pprof/" + profile + "?debug=1")

proxy := httputil.NewSingleHostReverseProxy(target)

proxy.Director = func(req *http.Request) {

req.URL.Scheme = target.Scheme

req.URL.Host = target.Host

req.URL.Path = target.Path

req.URL.RawQuery = target.RawQuery

if _, ok := req.Header["User-Agent"]; !ok {

// explicitly disable User-Agent so it"s not set to default value

req.Header.Set("User-Agent", "")

}

}

r.Host = r.URL.Host

proxy.ServeHTTP(w, r)

}

到这里这个需求已经完成了 90%,可是到目前为止想要看火焰图就一定要先把 profile 文件下载到本地,再使用 pprof 或者 go tool pprof。

那么有没有办法将 pprof 集成到现有服务中呢?当然有,我们先从 pprof 的 main 函数开始,注意是 google/pprof 库。

func main() {

if err := driver.PProf(&driver.Options{UI: newUI()}); err != nil {

fmt.Fprintf(os.Stderr, "pprof: %v

", err)

os.Exit(2)

}

}

可以看到官方提供了 option,完整的 option 包括:

// Options groups all the optional plugins into pprof.

type Options struct {

Writer Writer

Flagset FlagSet

Fetch Fetcher

Sym Symbolizer

Obj ObjTool

UI UI

HTTPServer func(*HTTPServerArgs) error

HTTPTransport http.RoundTripper

}

这些 option 并不需要全部更改,我们分开来讲。

0. UI 接口

UI 接口 UI: newUI() 直接去掉,它主要用于终端交互控制。

1. Flagset 接口

// A FlagSet creates and parses command-line flags.

// It is similar to the standard flag.FlagSet.

type FlagSet interface {

Bool(name string, def bool, usage string) *bool

Int(name string, def int, usage string) *int

Float64(name string, def float64, usage string) *float64

String(name string, def string, usage string) *string

BoolVar(pointer *bool, name string, def bool, usage string)

IntVar(pointer *int, name string, def int, usage string)

Float64Var(pointer *float64, name string, def float64, usage string)

StringVar(pointer *string, name string, def string, usage string)

StringList(name string, def string, usage string) *[]*string

ExtraUsage() string

AddExtraUsage(eu string)

Parse(usage func()) []string

}

正如字面意思,这个接口用来解析 flag。由于我们不想将 flag 写在现有服务的执行脚本中,所以我们需要实现一个自定义的 Flagset 结构体。同时,还要解决 go 不支持重复定义 flag 的问题。

1)GoFlags

结构体的内容根据所需要传入的参数为准,不同的项目可能不一样。

// GoFlags implements the plugin.FlagSet interface.

type GoFlags struct {

UsageMsgs []string

Profile string

Http string

NoBrowser bool

}

2)Bool

需要改动的参数通过结构体内的变量传入,不需要的可以直接写死。

// Bool implements the plugin.FlagSet interface.

func (f *GoFlags) Bool(o string, d bool, c string) *bool {

switch o {

case "no_browser":

return &f.NoBrowser

case "trim":

t := true

return &t

case "flat":

t := true

return &t

case "functions":

t := true

return &t

}

return new(bool)

}

3)Int

参数的默认值可以在 google/pprof/internal/driver/commands.go 中查找。

// Int implements the plugin.FlagSet interface.

func (*GoFlags) Int(o string, d int, c string) *int {

switch o {

case "nodecount":

t := -1

return &t

}

return new(int)

}

4)Float64

// Float64 implements the plugin.FlagSet interface.

func (*GoFlags) Float64(o string, d float64, c string) *float64 {

switch o {

case "divide_by":

t := 1.0

return &t

case "nodefraction":

t := 0.005

return &t

case "edgefraction":

t := 0.001

return &t

}

return new(float64)

}

注意有一些默认值是必须赋值的,否则无法正常展示图片。

5)String

// String implements the plugin.FlagSet interface.

func (f *GoFlags) String(o, d, c string) *string {

switch o {

case "http":

return &f.Http

case "unit":

t := "minimum"

return &t

}

return new(string)

}

6) Parse

Parse 方法返回文件名即可,不需要解析参数。

// Parse implements the plugin.FlagSet interface.

func (f *GoFlags) Parse(usage func()) []string {

// flag.Usage = usage

// flag.Parse()

// args := flag.Args()

// if len(args) == 0 {

// usage()

// }

return []string{f.Profile}

}

7)StringList

// StringList implements the plugin.FlagSet interface.

func (*GoFlags) StringList(o, d, c string) *[]*string {

return &[]*string{new(string)}

}

到此为止,第一个 option 改动完成。

if err := driver.PProf(&driver.Options{

Flagset: &internal.GoFlags{

Profile: profilePath + profile,

Http: "127.0.0.1:" + strconv.Itoa(internal.ListenPort),

NoBrowser: true,

}}); err != nil {

fmt.Fprintf(os.Stderr, "pprof: %v

", err)

os.Exit(2)

}

2. HTTPServer 函数

如果不注册 HTTPServer 函数,pprof 会使用默认的 defaultWebServer。

func defaultWebServer(args *plugin.HTTPServerArgs) error {

ln, err := net.Listen("tcp", args.Hostport)

if err != nil {

return err

}

isLocal := isLocalhost(args.Host)

handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {

if isLocal {

// Only allow local clients

host, _, err := net.SplitHostPort(req.RemoteAddr)

if err != nil || !isLocalhost(host) {

http.Error(w, "permission denied", http.StatusForbidden)

return

}

}

h := args.Handlers[req.URL.Path]

if h == nil {

// Fall back to default behavior

h = http.DefaultServeMux

}

h.ServeHTTP(w, req)

})

mux := http.NewServeMux()

mux.Handle("/ui/", http.StripPrefix("/ui", handler))

mux.Handle("/", redirectWithQuery("/ui"))

s := &http.Server{Handler: mux}

return s.Serve(ln)

}

1)s.Server(ln)

可以看到,在默认情况下,pprof 会自动监听。然而我们的服务已经启动监听,这些代码可以直接删掉,包括路由部分,建议写成项目同样的形式。

2)handler

handler 首先判断请求是否为本地请求,然后根据 path 注册对应的 handler。由于我们在上一步删除了路由部分 mux.Handle(),这些代码同样可以删除。

需要注意的是,handler 是不可以重复注册的,为此我们需要加一个标志位。

到此为止,第二个 option 完成。

var switcher bool

if err := driver.PProf(&driver.Options{

Flagset: &internal.GoFlags{

Profile: profilePath + profile,

Http: "127.0.0.1:" + strconv.Itoa(internal.ListenPort),

NoBrowser: true,

},

HTTPServer: func(args *driver.HTTPServerArgs) error {

if switcher {

return nil

}

for k, v := range args.Handlers {

http.Handle("/ui"+k, v)

}

switcher = true

return nil

}}); err != nil {

fmt.Fprintf(os.Stderr, "pprof: %v

", err)

os.Exit(2)

}

3. 复用 handler

我们对以上得到的代码打包,将其写入 http 接口中。

func readProfile(w http.ResponseWriter, r *http.Request) {

_ = r.ParseForm()

go pprof("profile")

time.Sleep(time.Second * 3)

http.Redirect(w, r, "/ui/", http.StatusTemporaryRedirect)

return

}

在启动 pprof 后,延迟三秒再重定向到 pprof 界面。

表面上,这个需求已经做完了,但是。。。

以上 pprof 是一次性的,在更换 profile 后重新读取生成的 webInterface 并不会重新注册到 handler 中。

为了解决最后的这个问题,我们不得不更改 pprof 源码。对此我是抗拒的,不是不会改,也不是不好改,主要是 pprof 放在公司通用的 vendor 库中,我害怕影响到别的项目(为此我提交了一个 feature 给官方库,希望能有更好的解决方法)。

在 internal/driver/webui.go 下进行如下改动,使 webI 可以被复用。

var webI = new(webInterface)

func makeWebInterface(p *profile.Profile, opt *plugin.Options) *webInterface {

templates := template.New("templategroup")

addTemplates(templates)

report.AddSourceTemplates(templates)

webI.prof = p

webI.options = opt

webI.help = make(map[string]string)

webI.templates = templates

return webI

}

至此,我们终于可以顺爽的看到火焰图啦。

至于 etcd 服务查询和 profile 文件生成,在此不再累述。

以上是 如何将google/pprof集成在已有服务中? 的全部内容, 来源链接: utcz.com/z/510411.html

回到顶部