網站首頁 編程語言 正文
背景
golang版本:1.16
之前遇到的問題,docker啟動時禁用了oom-kill(kill后服務受損太大),導致golang內存使用接近docker上限后,進程會hang住,不響應任何請求,debug工具也無法attatch。
前文分析見:golang進程在docker中OOM后hang住問題
本文主要嘗試給出解決方案
測試程序
測試程序代碼如下,協程h.allocate每秒檢查內存是否達到800MB,未達到則申請內存,協程h.clear每秒檢查內存是否超過800MB的80%,超過則釋放掉超出部分,模擬通常的業務程序頻繁進行內存申請和釋放的邏輯。程序通過http請求127.0.0.1:6060觸發開始執行方便debug。
docker啟動時加--memory 1G --memory-reservation 1G --oom-kill-disable=true
參數限制總內存1G并關閉oom-kill
package main import ( "fmt" "math/rand" "net/http" _ "net/http/pprof" "sync" "sync/atomic" "time" ) const ( maxBytes = 800 * 1024 * 1024 // 800MB arraySize = 4 * 1024 ) type handler struct { start uint32 // 開始進行內存申請釋放 total int32 // 4kB內存總個數 count int // 4KB內存最大個數 ratio float64 // 內存數達到count*ratio后釋放多的部分 bytesBuffers [][]byte // 內存池 locks []*sync.RWMutex // 每個4kb內存一個鎖減少競爭 wg *sync.WaitGroup } func newHandler(count int, ratio float64) *handler { h := &handler{ count: count, bytesBuffers: make([][]byte, count), locks: make([]*sync.RWMutex, count), wg: &sync.WaitGroup{}, ratio: ratio, } for i := range h.locks { h.locks[i] = &sync.RWMutex{} } return h } func (h *handler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { atomic.StoreUint32(&h.start, 1) // 觸發開始內存申請釋放 } func (h *handler) started() bool { return atomic.LoadUint32(&h.start) == 1 } // 每s檢查內存未達到count個則補足 func (h *handler) allocate() { h.wg.Add(1) go func() { defer h.wg.Done() ticker := time.NewTicker(time.Second) for range ticker.C { for i := range h.bytesBuffers { h.locks[i].Lock() if h.bytesBuffers[i] == nil { h.bytesBuffers[i] = make([]byte, arraySize) h.bytesBuffers[i][0] = 'a' atomic.AddInt32(&h.total, 1) } h.locks[i].Unlock() fmt.Printf("allocated size: %dKB\n", atomic.LoadInt32(&h.total)*arraySize/1024) } } }() } // 每s檢查內存超過count*ratio將超出的部分釋放掉 func (h *handler) clear() { h.wg.Add(1) go func() { defer h.wg.Done() ticker := time.NewTicker(time.Second) for range ticker.C { diff := int(atomic.LoadInt32(&h.total)) - int(float64(h.count)*h.ratio) tmp := diff for diff > 0 { i := rand.Intn(h.count) h.locks[i].RLock() if h.bytesBuffers[i] == nil { h.locks[i].RUnlock() continue } h.locks[i].RUnlock() h.locks[i].Lock() if h.bytesBuffers[i] == nil { h.locks[i].Unlock() continue } h.bytesBuffers[i] = nil h.locks[i].Unlock() atomic.AddInt32(&h.total, -1) diff-- } fmt.Printf("free size: %dKB, left size: %dKB\n", tmp*arraySize/1024, atomic.LoadInt32(&h.total)*arraySize/1024) } }() } // 每s打印日志檢查是否阻塞 func (h *handler) print() { h.wg.Add(1) go func() { defer h.wg.Done() ticker := time.NewTicker(time.Second) for range ticker.C { go func() { d := make([]byte, 1024) // trigger gc d[0] = 1 fmt.Printf("running...%d\n", d[0]) }() } }() } // 等待啟動 func (h *handler) wait() { h.wg.Add(1) go func() { defer h.wg.Done() addr := "127.0.0.1:6060" // trigger to start err := http.ListenAndServe(addr, h) if err != nil { fmt.Printf("failed to listen on %s, %+v", addr, err) } }() for !h.started() { time.Sleep(time.Second) fmt.Printf("waiting...\n") } } // 等待退出 func (h *handler) waitDone() { h.wg.Wait() } func main() { go func() { addr := "127.0.0.1:6061" // debug _ = http.ListenAndServe(addr, nil) }() h := newHandler(maxBytes/arraySize, 0.8) h.wait() h.allocate() h.clear() h.print() h.waitDone() }
程序執行一段時間后rss占用即達到1G,程序不再響應請求,docker無法通過bash連接上,已經連接的bash執行命令顯示錯誤bash: fork: Cannot allocate memory
一、為gc預留空間方案
之前的分析中,hang住的地方是調用mmap,golang內的堆棧是gc stw后的mark階段,所以最開始的解決方法是想在stw之前預留100MB空間,stw后釋放該部分空間給操作系統,改動如下:
但是進程同樣會hang住,debug單步調試發現存在三種情況
- 未觸發gc(是因為gc的步長參數默認為100%,下一次gc觸發的時機默認是內存達到上次gc的兩倍);
- gc的stw之前就阻塞住,多數在gcBgMarkStartWorkers函數啟動新的goroutine時陷入阻塞;
- gc的stw后mark prepare階段阻塞,即前文分析中的,申請新的workbuf時在mmap時阻塞;
可見,預留內存的方式只能對第3種情況有改善,增加了預留內存后多數為第2種情況阻塞。
從解決問題的角度看,預留內存,是讓gc去適配內存達到上限后系統調用阻塞的情況,對于其他情況gc反而更差了,因為有額外的內存和cpu開銷。更何況因為第2種情況的存在,導致gc的修改無法面面俱到。
而且即使第2種情況創建g不阻塞,創建g后仍然需要找到合適的m執行,但因為已有的m都會因為系統調用被阻塞,而創建新的m即新的線程,又會被阻塞在內存申請上。所以這是不光golang會遇到的問題,即使用其他語言寫也會有這種問題。在這種環境下運行的進程,必須對自身的內存大小做嚴格控制。
二、調整gc參數
通過第一種方案的嘗試,我們需要轉換角度,結合實際使用場景做適配, 避免影響golang運行機制。限制條件主要有:
- 進程會使用較多內存
- 進程的使用有上限, 達到上限后系統調用會阻塞
需要讓進程控制內存上限,同時在達到上限前多觸發gc。解決方式如下:
- 用內存池。測試程序中的allocate和clear的邏輯,實際上就是實現了一個內存池,控制總的內存在640~800MB之間波動。
- 增加gc頻率。程序啟動時加環境變量GOGC=12,控制gc步長在12%,例如內存池達到800MB時,會在800*112%=896MB時觸發gc,避免內存達到1G上限。
實測進程內存在900MB以下波動,沒有hang住。
原文鏈接:https://juejin.cn/post/7155863066968588325
相關推薦
- 2022-09-13 C語言創建數組實現函數init,empty,reverse_C 語言
- 2022-02-11 Android?studio?利用共享存儲進行用戶的注冊和登錄驗證功能_Android
- 2022-03-30 C語言入門之淺談數據類型和變量常量_C 語言
- 2022-09-06 python?numpy中array與pandas的DataFrame轉換方式_python
- 2022-09-30 QT實現多文件拖拽獲取路徑的方法_C 語言
- 2023-05-29 docker部署xxl-job-admin出現數據庫拒絕問題及解決方法_docker
- 2022-04-02 Android?studio實現日期?、時間選擇器與進度條_Android
- 2023-07-07 Spring整合Junit單元測試
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支