Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

การเข้าใจและแก้ไข Ruby Memory Leaks: คู่มือฉบับสมบูรณ์

ในส่วนแรกของชุด 2 ส่วนนี้เกี่ยวกับ memory leaks เราได้ดูว่า Ruby จัดการหน่วยความจำอย่างไร และ Garbage Collection (GC) ทำงานอย่างไร

คุณอาจซื้อเครื่องที่ powerful พร้อมหน่วยความจำมากขึ้นได้ และแอปของคุณอาจ restart บ่อยพอที่ผู้ใช้ไม่สังเกต แต่การใช้หน่วยความจำมีความสำคัญ

Allocation และ Garbage Collection ไม่ฟรี หากคุณมี leak คุณจะใช้เวลา越来越多 在 Garbage Collection 上而不是做你建立应用程序要做的事情。

ในโพสต์นี้ เราจะลึกลงไปในเครื่องมือที่คุณสามารถใช้เพื่อค้นพบและวินิจฉัย memory leak

ต่อไปกันเถอะ!

การค้นหา Leaks ใน Ruby

การตรวจจับ leak เรียบง่ายพอ คุณสามารถใช้ GC, ObjectSpace, และ RSS graphs ใน APM tool ของคุณเพื่อดูการเพิ่มขึ้นของ memory usage แต่แค่รู้ว่าคุณมี leak ไม่เพียงพอที่จะแก้ไข คุณต้องรู้ว่ามันมาจากไหน ตัวเลขดิบไม่บอกคุณได้

โชคดีที่ Ruby ecosystem มีเครื่องมือยอดเยี่ยมบางอย่างในการแนบ context เข้ากับตัวเลขเหล่านั้น สองตัวคือ memory-profiler และ derailed_benchmarks

memory_profiler ใน Ruby

Gem memory_profiler ให้ API ที่เรียบง่ายและรายงาน allocated และ retained memory ที่ละเอียด (แม้จะมี点 overwhelming) — รวมถึง classes ของ objects ที่ allocated, ขนาดของพวกเขา และที่พวกเขาถูก allocated มันตรงไปตรงมาในการเพิ่มเข้า leaky program ของเรา

 

Output รายงานที่ดูคล้ายกับนี้

 

มีข้อมูลมากที่นี่ แต่โดยทั่วไป ส่วน allocated objects by location และ retained objects by location สามารถเป็นประโยชน์ที่สุดเมื่อหา leaks นี่คือ file locations ที่ allocate objects เรียงตามจำนวน allocated objects

  • allocated objects คือ objects ทั้งหมดที่ allocated (สร้าง) ภายในบล็อก report
  • retained objects คือ objects ที่ไม่ได้ถูก garbage collected จนจบบล็อก report เรา force GC run ก่อนจบบล็อกเพื่อให้เห็น leaked objects ชัดขึ้น

ระมัดระวังในการเชื่อถือ retained object counts พวกเขา dependent มากบนสัดส่วนของ leaking code ที่อยู่ในบล็อก report

ตัวอย่างเช่น หากเรา move การประกาศ an_array เข้าไปในบล็อก report เราอาจถูกหลอกคิดว่าโค้ดไม่ leak

 

ด้านบนของรายงานที่ได้จะไม่รายงาน retained objects มาก (แค่ report เอง)

 

derailed_benchmarks ใน Ruby

Gem derailed_benchmarks เป็น suite ของเครื่องมือที่มีประโยชน์มากสำหรับ performance work ทุกประเภท โดยเน้น Rails apps สำหรับการหา leaks เราต้องการดู perf:mem_over_time, perf:objects, และ perf:heap_diff

Tasks นี้ทำงานโดยส่ง curl requests ไปยัง running app ดังนั้นเราไม่สามารถเพิ่มพวกเขาเข้า leaky program เล็กของเราได้ แทนที่จะเราต้องตั้งค่า Rails app เล็กๆ ที่มี endpoint ที่ leak memory แล้วติดตั้ง derailed_benchmarks บน app นั้น

 
 

