go 草稿

这里填写标题

go 草稿

Golang CPU

1. runtime.GOMAXPROCS

// 为了看协程抢占,这里设置了一个 cpu 跑
runtime.GOMAXPROCS(1)

2. runtime.Gosched()

Gosched yields the processor, allowing other goroutines to run. It does not suspend the current goroutine, so execution resumes automatically.
这个函数的作用是让当前 goroutine 让出 CPU, 好让其它的 goroutine 获得执行的机会。同时, 当前的 goroutine 也会在未来的某个时间点继续运行。

untime.Gosched() 用于让出 CPU 时间片。这就像跑接力赛, A 跑了一会碰到代码 runtime.Gosched() 就把接力棒交给 B 了, A 歇着了, B 继续跑。
package main

import (
    "fmt"
)

func showNumber (i int) {
    fmt.Println(i)
}

func main() {

    for i := 0; i < 10; i++ {
        go showNumber(i)
    }

    fmt.Println("Haha")
}

没有打印出数字, 可以看到 goroutine 没有获得机会运行。

package main

import (
    "fmt"
    "runtime"
)

func showNumber (i int) {
    fmt.Println(i)
}

func main() {
	// 为了看协程抢占,这里设置了一个 cpu 跑
	runtime.GOMAXPROCS(1)
    
    for i := 0; i < 10; i++ {
        go showNumber(i)
    }

    runtime.Gosched()
    fmt.Println("Haha")
}

可以看到 goroutine 获得了运行机会, 打印出了数字。

1. 性能分析基础数据

性能分析基础数据的获取有三种方式:

  1. runtime/pprof 包
  2. net/http/pprof 包
  3. go test 时添加收集参数

gob, protobuf, json 在 golang 中的序列化效率对比

1. 测试代码

先上代码:

func main() {
	looptimes:=10000
	u:=User{66,"nxin","beijing"}
	gobbegintimestamp:=strconv.FormatInt(time.Now().UTC().UnixNano(), 10)
	gobbeginint,_:=strconv.Atoi(gobbegintimestamp)
	fmt.Println("gob 序列化 ==============================",gobbeginint)
	buf := new(bytes.Buffer)   // 分配内存
	enc := gob.NewEncoder(buf) // 创建基于 buf 内存的编码器
	for i:=0;i<looptimes;i++ {

		err := enc.Encode(u)       // 编码器对结构体编码
		if err != nil {
			log.Fatal(err)
		}
	}
	gobendtimestamp:=strconv.FormatInt(time.Now().UTC().UnixNano(), 10)
	gobendint,_:=strconv.Atoi(gobendtimestamp)
	fmt.Println("===================END===================",gobendint)

	jsonbegintimestamp:=strconv.FormatInt(time.Now().UTC().UnixNano(), 10)
	jsonbeginint,_:=strconv.Atoi(jsonbegintimestamp)
	fmt.Println("json 序列化 ==============================", jsonbeginint)
	for j:=0;j<looptimes;j++ {
		_, e := json.Marshal(u)
		if e!=nil{
			log.Fatal(e)
		}
	}
	jsonendtimestamp:=strconv.FormatInt(time.Now().UTC().UnixNano(), 10)
	jsonendint,_:=strconv.Atoi(jsonendtimestamp)
	fmt.Println("===================END===================",jsonendint)


	protobufbegintimestamp:=strconv.FormatInt(time.Now().UTC().UnixNano(), 10)
	protobufbeginint,_:=strconv.Atoi(protobufbegintimestamp)
	fmt.Println("protobuf 序列化 ==============================", protobufbeginint)
	hw:=&model.Helloworld{10,"wang","beijing"}
	for j:=0;j<looptimes;j++ {
		_, e := proto.Marshal(hw)
		if e!=nil{
			log.Fatal(e)
		}
	}
	protobufendtimestamp:=strconv.FormatInt(time.Now().UTC().UnixNano(), 10)
	protobufendint,_:=strconv.Atoi(protobufendtimestamp)
	fmt.Println("===================END===================",protobufendint)


	fmt.Println("json:",jsonendint-jsonbeginint)
	fmt.Println("gob:",gobendint-gobbeginint)
	fmt.Println("protobuf:",protobufendint-protobufbeginint)
}

2. 总结

总体来说 protobuf 的效率最高, gob 的效率比 json 的还要低。

3. 测试细节

尝试了 100,1000,10000,100000 次的序列化对比时间:

100 次时三者相差不大。

=====================================================================

1000 次时三者表现不稳地, 测试出来的结果:

以前一种出现的次数更多。

=====================================================================

10000 次出现的结果, protobuf 效率明显要高, 但是 json 与 gob 差别不大:

=====================================================================

100000 次出现的结果:

protobuf 还是明显优势, 但是 gob 有点落后。

综上所述: 在数据量小的时候三者差不多, 但是数据量大了以后 protobuf 会更好, 但是 gob 显得力不从心, json 表现中庸。

Golang goimportdot : 一个帮你迅速了解 golang 项目结构的工具

https://github.com/yqylovy/goimportdot/blob/master/docs/examples-cn/goimportdot_guide.md#goimportdot–%E4%B8%80%E4%B8%AA%E5%B8%AE%E4%BD%A0%E8%BF%85%E9%80%9F%E4%BA%86%E8%A7%A3-golang-%E9%A1%B9%E7%9B%AE%E7%BB%93%E6%9E%84%E7%9A%84%E5%B7%A5%E5%85%B7

golang 内存分析 / 动态追踪

1. golang pprof

当你的 golang 程序在运行过程中消耗了超出你理解的内存时, 你就需要搞明白, 到底是 程序中哪些代码导致了这些内存消耗。此时 golang 编译好的程序对你来说是个黑盒, 该 如何搞清其中的内存使用呢? 幸好 golang 已经内置了一些机制来帮助我们进行分析和追 踪。

此时, 通常我们可以采用 golang 的 pprof 来帮助我们分析 golang 进程的内存使用。

1.1. pprof 实例

通常我们采用 http api 来将 pprof 信息暴露出来以供分析, 我们可以采用 net/http/pprof 这个 package。下面是一个简单的示例:

// pprof 的 init 函数会将 pprof 里的一些 handler 注册到 http.DefaultServeMux 上
// 当不使用 http.DefaultServeMux 来提供 http api 时, 可以查阅其 init 函数, 自己注册 handler
import _ "net/http/pprof"

go func() {
    http.ListenAndServe("0.0.0.0:8080", nil)
}()

此时我们可以启动进程, 然后访问 http://localhost:8080/debug/pprof/ 可以看到一个简单的 页面, 页面上显示: 注意: 以下的全部数据, 包括 go tool pprof 采集到的数据都依赖进程中的 pprof 采样率, 默认 512kb 进行一次采样, 当我们认为数据不够细致时, 可以调节采样率 runtime.MemProfileRate, 但是采样率越低, 进程运行速度越慢。

/debug/pprof/

profiles:
0         block
136840    goroutine
902       heap
0         mutex
40        threadcreate

full goroutine stack dump

上面简单暴露出了几个内置的 Profile 统计项。例如有 136840 个 goroutine 在运行, 点击相关链接 可以看到详细信息。

当我们分析内存相关的问题时, 可以点击 heap 项, 进入 http://127.0.0.1:8080/debug/pprof/heap?debug=1 可以查看具体的显示:

heap profile: 3190: 77516056 [54762: 612664248] @ heap/1048576
1: 29081600 [1: 29081600] @ 0x89368e 0x894cd9 0x8a5a9d 0x8a9b7c 0x8af578 0x8b4441 0x8b4c6d 0x8b8504 0x8b2bc3 0x45b1c1
#    0x89368d    github.com/syndtr/goleveldb/leveldb/memdb.(*DB).Put+0x59d
#    0x894cd8    xxxxx/storage/internal/memtable.(*MemTable).Set+0x88
#    0x8a5a9c    xxxxx/storage.(*snapshotter).AppendCommitLog+0x1cc
#    0x8a9b7b    xxxxx/storage.(*store).Update+0x26b
#    0x8af577    xxxxx/config.(*config).Update+0xa7
#    0x8b4440    xxxxx/naming.(*naming).update+0x120
#    0x8b4c6c    xxxxx/naming.(*naming).instanceTimeout+0x27c
#    0x8b8503    xxxxx/naming.(*naming).(xxxxx/naming.instanceTimeout)-fm+0x63

......

# runtime.MemStats
# Alloc = 2463648064
# TotalAlloc = 31707239480
# Sys = 4831318840
# Lookups = 2690464
# Mallocs = 274619648
# Frees = 262711312
# HeapAlloc = 2463648064
# HeapSys = 3877830656
# HeapIdle = 854990848
# HeapInuse = 3022839808
# HeapReleased = 0
# HeapObjects = 11908336
# Stack = 655949824 / 655949824
# MSpan = 63329432 / 72040448
# MCache = 38400 / 49152
# BuckHashSys = 1706593
# GCSys = 170819584
# OtherSys = 52922583
# NextGC = 3570699312
# PauseNs = [1052815 217503 208124 233034 1146462 456882 1098525 530706 551702 419372 768322 596273 387826 455807 563621 587849 416204 599143 572823 488681 701731 656358 2476770 12141392 5827253 3508261 1715582 1295487 908563 788435 718700 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
# NumGC = 31
# DebugGC = false

其中显示的内容会比较多, 但是主体分为 2 个部分: 第一个部分打印为通过 runtime.MemProfile() 获取的 runtime.MemProfileRecord 记录。 其含义为:

heap profile: 3190(inused objects): 77516056(inused bytes) [54762(alloc objects): 612664248(alloc bytes)] @ heap/1048576(2*MemProfileRate)
1: 29081600 [1: 29081600] (前面 4 个数跟第一行的一样, 此行以后是每次记录的, 后面的地址是记录中的栈指针)@ 0x89368e 0x894cd9 0x8a5a9d 0x8a9b7c 0x8af578 0x8b4441 0x8b4c6d 0x8b8504 0x8b2bc3 0x45b1c1
#    0x89368d    github.com/syndtr/goleveldb/leveldb/memdb.(*DB).Put+0x59d 栈信息

第二部分就比较好理解, 打印的是通过 runtime.ReadMemStats() 读取的 runtime.MemStats 信息。 我们可以重点关注一下

  • Sys 进程从系统获得的内存空间, 虚拟地址空间。
  • HeapAlloc 进程堆内存分配使用的空间, 通常是用户 new 出来的堆对象, 包含未被 gc 掉的。
  • HeapSys 进程从系统获得的堆内存, 因为 golang 底层使用 TCmalloc 机制, 会缓存一部分堆内存, 虚拟地址空间。
  • PauseNs 记录每次 gc 暂停的时间 (纳秒), 最多记录 256 个最新记录。
  • NumGC 记录 gc 发生的次数。

相信, 对 pprof 不了解的用户看了以上内容, 很难获得更多的有用信息。因此我们需要引用更多工具来帮助 我们更加简单的解读 pprof 内容。

2. go tool

我们可以采用 go tool pprof -inuse_space http://127.0.0.1:8080/debug/pprof/heap 命令连接到进程中 查看正在使用的一些内存相关信息, 此时我们得到一个可以交互的命令行。

我们可以看数据 top10 来查看正在使用的对象较多的 10 个函数入口。通常用来检测有没有不符合预期的内存对象引用。

(pprof) top10
1355.47MB of 1436.26MB total (94.38%)
Dropped 371 nodes (cum <= 7.18MB)
Showing top 10 nodes out of 61 (cum >= 23.50MB)
      flat  flat%   sum%        cum   cum%
  512.96MB 35.71% 35.71%   512.96MB 35.71%  net/http.newBufioWriterSize
  503.93MB 35.09% 70.80%   503.93MB 35.09%  net/http.newBufioReader
  113.04MB  7.87% 78.67%   113.04MB  7.87%  runtime.rawstringtmp
   55.02MB  3.83% 82.50%    55.02MB  3.83%  runtime.malg
   45.01MB  3.13% 85.64%    45.01MB  3.13%  xxxxx/storage.(*Node).clone
   26.50MB  1.85% 87.48%    52.50MB  3.66%  context.WithCancel
   25.50MB  1.78% 89.26%    83.58MB  5.82%  runtime.systemstack
   25.01MB  1.74% 91.00%    58.51MB  4.07%  net/http.readRequest
      25MB  1.74% 92.74%    29.03MB  2.02%  runtime.mapassign
   23.50MB  1.64% 94.38%    23.50MB  1.64%  net/http.(*Server).newConn

然后我们在用 go tool pprof -alloc_space http://127.0.0.1:8080/debug/pprof/heap 命令链接程序来查看内存对象分配的相关情况。然后输入 top 来查看累积分配内存较多的一些函数调用:

(pprof) top
523.38GB of 650.90GB total (80.41%)
Dropped 342 nodes (cum <= 3.25GB)
Showing top 10 nodes out of 106 (cum >= 28.02GB)
      flat  flat%   sum%        cum   cum%
  147.59GB 22.68% 22.68%   147.59GB 22.68%  runtime.rawstringtmp
  129.23GB 19.85% 42.53%   129.24GB 19.86%  runtime.mapassign
   48.23GB  7.41% 49.94%    48.23GB  7.41%  bytes.makeSlice
   46.25GB  7.11% 57.05%    71.06GB 10.92%  encoding/json.Unmarshal
   31.41GB  4.83% 61.87%   113.86GB 17.49%  net/http.readRequest
   30.55GB  4.69% 66.57%   171.20GB 26.30%  net/http.(*conn).readRequest
   22.95GB  3.53% 70.09%    22.95GB  3.53%  net/url.parse
   22.70GB  3.49% 73.58%    22.70GB  3.49%  runtime.stringtoslicebyte
   22.70GB  3.49% 77.07%    22.70GB  3.49%  runtime.makemap
   21.75GB  3.34% 80.41%    28.02GB  4.31%  context.WithCancel

可以看出 string-[]byte 相互转换、分配 map、bytes.makeSlice、encoding/json.Unmarshal 等调用累积分配的内存较多。 此时我们就可以 review 代码, 如何减少这些相关的调用, 或者优化相关代码逻辑。

当我们不明确这些调用时是被哪些函数引起的时, 我们可以输入 top -cum 来查找, -cum 的意思就是, 将函数调用关系中的数据进行累积, 比如 A 函数调用的 B 函数, 则 B 函数中的内存分配量也会累积到 A 上面, 这样就可以很容易的找出调用链。

(pprof) top20 -cum
322890.40MB of 666518.53MB total (48.44%)
Dropped 342 nodes (cum <= 3332.59MB)
Showing top 20 nodes out of 106 (cum >= 122316.23MB)
      flat  flat%   sum%        cum   cum%
         0     0%     0% 643525.16MB 96.55%  runtime.goexit
 2184.63MB  0.33%  0.33% 620745.26MB 93.13%  net/http.(*conn).serve
         0     0%  0.33% 435300.50MB 65.31%  xxxxx/api/server.(*HTTPServer).ServeHTTP
 5865.22MB  0.88%  1.21% 435300.50MB 65.31%  xxxxx/api/server/router.(*httpRouter).ServeHTTP
         0     0%  1.21% 433121.39MB 64.98%  net/http.serverHandler.ServeHTTP
         0     0%  1.21% 430456.29MB 64.58%  xxxxx/api/server/filter.(*chain).Next
   43.50MB 0.0065%  1.21% 429469.71MB 64.43%  xxxxx/api/server/filter.TransURLTov1
         0     0%  1.21% 346440.39MB 51.98%  xxxxx/api/server/filter.Role30x
31283.56MB  4.69%  5.91% 175309.48MB 26.30%  net/http.(*conn).readRequest
         0     0%  5.91% 153589.85MB 23.04%  github.com/julienschmidt/httprouter.(*Router).ServeHTTP
         0     0%  5.91% 153589.85MB 23.04%  github.com/julienschmidt/httprouter.(*Router).ServeHTTP-fm
         0     0%  5.91% 153540.85MB 23.04%  xxxxx/api/server/router.(*httpRouter).Register.func1
       2MB 0.0003%  5.91% 153117.78MB 22.97%  xxxxx/api/server/filter.Validate
151134.52MB 22.68% 28.58% 151135.02MB 22.68%  runtime.rawstringtmp
         0     0% 28.58% 150714.90MB 22.61%  xxxxx/api/server/router/naming/v1.(*serviceRouter).(git.intra.weibo.com/platform/vintage/api/server/router/naming/v1.service)-fm
         0     0% 28.58% 150714.90MB 22.61%  xxxxx/api/server/router/naming/v1.(*serviceRouter).service
         0     0% 28.58% 141200.76MB 21.18%  net/http.Redirect
132334.96MB 19.85% 48.44% 132342.95MB 19.86%  runtime.mapassign
      42MB 0.0063% 48.44% 125834.16MB 18.88%  xxxxx/api/server/router/naming/v1.heartbeat
         0     0% 48.44% 122316.23MB 18.35%  xxxxxx/config.(*config).Lookup

如上所示, 我们就很容易的查找到这些函数是被哪些函数调用的。

根据代码的调用关系, filter.TransURLTov1 会调用 filter.Role30x, 但是他们之间的 cum% 差值有 12.45%, 因此 我们可以得知 filter.TransURLTov1 内部自己直接分配的内存量达到了整个进程分配内存总量的 12.45%, 这可是一个 值得大大优化的地方。

然后我们可以输入命令 web, 其会给我们的浏览器弹出一个 .svg 图片, 其会把这些累积关系画成一个拓扑图, 提供给 我们。或者直接执行 go tool pprof -alloc_space -cum -svg http://127.0.0.1:8080/debug/pprof/heap > heap.svg 来生成 heap.svg 图片。

下面我们取一个图片中的一个片段进行分析:

每一个方块为 pprof 记录的一个函数调用栈, 指向方块的箭头上的数字是记录的该栈累积分配的内存向, 从方块指出的 箭头上的数字为该函数调用的其他函数累积分配的内存。他们之间的差值可以简单理解为本函数除调用其他函数外, 自身分配的。方块内部的数字也体现了这一点, 其数字为:(自身分配的内存 of 该函数累积分配的内存)。

2.1. --inuse/alloc_space --inuse/alloc_objects 区别

通常情况下:

  • --inuse_space 来分析程序常驻内存的占用情况;
  • --alloc_objects 来分析内存的临时分配情况, 可以提高程序的运行速度。

3. go-torch

除了直接使用 go tool pprof 外, 我们还可以使用更加直观了火焰图 。因此我们可以直接使用 go-torch 来生成 golang 程序的火焰图, 该工具也直接依赖 pprof/go tool pprof 等。该工具的相关安装请看该项目的介绍。该软件的 a4daa2b 以后版本才支持内存的 profiling

我们可以使用

go-torch -alloc_space http://127.0.0.1:8080/debug/pprof/heap --colors=mem
go-torch -inuse_space http://127.0.0.1:8080/debug/pprof/heap --colors=mem

注意:-alloc_space/-inuse_space 参数与 -u/-b 等参数有冲突, 使用了 -alloc_space/-inuse_space 后请将 pprof 的 资源直接追加在参数后面, 而不要使用 -u/-b 参数去指定, 这与 go-torch 的参数解析问题有关, 看过其源码后既能明白。 同时还要注意, 分析内存的 URL 一定是 heap 结尾的, 因为默认路径是 profile 的, 其用来分析 cpu 相关问题。

通过上面 2 个命令, 我们就可以得到 alloc_space/inuse_space 含义的 2 个火焰图, 例如 alloc_space.svg/inuse_space.svg。 我们可以使用浏览器观察这 2 张图, 这张图, 就像一个山脉的截面图, 从下而上是每个函数的调用栈, 因此山的高度跟函数 调用的深度正相关, 而山的宽度跟使用 / 分配内存的数量成正比。我们只需要留意那些宽而平的山顶, 这些部分通常是我们 需要优化的地方。

4. 优化建议

Debugging performance issues in Go programs 提供了一些常用的优化建议:

4.1. 将多个小对象合并成一个大的对象

4.2. 减少不必要的指针间接引用, 多使用 copy 引用

例如使用 bytes.Buffer 代替 *bytes.Buffer, 因为使用指针时, 会分配 2 个对象来完成引用。

4.3. 局部变量逃逸时, 将其聚合起来

这一点理论跟 1 相同, 核心在于减少 object 的分配, 减少 gc 的压力。 例如, 以下代码

for k, v := range m {
	k, v := k, v   // copy for capturing by the goroutine
	go func() {
		// use k and v
	}()
}

可以修改为:

for k, v := range m {
	x := struct{ k, v string }{k, v}   // copy for capturing by the goroutine
	go func() {
		// use x.k and x.v
	}()
}

修改后, 逃逸的对象变为了 x, 将 k, v2 个对象减少为 1 个对象。

4.4. []byte 的预分配

当我们比较清楚的知道 []byte 会到底使用多少字节, 我们就可以采用一个数组来预分配这段内存。 例如:

type X struct {
    buf      []byte
    bufArray [16]byte // Buf usually does not grow beyond 16 bytes.
}

func MakeX() *X {
    x := &X{}
    // Preinitialize buf with the backing array.
    x.buf = x.bufArray[:0]
    return x
}

4.5. 尽可能使用字节数少的类型

当我们的一些 const 或者计数字段不需要太大的字节数时, 我们通常可以将其声明为 int8 类型。

4.6. 减少不必要的指针引用

当一个对象不包含任何指针 (注意: strings, slices, maps 和 chans 包含隐含的指针), 时, 对 gc 的扫描影响很小。 比如, 1GB byte 的 slice 事实上只包含有限的几个 object, 不会影响垃圾收集时间。 因此, 我们可以尽可能的减少指针的引用。

4.7. 使用 sync.Pool 来缓存常用的对象

Golang pprof

1. pprof 包

1.1. runtime/pprof 包的使用

适用于运行一次的数据采集。生成的文件需要命令行进行调试, 非常不方便, 不推荐。

针对于应用程序, 通过命令行的启动参数来生成 prof 文件, 再使用 go tool pprof 工具进行分析。

package main
 
import (
	"flag"
	"log"
	"os"
	"runtime/pprof"
)
 
var cpuprofile = flag.String("cpuprofile", "","write cpu profile to file")
 
func main() {
	flag.Parse()
	if *cpuprofile != "" {
		f, err := os.Create(*cpuprofile)
		if err != nil {
			log.Fatal(err)
		}
		err = pprof.StartCPUProfile(f)
		if err != nil {
			log.Fatal(err)
		}
		defer pprof.StopCPUProfile()
	}
 
	Add("test")
}
 
func Add(str string) string {
	data := []byte(str)
	sData := string(data)
	var sum = 0
	for i := 0; i < 10000; i++ {
		sum += i
	}
	return sData
}

1.2. net/http/pprof 包的使用

net/http/pprofruntime/pprof 进行了封装, 并在 http 端口上暴露出来, 入口为 IP:PORT/debug/pprof/

  1. 若应用为 web 服务器, 只需引入包即可 _ "net/http/pprof", 会自动注册路由到 /debug/pprof/
  2. 若为服务时程, 可开启一个 goroutine 开启端口并监听, 如
go func(){log.Println(http.ListenAndServe(":8080",nil))}()

2. 创建火焰图

2.1. 安装 go-torch

go get github.com/uber/go-torch

2.2. 安装

cd $WORK_PATH && git clone https://github.com/brendangregg/FlameGraph.git
export PATH=$PATH:$WORK_PATH/FlameGraph

2.3. 安装 graphviz

https://graphviz.org/

yum install graphviz

2.4. 使用 pprof

package main

import (
    "net/http"
    _ "net/http/pprof"
)

func main() {
    // 服务端启动一个协程, 支持 pprof 的 handler
    // 导入 pprof 的包, 自动包含一些 handler
	// 项目加入如下代码
    go func() {
        http.ListenAndServe("0.0.0.0:8888", nil)
    }()
	//other code
}

2.5. ab 压测

ab -n 19999 -c 20 http://xxxxxxxxxxxx
-n 总数
-c 同时并发请求数

2.6. pprof 使用

2.6.1. 监听

go tool pprof http://localhost:port/debug/pprof/profile

2.6.2. 操作

进入 30 秒的 profile 收集时间, 在这段时间内请求服务, 尽量让 cpu 占用性能产生数据。

2.6.3. pprof 命令

top
在默认情况下, top 命令会输出以本地取样计数为顺序的列表。我们可以把这个列表叫做本地取样计数排名列表。
web
与 gv 命令类似, web 命令也会用图形化的方式来显示概要文件。但不同的是, web 命令是在一个 Web 浏览器中显示它。

2.7. 火焰图工具使用

2.7.1. 监听

//cpu 火焰图
go-torch -u http://ip:port/debug/pprof/ -p > profile-cpu.svg
// 内存火焰图
go-torch -u http://ip:port/debug/pprof/heap -p > profile-heap.svg

2.7.2. 操作

针对测试服务端, 进行操作, 上述步骤默认监听 30s, 即 30s 后可以生成相关图像

3. 更加易用的 pprof

golang 自带的 prof 包是 runtime/pprof, 这个是低级别的, 需要你手动做一些设置等等周边工作, 不利于我们快速上手, 利用 pprof 帮助我们解决实际的问题。这里推荐 davecheney 封装的 pprof, 它可以 1 行代码, 让你用上 pprof, 专心解决自己的代码问题, 下载:

go get github.com/pkg/profile

3.1. 修改 main 函数

只需要为 hi.go 增加这一行, defer profile.Start().Stop(), 程序运行时, 默认就会记录 cpu 数据:

上面的命令生成的 pprof 文件是在临时目录下的, 可以指定目录:

defer profile.Start(profile.ProfilePath(utils.GetCurrentDirectory())).Stop() // cpu
defer profile.Start(profile.ProfilePath(utils.GetCurrentDirectory()), profile.MemProfile).Stop() // inuse_space, mem
package main

import (
    "fmt"
    "github.com/pkg/profile"
)

func main() {
	defer profile.Start(profile.ProfilePath(utils.GetCurrentDirectory()), profile.MemProfile).Stop()

	// 为了看协程抢占, 这里设置了一个 cpu 跑
	runtime.GOMAXPROCS(1)

	f, _ := os.Create("trace.dat")
	defer f.Close()

	_ = trace.Start(f)
	defer trace.Stop()

	ctx, task := trace.NewTask(context.Background(), "sumTask")
	defer task.End()

	var wg sync.WaitGroup
	wg.Add(10)
	for i := 0; i < 10; i++ {
		// 启动 10 个协程, 只是做一个累加运算
		go func(region string) {
			defer wg.Done()

			// 标记 region
			trace.WithRegion(ctx, region, func() {
				sl := makeSlice()
				sum := sumSlice(sl)

				fmt.Println(region, sum)
			})
		}(fmt.Sprintf("region_%02d", i))
	}
	wg.Wait()
}

func makeSlice() []int {
	sl := make([]int, 10000000)
	for idx := range sl {
		sl[idx] = idx
	}
	return sl
}

func sumSlice(sl []int) int {
	sum := 0
	for _, x := range sl {
		sum += x
	}
	return sum
}
go build hi.go
./hi

3.2. 可视化

可视化有多种方式, 可以转换为 text、pdf、svg 等等。text 命令是

go tool pprof --text ./yourbinary ./cpu.pprof

还有 pdf 这种效果更好:

go tool pprof --pdf ./yourbinary ./cpu.pprof > cpu.pdf

至此, 已经搞定 cpu pprof 了。

3.3. 轻松获取内存 pprof

如果你掌握了 cpu pprof, mem pprof 轻而易举就能拿下, 只需要改 1 行代码:

defer profile.Start(profile.MemProfile).Stop()
go tool pprof -pdf ./hi /var/folders/5g/rz16gqtx3nsdfs7k8sb80jth0000gn/T/profile986580758/mem.pprof > mem.pdf

go 语言高级单元测试

https://juejin.cn/post/6844903853528186894

go 语言怎么写 test 测试

1. Go 怎么写 test 测试用例

1.1. Go 怎么写 test 测试用例

开发程序其中很重要的一点是测试, 我们如何保证代码的质量, 如何保证每个函数是可运行, 运行结果是正确的, 又如何保证写出来的代码性能是好的, 我们知道单元测试的重点在于发现程序设计或实现的逻辑错误, 使问题及早暴露, 便于问题的定位解决, 而性能测试的重点在于发现程序设计上的一些问题, 让线上的程序能够在高并发的情况下还能保持稳定。本小节将带着这一连串的问题来讲解 Go 语言中如何来实现单元测试和性能测试。

Go 语言中自带有一个轻量级的测试框架 testing 和自带的 go test 命令来实现单元测试和性能测试, testing 框架和其他语言中的测试框架类似, 你可以基于这个框架写针对相应函数的测试用例, 也可以基于该框架写相应的压力测试用例, 那么接下来让我们一一来看一下怎么写。

另外建议安装 gotests 插件自动生成测试代码:

go get -u -v github.com/cweill/gotests/...

1.2. 如何编写测试用例

由于 go test 命令只能在一个相应的目录下执行所有文件, 所以我们接下来新建一个项目目录 gotest, 这样我们所有的代码和测试代码都在这个目录下。

接下来我们在该目录下面创建两个文件: gotest.gogotest_test.go

  1. gotest.go: 这个文件在我们创建的 gotest 包里面, 里面有一个函数实现了除法运算:
package gotest

import (
    "errors"
)

func Division(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("除数不能为 0")
    }

    return a / b, nil
}
  1. gotest_test.go: 这是我们的单元测试文件, 但是记住下面的这些原则:
  • 文件名必须是 _test.go 结尾的 (文件名必须是 *_test.go 的类型, * 代表要测试的文件名), 这样在执行 go test 的时候才会执行到相应的代码
  • 你必须 import testing 这个包
  • 所有的测试用例函数必须是 Test 开头 (函数名必须以 Test 开头如: TestXxxTest_xxx)
  • 测试用例会按照源代码中写的顺序依次执行
  • 测试函数 TestXxx() 的参数是 testing.T, 我们可以使用该类型来记录错误或者是测试状态
  • 测试格式: func TestXxx (t *testing.T),Xxx 部分可以为任意的字母数字的组合, 但是首字母不能是小写字母 [a-z], 例如 Testintdiv 是错误的函数名。
  • 函数中通过调用 testing.TError, Errorf, FailNow, Fatal, FatalIf 方法, 说明测试不通过, 调用 Log 方法用来记录测试的信息。

下面是我们的测试用例的代码:

package gotest

import (
    "testing"
)

func Test_Division_1(t *testing.T) {
    if i, e := Division(6, 2); i != 3 || e != nil { //try a unit test on function
        t.Error("除法函数测试没通过") // 如果不是如预期的那么就报错
    } else {
        t.Log("第一个测试通过了") // 记录一些你期望记录的信息
    }
}

func Test_Division_2(t *testing.T) {
    t.Error("就是不通过")
}

我们在项目目录下面执行 go test, 就会显示如下信息:

--- FAIL: Test_Division_2 (0.00 seconds)
    gotest_test.go:16: 就是不通过
FAIL
exit status 1
FAIL    gotest  0.013s

从这个结果显示测试没有通过, 因为在第二个测试函数中我们写死了测试不通过的代码 t.Error, 那么我们的第一个函数执行的情况怎么样呢? 默认情况下执行 go test 是不会显示测试通过的信息的, 我们需要带上参数 go test -v, 这样就会显示如下信息:

=== RUN Test_Division_1
--- PASS: Test_Division_1 (0.00 seconds)
    gotest_test.go:11: 第一个测试通过了
=== RUN Test_Division_2
--- FAIL: Test_Division_2 (0.00 seconds)
    gotest_test.go:16: 就是不通过
FAIL
exit status 1
FAIL    gotest  0.012s

上面的输出详细的展示了这个测试的过程, 我们看到测试函数 1Test_Division_1 测试通过, 而测试函数 2Test_Division_2 测试失败了, 最后得出结论测试不通过。接下来我们把测试函数 2 修改成如下代码:

func Test_Division_2(t *testing.T) {
    if _, e := Division(6, 0); e == nil { //try a unit test on function
        t.Error("Division did not work as expected.") // 如果不是如预期的那么就报错
    } else {
        t.Log("one test passed.", e) // 记录一些你期望记录的信息
    }
}   

然后我们执行 go test -v, 就显示如下信息, 测试通过了:

=== RUN Test_Division_1
--- PASS: Test_Division_1 (0.00 seconds)
    gotest_test.go:11: 第一个测试通过了
=== RUN Test_Division_2
--- PASS: Test_Division_2 (0.00 seconds)
    gotest_test.go:20: one test passed. 除数不能为 0
PASS
ok      gotest  0.013s

1.3. 如何编写压力测试

压力测试用来检测函数 (方法) 的性能, 和编写单元功能测试的方法类似, 此处不再赘述, 但需要注意以下几点:

  • 创建 benchmark 性能测试用例文件 *_b_test.go(文件名使用 *_b_test.go 的类型 (也可直接放在 test 文件中), * 代表要测试的文件名, 函数名必须以 Benchmark 开头如: BenchmarkXxxBenchmark_xxx), 压力测试用例必须遵循如下格式, 其中 xxx 可以是任意字母数字的组合, 但是 Xxx 首字母不能是小写字母。
func BenchmarkXXX(b *testing.B) { ... }
  • go test 不会默认执行压力测试的函数, 如果要执行压力测试需要带上参数 -test.bench, 语法:-test.bench="test_name_regex", 例如 go test -test.bench=".*" 表示测试全部的压力测试函数
  • 在压力测试用例中, 请记得在循环体内使用 testing.B.N, 以使测试可以正常的运行
  • 文件名也必须以 _test.go 结尾

下面我们新建一个压力测试文件 webbench_test.go, 代码如下所示:

package gotest

import (
    "testing"
)

func Benchmark_Division(b *testing.B) {
    for i := 0; i < b.N; i++ { //use b.N for looping 
        Division(4, 5)
    }
}

func Benchmark_TimeConsumingFunction(b *testing.B) {
    b.StopTimer() // 调用该函数停止压力测试的时间计数

    // 做一些初始化的工作, 例如读取文件数据, 数据库连接之类的,
    // 这样这些时间不影响我们测试函数本身的性能

    b.StartTimer() // 重新开始时间
    for i := 0; i < b.N; i++ {
        Division(4, 5)
    }
}

我们执行命令 go test -file webbench_test.go -test.bench=".*", 可以看到如下结果:

PASS
Benchmark_Division  500000000       7.76 ns/op
Benchmark_TimeConsumingFunction 500000000      7.80 ns/op
ok      gotest  9.364s  

上面的结果显示我们没有执行任何 TestXXX 的单元测试函数, 显示的结果只执行了压力测试函数;

第一条显示了 Benchmark_Division 执行了 500000000 次, 每次的执行平均时间是 7.76 纳秒;

第二条显示了 Benchmark_TimeConsumingFunction 执行了 500000000, 每次的平均执行时间是 7.80 纳秒;

最后一条显示总共的执行时间。

1.4. 用性能测试生成 CPU 状态图

使用命令:

go test -bench=".*" -cpuprofile=cpu.prof -c

cpuprofile 是表示生成的 cpu profile 文件

-c 是生成可执行的二进制文件, 这个是生成状态图必须的, 它会在本目录下生成可执行文件 *.test

然后使用 go tool pprof 工具

go tool pprof *.test cpu.prof

调用 web(需要安装 graphviz) 来生成 svg 文件, 生成后使用浏览器查看 svg 文件, 具体图例看参考: golang test 测试实例

1.5. 小结

通过上面对单元测试和压力测试的学习, 我们可以看到 testing 包很轻量, 编写单元测试和压力测试用例非常简单, 配合内置的 go test 命令就可以非常方便的进行测试, 这样在我们每次修改完代码, 执行一下 go test 就可以简单的完成回归测试了。

2. golang test 测试实例

本文的目的是对 mymysql 进行单元测试和性能测试

2.1. go get

go get github.com/ziutek/mymysql/thrsafe

2.2. 在 mysql 建表和初始化数据 (db 是 test)

drop table if exists admin;
CREATE TABLE `admin` (
    `adminid` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
    `username` varchar(20) NOT NULL DEFAULT ''COMMENT'后台用户名',
    `password` char(32) NOT NULL DEFAULT ''COMMENT'密码, md5 存',
    PRIMARY KEY(`adminid`)
)
COMMENT='后台用户信息表'
COLLATE='utf8_general_ci'
ENGINE=InnoDB;
 
insert into admin set adminid=1, username='admin', password='21232f297a57a5a743894a0e4a801fc3';

2.3. gopath 下建立 mymysql

2.3.1. mymysql.go 的代码

package mymysql
import(
     "log"
     "github.com/ziutek/mymysql/mysql"
     _ "github.com/ziutek/mymysql/native"
)
func getAdmin(adminid int) (string, string){
     db := mysql.New("tcp", "","127.0.0.1:3306","root","password","test")
     err := db.Connect()
     if err != nil {
          panic(err)
     }
     rows, res, err := db.Query("select * from admin where adminid=%d", adminid)
     if err != nil {
          panic(err)
     }
     if len(rows) < 1 {
          log.Panic("rows error")
     }
     row := rows[0]
     first := res.Map("username")
     second := res.Map("password")
     username, password := row.Str(first), row.Str(second)
     return username, password
}

很好理解, 根据 adminid 获取用户名和密码

2.3.2. mymysql_test.go 的代码

package mymysql
import(
     "testing"
)
func Test_getAdmin(t *testing.T) {
    username, _ := getAdmin(1)
    if (username != "admin") {
         t.Error("getAdmin get data error")
    }
}

这里做单元测试的, 测试 getAdmin 函数. 写到这里你就可以在命令行中运行 go test 了.

这里有个 -v 参数, 如果不加这个参数的话, 只会显示错误的测试用例, 否则就显示所有的测试用例 (成功 + 错误)

2.3.3. 下面做性能测试

mymysql_b_test.go 的代码:

package mymysql
import (
     "testing"
)
func Benchmark_getAdmin(b *testing.B){
     for i := 0; i < b.N; i++ { //use b.N for looping
            getAdmin(1)
    }
}

然后运行 go test -v -bench=".*"

这里的 -bench 是可以指定运行的用例

返回结果表示这个测试用例在 1s 中内运行了 2000 次, 每次调用大约用了 891898ns

2.4. 用性能测试生成 CPU 状态图

使用命令:

go test -bench=".*" -cpuprofile=cpu.prof -c

cpuprofile 是表示生成的 cpu profile 文件

-c 是生成可执行的二进制文件, 这个是生成状态图必须的, 它会在本目录下生成可执行文件 mymysql.test

然后使用 go tool pprof 工具

go tool pprof mymysql.test cpu.prof

调用 web(需要安装 graphviz)

显示 svg 文件已经生成了

go Test 写法注意事项

1. go Test 写法注意事项

1.1. 每一个 test 文件须 import 一个 testing

1.2. test 文件必须以 Test 开头并且符合 TestXXX 形式,否则不会被执行

1.3. test case 的入参为 t *testing.T 或 b *testing.B

func TestPrint1(t *testing.T) {

1.4. t.Errorf 为打印错误信息,并且当前 test case 会被中断跳过

func TestPrint1(t *testing.T) {
	if true {
		t.Errorf("11")
	}
}

1.5. t.SkipNow() 为跳过当前 test,并且直接按 PASS 处理继续下一个 test

func TestPrint3(t *testing.T) {
	t.SkipNow()
	if true { // 走不到这里。
		t.Errorf("33")
	}
}

1.6. Go 的 test 不会保证多个 TestXXX 是顺序执行,但是通常会按顺序执行,使用 t.Run 来执行 subtests 可以做到控制 test 输出以及 test 的顺序

func testPrint1(t *testing.T) {
	if false {
		t.Errorf("11")
	}
}

func testPrint2(t *testing.T) {
	if false {
		t.Errorf("22")
	}
}

func testPrint3(t *testing.T) {
	t.SkipNow()
	if false {
		t.Errorf("33")
	}
}

func TestPrint(t *testing.T) {
	t.Run("TestPrint1", testPrint1)
	t.Run("TestPrint2", testPrint2)
	t.Run("TestPrint3", testPrint3)
}

1.7. 使用 TestMain 作为初始化 test,并且使用 m.Run() 来调用其它 tests 可以完成一些需要初始化操作的 testing,比如数据库连接、文件打开、REST 服务器等。

func TestMain(m *testing.M) {
	log.Println("test main entry")
	m.Run() // 没有这个,就不会跑这个文件里面的 tests
}

2. go test 测试单个文件和测试单个函数

  1. 测试单个文件, 一定要带上被测试的原文件
go test -v  wechat_test.go wechat.go 
  1. 测试单个方法
go test -v -test.run TestRefreshAccessToken

3. Misc

go test -v -test.run TestSyncFixConfirmedToNewDB 测试当前目录下的 TestSyncFixConfirmedToNewDB 函数,会自动查找,不用指定 .go 文件

BenchmarkSyncFixConfirmedToNewDB 测试 benchmark 需要 Benchmark 打头 并且类型是 b *testing.B

仅 benchmark 单个函数 go test -bench=BenchmarkSyncFixConfirmedToNewDB -run=^a

1. 什么时候 go tool trace 不合适?

当然, go tool trace 不能解决一切问题。 如果您想跟踪运行缓慢的函数, 或者找到大部分 CPU 时间花费在哪里, 这个工具就是不合适的。 为此, 您应该使用 go tool pprof, 它可以显示在每个函数中花费的 CPU 时间的百分比。 go tool trace 更适合于找出程序在一段时间内正在做什么, 而不是总体上的开销。 此外, 还有 “view trace” 链接提供的其他可视化功能, 这些对于诊断争用问题特别有用。 了解您的程序在理论上的表现(使用老式 Big-O 分析)也是无可替代的。

2. Go Tool Trace

3. Ref

https://making.pusher.com/go-tool-trace/

Go unsafe 包之内存布局

unsafe, 顾名思义, 是不安全的, Go 定义这个包名也是这个意思, 让我们尽可能的不要使用它, 如果你使用它, 看到了这个名字, 也会想到尽可能的不要使用它, 或者更小心的使用它。

虽然这个包不安全, 但是它也有它的优势, 那就是可以绕过 Go 的内存安全机制, 直接对内存进行读写, 所以有时候因为性能的需要, 会冒一些风险使用该包, 对内存进行操作。

1. Sizeof 函数

Sizeof 函数可以返回一个类型所占用的内存大小, 这个大小只有类型有关, 和类型对应的变量存储的内容大小无关, 比如 bool 型占用一个字节、int8 也占用一个字节。

func main() {
	fmt.Println(unsafe.Sizeof(true))
	fmt.Println(unsafe.Sizeof(int8(0)))
	fmt.Println(unsafe.Sizeof(int16(10)))
	fmt.Println(unsafe.Sizeof(int32(10000000)))
	fmt.Println(unsafe.Sizeof(int64(10000000000000)))
	fmt.Println(unsafe.Sizeof(int(10000000000000000)))
}

对于整型来说, 占用的字节数意味着这个类型存储数字范围的大小, 比如 int8 占用一个字节, 也就是 8bit, 所以它可以存储的大小范围是 - 128~~127, 也就是−2^(n-1) 到 2^(n-1)−1, n 表示 bit, int8 表示 8bit, int16 表示 16bit, 其他以此类推。

对于和平台有关的 int 类型, 这个要看平台是 32 位还是 64 位, 会取最大的。比如我自己测试, 以上输出, 会发现 int 和 int64 的大小是一样的, 因为我的是 64 位平台的电脑。

func Sizeof(x ArbitraryType) uintptr

以上是 Sizeof 的函数定义, 它接收一个 ArbitraryType 类型的参数, 返回一个 uintptr 类型的值。这里的 ArbitraryType 不用关心, 他只是一个占位符, 为了文档的考虑导出了该类型, 但是一般不会使用它, 我们只需要知道它表示任何类型, 也就是我们这个函数可以接收任意类型的数据。

// ArbitraryType is here for the purposes of documentation only and is not actually
// part of the unsafe package. It represents the type of an arbitrary Go expression.
type ArbitraryType int

2. Alignof 函数

Alignof 返回一个类型的对齐值, 也可以叫做对齐系数或者对齐倍数。对齐值是一个和内存对齐有关的值, 合理的内存对齐可以提高内存读写的性能, 关于内存对齐的知识可以参考相关文档, 这里不展开介绍。

func main() {
	var b bool
	var i8 int8
	var i16 int16
	var i64 int64

	var f32 float32

	var s string

	var m map[string]string

	var p *int32

	fmt.Println(unsafe.Alignof(b))
	fmt.Println(unsafe.Alignof(i8))
	fmt.Println(unsafe.Alignof(i16))
	fmt.Println(unsafe.Alignof(i64))
	fmt.Println(unsafe.Alignof(f32))
	fmt.Println(unsafe.Alignof(s))
	fmt.Println(unsafe.Alignof(m))
	fmt.Println(unsafe.Alignof(p))
}

从以上例子的输出, 可以看到, 对齐值一般是 2^n, 最大不会超过 8(原因见下面的内存对齐规则)。Alignof 的函数定义和 Sizeof 基本上一样。这里需要注意的是每个人的电脑运行的结果可能不一样, 大同小异。

func Alignof(x ArbitraryType) uintptr

此外, 获取对齐值还可以使用反射包的函数, 也就是说: unsafe.Alignof(x) 等价于 reflect.TypeOf(x).Align()。

3. Offsetof 函数

Offsetof 函数只适用于 struct 结构体中的字段相对于结构体的内存位置偏移量。结构体的第一个字段的偏移量都是 0.

func main() {
	var u1 user1

	fmt.Println(unsafe.Offsetof(u1.b))
	fmt.Println(unsafe.Offsetof(u1.i))
	fmt.Println(unsafe.Offsetof(u1.j))
}

type user1 struct {
	b byte
	i int32
	j int64
}

字段的偏移量, 就是该字段在 struct 结构体内存布局中的起始位置 (内存位置索引从 0 开始)。根据字段的偏移量, 我们可以定位结构体的字段, 进而可以读写该结构体的字段, 哪怕他们是私有的, 黑客的感觉有没有。偏移量的概念, 我们会在下一小结详细介绍。

此外, unsafe.Offsetof(u1.i) 等价于 reflect.TypeOf(u1).Field(i).Offset

4. 有意思的 struct 大小

我们定义一个 struct, 这个 struct 有 3 个字段, 它们的类型有 byte,int32 以及 int64, 但是这三个字段的顺序我们可以任意排列, 那么根据顺序的不同, 一共有 6 种组合。

type user1 struct {
	b byte
	i int32
	j int64
}

type user2 struct {
	b byte
	j int64
	i int32
}

type user3 struct {
	i int32
	b byte
	j int64
}

type user4 struct {
	i int32
	j int64
	b byte
}

type user5 struct {
	j int64
	b byte
	i int32
}

type user6 struct {
	j int64
	i int32
	b byte
}

根据这 6 种组合, 定义了 6 个 struct, 分别位 user1, user2, …, user6, 那么现在大家猜测一下, 这 6 种类型的 struct 占用的内存是多少, 就是 unsafe.Sizeof() 的值。

大家可能猜测 1+4+8=13, 因为 byte 的大小为 1, int32 大小为 4, int64 大小为 8, 而 struct 其实就是一个字段的组合, 所以猜测 struct 大小为字段大小之和也很正常。

但是, 但是, 我可以明确的说, 这是错误的。

为什么是错误的, 因为有内存对齐存在, 编译器使用了内存对齐, 那么最后的大小结果就不一样了。现在我们正式验证下, 这几种 struct 的值。

func main() {
	var u1 user1
	var u2 user2
	var u3 user3
	var u4 user4
	var u5 user5
	var u6 user6

	fmt.Println("u1 size is",unsafe.Sizeof(u1))
	fmt.Println("u2 size is",unsafe.Sizeof(u2))
	fmt.Println("u3 size is",unsafe.Sizeof(u3))
	fmt.Println("u4 size is",unsafe.Sizeof(u4))
	fmt.Println("u5 size is",unsafe.Sizeof(u5))
	fmt.Println("u6 size is",unsafe.Sizeof(u6))
}

从以上输出可以看到, 结果是:

u1 size is  16
u2 size is  24
u3 size is  16
u4 size is  24
u5 size is  16
u6 size is  16

结果出来了 (我的电脑的结果, Mac64 位, 你的可能不一样), 4 个 16 字节, 2 个 24 字节, 既不一样, 又不相同, 这说明: 1. 内存对齐影响 struct 的大小 2. struct 的字段顺序影响 struct 的大小

综合以上两点, 我们可以得知, 不同的字段顺序, 最终决定 struct 的内存大小, 所以有时候合理的字段顺序可以减少内存的开销。

内存对齐会影响 struct 的内存占用大小, 现在我们就详细分析下, 为什么字段定义的顺序不同会导致 struct 的内存占用不一样。

在分析之前, 我们先看下内存对齐的规则:

  1. 对于具体类型来说, 对齐值 = min(编译器默认对齐值, 类型大小 Sizeof 长度)。也就是在默认设置的对齐值和类型的内存占用大小之间, 取最小值为该类型的对齐值。我的电脑默认是 8, 所以最大值不会超过 8.
  2. struct 在每个字段都内存对齐之后, 其本身也要进行对齐, 对齐值 = min(默认对齐值, 字段最大类型长度)。这条也很好理解, struct 的所有字段中, 最大的那个类型的长度以及默认对齐值之间, 取最小的那个。

以上这两条规则要好好理解, 理解明白了才可以分析下面的 struct 结构体。在这里再次提醒, 对齐值也叫对齐系数、对齐倍数, 对齐模数。这就是说, 每个字段在内存中的偏移量是对齐值的倍数即可。

我们知道 byte, int32, int64 的对齐值分别为 1, 4, 8, 占用内存大小也是 1, 4, 8。那么对于第一个 structuser1, 它的字段顺序是 byte、int32、int64, 我们先使用第 1 条内存对齐规则进行内存对齐, 其内存结构如下。

bxxx|iiii|jjjj|jjjj

user1 类型, 第 1 个字段 byte, 对齐值 1, 大小 1, 所以放在内存布局中的第 1 位。

第 2 个字段 int32, 对齐值 4, 大小 4, 所以它的内存偏移值必须是 4 的倍数, 在当前的 user1 中, 就不能从第 2 位开始了, 必须从第 5 位开始, 也就是偏移量为 4。第 2, 3, 4 位由编译器进行填充, 一般为值 0, 也称之为内存空洞。所以第 5 位到第 8 位为第 2 个字段 i。

第 3 字段, 对齐值为 8, 大小也是 8。因为 user1 前两个字段已经排到了第 8 位, 所以下一位的偏移量正好是 8, 是第 3 个字段对齐值的倍数, 不用填充, 可以直接排列第 3 个字段, 也就是从第 9 位到第 16 位为第 3 个字段 j。

现在第一条内存对齐规则后, 内存长度已经为 16 个字节, 我们开始使用内存的第 2 条规则进行对齐。根据第二条规则, 默认对齐值 8, 字段中最大类型长度也是 8, 所以求出结构体的对齐值位 8, 我们目前的内存长度为 16, 是 8 的倍数, 已经实现了对齐。

所以到此为止, 结构体 user1 的内存占用大小为 16 字节。

现在我们再分析一个 user2 类型, 它的大小是 24, 只是调换了一下字段 i 和 j 的顺序, 就多占用了 8 个字节, 我们看看为什么? 还是先使用我们的内存第 1 条规则分析。

bxxx|xxxx|jjjj|jjjj|iiii

按对齐值和其占用的大小, 第 1 个字段 b 偏移量为 0, 占用 1 个字节, 放在第 1 位。

第 2 个字段 j, 是 int64, 对齐值和大小都是 8, 所以要从偏移量 8 开始, 也就是第 9 到 16 位为 j, 这也就意味着第 2 到 8 位被编译器填充。

目前整个内存布局已经偏移了 16 位, 正好是第 3 个字段 i 的对齐值 4 的倍数, 所以不用填充, 可以直接排列, 第 17 到 20 位为 i。

现在所有字段对齐好了, 整个内存大小为 1+7+8+4=20 个字节, 我们开始使用内存对齐的第 2 条规则, 也就是结构体的对齐, 通过默认对齐值和最大的字段大小, 求出结构体的对齐值为 8。

现在我们的整个内存布局大小为 20, 不是 8 的倍数, 所以我们需要进行内存填充, 补足到 8 的倍数, 最小的就是 24, 所以对齐后整个内存布局为

bxxx|xxxx|jjjj|jjjj|iiii|xxxx

所以这也是为什么我们最终获得的 user2 的大小为 24 的原因。 基于以上办法, 我们可以得出其他几个 struct 的内存布局。

user3

iiii|bxxx|jjjj|jjjj

user4

iiii|xxxx|jjjj|jjjj|bxxx|xxxx

user5

jjjj|jjjj|bxxx|iiii

user6

jjjj|jjjj|iiii|bxxx

以上给出了答案, 推到过程大家可以参考 user1 和 user2 试试。下一篇我们介绍通过 unsafe.Pointer 进行内存的运算, 以及对内存的读写。

Go 可视化性能分析工具

原文: A Short Survey of PProf Visualization Tools by Jordan Crabtree

调试 CPU 相关的问题经常会涉及关于趋势的微妙问题。堆使用的峰值是否逐渐的增长? routine 在什么地方被调用, 调用的频度如何?

一图胜千言。

一张图片就可以提供很多有用的上下文信息, 否则如果用语言解释起来累的半死。将 pprof 可视化显示可以将有用的 CPU 统计数据与整个时间的上下文关联起来。

1. pprof 是什么?

PProf 是一个 CPU 分析器 ( cpu profiler), 它是 gperftools 工具的一个组件, 由 Google 工程师为分析多线程的程序所开发。

Go 标准库中的 pprof package 通过 HTTP 的方式为 pprof 工具提供数据。

(译者注: 不止这个包, runtime/pprof 还可以为控制台程序或者测试程序产生 pprof 数据)

既然 pprof 数据通过 HTTP 提供, 所以它需要在你的应用中运行一个 web 服务器。可以通过 import pprof 的副作用 (这里副作用 side-effect 是指引入这个包让其初始化, 不是贬义词), 这个包就可以在缺省的 web 服务器中注册它的 handler, 并补需要其它额外的操作。

下面是一个使用 pprof 长时间运行的例子:

import (
	"log"
	"net/http"
	_ "net/http/pprof"
)
func main(){
	go func() {
		log.Println(http.ListenAndServe("localhost:6060", nil))
	}()
	...

2. 使用 pprof

pprof 能够借助 grapgviz 产生程序的调用图。默认情况下 pprof 基于对程序的 30 秒采样产生调用图。

  • 图中的连线代表对方法的调用, 连线上的标签代表指定的方法调用的采样值 (译者注: go tool pprof 显示的是调用时间)。
  • 方框的大小与方法运行的采样值的大小有关。
  • 每个方框由两个标签: 一个是方法运行的时间占比, 一个是它在采样的堆栈中出现的时间占比 (译者注: 前者你可以看成 flat 时间, 后者看成 cumulate 时间)。

2.1. 在 Mac 上安装工具:

$ brew install gperftools
$ brew install graphviz

$ pprof --web localhost:6060/debug

译者注:

我并没有使用上面的工具, 因为 Go 开发环境已经集成了 pprof 工具, 所以你不需要安装 gperftools 工具, 而且使用的命令和上面的命令不一样。当然 graphviz 是必装的。

你可以通过下面的命令采集 30s 的数据并生成 SVG 调用图:

go tool pprof -web http://10.75.25.126:9091/debug/pprof/profile

3. go-torch

go-torch 是 uber 开发的一个工具, 使用性能分析的专家 Brendan Gregg (译者注: 口口相传的性能分析的工具图就是来自他, 性能之巅一书的作者。看他的网站可以学到很多性能相关的知识) 的脚本生成火焰图。

和 pprof 一样, 它也采用 30 秒的采样数据生成火焰图。

  • 栈帧 (Stack frame, 调用链) 垂直堆叠, 显示栈的深度
  • 帧的宽度代表一个方法的运行的时间占比
  • 如果一个方法被调用者调用, 它会显示多次, 分列在不同的调用者的堆栈上。(译者注: 这点和 pprof 工具不同, pprof 至显示一个方法的框, 上面的时间标签是所有的调用者调用的时间)
  • 颜色是任意的 (arbitrary), 横坐标根据字母顺序排列

3.1. 安装

需要安装 go-torch 工具和 brandangregg 的火焰图生成脚本:

$ go get github.com/uber/go-torch
$ git clone git@github.com:brendangregg/FlameGraph.git
$ export PATH-$PATH:/path/to/FlameGraph

go-torch --file "torch.svg" --url http://localhost:6060

4. GOM

gom 是一个实时的 curses - 风格的命令行工具, 由 Google 的工程师 Jaana Dogan 开发。

  • 可以显示运行的 goroutine 和机器线程数
  • 实时更新
  • 出现可视化, gom 还提供了基于文本的 CPU 和 heap 的数据
$ go get github.com/rakyll/gom/cmd/gom

通过 import gom, gom 同样可以注册额外的 handler 在缺省的服务器上, 就像 pprof 一样。

import (
	_ "github.com/rakyll/gom/http"
)
$ gom --target http://localhost:6060

5. Debug charts

Debug charts 是由 Marko Kevac 开发的一个工具, 使用 plotly.js 库来为运行的程序创建一个可视化的 web 视图。

  • 运行在浏览器中
  • 可视化 gc pause, 内存分配和 cpu 占用等信息
  • 实时更新

通过 import debugcharts, 可以为缺省的 web 服务器注册额外的 handler, 就像 pprof、gom 一样。

import (
	_ "github.com/mkevac/debugcharts"
)

然后访问 localhost:6060/debug/charts 就可以显示相关的实时性能图表了。

另外: golang 使用pprof和go-torch做性能分析

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Redis是一种内存数据库,可以用于实现草稿箱功能。下面是一个使用Redis实现草稿箱的简单示例: 1. 连接到Redis:首先,你需要通过Java Redis客户端连接到Redis数据库。例如,可以使用Jedis或Lettuce等流行的Redis客户端库。 2. 保存草稿:当用户选择保存内容为草稿时,将草稿的内容存储在Redis中。可以使用哈希表(Hash)来表示每个草稿,其中键是草稿的唯一标识符,而字段和值可以表示草稿的各个属性,如标题、内容和创建时间等。 ```java Jedis jedis = new Jedis("localhost", 6379); String draftId = "draft:123"; Map<String, String> draftData = new HashMap<>(); draftData.put("title", "My Draft"); draftData.put("content", "This is my draft content"); draftData.put("created", "2021-10-01"); jedis.hmset(draftId, draftData); ``` 3. 获取草稿:当用户需要编辑草稿时,通过草稿的唯一标识符从Redis中获取草稿的详细信息。 ```java Map<String, String> draftData = jedis.hgetAll(draftId); String title = draftData.get("title"); String content = draftData.get("content"); // 显示在编辑界面供用户修改 ``` 4. 更新草稿:当用户对草稿进行修改后,更新Redis中对应草稿的内容。 ```java Map<String, String> updatedData = new HashMap<>(); updatedData.put("title", "Updated Draft"); updatedData.put("content", "This is the updated draft content"); jedis.hmset(draftId, updatedData); ``` 5. 删除草稿:如果用户决定删除草稿,从Redis中删除对应的草稿数据。 ```java jedis.del(draftId); ``` 需要注意的是,上述示例只提供了基本的操作,实际应用中可能还需要考虑并发访问、草稿列表的管理、过期时间设置等其他方面的功能。此外,你还可以根据具体需求添加其他字段或操作来扩展草稿功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云满笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值