前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >修复go tool pprof存在的“bug”

修复go tool pprof存在的“bug”

作者头像
fliter
发布2024-03-22 10:15:50
860
发布2024-03-22 10:15:50
举报
文章被收录于专栏:旅途散记旅途散记

问题源起

之前写了一段根据当前内存占用,获取pprof指标文件的代码,如下:

代码语言:javascript
复制
package main

import (
 "fmt"
 "os"
 "runtime"
 "runtime/pprof"
 "strconv"
 "time"
)

func main() {

 ticker := time.NewTicker(10 * time.Second)

 go func() {
  for {
   time.Sleep(1e9)
   var m runtime.MemStats
   runtime.ReadMemStats(&m)

   memUsage := m.Sys / 1024 / 1024 // 将字节转换为兆字节

   if memUsage > 1024 {
    fmt.Printf("内存使用超过1GB:%dMB\n", memUsage)

    // 保存pprof数据到指定目录
    filepath := "某个目录/profile.pprof" + "." + strconv.Itoa(int(time.Now().Unix()))
    f, err := os.Create(filepath)
    if err != nil {
     fmt.Println("错误1:", err)
    }
    defer f.Close()

    //if err := pprof.StartCPUProfile(f); err != nil {
    // fmt.Println("错误2:", err)
    //}
    //defer pprof.StopCPUProfile()

    if err2 := pprof.WriteHeapProfile(f); err2 != nil {
     fmt.Println("错误2:", err2)
    }

    // 在这里执行想要进行性能分析的代码

    fmt.Println("pprof数据已保存到", filepath)

   } else {
    fmt.Printf("内存使用:%dMB\n", memUsage)
   }
  }

 }()

 for {
  select {
  case <-ticker.C:
   // 在这里执行您想要定时执行的代码
   var a = make([]byte, 1073741824)
   _ = a
   fmt.Println("定时器触发:", time.Now())
  }
 }

 select {}

}

同事做了一定修改,把时间戳改成了看起来更直观的 Y-m-d H:i:s形式,最终得到的采样文件类似 mem_2023-11-02_05:47:58

后面内存突增,采集到了pprof文件,但在执行 go tool pprof mem_2023-11-02_05:47:58 时报错:

代码语言:javascript
复制
Fetching profile over HTTP from http://mem_2023-11-02_05:47:58/debug/pprof/profile
mem_2023-11-02_05:47:58: Get "http://mem_2023-11-02_05:47:58/debug/pprof/profile": dial tcp: lookup mem_2023-11-02_05:47: no such host
failed to fetch any source profiles

上图go的版本是go version go1.21.0 darwin/arm64

而在其他go版本下,报错信息如下:

可见和Go版本似乎问题不大

而似乎在Windows设备上没这问题

经过试验,这和指标文件的命名有关。如果包含:,可能会当成http请求,去远程获取pprof文件

显然这并不符合使用者的初衷,一定程度算是个"bug" (而Windows系统不允许文件名称中存在冒号,会自动转为_,所以没有该问题)


探索过程

现在go源码中加一些调试信息,使用重新编译得到的二进制,执行之前的命令

这里指针指来指去,很不直观,希望能用spew.Dump,一次打印一目了然。

但在源码中如何使用呢?

(经过此次探索又开发了新技能。。。下面展示如何在源码中使用spew)

如何在源码中使用spew

先在 这个文件上方 import进来 "github.com/davecgh/go-spew/spew"

这时候编译肯定会报错的:

go get并没有啥用..

办法是,找一个spew文件夹,

打开文件夹,任意选一个版本,并把@及后面的去掉

打开 go/src/cmd/vendor/github.com

把刚才的文件夹复制进来

然后再编译,就ok了~

问题定位

再执行 go tool pprof mem_2023-11-02_05:47:58 就能看到输出:

问题出在这里

代码语言:javascript
复制
 for i := range sources {
  s := &sources[i]
  if err := s.err; err != nil {
   ui.PrintErr(s.addr + ": " + err.Error())
   continue
  }
  save = save || s.remote
  profiles = append(profiles, s.p)
  msrcs = append(msrcs, s.msrc)
  *s = profileSource{}
 }

 fmt.Println("爽哥说,到了此处666666")
 fmt.Println("爽哥说,profiles 元素数量为:", len(profiles))

 if len(profiles) == 0 {
  return nil, nil, false, 0, nil
 }

更本质而言,是上面的这块处理:

代码语言:javascript
复制
cui := &sources[0]
 spew.Println("cui is: ",cui)
 spew.Println("cui.err is: ",cui.err)

 wg := sync.WaitGroup{}
 wg.Add(len(sources))
 for i := range sources {
  go func(s *profileSource) {
   defer wg.Done()
   s.p, s.msrc, s.remote, s.err = grabProfile(s.source, s.addr, fetch, obj, ui, tr)
  }(&sources[i])
 }
 wg.Wait()


 fmt.Println("----一番处理后------")

 spew.Println("cui2 is: ",cui)
 spew.Println("cui2.err is: ",cui.err)

代码语言:javascript
复制
cui is:  <*>{mem_2023-11-02_05:47:58 <*>{[mem_2023-11-02_05:47:58]   <nil> false false -1 -1   false } <nil> <nil> false <nil>}
cui.err is:  <nil>
shuang said  Fetching profile over HTTP from http://mem_2023-11-02_05:47:58/debug/pprof/profile
----一番处理后------
cui2 is:  <*>{mem_2023-11-02_05:47:58 <*>{[mem_2023-11-02_05:47:58]   <nil> false false -1 -1   false } <nil> <nil> false <*>Get "http://mem_2023-11-02_05:47:58/debug/pprof/profile": dial tcp: lookup mem_2023-11-02_05:47: no such host}
cui2.err is:  <*>Get "http://mem_2023-11-02_05:47:58/debug/pprof/profile": dial tcp: lookup mem_2023-11-02_05:47: no such host
爽哥说,s.err错误是: %#v Get "http://mem_2023-11-02_05:47:58/debug/pprof/profile": dial tcp: lookup mem_2023-11-02_05:47: no such host
代码语言:javascript
复制
// grabProfile fetches a profile. Returns the profile, sources for the
// profile mappings, a bool indicating if the profile was fetched
// remotely, and an error.
func grabProfile(s *source, source string, fetcher plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI, tr http.RoundTripper) (p *profile.Profile, msrc plugin.MappingSources, remote bool, err error) {
 var src string
 duration, timeout := time.Duration(s.Seconds)*time.Second, time.Duration(s.Timeout)*time.Second
 if fetcher != nil {
  p, src, err = fetcher.Fetch(source, duration, timeout)
  if err != nil {
   return
  }
 }
 if err != nil || p == nil {
  // Fetch the profile over HTTP or from a file.
  p, src, err = fetch(source, duration, timeout, ui, tr)
  if err != nil {
   return
  }
 }

 if err = p.CheckValid(); err != nil {
  return
 }

 // Update the binary locations from command line and paths.
 locateBinaries(p, s, obj, ui)

 // Collect the source URL for all mappings.
 if src != "" {
  msrc = collectMappingSources(p, src)
  remote = true
  if strings.HasPrefix(src, "http://"+testSourceAddress) {
   // Treat test inputs as local to avoid saving
   // testcase profiles during driver testing.
   remote = false
  }
 }
 return
}

问一下ChatGPT grabProfile的作用:

grabProfile 函数的作用是获取一个分析数据文件(profile)。它接受一个源(source)和一些工具和接口作为参数,然后尝试从不同的位置获取分析数据文件,并返回获取的结果。

函数的具体行为如下:

  1. 首先,它会根据给定的 fetcher(一个实现了 plugin.Fetcher 接口的对象)尝试从 source(分析数据文件的地址)中获取分析数据。如果 fetcher 不为 nil,则调用 fetcher.Fetch 方法来获取分析数据,并将结果存储在 psrc 变量中。如果发生错误,函数会返回错误信息。
  2. 如果上述步骤中出现了错误,或者获取的分析数据为空,函数会尝试通过其他方式获取分析数据。它会调用 fetch 函数来通过 HTTP 或从文件中获取分析数据,并将结果存储在 psrc 变量中。如果发生错误,函数会返回错误信息。
  3. 接下来,函数会检查获取的分析数据的有效性,调用 p.CheckValid() 方法。如果分析数据无效,函数会返回错误信息。
  4. 然后,函数会使用命令行和路径信息更新二进制文件的位置,调用 locateBinaries 函数。
  5. 最后,函数会收集所有映射的源URL,即调用 collectMappingSources 函数来获取映射的源URL,并将结果存储在 msrc 变量中。如果 src 不为空,表示分析数据是通过远程方式获取的,将 remote 设置为 true。如果 src 的前缀是 http:// 并且与测试源地址(testSourceAddress)相匹配,将 remote 设置为 false,以避免在驱动程序测试期间保存测试用例的分析数据文件。

最终,函数返回获取的分析数据文件(p),用于分析数据映射的源(msrc),一个布尔值指示分析数据是否是远程获取的(remote),以及可能的错误信息(err)。

总之,grabProfile 函数尝试从不同的位置获取分析数据文件,并返回获取的结果。它还负责处理获取的分析数据,更新二进制文件的位置,并收集映射的源URL。


要看看var src string 在哪里变成非空字符串的

所以绕了一圈,又回到了go代码

代码语言:javascript
复制
func (f *fetcher) Fetch(src string, duration, timeout time.Duration) (*profile.Profile, string, error) {
 fmt.Println("走到了这个Fetch??")

 sourceURL, timeout := adjustURL(src, duration, timeout)
 if sourceURL == "" {
  // Could not recognize URL, let regular pprof attempt to fetch the profile (eg. from a file)
  return nil, "", nil
 }
 fmt.Fprintln(os.Stderr, "shuang said  Fetching profile over HTTP from", sourceURL)
 if duration > 0 {
  fmt.Fprintf(os.Stderr, "Please wait... (%v)\n", duration)
 }
 p, err := getProfile(sourceURL, timeout)
 return p, sourceURL, err
}
代码语言:javascript
复制

// adjustURL applies the duration/timeout values and Go specific defaults.
func adjustURL(source string, duration, timeout time.Duration) (string, time.Duration) {
 u, err := url.Parse(source)
 if err != nil || (u.Host == "" && u.Scheme != "" && u.Scheme != "file") {
  // Try adding http:// to catch sources of the form hostname:port/path.
  // url.Parse treats "hostname" as the scheme.
  u, err = url.Parse("http://" + source)
 }
 if err != nil || u.Host == "" {
  return "", 0
 }

 if u.Path == "" || u.Path == "/" {
  u.Path = cpuProfileHandler
 }

 // Apply duration/timeout overrides to URL.
 values := u.Query()
 if duration > 0 {
  values.Set("seconds", fmt.Sprint(int(duration.Seconds())))
 } else {
  if urlSeconds := values.Get("seconds"); urlSeconds != "" {
   if us, err := strconv.ParseInt(urlSeconds, 10, 32); err == nil {
    duration = time.Duration(us) * time.Second
   }
  }
 }
 if timeout <= 0 {
  if duration > 0 {
   timeout = duration + duration/2
  } else {
   timeout = 60 * time.Second
  }
 }
 u.RawQuery = values.Encode()
 return u.String(), timeout
}

问一下ChatGPT adjustURL的作用:

adjustURL 函数的作用是根据给定的参数调整URL,并应用持续时间(duration)和超时(timeout)的值以及Go语言的默认设置。

函数的具体行为如下:

  1. 首先,函数尝试将给定的源URL解析为一个URL对象,使用 url.Parse 方法进行解析。如果解析过程中发生错误,或者解析结果的主机(Host)为空且协议(Scheme)不为空且不是 "file",则进入下一步处理。
  2. 如果解析过程中发生错误,或者解析结果的主机为空,函数将返回空字符串和零值持续时间。
  3. 如果解析结果的路径(Path)为空或者为根路径("/"),函数将路径设置为 cpuProfileHandler
  4. 接下来,函数会在URL的查询参数中应用持续时间和超时的值。首先,它会获取URL的查询参数,并存储在 values 变量中。如果持续时间大于0,函数会将持续时间的秒数设置为查询参数中的 "seconds" 值。否则,如果查询参数中存在 "seconds" 值且可以成功解析为整数,函数会将持续时间设置为解析结果。这样就可以根据URL的查询参数覆盖持续时间的值。
  5. 如果超时小于等于0,函数会根据持续时间的值来确定超时的值。如果持续时间大于0,超时的值将设置为持续时间加上持续时间的一半。否则,超时的值将设置为60秒。
  6. 最后,函数会将查询参数重新编码,并将其附加到URL的原始查询字符串中。然后,函数返回调整后的URL字符串和超时的值。

总之,adjustURL 函数用于调整URL,并根据给定的持续时间和超时的值以及Go语言的默认设置来修改URL。它处理URL的解析、路径设置、查询参数的应用和编码,以及超时值的计算。

修改源码,提merge request

想解决这个问题, 只改Go代码还不够,先要修改上游的google/pprof的代码

修改上游的google/pprof

这是最终被合入pprof的改动:

下面详细解释一下

在 /Users/fliter/20231014/go/src/pprof2 目录下,执行 go tool pprof mem_2023-11-02_04:44:30

完整内容:

代码语言:javascript
复制
go tool pprof mem_2023-11-02_04:44:30

爽哥说: 从此处启动
爽哥说: o is plugin.Options{Writer:driver.oswriter{}, Flagset:(*driver.GoFlags)(0x1400010e3c0), Fetch:(*main.fetcher)(0x1035d9000), Sym:(*symbolizer.Symbolizer)(0x140001cacc0), Obj:(*driver.internalOool)(0x14000102f30), UI:(*main.readlineUI)(0x140001041a8), HTTPServer:(func(*plugin.HTTPServerArgs) error)(nil), HTTPTransport:(*transport.transport)(0x14000122660)}
(string) (len=19) "爽哥说 source is"
([]driver.profileSource) (len=1 cap=1) {
 (driver.profileSource) {
  addr: (string) (len=23) "mem_2023-11-02_04:44:30",
  source: (*driver.source)(0x140001e4780)({
   Sources: ([]string) (len=1 cap=1) {
    (string) (len=23) "mem_2023-11-02_04:44:30"
   },
   ExecName: (string) "",
   BuildID: (string) "",
   Base: ([]string) <nil>,
   DiffBase: (bool) false,
   Normalize: (bool) false,
   Seconds: (int) -1,
   Timeout: (int) -1,
   Symbolize: (string) "",
   HTTPHostport: (string) "",
   HTTPDisableBrowser: (bool) false,
   Comment: (string) ""
  }),
  p: (*profile.Profile)(<nil>),
  msrc: (plugin.MappingSources) <nil>,
  remote: (bool) false,
  err: (error) <nil>
 }
}
cui is:  <*>{mem_2023-11-02_04:44:30 <*>{[mem_2023-11-02_04:44:30]   <nil> false false -1 -1   false } <nil> <nil> false <nil>}
cui.err is:  <nil>
这里肯定到了6789
爽哥第一重关卡,src为: 
source, duration, timeout, ui, tr为: mem_2023-11-02_04:44:30 -1s -1s &{0x14000168900} &{0x14000102f50 0x14000102f60 0x14000102f70 <nil> [] {{{} 0} {0 0}} <nil>}
------
此处的err和p是: <nil>
(string) (len=11) "----p-----:"
(*profile.Profile)(<nil>)
xxxxxx:到了这里
***不会到这里111***
Fetching profile over HTTP from http://mem_2023-11-02_04:44:30
src777 is: http://mem_2023-11-02_04:44:30
err888 is: http fetch: Get "http://mem_2023-11-02_04:44:30": dial tcp: lookup mem_2023-11-02_01:40: no such host
----一番处理后------
cui2.err is:  <*>http fetch: Get "http://mem_2023-11-02_04:44:30": dial tcp: lookup mem_2023-11-02_01:40: no such host
爽哥说,s.err错误是: %#v http fetch: Get "http://mem_2023-11-02_04:44:30": dial tcp: lookup mem_2023-11-02_01:40: no such host
mem_2023-11-02_04:44:30: http fetch: Get "http://mem_2023-11-02_04:44:30": dial tcp: lookup mem_2023-11-02_01:40: no such host
爽哥说,到了此处666666
爽哥说,profiles 元素数量为: 0
爽哥说 出错了: failed to fetch any source profiles
failed to fetch any source profiles

通过增加的调试信息可以得知,执行 go tool pprof时,入口文件是在src/cmd/pprof/pprof.go:

代码语言:javascript
复制
func main() {
 options := &driver.Options{
  Fetch: new(fetcher),
  Obj:   new(objTool),
  UI:    newUI(),
 }

 fmt.Println("爽哥说: 从此处启动")
 if err := driver.PProf(options); err != nil {
  fmt.Println("爽哥说 出错了:", err)
  fmt.Fprintf(os.Stderr, "%v\n", err)
  os.Exit(2)
 }
}

然后driver.PProf会到 /Users/fliter/20231014/go/src/cmd/vendor/github.com/google/pprof/internal/driver/driver.go:

代码语言:javascript
复制
// PProf acquires a profile, and symbolizes it using a profile
// manager. Then it generates a report formatted according to the
// options selected through the flags package.
func PProf(eo *plugin.Options) error {
 // Remove any temporary files created during pprof processing.
 defer cleanupTempFiles()

 o := setDefaults(eo)

 src, cmd, err := parseFlags(o)
 if err != nil {
  return err
 }

 p, err := fetchProfiles(src, o)
 if err != nil {
  return err
 }

 if cmd != nil {
  return generateReport(p, cmd, currentConfig(), o)
 }

 fmt.Println("爽哥说:src.HTTPHostport is ",src.HTTPHostport)
 if src.HTTPHostport != "" {
  return serveWebInterface(src.HTTPHostport, p, o, src.HTTPDisableBrowser)
 }
 return interactive(p, o)
}

p, err := fetchProfiles(src, o) 进入到go/src/cmd/vendor/github.com/google/pprof/internal/driver/fetch.go中:

代码语言:javascript
复制
// fetchProfiles fetches and symbolizes the profiles specified by s.
// It will merge all the profiles it is able to retrieve, even if
// there are some failures. It will return an error if it is unable to
// fetch any profiles.
func fetchProfiles(s *source, o *plugin.Options) (*profile.Profile, error) {
 sources := make([]profileSource, 0, len(s.Sources))
 for _, src := range s.Sources {
  sources = append(sources, profileSource{
   addr:   src,
   source: s,
  })
 }

 bases := make([]profileSource, 0, len(s.Base))
 for _, src := range s.Base {
  bases = append(bases, profileSource{
   addr:   src,
   source: s,
  })
 }

 fmt.Printf("爽哥说: o is %#v\n", *o)
 //spew.Dump("o is",o)
 spew.Dump("爽哥说 source is", sources)
 p, pbase, m, mbase, save, err := grabSourcesAndBases(sources, bases, o.Fetch, o.Obj, o.UI, o.HTTPTransport)
 if err != nil {
  return nil, err
 }

 if pbase != nil {
  if s.DiffBase {
   pbase.SetLabel("pprof::base", []string{"true"})
  }
  if s.Normalize {
   err := p.Normalize(pbase)
   if err != nil {
    return nil, err
   }
  }
  pbase.Scale(-1)
  p, m, err = combineProfiles([]*profile.Profile{p, pbase}, []plugin.MappingSources{m, mbase})
  if err != nil {
   return nil, err
  }
 }

 // Symbolize the merged profile.
 if err := o.Sym.Symbolize(s.Symbolize, m, p); err != nil {
  return nil, err
 }
 p.RemoveUninteresting()
 unsourceMappings(p)

 if s.Comment != "" {
  p.Comments = append(p.Comments, s.Comment)
 }

 // Save a copy of the merged profile if there is at least one remote source.
 if save {
  dir, err := setTmpDir(o.UI)
  if err != nil {
   return nil, err
  }

  prefix := "pprof."
  if len(p.Mapping) > 0 && p.Mapping[0].File != "" {
   prefix += filepath.Base(p.Mapping[0].File) + "."
  }
  for _, s := range p.SampleType {
   prefix += s.Type + "."
  }

  tempFile, err := newTempFile(dir, prefix, ".pb.gz")
  if err == nil {
   if err = p.Write(tempFile); err == nil {
    o.UI.PrintErr("Saved profile in ", tempFile.Name())
   }
  }
  if err != nil {
   o.UI.PrintErr("Could not save profile: ", err)
  }
 }

 if err := p.CheckValid(); err != nil {
  return nil, err
 }

 return p, nil
}

而后通过 p, pbase, m, mbase, save, err := grabSourcesAndBases(sources, bases, o.Fetch, o.Obj, o.UI, o.HTTPTransport)执行grabSourcesAndBases:

代码语言:javascript
复制
func grabSourcesAndBases(sources, bases []profileSource, fetch plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI, tr http.RoundTripper) (*profile.Profile, *profile.Profile, plugin.MappingSources, plugin.MappingSources, bool, error) {
 wg := sync.WaitGroup{}
 wg.Add(2)
 var psrc, pbase *profile.Profile
 var msrc, mbase plugin.MappingSources
 var savesrc, savebase bool
 var errsrc, errbase error
 var countsrc, countbase int
 go func() {
  defer wg.Done()
  psrc, msrc, savesrc, countsrc, errsrc = chunkedGrab(sources, fetch, obj, ui, tr)
 }()
 go func() {
  defer wg.Done()
  pbase, mbase, savebase, countbase, errbase = chunkedGrab(bases, fetch, obj, ui, tr)
 }()
 wg.Wait()
 save := savesrc || savebase

 if errsrc != nil {
  return nil, nil, nil, nil, false, fmt.Errorf("problem fetching source profiles: %v", errsrc)
 }
 if errbase != nil {
  return nil, nil, nil, nil, false, fmt.Errorf("problem fetching base profiles: %v,", errbase)
 }
 if countsrc == 0 {
  return nil, nil, nil, nil, false, fmt.Errorf("failed to fetch any source profiles")
 }
 if countbase == 0 && len(bases) > 0 {
  return nil, nil, nil, nil, false, fmt.Errorf("failed to fetch any base profiles")
 }
 if want, got := len(sources), countsrc; want != got {
  ui.PrintErr(fmt.Sprintf("Fetched %d source profiles out of %d", got, want))
 }
 if want, got := len(bases), countbase; want != got {
  ui.PrintErr(fmt.Sprintf("Fetched %d base profiles out of %d", got, want))
 }

 return psrc, pbase, msrc, mbase, save, nil
}

最终因为 countsrc == 0从而抛出 failed to fetch any source profiles

先看 psrc, msrc, savesrc, countsrc, errsrc = chunkedGrab(sources, fetch, obj, ui, tr)这部分:

代码语言:javascript
复制
// chunkedGrab fetches the profiles described in source and merges them into
// a single profile. It fetches a chunk of profiles concurrently, with a maximum
// chunk size to limit its memory usage.
func chunkedGrab(sources []profileSource, fetch plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI, tr http.RoundTripper) (*profile.Profile, plugin.MappingSources, bool, int, error) {
 const chunkSize = 128

 var p *profile.Profile
 var msrc plugin.MappingSources
 var save bool
 var count int

 for start := 0; start < len(sources); start += chunkSize {
  end := start + chunkSize
  if end > len(sources) {
   end = len(sources)
  }
  chunkP, chunkMsrc, chunkSave, chunkCount, chunkErr := concurrentGrab(sources[start:end], fetch, obj, ui, tr)
  switch {
  case chunkErr != nil:
   return nil, nil, false, 0, chunkErr
  case chunkP == nil:
   continue
  case p == nil:
   p, msrc, save, count = chunkP, chunkMsrc, chunkSave, chunkCount
  default:
   p, msrc, chunkErr = combineProfiles([]*profile.Profile{p, chunkP}, []plugin.MappingSources{msrc, chunkMsrc})
   if chunkErr != nil {
    return nil, nil, false, 0, chunkErr
   }
   if chunkSave {
    save = true
   }
   count += chunkCount
  }
 }

 return p, msrc, save, count, nil
}

通过 s.p, s.msrc, s.remote, s.err = grabProfile(s.source, s.addr, fetch, obj, ui, tr) 进入到grabProfile方法中:

代码语言:javascript
复制
// grabProfile fetches a profile. Returns the profile, sources for the
// profile mappings, a bool indicating if the profile was fetched
// remotely, and an error.
func grabProfile(s *source, source string, fetcher plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI, tr http.RoundTripper) (p *profile.Profile, msrc plugin.MappingSources, remote bool, err error) {

 fmt.Println("这里肯定到了6789")
 var src string
 duration, timeout := time.Duration(s.Seconds)*time.Second, time.Duration(s.Timeout)*time.Second

 fetcher = nil // 这步是我强行指定的
 if fetcher != nil {
  fmt.Println("有没有走这里?8888")
  fmt.Println("source, duration, timeout:", source, duration, timeout)
  p, src, err = fetcher.Fetch(source, duration, timeout)
  if err != nil {
   fmt.Println("fetcher.Fetch 这里的错误为:", err)
   return
  }
 }

 fmt.Println("爽哥第一重关卡,src为:", src)
 fmt.Println("source, duration, timeout, ui, tr为:", source, duration, timeout, ui, tr)

 fmt.Println("------")

 fmt.Println("此处的err和p是:", err)
 spew.Dump("----p-----:", p)

 if err != nil || p == nil {
  // Fetch the profile over HTTP or from a file.
  p, src, err = fetch(source, duration, timeout, ui, tr)

  fmt.Println("src777 is:", src)
  fmt.Println("err888 is:", err)

  if err != nil {
   return
  }
 }

 fmt.Println("爽哥第二重关卡,src为:", src)
 fmt.Println("~~~~~~~~~")

 if err = p.CheckValid(); err != nil {
  return
 }

 // Update the binary locations from command line and paths.
 locateBinaries(p, s, obj, ui)

 // Collect the source URL for all mappings.
 if src != "" {
  msrc = collectMappingSources(p, src)
  remote = true
  if strings.HasPrefix(src, "http://"+testSourceAddress) {
   // Treat test inputs as local to avoid saving
   // testcase profiles during driver testing.
   remote = false
  }
 }
 return
}

这里的fetcher,其实就是go源码src/cmd/pporf/pprof.go中定义的Fetch:

代码语言:javascript
复制

func (f *fetcher) Fetch(src string, duration, timeout time.Duration) (*profile.Profile, string, error) {
 fmt.Println("走到了这个Fetch??")

 sourceURL, timeout := adjustURL(src, duration, timeout)

 fmt.Println("pengpeng888 said sourceURL is:", sourceURL)

 if sourceURL == "" {
  // Could not recognize URL, let regular pprof attempt to fetch the profile (eg. from a file)
  return nil, "", nil
 }
 fmt.Fprintln(os.Stderr, "shuang said  Fetching profile over HTTP from", sourceURL)
 if duration > 0 {
  fmt.Fprintf(os.Stderr, "Please wait... (%v)\n", duration)
 }
 p, err := getProfile(sourceURL, timeout)
 return p, sourceURL, err
}

即如果定义了Fetch,会先执行自己定义的fetch

代码语言:javascript
复制
 fetcher = nil // 这步是我强行指定,原代码中没有
 if fetcher != nil {
  fmt.Println("有没有走这里?8888")
  fmt.Println("source, duration, timeout:", source, duration, timeout)
  p, src, err = fetcher.Fetch(source, duration, timeout)
  if err != nil {
   fmt.Println("fetcher.Fetch 这里的错误为:", err)
   return
  }
 }

先忽略自定义的这个fetch (即先让fetcher = nil), 继续往下,就到了

代码语言:javascript
复制
 if err != nil || p == nil {
  // Fetch the profile over HTTP or from a file.
  p, src, err = fetch(source, duration, timeout, ui, tr)

  fmt.Println("src777 is:", src)
  fmt.Println("err888 is:", err)

  if err != nil {
   return
  }
 }

也就是我给google/pprof修改的代码:

代码语言:javascript
复制
// fetch fetches a profile from source, within the timeout specified,
// producing messages through the ui. It returns the profile and the
// url of the actual source of the profile for remote profiles.
func fetch(source string, duration, timeout time.Duration, ui plugin.UI, tr http.RoundTripper) (p *profile.Profile, src string, err error) {

 fmt.Println("xxxxxxx:到了这里")

 var f io.ReadCloser

 if adjustFile(source) {
  fmt.Println("如果文件存在,就到这里~")
  f, err = os.Open(source)
 } else if sourceURL, timeout := adjustURL(source, duration, timeout); sourceURL != "" {
  //if sourceURL, timeout := adjustURL(source, duration, timeout); sourceURL != "" && 1==2 {
  fmt.Println("***不会到这里111***")
  ui.Print("Fetching profile over HTTP from " + sourceURL)
  if duration > 0 {
   ui.Print(fmt.Sprintf("Please wait... (%v)", duration))
  }
  f, err = fetchURL(sourceURL, timeout, tr)
  src = sourceURL
 } else if isPerfFile(source) {
  fmt.Println("***会到这里吗2222??***")
  f, err = convertPerfData(source, ui)
 } else {
  fmt.Println("***还是会到这里3333?***")
  f, err = os.Open(source)
 }
 if err == nil {
  defer f.Close()
  p, err = profile.Parse(f)
 }
 return
}

