溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

將google/pprof集成在已有服務(wù)中的方法

發(fā)布時(shí)間:2021-09-29 16:24:56 來源:億速云 閱讀:149 作者:iii 欄目:大數(shù)據(jù)

本篇內(nèi)容主要講解“將google/pprof集成在已有服務(wù)中的方法”,感興趣的朋友不妨來看看。本文介紹的方法操作簡(jiǎn)單快捷,實(shí)用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“將google/pprof集成在已有服務(wù)中的方法”吧!

寫一個(gè)監(jiān)控服務(wù) monitor server,用于分析 etcd 中注冊(cè)的服務(wù)的狀態(tài)。項(xiàng)目中的大多數(shù)服務(wù)都已經(jīng)引入了 pprof 庫(kù),想要查看這些服務(wù)的 /debug/pprof 只需要走一層代理即可,這里用官方 httputil 庫(kù)中的 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)
}

到這里這個(gè)需求已經(jīng)完成了 90%,可是到目前為止想要看火焰圖就一定要先把 profile 文件下載到本地,再使用 pprof 或者 go tool pprof。

那么有沒有辦法將 pprof 集成到現(xiàn)有服務(wù)中呢?當(dāng)然有,我們先從 pprof 的 main 函數(shù)開始,注意是 google/pprof 庫(kù)。

func main() {
	if err := driver.PProf(&driver.Options{UI: newUI()}); err != nil {
		fmt.Fprintf(os.Stderr, "pprof: %v\n", 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
}

正如字面意思,這個(gè)接口用來解析 flag。由于我們不想將 flag 寫在現(xiàn)有服務(wù)的執(zhí)行腳本中,所以我們需要實(shí)現(xiàn)一個(gè)自定義的 Flagset 結(jié)構(gòu)體。同時(shí),還要解決 go 不支持重復(fù)定義 flag 的問題。

1)GoFlags

結(jié)構(gòu)體的內(nèi)容根據(jù)所需要傳入的參數(shù)為準(zhǔn),不同的項(xiàng)目可能不一樣。

// GoFlags implements the plugin.FlagSet interface.
type GoFlags struct {
	UsageMsgs []string
	Profile   string
	Http      string
	NoBrowser bool
}
2)Bool

需要改動(dòng)的參數(shù)通過結(jié)構(gòu)體內(nèi)的變量傳入,不需要的可以直接寫死。

// 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

參數(shù)的默認(rèn)值可以在 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)
}

注意有一些默認(rèn)值是必須賦值的,否則無法正常展示圖片。

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 方法返回文件名即可,不需要解析參數(shù)。

// 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)}
}

到此為止,第一個(gè) option 改動(dòng)完成。

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\n", err)
    os.Exit(2)
}

2. HTTPServer 函數(shù)

如果不注冊(cè) HTTPServer 函數(shù),pprof 會(huì)使用默認(rèn)的 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)

可以看到,在默認(rèn)情況下,pprof 會(huì)自動(dòng)監(jiān)聽。然而我們的服務(wù)已經(jīng)啟動(dòng)監(jiān)聽,這些代碼可以直接刪掉,包括路由部分,建議寫成項(xiàng)目同樣的形式。

2)handler

handler 首先判斷請(qǐng)求是否為本地請(qǐng)求,然后根據(jù) path 注冊(cè)對(duì)應(yīng)的 handler。由于我們?cè)谏弦徊絼h除了路由部分 mux.Handle(),這些代碼同樣可以刪除。

需要注意的是,handler 是不可以重復(fù)注冊(cè)的,為此我們需要加一個(gè)標(biāo)志位。

到此為止,第二個(gè) 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\n", err)
    os.Exit(2)
}

3. 復(fù)用 handler

我們對(duì)以上得到的代碼打包,將其寫入 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

}

在啟動(dòng) pprof 后,延遲三秒再重定向到 pprof 界面。

表面上,這個(gè)需求已經(jīng)做完了,但是。。。

以上 pprof 是一次性的,在更換 profile 后重新讀取生成的 webInterface 并不會(huì)重新注冊(cè)到 handler 中。

為了解決最后的這個(gè)問題,我們不得不更改 pprof 源碼。對(duì)此我是抗拒的,不是不會(huì)改,也不是不好改,主要是 pprof 放在公司通用的 vendor 庫(kù)中,我害怕影響到別的項(xiàng)目(為此我提交了一個(gè) feature 給官方庫(kù),希望能有更好的解決方法)。

在 internal/driver/webui.go 下進(jìn)行如下改動(dòng),使 webI 可以被復(fù)用。

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
}

至此,我們終于可以順?biāo)目吹交鹧鎴D啦。

將google/pprof集成在已有服務(wù)中的方法

到此,相信大家對(duì)“將google/pprof集成在已有服務(wù)中的方法”有了更深的了解,不妨來實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI