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

目录
  • 背景
  • 测试程序
  • 一、为gc预留空间方案
  • 二、调整gc参数

背景

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住。

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

(0)

相关推荐

  • golang守护进程用法示例

    本文实例讲述了golang守护进程用法.分享给大家供大家参考,具体如下: 用node写了一个socket后台服务,可是有时候会挂,node一个异常就game over了,所以写了一个守候. 复制代码 代码如下: package main import (         "log"         "os"         "os/exec"         "time" ) func main() {         lf,

  • Golang信号处理及如何实现进程的优雅退出详解

    Linux系统中的信号类型 各操作系统的信号定义或许有些不同.下面列出了POSIX中定义的信号. 在linux中使用34-64信号用作实时系统中. 命令 man 7 signal 提供了官方的信号介绍.也可以是用kill -l来快速查看 列表中,编号为1 ~ 31的信号为传统UNIX支持的信号,是不可靠信号(非实时的),编号为32 ~ 63的信号是后来扩充的,称做可靠信号(实时信号).不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会. Linux支持的标准信号有以下一

  • golang如何实现mapreduce单进程版本详解

    前言   MapReduce作为hadoop的编程框架,是工程师最常接触的部分,也是除去了网络环境和集群配 置之外对整个Job执行效率影响很大的部分,所以很有必要深入了解整个过程.元旦放假的第一天,在家没事干,用golang实现了一下mapreduce的单进程版本,github地址.处理对大文件统计最高频的10个单词,因为功能比较简单,所以设计没有解耦合.   本文先对mapreduce大体概念进行介绍,然后结合代码介绍一下,如果接下来几天有空,我会实现一下分布式高可用的mapreduce版本.

  • golang 后台进程的启动和停止操作

    启动命令 我们先来个非后台运行的启动命令 func init() { startCmd := &cobra.Command{ Use: "start", Short: "Start Gonne", Run: func(cmd *cobra.Command, args []string) { startHttp() }, } startCmd.Flags().BoolVarP(&daemon, "deamon", "d&q

  • golang 输出重定向:fmt Log,子进程Log,第三方库logrus的详解

    独立 fmt Log输出重定向 golang的fmt包的输出函数 Println.Printf.PrintStack等,默认将打印输出到os.Stdout.错误打印输出到os.Stderr,os.Stdout 和 os.Stderr 默认值 /dev/stdout /dev/stderr 设备. //代码摘自:golang封装包 -> /lib/golang/src/os var ( Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin&q

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

    目录 背景 测试程序 一.为gc预留空间方案 二.调整gc参数 背景 golang版本:1.16 之前遇到的问题,docker启动时禁用了oom-kill(kill后服务受损太大),导致golang内存使用接近docker上限后,进程会hang住,不响应任何请求,debug工具也无法attatch. 前文分析见:golang进程在docker中OOM后hang住问题 本文主要尝试给出解决方案 测试程序 测试程序代码如下,协程h.allocate每秒检查内存是否达到800MB,未达到则申请内存,协

  • docker 查看进程, 内存, cup消耗的情况

    docker 查看进程, 内存,cup 消耗 启动 docker 容器,可以通过 docker inspect 查看进程号 # docker inspect -f '{{.State.Pid}}' 通过 docker stats 查看内存,cpu 使用 docker stats docker stats --no-stream docker stats container-name docker stats $(docker ps --format={{.Names}}) docker stat

  • 搭建docker内网私服的方法(docker-registry with nginx&ssl on centos)

    本文介绍了搭建docker内网私服的方法,分享给大家.具体如下: 主要思路: 1. Docker Registry 说明 关于如何创建和使用本地仓库,其实已经有很多文章介绍了.因为docker技术正处于发展和完善阶段,所以有些文章要么内容已经过时,要么给出了错误的配置,导致无法正常创建仓库.本文记录的是个人完整的搭建过程,docker version为1.1.2. 官方提供了Docker Hub网站来作为一个公开的集中仓库.然而,本地访问Docker Hub速度往往很慢,并且很多时候我们需要一个

  • docker内的容器如何与宿主机共享IP的方法

    目录 问题 原因 解决 问题 有个项目里面需要将一些服务打包到docker镜像中,打包完成后,发现有些服务有问题,主要集中在一些端侧设备接入用的服务,主要是工业相机.相机扫描不到. 原因 当Docker进程启动时,会在主机上创建一个名为docker0的虚拟网桥,此主机上启动的Docker容器会连接到这个虚拟网桥上.虚拟网桥的工作方式和物理交换机类似,这样主机上的所有容器就通过交换机连在了一个二层网络中.从docker0子网中分配一个IP给容器使用,并设置docker0的IP地址为容器的默认网关.

  • 从进程中去理解 Docker隔离技术

    目录 1.起源 2.容器类比进程 3.隔离技术 4.总结 1.起源 “容器”这项技术的来龙去脉: 容器技术的兴起源于 PaaS 技术的普及: Docker 公司发布的 Docker 项目具有里程碑式的意义: Docker 项目通过“容器镜像”,解决了应用打包这个根本性难题. 容器本身没有价值,有价值的是“容器编排”. 也正因为如此,容器技术生态才爆发了一场关于“容器编排”的“战争”.而这次战争,最终以 Kubernetes 项目和 CNCF 社区的胜利而告终. 容器,到底是怎么一回事儿? 容器其

  • docker内网搭建dns使用域名访问替代ip:port的操作

    比如我内网有个jenkins,我如果要访问它我得牢牢记住它的ip和端口,一个服务我还能记住,多个的话我可能需要一个方便记忆的域名记录一些内网服务 第1步: 准备好docker环境 第2步:下载好镜像 docker pull andyshinn/dnsmasq:2.75 第3步:运行dnsmasq #后台启动 docker run -d -p 53:53/tcp -p 53:53/udp --cap-add=NET_ADMIN --name dns-server andyshinn/dnsmasq

  • docker内服务访问宿主机服务的实现

    目录 1. 场景 2. 解决 3. 总结 4. 参考 1. 场景 使用windows, wsl2 进行日常开发测试工作. 但是wsl2经常会遇到网络问题.比如今天在测试一个项目,核心功能是将postgres 的数据使用开源组件synch 同步到clickhouse 这个工作. 测试所需组件 postgres kafka zookeeper redis synch容器 最开始测试时,选择的方案是, 将上述五个服务使用 docker-compose 进行编排, network_modules使用ho

  • python 监控某个进程内存的情况问题

    目录 python监控某个进程内存 python监控进程并重启 分析了具体思路 相关代码很简单 python监控某个进程内存 测试场景: 某个客户端程序长时间运行后存在内存泄漏问题,现在开发解决了需要去验证这个问题是否还存在,并要求出具相应测试验证报告. 手段: 需要有一个工具能够实时去获取该程序进程一直运行下占用内存,CPU使用率情况. 方法: python去实现这么个监控功能 import sys import time import psutil sys.argv # get pid fr

  • golang数组内存分配原理

    目录 编译时数组类型解析 ArrayType types2.Array types.Array 编译时数组字面量初始化 编译时数组索引越界检查 运行时数组内存分配 总结 编译时数组类型解析 ArrayType 数组是内存中一片连续的区域,在声明时需要指定长度,数组的声明有如下三种方式,[...]的方式在编译时会自动推断长度. var arr1 [3]int var arr2 = [3]int{1,2,3} arr3 := [...]int{1,2,3} 在词法及语法解析时,上述三种方式声明的数组

  • golang通过context控制并发的应用场景实现

    golang 里出现多 goroutine 的场景很常见, 最常用的两种方式就是 WaitGroup 和 Context, 今天我们了解一下 Context 的应用场景 使用场景 场景一: 多goroutine执行超时通知 并发执行的业务中最常见的就是有协程执行超时, 如果不做超时处理就会出现一个僵尸进程, 这累计的多了就会有一阵手忙脚乱了, 所以我们要在源头上就避免它们 看下面这个示例: package main import ( "context" "fmt"

随机推荐