ตอนนี้คุณควรสามารถ boot app ด้วย bin/rails s คุณจะสามารถ curl endpoint ที่ leak ทุก request

 

ตอนนี้เราสามารถใช้ derailed_benchmarks เพื่อดู leak ของเราในการทำงาน

perf:mem_over_time

นี่จะแสดง memory use ตามเวลา (คล้ายกับที่เราดู memory growth ของ leaky script ด้วย watch และ ps)

Derailed จะ boot app ใน production mode, hit endpoint ซ้ำๆ (/ by default) และรายงาน memory usage หากมันไม่หยุดเติบโต เรามี leak!

 

หมายเหตุ: Derailed จะ boot Rails app ใน production mode เพื่อทำ tests โดยค่าเริ่มต้น มันจะ require rails/all ก่อนด้วย เนื่องจากเราไม่มี database ใน app นี้ เราต้อง override พฤติกรรมนี้ด้วย DERAILED_SKIP_ACTIVE_RECORD=true

เราสามารถรัน benchmark นี้ต่อ endpoints ต่างๆ เพื่อดูว่าตัวไหน (ถ้ามี) leak

perf:objects

Task perf:objects ใช้ memory_profiler ใต้ฝาผืน ดังนั้นรายงานที่ผลิตจะดูคุ้นเคย

 

รายงานนี้ช่วย narrow down ว่าหน่วยความจำที่ leak ถูก allocate ที่ไหน ในตัวอย่างของเรา ส่วนสุดท้ายของรายงาน — Retained String Report — บอกเราทันทีว่าปัญหาของเราคืออะไร

 

เรา leak 10,000 strings ที่มี "ABC" จาก LeaksController บรรทัด 3 ใน non-trivial app รายงานนี้จะใหญ่กว่ามากและมี retained strings ที่คุณต้องการ retain — query caches, ฯลฯ — แต่ส่วนนี้และส่วน 'by location' อื่นๆ ควรช่วย narrow down leak ของคุณ

perf:heap_diff

Benchmark perf:heap_diff ช่วยได้หากรายงานจาก perf:objects ซับซ้อนเกินไปที่จะดูว่า leak ของคุณมาจากไหน

如名字所示,perf:heap_diff 产生三个堆转储并计算它们之间的差异。它创建一个包含保留对象类型及其分配位置的报告。

 

您还可以阅读 Tracking a Ruby memory leak in 2021 以更好地了解发生了什么。

报告准确地指明了我们针对这个 leaky baby app 需要去的地方。在差异的顶部,我们看到了 999991 个保留的字符串对象,它们是从 LeaksController 第 3 行分配的。

Leaks ในแอป Ruby และ Rails จริง

希望我们至今使用的例子从未被投入实际应用——我希望没有人打算泄露内存!

在非平凡的应用中,内存泄漏可能更难追踪。Retained objects 不总是坏事——一个带有垃圾收集项目的缓存不会有多大用处。

但所有泄漏之间有一些共同点。在某个地方,一个 root-level 对象(一个类/全局变量等)持有对一个对象的引用。

一个常见的例子是没有限制或驱逐策略的缓存。根据定义,这将泄露内存,因为放入缓存的每个对象将永远保留。随着时间的推移,这个缓存将占用越来越多的应用内存,其中实际使用的百分比越来越小。

考虑以下代码,它获取游戏的高分。这类似于我过去见过的东西。这是一个昂贵的请求,当它改变时我们可以轻松地使缓存失效,所以我们想缓存它。

 

@scores hash 完全未被检查。它将增长以保存每个用户的每个高分 — 如果你有很多用户或分数,这并不理想。

在 Rails 应用中,我们可能想使用带有合理过期时间的 Rails.cache(Redis 中的内存泄漏仍然是内存泄漏!)

在非 Rails 应用中,我们想限制 hash 大小,驱逐最旧或最少最近使用的项目。LruRedux 是一个不错的实现。

