从项目实战的角度看golang的pprof

语言: CN / TW / HK

golang程序优化起来是比较简单的,这得益于pprof这样的性能分析工具,有了它,程序能很容易分析像内存泄漏、cpu泄漏这样的问题。网上关于使用pprof或者分析pprof源码实现的文章也不少,但是在项目中实战的文章比较少。大多数关于pprof的文章讲的是,这样:

//引入pprof
import _ "net/http/pprof"
......
   //起一个http服务器,接着请求/debug/pprof等接口
    http.ListenAndServe("0.0.0.0:6060", nil)
......

类似这样的做法,大多是本地调试可以这么用。我们可以把这段直接copy放进我们的项目里面吗?答案是可以。但是有个问题,可能需要我们思考,如果我们直接把它放进我们的代码里,而没有任何代价的话,那这段代码应该成为标配,放进所有项目里面。然而,只有利没有弊的事情没那么多,需要以一定性能作为代价。

我们可以参考一下tarsgo的做法:

// Notify handler for cmds from admin
func (a *Admin) Notify(command string) (string, error) {
    cmd := strings.Split(command, " ")
    // report command to notify
    go ReportNotifyInfo(NOTIFY_NORMAL, "AdminServant::notify:"+command)
    switch cmd[0] {
    case "tars.viewversion":
        return GetServerConfig().Version, nil
    case "tars.setloglevel":
        if len(cmd) >= 2 {
            appCache.LogLevel = cmd[1]
            switch cmd[1] {
            case "INFO":
                logger.SetLevel(logger.INFO)
            case "WARN":
                logger.SetLevel(logger.WARN)
            case "ERROR":
                logger.SetLevel(logger.ERROR)
            case "DEBUG":
                logger.SetLevel(logger.DEBUG)
            case "NONE":
                logger.SetLevel(logger.OFF)
            default:
                return fmt.Sprintf("%s failed: unknown log level [%s]!", cmd[0], cmd[1]), nil
            }
            return fmt.Sprintf("%s succ", command), nil
        }
        return fmt.Sprintf("%s failed: missing loglevel!", command), nil
    case "tars.dumpstack":
        debug.DumpStack(true, "stackinfo", "tars.dumpstack:")
        return fmt.Sprintf("%s succ", command), nil
    case "tars.loadconfig":
        cfg := GetServerConfig()
        remoteConf := NewRConf(cfg.App, cfg.Server, cfg.BasePath)
        _, err := remoteConf.GetConfig(cmd[1])
        if err != nil {
            return fmt.Sprintf("Getconfig Error!: %s", cmd[1]), err
        }
        return fmt.Sprintf("Getconfig Success!: %s", cmd[1]), nil
    case "tars.connection":
        return fmt.Sprintf("%s not support now!", command), nil
    case "tars.gracerestart":
        graceRestart()
        return "restart gracefully!", nil
    case "tars.pprof":
        port := ":8080"
        timeout := time.Second * 600
        if len(cmd) > 1 {
            port = ":" + cmd[1]
        }
        if len(cmd) > 2 {
            t, _ := strconv.ParseInt(cmd[2], 10, 64)
            if 0 < t && t < 3600 {
                timeout = time.Second * time.Duration(t)
            }
        }
        cfg := GetServerConfig()
        addr := cfg.LocalIP + port
        go func() {
            mux := http.NewServeMux()
            mux.Handle("/debug/pprof/", http.HandlerFunc(pprof.Index))
            mux.Handle("/debug/pprof/cmdline", http.HandlerFunc(pprof.Cmdline))
            mux.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile))
            mux.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol))
            mux.Handle("/debug/pprof/trace", http.HandlerFunc(pprof.Trace))
            s := &http.Server{Addr: addr, Handler: mux}
            TLOG.Info("start serve pprof ", addr)
            go s.ListenAndServe()
            time.Sleep(timeout)
            s.Shutdown(context.Background())
            TLOG.Info("stop serve pprof ", addr)
        }()
        return "see http://" + addr + "/debug/pprof/", nil
    default:
        if fn, ok := adminMethods[cmd[0]]; ok {
            return fn(command)
        }
        return fmt.Sprintf("%s not support now!", command), nil
    }
}

tarsgo的做法是用一个命令来开启pprof,一定时间之后就关闭。它并不是隐式地引用pprof包,而是自己调用了pprof的方法,这样的好处是想关闭pprof的时候就可以关闭它。我之前搭建了一个自己平时用到的项目框架,我参考tarsgo的做法改了一下,用channel来开关pprof:

for {
        select {
        case <-pCh.OnOff:
            timeout := time.Second * time.Duration(pCh.Timeout)
            go func() {
                mux := http.NewServeMux()
                mux.Handle("/debug/pprof/", http.HandlerFunc(pprof.Index))
                mux.Handle("/debug/pprof/cmdline", http.HandlerFunc(pprof.Cmdline))
                mux.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile))
                mux.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol))
                mux.Handle("/debug/pprof/trace", http.HandlerFunc(pprof.Trace))
                s := &http.Server{Addr: pCh.Host, Handler: mux}
                logger.GetLogger().Info("start serve pprof")
                go s.ListenAndServe()
                time.Sleep(timeout)
                s.Shutdown(context.Background())
                <-pCh.Used
                logger.GetLogger().Info("stop serve pprof")
            }()
            pCh.Used <- struct{}{}
        }
    }

完整的代码,可以参考: https://github.com/TomatoMr/awesomeframework

现在我们可以随意地开关pprof的接口了,十分实用。可以结合我们的监控工具来监控我们的服务,比如,当内存或者cpu超过一定的限额的时候,就开启pprof,并收集日志,这样,我们可以在事后分析当时的异常原因,而不用添加代码,也不用担心收集不到。

以上就是我对pprof在项目中实战的理解,希望我的使用姿势是正确的,如有说的不对的,或者有其他更好的方式,欢迎给我留言。

欢迎关注我的公众号: onepunchgo ,会整理相关的文档和资料。

image

分享到: