0

Golang-GC 笔记 - Semantics

 2 years ago
source link: https://greenhathg.github.io/2022/04/07/Golang-GC%E7%AC%94%E8%AE%B0-Semantics/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

GreenHatHG の Blog

Golang-GC 笔记 - Semantics

发表于 2022-04-07|更新于 2022-04-07|编程
字数总计:2.2k|阅读时长:8 分钟 | 阅读量:6| 评论数:0

来源:https://www.ardanlabs.com/blog/2018/12/garbage-collection-in-go-part1-semantics.html

Garbage collectors responsibility

  • tracking heap memory allocations
  • freeing up allocations that are no longer needed
  • keeping allocations that are still in-use

As of version 1.12, the Go programming language uses a non-generational concurrent tri-color mark and sweep collector.

非分代并发三色标记和扫描收集器

Collector Behavior

collection 工作会经历三个阶段

  • Mark Setup - STW(Stop The World)
  • Marking - Concurrent
  • Mark Termination - STW

Mark Setup - STW

collection 开始时候第一个活动就是打开 Write Barrier,目的是允许 collector 在收集期间保持堆上的数据完整性,因为 collection 和 application goroutines 会同时运行。

为了打开 Write Barrier,必须停止运行的 application goroutine,该动作通常会很快,10~30 microseconds。

在 collection 之前必须停止正在运行的 4 个 application goroutines,唯一停止的方法是让 collector 监视并等待每个 goroutine 进行一次 function call,function call 保证 goroutine 在 safe point 处停止。

假如其中一个 goroutine 没有进行 function call,而其他的都进行了,会发生什么呢

在 P4 上的 goroutine 停止之前,collection 无法启动,因为处于数学计算中。
https://github.com/golang/go/issues/10958

Marking - Concurrent

25% CPU capacity

一旦打开了 Write Barrier,collector 就进入了 Marking 阶段,第一件事就是为自己申请 25% 可用 CPU 容量(CPU capacity)的占用。

collector 使用 goroutine 完成 collection 工作,并且需要使用与 application goroutine 一样的 P 和 M。

对于 4 线程的 go 程序(4 threaded Go program),一个完整的 P 将专门用于 collection 工作。

Marking

接着进入 Marking 阶段,标记堆内存(heap memory)中仍在使用的值。

首先会检查所有现有 goroutine 的 stacks 以找到指向 heap memory 的 root pointers,然后 collector 必须从 root pointers 遍历 heap memory graph。Marking 进行时候,应用依旧可以在 P2、P3、P4 上继续进行。

如果 P1 上专门用于 GC 的 goroutine 在达到堆内存上限之前无法完成 Markding 工作,在这种情况下,新分配(new allocation)的速度必须放慢。collector 确定需要放慢分配速度后,会招募 application Goroutine 来协助 Marking 工作,这称为 MA(Mark Assist)。

collector 的一个目标是减少 MA 的需求。为了减少下一次 collection 工作需要的 MA,某一次 collection 工作可能需要大量的 MA,以便 collector 可以更早的开始下一轮 GC(理解:时间间隔短应用分配的内存也少,需要的 MA 也少)。

标记的速度:4 CPU-milliseconds per MB of live heap

估计标记阶段要运行多久:take the live heap size in MB and divide by 0.25 the number of CPUs*

Mark Termination - STW

Marking 完成后,下一个阶段是 Mark Termination,将会关闭 Write Barrier、执行各种清理任务、计算下一个收集目标。执行得同样迅速,60~90 microseconds。可以不通过 STW 实现以增加性能,但增加了复杂性,相比之下使用更好。

一旦 collection 工作完成,每个 P 都又可以被 application goroutine 使用。

Sweeping - Concurrent

collection 完成之后会发生另外一个动作:Sweeping,该动作会回收未标记的值的相关内存,当 application goroutine 尝试在堆内存中分配新值(allocate new values)的时候会触发该动作,产生的时间开销附加在堆内存分配产生的开销中。

12 个 hardware threads 可用于执行 goroutine

在 collection 期间(左上角蓝色 GC 部分),十二个 P 中的三个专用于 GC。goroutine2450、1978、2696 在这段时间内执行 Mark Assit 工作。collection 工作完成后,只有一个 P 专门用于 GC 并执行 Mark Termination 工作。收集完成后,所有的 P 又可以被 application goroutine 使用(除了玫瑰色的细细线条外)。

玫瑰色表示 gouroutine 执行 sweeping 工作而不是 application 工作,发生于 goroutine 尝试在堆内存中分配新值的时刻。

上图是执行了 sweep 工作 goroutine 的 stack trace

runtime.mallocgc 代表堆内存中分配新值,runtime.(*mcache).nextFree 代表 sweep 动作。

GC Percentage

runtime 中有个配置项目是 GC Percentage,决定下一个 collection 动作何时发生。

上一次 collection 动作完成后显示正在使用(收集阶段的任务就是标记正在使用的内存)的堆内存为 2MB(上图中的内存不一定是连续分配的,仅为了好理解),由于 GC Percentage 设置为了 100(默认值),那么下一次 collection 动作会发生于堆内存新分配达到 2MB 时或者在这之前。

GC Trace

GODEBUG=gctrace=1 ./app

gc 1405 @6.068s 11%: 0.058+1.2+0.083 ms clock, 0.70+2.5/1.5/0+0.99 ms cpu, 7->11->6 MB, 10 MB goal, 12 P

gc 1406 @6.070s 11%: 0.051+1.8+0.076 ms clock, 0.61+2.0/2.5/0+0.91 ms cpu, 8->11->6 MB, 13 MB goal, 12 P

gc 1407 @6.073s 11%: 0.052+1.8+0.20 ms clock, 0.62+1.5/2.2/0+2.4 ms cpu, 8->14->8 MB, 13 MB goal, 12 P

每次发生 collection 动作时候,runtime 都会将 GC trace information 写入到 stderr

GC1405

gc 1405 @6.068s 11%: 0.058+1.2+0.083 ms clock, 0.70+2.5/1.5/0+0.99 ms cpu, 7->11->6 MB, 10 MB goal, 12 P

// General
gc 1404 : The 1404 GC run since the program started
@6.068s : Six seconds since the program started
11% : Eleven percent of the available CPU so far has been spent in GC

// Wall-Clock
0.058ms : STW : Mark Start - Write Barrier on
1.2ms : Concurrent : Marking
0.083ms : STW : Mark Termination - Write Barrier off and clean up

// CPU Time
0.70ms : STW : Mark Start
2.5ms : Concurrent : Mark - Assist Time (GC performed in line with allocation)
1.5ms : Concurrent : Mark - Background GC time
0ms : Concurrent : Mark - Idle GC time
0.99ms : STW : Mark Term

// Memory
7MB : Heap memory in-use before the Marking started
11MB : Heap memory in-use after the Marking finished
6MB : Heap memory marked as live after the Marking finished
10MB : Collection goal for heap memory in-use after Marking finished

// Threads
12P : Number of logical processors or threads used to run Goroutines

在 Marking 动作之前,正在使用的堆内存量为 7MB,Marking 完成后,使用中的堆内存达到 11MB,意味着在 collection 期间发生了额外的 4MB 分配。Marking 完成后,标记为活动中的堆内存量为 6MB,这意味着应用程序在下一次 collection 开始之前可以将使用中的堆内存量增加到 12MB(理想情况)。

global 是根据使用中的堆内存量(before marking)、标记为活动中的堆内存量(after marking)、以及 collection 运行时发生的额外分配计算而来的。

GC1406

gc1406 发生在 gc1405 2ms 之后

gc1405 完成之后根据 GC Percentage 推测下一次触发时间为堆内存使用量达到 12MB,但是这个是理想情况,如果 collector 决定最好早点开始 collection 动作那么则会早点开始,因为应用程序分配了大量内存,而 collector 希望减少收集期间 Mark Assist 使用量,所以当使用量达到 8M 的时候就开始了下一轮。

Pacing

collector 有一个 pacing 算法用于确定何时开始 collection 工作。该算法依赖于 feedback loop,collector 用它来收集有关正在运行的应用程序和应用程序给堆施加的压力的信息,压力可以理解为给定时间内分配堆内存的速度,正是这种压力决定了 collector 的工作频率。

在 collector 开始 collection 工作之前,它会计算完成 collection 工作所需的时间。一旦 collection 工作运行,就会对在正在运行的应用程序上产生延迟,从而减慢应用程序的速度。

一个误解是认为降低 collector 的工作频率是提高性能的一种方式,比如将 GC Percentage 的值调大,这样可能就会导致 collection 频率降低?实际上这对于提高性能没有直接关系。

实际上与提高性能有关系的是每次 collection 完成之间(两个 collectopn 的间隔)应用程序执行的任务数量,可以通过减少堆内存的使用来影响到它以提高吞吐量。

蓝色版本没有任何优化,绿色版本则是发现并去掉了 4.48GB 无关的内存分配(non-productive memory allocations)后的结果。

两个版本的 collection 平均频率相差不大(2.08ms vs 1.96ms),根本变化是每个 collection 之间完成的工作量,从每个间隔平均处理 3.98 个请求到 7.13 个请求。collection 的频率并没有随着内存分配减少而减少,而是保存不变。

总的来说,尝试减少 collection 工作频率的措施并不是提高性能的方式,而减少 collector 运行需要的时间才是重要的,因为这个会减少因为延迟带来的成本。

当压力降低时候,collector 造成的延迟将会减少,而这种 GC 延迟又是减慢应用程序的主要原因,所以减少堆内存的压力是正确的方法。

collector 造成的影响有:

  • Marking - Concurrent 阶段会占用 25% CPU 性能
  • Mark Assist
  • STW(一般情况下花费时间少于 100 microsecond)

可以减少延迟的措施有:

  • 保持最少的使用堆内存
  • 最大限度缩短每次 collection、STW、Mark Assist 持续的时间

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK