前言

前阵子写了个ETCD选主的代码,持续后台执行,相安无事一阵我就干别的事儿去了,我寻思小爷虽然代码写的一般,但是不至于出错啊。

但是我想的实在是太单纯了,应了那句话,我还是too young啊,高估了自己的姿势水平,一下搞出来一个大新闻。周六大半夜告警在那里 biubiubiu 的往我邮箱里塞,我当时正在COD战场上挥汗如雨,手机的震动就像电动马达一样给我腿都快整麻了,一看邮箱,好家伙,CPU占用百分之200多!Cgroup都没给它限制住。

所以我寻思,咱就好好分析下到底怎么回事儿吧。

工具准备

一般验证一个server详细的CPU和内存占用,Python我会选择PDB,C++我会选择GDB,但是GOlang这个就相对来说比较陌生,经过我查询资料(谷歌)过后得知,Golang有神奇pprof,还可以分析整个函数占用,这就很牛逼了,果断学习了一波之后上手。

pprof的简单使用

首先pprof分为两个大包

net/http/pprof 

runtime/pprof

如果你的go程序是用http包启动的web服务器,你想查看自己的web服务器的状态。这个时候就可以选择net/http/pprof。你只需要引入包_“net/http/pprof”。

例如

import _ "net/http/pprof"

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

我现在要分析我的go server为什么占用了过大的CPU,所以,我需要在我的Go server的main中添加几行导入pprof的代码,一些业务代码我这里会直接隐去,请见谅

import ( 
    _ "net/http/pprof"
    "github.com/gin-gonic/gin"
    ······
)

func main() {
    // 这里使用了gin框架,模拟我的业务环境
    gin.SetMode(gin.ReleaseMode)
    router := gin.Default()
    //ETCD选主的逻辑,这里很可能是持续占用CPU的罪魁祸首
    go infimanage.Retry(infiapi.InitSyncStart)
    //调用pprof的逻辑,指定6161端口
    go func() {
		http.ListenAndServe(":6161", nil)
    }()
    //这里是原本的服务接口,这里只是举个例子,模拟一波
    ....
    router.POST("/synctask/add", addwork, AddSyncWorks)
    router.GET("/synctask/del", DeleteSyncWorks)
    s := &http.Server{
		Addr:           ":8788",
		Handler:        router,
		ReadTimeout:    10 * time.Second,
		WriteTimeout:   10 * time.Second,
	}
	_ = s.ListenAndServe()
}

现在走一波

go run main.go

现在server就启起来了,然后咱们要来一波分析 首先指定监控main server 30s

go tool pprof http://127.0.0.1:6161/debug/pprof/profile -seconds 30

会冒出下面一大堆东西,就相当于进入了pprof,类似pdb,gdb的调试shell下

Fetching profile over HTTP from http://localhost:6161/debug/pprof/profile?seconds=30
Saved profile in /Users/eddycjy/pprof/pprof.samples.cpu.007.pb.gz
Type: cpu
Duration: 1mins, Total samples = 26.55s (44.15%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) 

获取CPU占比前10的函数

(pprof) top10
Showing nodes accounting for 25.92s, 97.63% of 26.55s total
Dropped 85 nodes (cum <= 0.13s)
Showing top 10 nodes out of 21
      flat  flat%   sum%        cum   cum%
    23.28s 87.68% 87.68%     23.29s 87.72%  syscall.Syscall
     0.77s  2.90% 90.58%      21.77s  80.90%  runtime.selectgo
     0.58s  2.18% 92.77%      0.58s  2.18%  runtime.mcall
     0.53s  2.00% 94.76%      1.42s  5.35%  runtime.timerproc
     0.36s  1.36% 96.12%      30.39s  98.47%  go.etcd.io/etcd/clientv3/...
     0.35s  1.32% 97.44%      0.45s  1.69%  runtime.greyobject
     0.02s 0.075% 97.51%     24.96s 94.01%  main.main.func1
     0.01s 0.038% 97.55%     23.91s 90.06%  os.(*File).Write
     0.01s 0.038% 97.59%      0.19s  0.72%  runtime.mallocgc
     0.01s 0.038% 97.63%     23.30s 87.76%  syscall.Write

这一波就能看出来了,到底谁才是占用CPU过大的罪魁祸首,看到了么,有个select和etcd的keeplive,但是这样似乎还不那么直观,这时候你需要一个叫做火焰图的东西

pprof输出火焰图

安装输出火焰图的server

安装配置FlameGraph

git clone https://github.com/brendangregg/FlameGraph.git

配置FlameGraph

PATH=$PATH:/rootg/go/src/github.com/brendangregg/FlameGraph

配置go-torch,这是生成火焰图的必备工具

go get -v github.com/uber/go-torch

生成火焰图

确保都安装完成了之后

直接执行命令

此时你要确保第一步那些监控代码已经写入

go-torch -u http://127.0.0.1:6161/debug/pprof/ -p > cpu-local.svg

这里会生成一个svg,这个东西可以用浏览器打开,打开是这样的

看到没,这一下就暴露出来了,谁占比多,你还可以点进去看详细,比如

这下可以定位了,就是选主逻辑当中的select或者是for循环出了问题

解决问题

我们回头去反查ETCD的选主逻辑代码

//ElectMasterNode 争抢主节点服务
func ElectMasterNode() error {
	ips, err := GetNodeIp()
	if err != nil {
		return err
	}
	etcd, err := infidb.New()
	if err != nil {
		return err
	}
	for {
		_ = etcd.Newleaseslock(ips)
	}
}

//Masterwork 选主尝试
func Masterwork() {
    select {
        ElectMasterNode()
    }
}

这里的问题就是,在select中重复调用了for循环,应该是出现了for死循环,导致了CPU持续被抢占,这边修改一下代码逻辑

//ElectMasterNode 争抢主节点服务
func ElectMasterNode() error {
	ips, err := GetNodeIp()
	if err != nil {
		return err
	}
	etcd, err := infidb.New()
	if err != nil {
		return err
	}
	_ = etcd.Newleaseslock(ips)
}

//Masterwork 选主尝试
func Masterwork() {
    select {
        ElectMasterNode()
    }
}

这样问题应该就完全解决了。