脚本之家,脚本语言编程技术及教程分享平台!
分类导航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|

服务器之家 - 脚本之家 - Golang - golang进程内存控制避免docker内oom

golang进程内存控制避免docker内oom

2022-11-29 11:24硅基生命 Golang

这篇文章主要为大家介绍了golang进程内存控制避免docker内oom示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

背景

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
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后释放该部分空间给操作系统,改动如下:

golang进程内存控制避免docker内oom

golang进程内存控制避免docker内oom

golang进程内存控制避免docker内oom

但是进程同样会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住。

以上就是golang进程内存控制避免docker内oom的详细内容,更多关于golang进程避免docker oom的资料请关注服务器之家其它相关文章!

原文链接:https://juejin.cn/post/7155863066968588325

延伸 · 阅读

精彩推荐
  • GolangGo chassis云原生微服务开发框架应用编程实战

    Go chassis云原生微服务开发框架应用编程实战

    这篇文章主要为大家介绍了Go chassis云原生微服务开发框架应用编程实战示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早...

    二手雄狮8562022-08-24
  • GolangGo与Redis实现分布式互斥锁和红锁

    Go与Redis实现分布式互斥锁和红锁

    这篇文章主要介绍了Go与Redis实现分布式互斥锁和红锁,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以参考一下...

    jiaxwu4892022-11-24
  • GolangGo语言里切片slice的用法介绍

    Go语言里切片slice的用法介绍

    这篇文章介绍了Go语言里切片slice的用法,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...

    奋斗的大橙子11762022-07-16
  • GolangGo 结构体函数调用底层实现

    Go 结构体函数调用底层实现

    我们来了解一下结构体变量声明和相关函数调用在机器码或汇编层面的体现。我们以下面代码为案例进行分析。...

    程序员历小冰7382021-11-02
  • Golanggolang 基于 mysql 简单实现分布式读写锁

    golang 基于 mysql 简单实现分布式读写锁

    这篇文章主要介绍了golang 基于mysql简单实现分布式读写锁,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以参考一下...

    二牛QAQ6732022-11-23
  • GolangGo语言 go程释放操作(退出/销毁)

    Go语言 go程释放操作(退出/销毁)

    这篇文章主要介绍了Go语言 go程释放操作(退出/销毁),具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    cqu_jiangzhou10912021-06-11
  • GolangGo语言中的Slice学习总结

    Go语言中的Slice学习总结

    这篇文章主要介绍了Go语言中的Slice学习总结,本文讲解了Slice的定义、Slice的长度和容量、Slice是引用类型、Slice引用传递发生“意外”等内容,需要的朋友可...

    junjie5022020-04-10
  • GolangGo语言实现字符串切片赋值的方法小结

    Go语言实现字符串切片赋值的方法小结

    这篇文章主要给大家介绍了Go语言实现字符串切片赋值的两种方法,分别是在for循环的range中以及在函数的参数传递中实现,有需要的朋友们可以根据自己的...

    steveye5912020-05-03