• Go 1.19中终于实现了SetMemoryLimit的功能


    Go 1.19中终于实现了SetMemoryLimit的功能。Go的GC并不像Java那样提供了很多的参数可以调整,目前也就有GOGC这么一个参数,所以能增加一个可以调整GC的参数确实让人兴奋。

    一直关注Go性能同学一定知道,最近几年有两个调整Go GC的hack方式:

    • ballast[1]: 压舱石技术。使用一个"虚假"的内存占用,让Go运行时难以达到触发GC的阈值,来实现减少GC的次数,从而提高性能。如果你的程序的内存占用基本都会在某个阈值之下的话,这个技术非常有效,毕竟,Go很大的一部分性能消耗都是在GC上。这是twitch.tv的工程师提供的一种技术。
    • GOGC tuner[2]: 通过自动调整GOGC,来动态的调整GC的target,用来在内存足够的时候调整GOGC来减少GC的次数,这也是一个非常有趣有效的技术,在uber公司的实践中行之有效。这是uber工程师提供的一项技术,Uber的工程师并没有把它开源出来,不过曹大根据文章的原理实现了一个cch123/gogctuner[3]

    现在, Go 1.19 提供了SetMemoryLimit的功能,通过这个方法,可以替换ballast的方案,部分替换GOGC Tuner的方案。

    谈起这个功能的历史,可以追溯到2017年12月的#23044[4],它提议增加一个方法,可以指定最小的目标堆大小。这个issue大家讨论的热火朝天,结果就是2019年twitch.tv的工程师实现了ballast,从工程的角度验证了GC是可以优化,而且在实践中也有效。

    2021年Go team的工程师 Michael Knyszek 发起一个提案#44309[5],包括设计文档user configurable memory target[6]。这个提案的跟踪issue最终归于#48409[7]

    本来,这个提案预期在Go 1.18中实现,不过因为提案迟迟没有批准,所以最终会在Go 1.19中实现。

    在撰写本文的时候,Go 1.19还在开发之中,不过这个提案的功能已经实现,剩下的是一些文档和bug修复的工作了,所以我们可以使用gotip[8]来测试。

    这个提案的实现原来就是要实现(替换)ballast的功能,所以一旦Go 1.19发布, ballast的方案就可以废弃了。没想到今年突然Uber的工程师来了一个自动调整GOGC的方案,所以当前方案还不能完全代替GOGC tuner, 毕竟GOGC Tuner可以更灵活的调整GC的target,而SetMemoryLimit在设定的MemoryLimit之下,还是会频繁的进行GC, 如果加上GOGC=off的话,只能等待达到MemoryLimit才能GC,和GOGC Tuner的方式还有有所不同的,所以并不能完全替代GOGC tuner。

    详细的 GC调优指导的官方文档[9]还没有完成,大家也可以关注一下,看看官方的建议。

    This page is currently a work-in-progress and is expected to be complete by the time of the Go 1.19 release. See this tracking issue[10] for more details.

    即使官方文档还没有完成,依照提案的内容,我们还是可以早点了解这个提案的功能以及带给我们的收益。

    下面通过四个场景,观察一下此功能对GC的影响:

    • SetMemoryLimit + GOGC=off + MemoryLimit足够大
    • SetMemoryLimit + GOGC=off + MemoryLimit不足够大
    • SetMemoryLimit + GOGC=100 + MemoryLimit足够大
    • SetMemoryLimit + GOGC=100 + MemoryLimit不足够大

    基本例子

    本文通过Debian的benchmarks game中的btree例子[11]演示这四个场景。

    因为这个例子会频繁生成生成二叉树,正适合内存分配和回收的场景。

    package main

    import (
     "flag"
     "fmt"
     "sync"
     "time"
    )

    type node struct {
     next *next
    }

    type next struct {
     left, right node
    }

    func create(d int) node {
     if d == 1 {
      return node{&next{node{}, node{}}}
     }
     return node{&next{create(d - 1), create(d - 1)}}
    }

    func (p node) check() int {
     sum := 1
     current := p.next
     for current != nil {
      sum += current.right.check() + 1
      current = current.left.next
     }
     return sum
    }

    var (
     depth = flag.Int("depth", 10, "depth")
    )

    func main() {
     flag.Parse()

     start := time.Now()
     const MinDepth = 4
     const NoTasks = 4
     maxDepth := *depth

     longLivedTree := create(maxDepth)

     stretchTreeCheck := ""
     wg := new(sync.WaitGroup)
     wg.Add(1)
     go func() {
      stretchDepth := maxDepth + 1
      stretchTreeCheck = fmt.Sprintf("stretch tree of depth %d\t check: %d",
       stretchDepth, create(stretchDepth).check())
      wg.Done()
     }()

     results := make([]string, (maxDepth-MinDepth)/2+1)
     for i := range results {
      depth := 2*i + MinDepth

      n := (1 << (maxDepth - depth + MinDepth)) / NoTasks

      tasks := make([]int, NoTasks)
      wg.Add(NoTasks)
      // 执行NoTasks个goroutine, 每个goroutine执行n个深度为depth的tree的check
      // 一共是n*NoTasks个tree,每个tree的深度是depth
      for t := range tasks {
       go func(t int) {
        check := 0
        for i := n; i > 0; i-- {
         check += create(depth).check()
        }
        tasks[t] = check
        wg.Done()
       }(t)
      }

      wg.Wait()
      check := 0 // 总检查次数
      for _, v := range tasks {
       check += v
      }
      results[i] = fmt.Sprintf("%d\t trees of depth %d\t check: %d",
       n*NoTasks, depth, check)
     }

     fmt.Println(stretchTreeCheck)

     for _, s := range results {
      fmt.Println(s)
     }

     fmt.Printf("long lived tree of depth %d\t check: %d\n",
      maxDepth, longLivedTree.check())

     fmt.Printf("took %.02f s", float64(time.Since(start).Milliseconds())/1000)
    }

    可以使用gotip build main.go生成Go 1.19编译的二进制文件。

    后面的例子中我并没有使用debug.SetMemoryLimit设置MemoryLimit,而是使用环境变量GOMEMLIMIT

    SetMemoryLimit + GOGC=off + MemoryLimit足够大

    首先使用gotip build main.go编译出可执行的二进制文件soft_memory_limit

    运行 GOMEMLIMIT=10737418240 GOGC=off GODEBUG=gctrace=1 ./soft_memory_limit -depth=21查看效果:

    图片

    这里我设置的MemoryLimit为10G,整个程序中并没有达到这个内存阈值,所以没有GC发生。

    是不是和设置ballast的效果一样。

    SetMemoryLimit + GOGC=off + MemoryLimit不足够大

    我们将MemoryLimit设置为1G,看看GC的表现(GOMEMLIMIT=1073741824 GOGC=off GODEBUG=gctrace=1 ./soft_memory_limit -depth=21):

    图片

    可以看到程序的运行过程内存占用还是能够触达阈值1G的,这会导致几次的垃圾回收,整体运行时间和case1差别不到,原因是GC回收仅仅几次,可以忽略。

    如果你把阈值设置更小,比如缩小10倍(GOMEMLIMIT=107374182 GOGC=off GODEBUG=gctrace=1 ./soft_memory_limit -depth=21),可以看到更频繁的垃圾回收,程序整体运行时间也显著增加:

    图片

    SetMemoryLimit + GOGC=100 + MemoryLimit足够大

    为了达到ballast的效果,前面的case都把GOGC设置为了off,如果我们设置为默认值100呢?

    GOMEMLIMIT=10737418240 GOGC=100 GODEBUG=gctrace=1 ./soft_memory_limit -depth=21

    图片

    可以看到,会有大量的GC事件,并且很多并没有达到阈值就发生GC了。这也是显而易见的,因为在没有达到MemoryLimit阈值的情况下,还是遵循GOGC的target决定要不要进行垃圾回收。

    在这种情况下,可以使用GOGC tuner进行调优,避免这么多次的垃圾回收。

    SetMemoryLimit + GOGC=100 + MemoryLimit不足够大

    如果设置的MemoryLimit不足够大,在内存触达MemoryLimit的时候也会触发GC,只不过因为没有关闭GOGC,所以GOGC和触达MemoryLimit两种情况下都有可能触发GC,程序整体运行还是比较慢的。

    图片

    综上所述,通过SetMemoryLimit设置一个较大的值,再加上 GOGC=off,可以实现ballast的效果。

    但是在没有关闭GOGC的情况下,还是有可能会触发很多次的GC,影响性能,这个时候还得GOGC Tuner调优,减少触达MemoryLimit之前的GC次数。

    参考资料

    [1]

    ballast: https://blog.twitch.tv/en/2019/04/10/go-memory-ballast-how-i-learnt-to-stop-worrying-and-love-the-heap/

    [2]

    GOGC tuner: https://eng.uber.com/how-we-saved-70k-cores-across-30-mission-critical-services/

    [3]

    cch123/gogctuner: https://github.com/cch123/gogctuner

    [4]

    #23044: https://github.com/golang/go/issues/23044

    [5]

    #44309: https://github.com/golang/go/issues/44309

    [6]

    user configurable memory target: https://github.com/golang/proposal/blob/7f0d01687e030f21e8bdc36dfd9d5aac3a6f4a71/design/44309-user-configurable-memory-target.md

    [7]

    #48409: https://github.com/golang/go/issues/48409

    [8]

    gotip: https://pkg.go.dev/golang.org/dl/gotip

    [9]

    官方文档: https://tip.golang.org/doc/gc-guide

    [10]

    tracking issue: https://tip.golang.org/doc/gc-guide#:~:text=This%20page%20is%20currently%20a%20work%2Din%2Dprogress%20and%20is%20expected%20to%20be%20complete%20by%20the%20time%20of%20the%20Go%201.19%20release.%20See%20this%20tracking%20issue%20for%20more%20details.

    [11]

    btree例子: https://benchmarksgame-team.pages.debian.net/benchmarksgame/program/binarytrees-go-2.html

  • 相关阅读:
    jquery 表单清空
    CK-Editor content.replace
    CSS DIV HOVER
    返回上一页并刷新与返回上一页不刷新代码
    Google Java编程风格指南中文版
    编程常见英语词汇
    教你如何删除tomcat服务器的stdout.log文件
    @Autowired @Resource @Qualifier的区别
    JSTL标签,EL表达式,OGNL表达式,struts2标签 汇总
    4.11 application未注入报错解决
  • 原文地址:https://www.cnblogs.com/ExMan/p/16393484.html
Copyright © 2020-2023  润新知