没改之前因为包含冒号,当成url处理,于是出错,即:

代码语言:javascript
复制
src777 is: http://mem_2023-11-02_04:44:30
err888 is: http fetch: Get "http://mem_2023-11-02_04:44:30": dial tcp: lookup mem_2023-11-02_01:40: no such host

修改并合入之后,会判断本地有没有source这个文件,如果有,则当成文件处理,不管这个文件名称中是否包含冒号.

另外,如果source是localhost:8080,而本地当前目录也有一个叫localhost:8080的文件,那这里其实有歧义,到底是把source当成文件还是url呢? --- 这个问题在issue[1]mr[2]中有讨论,最终维护者建议,如果本地有source这个字符串所对应的文件,则一律当成文件处理( 即本地也恰好有个文件叫localhost:8080,则使用go tool pprof localhost:8080时,会优先将其作为本地文件处理,而不是请求http://localhost:8080)

最终,在2023.12.05, 提交给google/pprof的改动[3]已经合入,可以继续改Golang~

针对上面代码及单测的修改,下面几个问ChatGPT的问题可供参考

go语言中os.IsNotExist有何作用?

在Go语言中,os.IsNotExist 是一个用于检查文件或目录是否不存在的函数。它通常与其他函数一起使用,例如 os.Stat 来检查文件或目录的状态,然后使用 os.IsNotExist 来判断是否文件或目录不存在。

具体来说,os.Stat 用于获取文件或目录的信息,如果文件或目录不存在,它会返回一个 *os.PathError 错误。然后,您可以使用 os.IsNotExist 方法来检查这个错误是否表示文件或目录不存在。示例代码如下:

代码语言:javascript
复制
package main

import (
    "fmt"
    "os"
)

func main() {
    // 检查文件是否存在
    _, err := os.Stat("file.txt")
    if err != nil {
        if os.IsNotExist(err) {
            fmt.Println("文件不存在")
        } else {
            fmt.Println("发生其他错误:", err)
        }
    }

    // 检查目录是否存在
    _, err = os.Stat("directory")
    if err != nil {
        if os.IsNotExist(err) {
            fmt.Println("目录不存在")
        } else {
            fmt.Println("发生其他错误:", err)
        }
    }
}

在上面的示例中,我们首先使用 os.Stat 检查文件 "file.txt" 和目录 "directory" 是否存在,然后使用 os.IsNotExist 来判断是否它们不存在。这有助于您在操作文件和目录时更好地处理错误情况。

有了os.Stat,为什么还需要os.IsNotExist?

os.Stat 用于获取文件或目录的信息,它返回一个 *os.PathError 类型的错误,其中包含了详细的错误信息,包括文件或目录不存在的情况。os.IsNotExist 是一个辅助函数,用于检查 *os.PathError 中的错误是否表示文件或目录不存在。

为什么需要 os.IsNotExist 呢?主要有两个原因:

  1. 错误类型多样性:os.Stat 返回的错误不仅仅表示文件或目录不存在。它可能还表示其他错误情况,如权限问题、路径不存在、文件已被锁定等。使用 os.IsNotExist 可以帮助您准确地区分出文件或目录不存在的情况,而不必关心其他错误类型。
  2. 跨平台兼容性:不同操作系统可能在文件或目录不存在时返回不同的错误信息。使用 os.IsNotExist 可以帮助您编写跨平台的代码,而不必关心不同操作系统下的具体错误消息。

综合这两点,os.IsNotExist 使得代码更具可读性和可维护性,因为您可以明确地检查文件或目录是否不存在,而无需详细比对错误消息的字符串。这使得代码更健壮,适用于不同的操作系统和环境。

golang中func (c *T) TempDir() string如何使用?举例说明

https://pkg.go.dev/testing#T.TempDir

func (c *T) TempDir() string在Go测试中用于获取临时目录,其用法和示例如下:

  1. 获取临时目录:
代码语言:javascript
复制
func TestMyTest(t *testing.T) {
  dir := t.TempDir() 
  // dir now points to a unique temporary directory
}
  1. 在临时目录中创建和操作文件:
代码语言:javascript
复制
func TestFileOps(t *testing.T) {
  dir := t.TempDir()

  file := filepath.Join(dir, "file.txt")
  f, err := os.Create(file)
  if err != nil {
    t.Fatal(err) 
  }

  // write to file
  f.WriteString("Hello world!")

  // check file content
  data, err := ioutil.ReadFile(file)
  if err != nil {
    t.Error(err)
  }
  if string(data) != "Hello world!" {
    t.Error("Wrong file content")
  }
}
  1. 清理临时目录:

由于使用了t.TempDir(),所有在该临时目录下的文件和目录在测试结束后会被自动清理。不需要手动删除。

所以TempDir()主要用于测试中需要使用临时目录或文件的场景,它可以很方便地获取一个唯一的目录路径并在测试结束后自动清理临时文件。

修改golang

本地调试

/Users/fliter/20231014/go/src/cmd/vendor/github.com/google/pprof/internal/driver/fetch.go中的fetch方法,替换为我合入google/pprof的新方法:

代码语言:javascript
复制
// fetch fetches a profile from source, within the timeout specified,
// producing messages through the ui. It returns the profile and the
// url of the actual source of the profile for remote profiles.
func fetch(source string, duration, timeout time.Duration, ui plugin.UI, tr http.RoundTripper) (p *profile.Profile, src string, err error) {
 var f io.ReadCloser
 fmt.Println("这是爽哥新修改的google/pprof的fetch方法")

 // First determine whether the source is a file, if not, it will be treated as a URL.
 if _, openErr := os.Stat(source); openErr == nil {
  if isPerfFile(source) {
   f, err = convertPerfData(source, ui)
  } else {
   f, err = os.Open(source)
  }
 } else {
  sourceURL, timeout := adjustURL(source, duration, timeout)
  if sourceURL != "" {
   ui.Print("Fetching profile over HTTP from " + sourceURL)
   if duration > 0 {
    ui.Print(fmt.Sprintf("Please wait... (%v)", duration))
   }
   f, err = fetchURL(sourceURL, timeout, tr)
   src = sourceURL
  }
 }
 if err == nil {
  defer f.Close()
  p, err = profile.Parse(f)
 }
 return
}

执行 ./all.bash重新编译Go源码

而后回到 /Users/fliter/20231014/go/src/pprof2 目录下,再次执行 go tool pprof mem_2023-11-02_04:44:30

已经能够正常使用pprof。但这是因为之前忽略了自定义的fetch,即加了fetcher = nil

grabProfile方法中我新增的 fetcher = nil注释掉,再次执行 ./all.bash 编译Go源代码

然后执行 go tool pprof mem_2023-11-02_04:44:30

会走到这段代码中,

代码语言:javascript
复制
if fetcher != nil {
  fmt.Println("有没有走这里?8888")
  fmt.Println("source, duration, timeout:", source, duration, timeout)
  p, src, err = fetcher.Fetch(source, duration, timeout)
  if err != nil {
   fmt.Println("fetcher.Fetch 这里的错误为:", err)
   return
  }
 }

相应输出为:

代码语言:javascript
复制
有没有走这里?8888

source, duration, timeout: mem_2023-11-02_04:44:30 -1s -1s

走到了这个Fetch??

source is: mem_2023-11-02_04:44:30

(string) (len=10) "u ,err is:"

(*url.URL)(<nil>)

(*url.Error)(0x140001cafc0)(parse "mem_2023-11-02_04:44:30": first path segment in URL cannot contain colon)

xx777

(string) (len=15) "uuuuuu ,err is:"

(*url.URL)(0x140002161b0)(http://mem_2023-11-02_04:44:30)

(interface {}) <nil>

err  || u.Host  <nil> mem_2023-11-02_04:44:30

xx888

pengpeng888 said sourceURL is:

发现没有出错,这是因为我已经修改了go源码中的 func adjustURL(source string, duration, timeout time.Duration)

代码语言:javascript
复制

// adjustURL applies the duration/timeout values and Go specific defaults.
func adjustURL(source string, duration, timeout time.Duration) (string, time.Duration) {
 //

 //path, err := os.Getwd()
 //if err != nil {
 // fmt.Println("Failed to get current path:", err)
 //}
 //fmt.Println("Current path:", path)
 //fmt.Println("source789:", source)
 //
 //// 判断当前路径有没有同名的这个文件。。如果有,按文件处理。。。
 //_, err = os.Stat(path + "/" + source)
 //if err == nil {
 // fmt.Println("111文件存在")
 // return "", 0
 //} else if os.IsNotExist(err) {
 // fmt.Println("22文件不存在")
 //} else {
 // fmt.Println("33无法确定文件是否存在:", err)
 //}

 u, err := url.Parse(source)
 fmt.Println("source is:", source)
 spew.Dump("u ,err is:", u, err)

 if err != nil || (u.Host == "" && u.Scheme != "" && u.Scheme != "file") {

  fmt.Println("xx777")
  // Try adding http:// to catch sources of the form hostname:port/path.
  // url.Parse treats "hostname" as the scheme.
  u, err = url.Parse("http://" + source)
  spew.Dump("uuuuuu ,err is:", u, err)

 }

 fmt.Println("err  || u.Host ", err, u.Host)
 if err != nil || u.Host == "" || adjustFile(source) {
  fmt.Println("xx888")
  return "", 0
 }

 if u.Path == "" || u.Path == "/" {
  fmt.Println("u.Path:", u.Path, "|", "u.Host", u.Host)
  fmt.Println("xx999")
  u.Path = cpuProfileHandler
 }

 // Apply duration/timeout overrides to URL.
 values := u.Query()

 fmt.Println("duration is:", duration)
 spew.Dump("values is:", values)
 if duration > 0 {
  values.Set("seconds", fmt.Sprint(int(duration.Seconds())))
 } else {
  fmt.Println("xx789")
  if urlSeconds := values.Get("seconds"); urlSeconds != "" {
   fmt.Println("aaaaaaaaaaaaaaaaa")
   if us, err := strconv.ParseInt(urlSeconds, 10, 32); err == nil {
    duration = time.Duration(us) * time.Second
   }
  }
 }

 fmt.Println("timeout is:", timeout)
 if timeout <= 0 {
  if duration > 0 {
   timeout = duration + duration/2
  } else {
   timeout = 60 * time.Second
  }
 }

 fmt.Println("xxabc")

 u.RawQuery = values.Encode()

 fmt.Println("u.String(), timeout", u.String(), timeout)
 return u.String(), timeout
}

番外:

/Users/fliter/20231014/go/src/cmd/vendor/github.com/google/pprof/internal/driver/fetch.go中Fetcher接口的定义, 确实会先执行自定义的fetch,再执行google/pprof中自己带的fetch(已经被我修改的那个)

代码语言:javascript
复制
// A Fetcher reads and returns the profile named by src. src can be a
// local file path or a URL. duration and timeout are units specified
// by the end user, or 0 by default. duration refers to the length of
// the profile collection, if applicable, and timeout is the amount of
// time to wait for a profile before returning an error. Returns the
// fetched profile, the URL of the actual source of the profile, or an
// error.
type Fetcher interface {
 Fetch(src string, duration, timeout time.Duration) (*profile.Profile, string, error)
}

换回go现在最新版本的代码 ( 给go提的pr[4] 还未合入,而且需要修改 )

代码语言:javascript
复制
// adjustURL applies the duration/timeout values and Go specific defaults.
func adjustURL(source string, duration, timeout time.Duration) (string, time.Duration) {
 u, err := url.Parse(source)
 if err != nil || (u.Host == "" && u.Scheme != "" && u.Scheme != "file") {
  // Try adding http:// to catch sources of the form hostname:port/path.
  // url.Parse treats "hostname" as the scheme.
  u, err = url.Parse("http://" + source)
 }
 if err != nil || u.Host == "" {
  return "", 0
 }

 if u.Path == "" || u.Path == "/" {
  u.Path = cpuProfileHandler
 }

 // Apply duration/timeout overrides to URL.
 values := u.Query()
 if duration > 0 {
  values.Set("seconds", fmt.Sprint(int(duration.Seconds())))
 } else {
  if urlSeconds := values.Get("seconds"); urlSeconds != "" {
   if us, err := strconv.ParseInt(urlSeconds, 10, 32); err == nil {
    duration = time.Duration(us) * time.Second
   }
  }
 }
 if timeout <= 0 {
  if duration > 0 {
   timeout = duration + duration/2
  } else {
   timeout = 60 * time.Second
  }
 }
 u.RawQuery = values.Encode()
 return u.String(), timeout
}

再次./all.bash编译源码,并执行 go tool pprof mem_2023-11-02_04:44:30

果然,现在的代码会出错:

对比之前的返回的调试信息,

现在的adjustURL方法,返回的sourceURL是http://mem_2023-11-02_04:44:30/debug/pprof/profile,而不是空字符串

按google/pprof的建议,改fetch,而不要改adjustURL

在Fetch方法的开始新增:

代码语言:javascript
复制
 if _, openErr := os.Stat(src); openErr == nil {
  // Firstly, determine whether src is a file in the current directory.
  return nil, "", nil
 }

然后重新编译并执行 go tool pprof mem_2023-11-02_04:44:30再次验证,已经可用!

更新依赖,提交和code review

更新go用到的google/pprof的版本,把我的改动引入进来

代码语言:javascript
复制
cd go/src/cmd
# 这步可以通过IDE来操作
go get github.com/google/pprof v0.0.0-20231205033806-a5a03c77bf08
go mod tidy
go mod vendor

修改 /Users/fliter/xxxxx/go/src/cmd/go.mod中的 github.com/google/pprof 后面的版本, go mod tidy & go mod vendor 后提交

根据之前给go vet添加分析器的经验,代码和依赖最好提两个cl

更新依赖: https://go-review.googlesource.com/c/go/+/547236

代码:https://go-review.googlesource.com/c/go/+/539595

2024.02.02 可以继续更新了..

但更新依赖的cl发生了冲突,

git codereview sync (如果有未提交的改动,先commit或者stash)

然后解决冲突..

再之后更新依赖,方法见下图

然后也一直没合,直到2月16号,go team内部的这个提交[5],和我的cl产生了冲突

然后我只有废弃掉之前更新依赖的这个cl,我猜大概更新依赖这种事,更倾向go team内部的人去做吧..

2024.03.18,go team的大佬做了评论,指出了注释中“当前目录”存在问题,经过尝试,确实如此。

在这次修改被合入之前,不仅go tool pprof mem_2023-11-02_03:55:24 会报错, 对于

go tool pprof abc:123/mem

go tool pprof abc:123/mem_2023-11-02_03:55:24

也会有相同的问题

go tool pprof abc:def/mem_2023-11-02_03:55:24 则没有该问题,这是因为

u, err = url.Parse("http://" + source)这步,发生了error:

代码语言:javascript
复制
The err of this code is not nil: parse "http://abc:def /mem": invalid port ":def" after host

从而走到了

代码语言:javascript
复制
if err != nil || u.Host == "" {
  return "", 0
}

最终改动如下:

代码语言:javascript
复制
// Firstly, determine if the src is an existing file on the disk.
 // If it is a file, let regular pprof open it.
 // If it is not a file, when the src contains `:`
 // (e.g. mem_2023-11-02_03:55:24 or abc:123/mem_2023-11-02_03:55:24),
 // url.Parse will recognize it as a link and ultimately report an error,
 // similar to `abc:123/mem_2023-11-02_03:55:24:
 // Get "http://abc:123/mem_2023-11-02_03:55:24": dial tcp: lookup abc: no such host`
 if _, openErr := os.Stat(src); openErr == nil {
  return nil, "", nil
 }

改动很少,但探究和定位问题,还是花了很多功夫,也收获满满

从Go 1.23及之后,go tool pprof xxx时,会优先将xxx作为本地文件路径进行处理。如果本地不存在xxx这个文件,才会作为一个链接

参考资料

[1]

issue: https://github.com/google/pprof/issues/816

[2]

mr: https://github.com/google/pprof/pull/817

[3]

google/pprof的改动: https://github.com/google/pprof/pull/817

[4]

给go提的pr: https://go-review.googlesource.com/c/go/+/539595

[5]

这个提交: https://go-review.googlesource.com/c/go/+/564636

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-03-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 旅途散记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 问题源起
  • 探索过程
    • 如何在源码中使用spew
      • 问题定位
        • 问一下ChatGPT grabProfile的作用:
        • 问一下ChatGPT adjustURL的作用:
    • 修改源码,提merge request
      • 修改上游的google/pprof
        • go语言中os.IsNotExist有何作用?
        • 有了os.Stat,为什么还需要os.IsNotExist?
        • golang中func (c *T) TempDir() string如何使用?举例说明
      • 修改golang
        • 本地调试
        • 更新依赖,提交和code review
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档