| 注册
请输入搜索内容

热门搜索

Java Linux MySQL PHP JavaScript Hibernate jQuery Nginx
lohc0791
5年前发布

Golang 实时垃圾回收理论和实践

   <h2>Golang实时垃圾回收理论和实践</h2>    <p>每天,Pusher实时发送数十亿条消息:从消息源到达目的地控制在100ms内。 我们如何实现这一目标? 一个关键因素是Go的低延迟垃圾回收器。</p>    <p>垃圾收集器是实时系统的祸根,因为他们会暂停程序。 因此,在设计我们的新消息总线时,我们仔细选择了语言。 虽然Go强调低延迟垃圾回收,但我们很警惕:Go真的做到这一点吗? 如果能做到,效果如何呢?</p>    <p>在这篇博文中,我们会审视Go的垃圾收集器。 我们将看看它是如何工作的(三色算法),为什么它有这样短的GC暂停,最重要的是,它是否工作(对其进行GC基准测试,并与其他语言进行比较)。</p>    <h2>From Haskell to Go</h2>    <p>我们一直在构建的系统是一个带有已发布消息内存存储的pub / sub消息总线。 Go中的这个版本是Haskell版本的重新实现。在发现GHC的垃圾收集器的延迟问题后,我们在5月停止了在Haskell版本的工作。</p>    <p>我们发布了Haskell版本的细节。基本问题是GHC的暂停时间与工作集的大小(即内存中的对象数量)成正比。在我们的例子中,我们在内存中有很多对象,这导致了几百毫秒的暂停时间。任何GC在完成收集之前都会阻塞程序。</p>    <p>Go与GHC的STW(stop-the-world)收集器不同,Go的垃圾回收器与程序同时运行,这使得避免更长的停顿时间成为可能。我们对Go的低延迟垃圾回收感到鼓舞,并发现随着版本改进延迟得到进一步降低。</p>    <h2>并发垃圾收集器如何工作?</h2>    <p>Go的GC如何实现并发?其 核心是三色标记扫描算法。 它使GC与程序同时运行; 这意味着暂停时间成为调度问题。 调度程序可以配置为仅在短时间内运行GC收集,与程序交叉运行。 这对我们的低延迟要求是个好消息!</p>    <p>GC仍然有两个暂停阶段:对根对象的初始堆栈扫描,以及标记终止阶段。 令人兴奋的是,这个终止阶段最近已经消除。 我们将在后面讨论这个优化。 在实践中,我们发现即使具有非常大的堆,这些阶段的暂停时间也可以<1ms。</p>    <p>使用并发GC,也有可能在多个处理器上并行运行GC。</p>    <h2>延迟VS吞吐量</h2>    <p>如果使用并发GC可以在大堆上得到低得多的延迟,为什么要使用stop-the-world收集器?是不是Go的并发垃圾收集器比GHC的stop-the-world收集器更好吗?</p>    <p>不必要。低延迟有成本。最重要的成本是减少吞吐量。并发性需要额外的工作来同步和复制,这会减少程序正常运行的时间。 GHC的垃圾收集器针对吞吐量进行了优化,但Go收集器对延迟进行优化。在Pusher,我们关心延迟,所以这是一个对我们来说很好的折衷。</p>    <p>并发垃圾收集器的第二个成本是不可预测的堆增长。程序可以在GC运行时分配任意数量的内存。这意味着GC必须在堆达到目标最大大小之前运行。但是如果GC运行得太快,那么将执行更多的收集工作。这种权衡是棘手的(Austin Clements提供了一个很好的概述)。在Pusher,这种不可预测性不是一个问题;我们的程序倾向于以可预测的恒定速率分配内存。</p>    <h2>在实践中如何?</h2>    <p>到目前为止,Go的GC看起来很适合我们的延迟要求。 但它在实践中如何?</p>    <p>今年早些时候,当调查Haskell实现的暂停时间时,我们为测量暂停创建了一个基准。 基准程序重复地将消息推送到大小受限的缓冲区中。 旧消息不断地过期并变成垃圾。 堆大小保持很大,这很重要,因为必须遍历堆才能检测哪些对象仍被引用。 这就是为什么GC运行时间与它们之间的活对象/指针的数量成正比。</p>    <p>这里是Go中的基准,其中缓冲区被建模为数组:</p>    <pre>  <code class="language-go">package main    import (          "fmt"          "time"  )    const (          windowSize = 200000          msgCount   = 1000000  )    type (          message []byte          buffer [windowSize]message  )    var worst time.Duration    func mkMessage(n int) message {          m := make(message, 1024)          for i := range m {                  m[i] = byte(n)          }          return m  }    func pushMsg(b *buffer, highID int) {          start := time.Now()          m := mkMessage(highID)          (*b)[highID%windowSize] = m          elapsed := time.Since(start)          if elapsed > worst {                  worst = elapsed          }  }    func main() {          var b buffer          for i := 0; i < msgCount; i++ {                  pushMsg(&b, i)          }          fmt.Println("Worst push time: ", worst)  }</code></pre>    <p>根据James Fisher的博客,Gabriel Scherer写了一篇后续博客文章,将原来的Haskell基准与OCaml和Racket的版本进行比较。 他创建了一个包含这些基准的 仓库 ,Santeri Hiltunen添加了一个 Java版本 。 我决定将基准移植到Go,以便比较它的效果。</p>    <p>不用多说,这里是我的系统上的基准测试结果:</p>    <p>在这里是Java表现很差,OCaml表现非常好。 OCaml的~3ms暂停时间是由于OCaml用旧一代的增量GC算法。 (我们不选择OCaml的主要原因是它的并发支持很差)。</p>    <p>如你所见,Go执行顺利,暂停时间约为7ms。 这达到我们的要求。</p>    <h2>一些注意事项</h2>    <p>警惕基准!不同的运行时针对不同的用例和不同的平台进行了优化。然而,由于我们有明确的延迟要求,并且这个基准代表我们的用例,它表明Go对我们来说很好。</p>    <p>map vs array - 最初我们的基准是基于从map中插入和删除项目。然而,Go的垃圾收集器在处理大map的时候有 bug ,这掩盖了我们的结果。为此,我们决定切换为可变数组的map。Go Map bug在Go 1.8中已经修复,但是并不是所有的基准都被移植到1.8,这就是为什么我要区分这两者。尽管如此,没有理由期望GC时间比map(除了错误或不良实现)更糟糕。</p>    <p>手动vs rts计时 - 作为第二个警告,基准在计时方面不同:一些基准使用手动计时器,但其他使用运行时系统统计。存在此差异,因为某些运行时不会使该统计信息可用(例如在Go中)。我们还担心,打开profiling会对影响一些语言的垃圾收集器。为此,我们将所有基准移植到手动计时。</p>    <p>最后一个警告是基准实现中的最坏情况。有一种情况, insert/delete map操作可能不利地影响定时,这是切换到使用简单数组的另一个原因。</p>    <p>请为我们的基准贡献更多的语言!这个简单的基准是非常通用的,在选择语言时是一个重要的基准。你想看看$ YOUR_LANGUAGE的GC执行情况,然后请提交PR! :)我会特别感兴趣的是知道为什么Java暂停时间是如此糟糕,因为按理论它应该更好。</p>    <h2>为什么Go的结果不好?</h2>    <p>使用已修复mapbug的编译器,或使用数组时,我们得到暂停时间~7ms。 这是非常好的,但是根据Go团队在演示幻灯片标题为“1.5 Garbage Benchmark Latency”的基准测试结果,我们预计我们的堆大小为200MB时暂停大约1m(GC次数往往与 指针数量而不是字节数,但是它们不能提供该信息)。 Twitch团队使用Go1.7达到约1ms的暂停时间(尽管它们不清楚堆对象的数量)。</p>    <p>我在golang-nuts邮件列表问这个原因。 Rhys Hilter的想法是,这些暂停时间可能是由这个当前未定的错误引起的,GC中的空闲标记worker可能会阻止程序,即使有工作要做。 为了尝试并确认这一点,我启动了go tool trace [3],它可视化程序运行时行为。</p>    <p>从这个例子可以看出,有一个12ms的周期,其中背景标记woker正在所有四个处理器上运行,阻塞程序。这使我强烈怀疑我遇到上述错误。</p>    <p>到目前为止,我很高兴看到基准测试的现有暂停时间满足要求,但我也保持关注,以解决上述问题。</p>    <p>如前所述,Go团队最近宣布了一项改进措施,导致GC暂停时间小于1ms。它燃起我的希望,但我很快就意识到这个优化是去掉了stw阶段,swt阶段在我使用的基准下已经<1ms。我们的暂停时间主要是由GC的并发阶段引起的。</p>    <p>尽管如此,这是一个值得欢迎的改进,并表明团队继续关注并改进GC的延迟。这种优化的技术描述本身是一个有趣的读物。</p>    <h2>结论</h2>    <p>这项调查的关键是GC是针对更低的延迟或更高的吞吐量进行优化。程序可能执行更好或更差在这取决于您的程序的堆使用率。 (有很多的对象吗?他们有长或短的生命吗?)</p>    <p>重要的是要了解底层的GC算法,以决定它是否适合您的用例。在实践中测试GC实现也很重要。您的基准测试应该与您打算实现的程序具有相同的堆使用率。这将在实践中验证GC实现的有效性。正如我们所看到的,Go的实现不是没有bug的,但在我们的情况下,问题是可以接受的。我想在更多的语言中看到相同的基准,如果你想贡献的话:)</p>    <p>尽管存在一些问题,Go的GC与其他GC语言相比表现良好。 Go团队一直在改进延迟,并继续这样做。我们对Go语言GC的理论和实践感到满意。</p>    <p> </p>    <p> </p>    <p>来自:http://mp.weixin.qq.com/s/IX0zO2MliP01ixtLtih6gQ</p>    <p> </p>    
 本文由用户 lohc0791 自行上传分享,仅供网友学习交流。所有权归原作者,若您的权利被侵害,请联系管理员。
 转载本站原创文章,请注明出处,并保留原始链接、图片水印。
 本站是一个以用户分享为主的开源技术平台,欢迎各类分享!
 本文地址:https://www.open-open.com/lib/view/open1481180990331.html
Go语言 Haskell OCaml Google Go/Golang开发