运维八一 运维八一
首页
运维杂记
编程浅尝
周积跬步
专栏
生活
关于
收藏
  • 分类
  • 标签
  • 归档
Source (opens new window)

运维八一

运维,运维!
首页
运维杂记
编程浅尝
周积跬步
专栏
生活
关于
收藏
  • 分类
  • 标签
  • 归档
Source (opens new window)
  • Go

    • 前言

    • Go基础知识

    • Go基本语法

    • 实战项目:简单web服务

    • 基本数据类型

    • 内置运算符

    • 分支和循环

    • 函数 function

    • 结构体 struct

    • 方法 method

    • 实战项目:跟踪函数调用链

      • v0_1 使用 defer 跟踪函数的执行过程
      • v0_2 自动获取所跟踪函数的函数名
      • v0_3 增加 Goroutine 标识
        • v0.3 增加 Goroutine 标识
      • v0_4 让输出的跟踪信息更具层次感
      • v0_5 利用代码生成自动注入 Trace 函数
      • 总结
    • 接口 interface

    • 并发 concurrency

    • 指针

    • 实战项目:实现轻量级线程池

    • 实战项目:实现TCP服务器

    • go常用包

    • Gin框架

    • go随记

  • Python

  • Shell

  • Java

  • Vue

  • 前端

  • 编程浅尝
  • Go
  • 实战项目:跟踪函数调用链
lyndon
2022-06-07
目录

v0_3 增加 Goroutine 标识

# v0.3 增加 Goroutine 标识

当程序中并发运行多个 Goroutine 的时候,多个函数调用链的出入口信息输出就会混杂在一起,无法分辨。

**解决方案:**在输出的函数出入口信息时,带上一个在程序每次执行时能唯一区分 Goroutine 的 Goroutine ID。

注意:Go 核心团队为了避免Goroutine ID 的滥用 (opens new window),故意没有将 Goroutine ID 暴露给开发者。但在 Go 标准库的 h2_bundle.go 中,有一个获取 Goroutine ID 的标准方法,如下:

// $GOROOT/src/net/http/h2_bundle.go
var http2goroutineSpace = []byte("goroutine ")

func http2curGoroutineID() uint64 {
 bp := http2littleBuf.Get().(*[]byte)
 defer http2littleBuf.Put(bp)
 b := *bp
 b = b[:runtime.Stack(b, false)]
 // Parse the 4707 out of "goroutine 4707 ["
 b = bytes.TrimPrefix(b, http2goroutineSpace)
 i := bytes.IndexByte(b, ' ')
 if i < 0 {
     panic(fmt.Sprintf("No space found in %q", b))
 }
 b = b[:i]
 n, err := http2parseUintBytes(b, 10, 64)
 if err != nil {
     panic(fmt.Sprintf("Failed to parse goroutine ID out of %q: %v", b, err))
 }
 return n
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

由于 http2curGoroutineID 不是一个导出函数,无法直接使用。可以把它复制出来改造一下:

// trace2/trace.go
var goroutineSpace = []byte("goroutine ")

func curGoroutineID() uint64 {
    b := make([]byte, 64)
    b = b[:runtime.Stack(b, false)]
    // Parse the 4707 out of "goroutine 4707 ["
    b = bytes.TrimPrefix(b, goroutineSpace)
    i := bytes.IndexByte(b, ' ')
    if i < 0 {
        panic(fmt.Sprintf("No space found in %q", b))
    }
    b = b[:i]
    n, err := strconv.ParseUint(string(b), 10, 64)
    if err != nil {
        panic(fmt.Sprintf("Failed to parse goroutine ID out of %q: %v", b, err))
    }
    return n
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

主要改造两个地方:

  1. 通过直接创建一个 byte 切片赋值给 b,替代原 http2curGoroutineID 函数中从一个 pool 池获取 byte 切片的方式;
  2. 使用 strconv.ParseUint 替代了原先的 http2parseUintBytes。

改造后,就可以直接使用 curGoroutineID 函数来获取 Goroutine 的 ID 信息了。

在 Trace 函数中添加 Goroutine ID 信息的输出:

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

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

    gid := curGoroutineID()
    fmt.Printf("g[%05d]: enter: [%s]\n", gid, name)
    return func() { fmt.Printf("g[%05d]: exit: [%s]\n", gid, name) }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在出入口输出的跟踪信息中加入了 Goroutine ID 信息,输出的 Goroutine ID 为 5 位数字,如果 ID 值不足 5 位,则左补零,这一切都是 Printf 函数的格式控制字符串“%05d”帮助实现的。

这样对齐 Goroutine ID 的位数,为的是输出信息格式的一致性更好。如果你的 Go 程序中 Goroutine 的数量超过了 5 位数可以表示的数值范围,也可以自行调整控制字符串。

下面将程序由单 Goroutine 改为多 Goroutine 并发的,验证支持多 Goroutine 的新版 Trace 函数:

// trace2/trace.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()
    }()

    A1()
    wg.Wait()
}

/*
g[00001]: enter: [main.A1]
g[00001]: enter: [main.B1]
g[00018]: enter: [main.A2]
g[00001]: enter: [main.C1]
g[00001]: enter: [main.D]
g[00001]: exit: [main.D]
g[00001]: exit: [main.C1]
g[00001]: exit: [main.B1]
g[00001]: exit: [main.A1]
g[00018]: enter: [main.B2]
g[00018]: enter: [main.C2]
g[00018]: enter: [main.D]
g[00018]: exit: [main.D]
g[00018]: exit: [main.C2]
g[00018]: exit: [main.B2]
g[00018]: exit: [main.A2]
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

新示例程序共有两个 Goroutine,main groutine 的调用链为A1 -> B1 -> C1 -> D,而另外一个 Goroutine 的函数调用链为A2 -> B2 -> C2 -> D。

新示例程序输出了带有 Goroutine ID 的出入口跟踪信息,通过 Goroutine ID 可以快速确认某一行输出是属于哪个 Goroutine 的。

由于 Go 运行时对 Goroutine 调度顺序的不确定性,各个 Goroutine 的输出还是会存在交织在一起的问题,这会给查看某个 Goroutine 的函数调用链跟踪信息带来阻碍。这里提供一个小技巧:可以将程序的输出重定向到一个本地文件中,然后通过 Goroutine ID 过滤出(可使用 grep 工具)你想查看的 groutine 的全部函数跟踪信息。

上次更新: 2022/10/06, 00:04:41
v0_2 自动获取所跟踪函数的函数名
v0_4 让输出的跟踪信息更具层次感

← v0_2 自动获取所跟踪函数的函数名 v0_4 让输出的跟踪信息更具层次感→

最近更新
01
ctr和crictl显示镜像不一致
03-13
02
alpine镜像集成常用数据库客户端
03-13
03
create-cluster
02-26
更多文章>
Theme by Vdoing | Copyright © 2015-2024 op81.com
苏ICP备18041258号-2
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式