这个泄漏的一个更微妙的版本是有限制的缓存,但其 keys 是任意大小的。如果 keys 本身增长,缓存也会增长。通常你不会遇到这个。但如果你将对象序列化为 JSON 并用作 key,请仔细检查你是否也在序列化那些随使用增长的东西 — 例如用户已读消息的列表。

Circular References

Circular references 可以 被垃圾收集。Ruby 中的垃圾收集使用 "Mark and Sweep" 算法。在他们的演示中介绍 variable width allocation 时,Peter Zhu 和 Matt Valentine-House 很好地解释了这个算法的工作原理。

本质上,有两个阶段:marking 和 sweeping。

  • 在 marking 阶段,垃圾收集器从 root 对象(类、全局变量等)开始,标记它们,然后查看它们引用的对象。

    然后它标记所有被引用的对象。已经被标记的被引用对象不会被再次查看。这将继续直到没有更多对象可看 — 即所有被引用的对象都被标记了。

  • 垃圾收集器然后移至 sweeping 阶段。任何未被标记的对象将被清理。

因此,具有 live references 的对象仍然可以被清理。只要 root 对象最终不引用一个对象,它就会被收集。以这种方式,具有 circular references 的对象簇仍然可以被垃圾收集。

Application Performance Monitoring: Event Timeline 和 Allocated Objects Graph

如这系列的第一部分所述,任何 production-level 应用都应该使用某种形式的 Application Performance Monitoring (APM)。

有许多选项可用,包括自己构建(仅推荐给较大的团队)。APM 的一个关键特性是能够看到一个 action (或 background job) 进行的分配数量。好的 APM 工具会分解这一点,给出关于分配来自哪里的见解 — controller, view 等。

这通常被称为 'event timeline'。如果您的 APM 允许您编写自定义代码进一步分解时间线,则加分。

考虑以下 Rails controller 代码。

 

当由 APM 报告时,'event timeline' 可能看起来像来自 AppSignal 的以下屏幕截图。

การเข้าใจและแก้ไข Ruby Memory Leaks: คู่มือฉบับสมบูรณ์

这可以被 instrumented 以便我们可以看到代码的哪一部分在时间线中进行分配。在真实应用中,这可能从代码中不那么明显 😅

 

这是一个 instrumented event timeline 的例子,再次来自 AppSignal:

การเข้าใจและแก้ไข Ruby Memory Leaks: คู่มือฉบับสมบูรณ์

知道在哪里 instrument 通常很难掌握。没有替代品真正理解您的应用程序代码,但有一些信号可以作为 'smells'。

如果您的 APM 显示 GC runs 或 allocations 随时间变化,您可以寻找 spikes 看它们是否与某些 endpoints 被 hit 或某些正在运行的 background jobs 匹配。这是来自 AppSignal 的 Ruby VM magic dashboard 的另一个例子:

การเข้าใจและแก้ไข Ruby Memory Leaks: คู่มือฉบับสมบูรณ์

通过这种方式查看 allocations,我们可以在调查内存问题时缩小搜索范围。这使得使用像 memory_profiler 和 derailed_benchmarks 这样的工具变得更加高效。

阅读关于 AppSignal Ruby gem 最新添加的内容,如 allocation 和 GC stats tracking。

总结

在这篇文章中,我们深入探讨了一些可以帮助查找和修复内存泄漏的工具,包括 memory_profiler, derailed_benchmarks, perf:mem_over_time, perf:objects, perf:heap_diff, AppSignal 中的 event timeline 和 allocated objects graph。

我希望您发现这篇文章,连同第一部分,对诊断和解决您的 Ruby 应用中的内存泄漏有用。

阅读更多关于我们使用的工具:

  • memory_profiler
  • derailed_benchmarks
  • Leaky Rails app

额外的详细阅读:

  • GC 模块文档
  • ObjectSpace 模块文档
  • Garbage Collection Deep Dive
  • Variable Width Allocation

快乐编码!

P.S. 如果您想在 Ruby Magic 文章一发表就阅读,请订阅我们的 Ruby Magic 时事通讯,永远不错过任何一篇文章!