[TOC]
# 什么是goroutine leak
goroutine leak,是go协程泄漏,什么是go协程泄漏,通俗来说,开启了一个goroutine,用完后,我们要正确让其结束。如果它没用了,还没结束,那就是goroutine leak。
泄漏的goroutine占用一部分cpu,还可能占着一些其他资源,从而影响主协程效率,有时甚至产生异常。
我们看下面的一个例子。
例子中我们的主协程需要通过某远程服务查询到一个结果。使用一个multiQuery的函数启动多个协程,分别向不同的服务器发起查询,只要收到一个服务器返回,multiQeury就返回结果
~~~
package main
import (
"fmt"
"math/rand"
"time"
)
func queryFromSrc(src string) (ret string) {
nanoSec := time.Now().Nanosecond()
rand.Seed(int64(nanoSec))
sec := (rand.Int31() % 10) + 1
// time sleep simulates dns lookup and query
time.Sleep(time.Second * time.Duration(sec))
ret = fmt.Sprintf("src=%s use sec=%d", src, sec)
fmt.Println("a query ok, ret=", ret)
return ret
}
func multiQuery() (ret string) {
res := make(chan string, 3)
go func() {
res <- queryFromSrc("ns1.dnsserver.com")
}()
go func() {
res <- queryFromSrc("ns2.dnsserver.com")
}()
go func() {
res <- queryFromSrc("ns3.dnsserver.com")
}()
return <-res
}
func main() {
fmt.Println("start multi query:")
res := multiQuery()
fmt.Println("res=", res)
//time.Sleep(time.Second * 20)
}
~~~
本案例使用了一个带缓冲区的channel,multiQuery中的三个并行go func不分先后从远程获取一个结果返回。获取的结果写入channel res,在第一个结果收到后,multiQuery就返回。返回的结果肯定是三个go func中最快返回的。(go func 中的queryFromSrc使用time.Sleep(random)来模拟不同请求延时)。显然,当第一个结果返回后,multiQuery函数就结束了,而其他两个go func还在等待返回。
**如果我们使用不带缓冲区的channel,两个慢的goroutine将会卡在尝试去发送他们的结果到同一个channel,而这个channel将没有任何一个goroutine去读**。因为multiQeury已经执行结束。这种情况叫做goroutine leak。与gc回自动回收的变量不同,泄漏的goroutine不会自动被回收。
所以编程中一定要注意,不使用的goroutine要让其正确地终止
# GC
在runtime的doc中描述了,通过设置环境变量GODEBUG='gctrace=1'可以让go的运行时把gc信息打印到stderr。
~~~
GODEBUG='gctrace=1' ./sentinel-agent >gc.log &
~~~
gc.log的输出如下:
~~~
gc781(1): 1+2385+17891+0 us, 60 -> 60 MB, 21971 (3503906-3481935) objects, 13818/14/7369 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields
gc782(1): 1+1794+18570+1 us, 60 -> 60 MB, 21929 (3503906-3481977) objects, 13854/1/7315 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields
gc783(1): 1+1295+20499+0 us, 59 -> 59 MB, 21772 (3503906-3482134) objects, 13854/1/7326 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields
gc781:从程序启动开始,第781次gc
~~~
(1):参与gc的线程个数
1+2385+17891+0:分别是1)stop-the-world的时间,即暂停所有goroutine;2)清扫标记对象的时间;3)标记垃圾对象的时间;4)等待线程结束的耗时。单位都是us,4者之和就是gc暂停的整体耗时
60 -> 60 MB:gc后,堆上存活对象占用的内存,以及整个堆大小(包括垃圾对象)
21971 (3503906-3481935) objects:gc后,堆上的对象数量,gc前分配的对象以及本次释放的对象
13818/14/7369 sweeps:描述对象清扫阶段。一共有13818个memory span,其中14在后台被清扫,7369在stop-the-world期间被清扫
0(0) handoff,0(0) steal:描述并行标记阶段的负载均衡特性。当前在不同线程间传送操作数和总传送操作数,以及当前steal操作数和总steal操作数
0/0/0 yields:描述并行标记阶段的效率。在等待其他线程的过程中,一共有0次yields操做
经过观察gc的输出,发现当前堆上对象总数不断增多,没有减少的趋势,这说明存在对象的泄露,从而导致内存泄露
# memory profile
根据golang官网profile指南 http://blog.golang.org/profiling-go-programs ,在代码中添加
~~~
import _ "net/http/pprof"
func main() {
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
}
~~~
可以在运行时对程序进行profile,通过http访问:
~~~
go tool pprof http://localhost:6060/debug/pprof/heap
~~~
进行memory profile,默认是`--inuse_space`,显示当前活跃的对象(不包括垃圾对象)占用的空间。使用`--alloc_space`可以显示所有分配的对象(包括垃圾对象)。不过这两种方式都没有发现异常
# 监控goroutine个数
通过runtime.NumGoroutine()可以获取当前的goroutine的个数。通过给程序添加http server获取一些统计信息来了解程序的运行状态,这是Jeff Dean推崇的方法。通过添加下述代码来实时查看goroutine的个数
~~~
// goroutine stats and pprof
go func() {
http.HandleFunc("/goroutines", func(w http.ResponseWriter, r *http.Request) {
num := strconv.FormatInt(int64(runtime.NumGoroutine()), 10)
w.Write([]byte(num))
});
http.ListenAndServe("localhost:6060", nil)
glog.Info("goroutine stats and pprof listen on 6060")
}()
~~~
通过命令:
~~~
curl localhost:6060/goroutines
~~~
查询当前的goroutine的个数。通过不程序运行期间,不断查看,发现goroutine个数不断增加,没有销毁的迹象
# goroutine泄露
通过上面的观察,发现存在goroutine泄露,即goroutine没有正常退出。由于每轮(每隔10秒执行一次)都会创建多个goroutine,如果不能正常退出,则会存在大量的goroutine。go的gc使用的是mark and sweep,会从全局变量、goroutine的栈为根集合扫描所有的存活对象,如果goroutine不退出,就会泄露大量内存。
在确定是由于goroutine没有正常退出后,重新review代码,发现了泄露的根本原因。在重构前,在信号处理程序中,为了正常结束程序,对于每个goroutine都有一个channel,用于主goroutine等待所有goroutine正常结束后再退出。主goroutine中,信号处理程序用于等待所有goroutine的代码:
~~~
waiters = make([]chan int, Num)
for _, w := range waiters {
<- w
}
~~~
执行检查逻辑的goroutine在结束后,会调用`ag.w <- 1`,用于向主goroutine发送消息。
重构后,由于每轮都会创建goroutine,由于用于主goroutine和检查逻辑的goroutine之间的channel的大小是1,所以所有创建的检查goroutine都阻塞在`ag.w <- 1`上,不能正常退出。最后,把channel逻辑去掉,就不存在goroutine泄露了
- 基础
- 简介
- 主要特征
- 变量和常量
- 编码转换
- 数组
- byte与rune
- big
- sort接口
- 和mysql类型对应
- 函数
- 闭包
- 工作区
- 复合类型
- 指针
- 切片
- map
- 结构体
- sync.Map
- 随机数
- 面向对象
- 匿名组合
- 方法
- 接口
- 权限
- 类型查询
- 异常处理
- error
- panic
- recover
- 自定义错误
- 字符串处理
- 正则表达式
- json
- 文件操作
- os
- 文件读写
- 目录
- bufio
- ioutil
- gob
- 栈帧的内存布局
- shell
- 时间处理
- time详情
- time使用
- new和make的区别
- container
- list
- heap
- ring
- 测试
- 单元测试
- Mock依赖
- delve
- 命令
- TestMain
- path和filepath包
- log日志
- 反射
- 详解
- plugin包
- 信号
- goto
- 协程
- 简介
- 创建
- 协程退出
- runtime
- channel
- select
- 死锁
- 互斥锁
- 读写锁
- 条件变量
- 嵌套
- 计算单个协程占用内存
- 执行规则
- 原子操作
- WaitGroup
- 定时器
- 对象池
- sync.once
- 网络编程
- 分层模型
- socket
- tcp
- udp
- 服务端
- 客户端
- 并发服务器
- Http
- 简介
- http服务器
- http客户端
- 爬虫
- 平滑重启
- context
- httptest
- 优雅中止
- web服务平滑重启
- beego
- 安装
- 路由器
- orm
- 单表增删改查
- 多级表
- orm使用
- 高级查询
- 关系查询
- SQL查询
- 元数据二次定义
- 控制器
- 参数解析
- 过滤器
- 数据输出
- 表单数据验证
- 错误处理
- 日志
- 模块
- cache
- task
- 调试模块
- config
- 部署
- 一些包
- gjson
- goredis
- collection
- sjson
- redigo
- aliyunoss
- 密码
- 对称加密
- 非对称加密
- 单向散列函数
- 消息认证
- 数字签名
- mysql优化
- 常见错误
- go run的错误
- 新手常见错误
- 中级错误
- 高级错误
- 常用工具
- 协程-泄露
- go env
- gometalinter代码检查
- go build
- go clean
- go test
- 包管理器
- go mod
- gopm
- go fmt
- pprof
- 提高编译
- go get
- 代理
- 其他的知识
- go内存对齐
- 细节总结
- nginx路由匹配
- 一些博客
- redis为什么快
- cpu高速缓存
- 常用命令
- Go 永久阻塞的方法
- 常用技巧
- 密码加密解密
- for 循环迭代变量
- 备注
- 垃圾回收
- 协程和纤程
- tar-gz
- 红包算法
- 解决golang.org/x 下载失败
- 逃逸分析
- docker
- 镜像
- 容器
- 数据卷
- 网络管理
- 网络模式
- dockerfile
- docker-composer
- 微服务
- protoBuf
- GRPC
- tls
- consul
- micro
- crontab
- shell调用
- gorhill/cronexpr
- raft
- go操作etcd
- mongodb