[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