Memory leak คือการเพิ่มขึ้นของการใช้หน่วยความจำที่ไม่ได้ตั้งใจ ไม่ได้ควบคุม และไม่มีที่สิ้นสุด ไม่ว่าจะเล็กแค่ไหน สุดท้าย leak จะทำให้ process ของคุณหมดหน่วยความจำและ crash แม้คุณจะ restart แอปเป็นระยะเพื่อหลีกเลี่ยง crash นี้ (ไม่ตัดสิน คุณผมทำมาก่อน!) คุณยังคงต้องทนกับ performance implications ของ memory leak
ในโพสต์นี้ ส่วนแรกของชุด 2 ส่วนเกี่ยวกับ memory leaks เราจะเริ่มด้วยการดูว่า Ruby จัดการหน่วยความจำอย่างไร Garbage Collection (GC) ทำงานอย่างไร และวิธีหา leak
ในส่วนที่สอง เราจะเจาะลึกการติดตาม leaks
ลองเริ่มกันเถอะ!
Ruby Memory Management
Ruby objects ถูกจัดเก็บบน heap และแต่ละ object เติมหนึ่ง slot บน heap
ก่อน Ruby 3.1 ทุก slots บน heap มีขนาดเท่ากัน — 40 bytes เพื่อความแม่นยำ Objects ที่ใหญ่เกินกว่าจะวางใน slot ถูกจัดเก็บนอก heap แต่ละ slot รวม reference ไปที่ที่ objects ถูกย้าย
ใน Ruby 3.1 variable width allocation สำหรับ objects String ถูก merged เร็วๆ นี้ variable width allocation จะเป็นมาตรฐานสำหรับทุกประเภท object
Variable width allocation มุ่งปรับปรุงประสิทธิภาพโดยปรับปรุง cache locality — ข้อมูลทั้งหมดของ object จะถูกจัดเก็บในที่เดียว แทนที่จะกระจายข้ามสอง memory locations
มันยังควรทำให้ (บางส่วนของ) memory management เรียบง่ายขึ้น ตอนนี้มีสอง 'heaps':
- Ruby heap (หรือ GC heap) ที่จัดเก็บ Ruby objects ขนาดเล็ก
- C heap (หรือ malloc/transient heap) ที่จัดเก็บ objects ขนาดใหญ่
เมื่อ variable width allocation กลายเป็นมาตรฐาน ไม่ควรมีความจำเป็นสำหรับ heap ตัวหลังแล้ว
Heap เริ่มต้นที่ขนาดที่กำหนด (10,000 slots โดยค่าเริ่มต้น) และ objects ถูก assigned ให้ free slots เมื่อพวกเขาถูกสร้าง เมื่อ Ruby พยายามสร้าง object และไม่มี free slots ให้ Garbage Collection (GC) จะเกิดขึ้นเพื่อทำให้มี free slots ให้ใช้
หากมี free slots น้อยเกินไปหลัง GC heap จะถูกขยาย (เพิ่มเติมเกี่ยวกับนี้นิดหน่อย)
นี่คือ factors ที่คุณควบคุมได้ พร้อม environment variables ของพวกเขา:
- ขนาดเริ่มต้นของ heap -
RUBY_GC_HEAP_INIT_SLOTS - จำนวน free slots ที่ควรมีหลัง GC เกิดขึ้น -
RUBY_GC_HEAP_FREE_SLOTS - ปริมาณที่ heap ขยาย -
RUBY_GC_HEAP_GROWTH_FACTOR
Garbage Collection ใน Ruby
Garbage Collection ใน Ruby 'stops the world' — ไม่มี process อื่นเกิดขึ้นเมื่อ GC เกิดขึ้น Garbage Collection ใน Ruby (ตั้งแต่ 2.1) ยังเป็น generational หมายความว่า garbage collector มีสองโหมด:
- Minor GC - ตรวจสอบ 'young' objects (objects ที่สร้างเมื่อเร็วๆ นี้)
- Major GC - ตรวจสอบ 'old' objects รวมถึง 'young' objects (ทั้งหมด objects)
หมายเหตุ: Object 'old' คือ object ที่รอดชีวิต 3 GC runs ไม่ว่าจะ major หรือ minor
เมื่อ heap เต็ม minor GC จะถูก invoke ก่อน หากมันไม่สามารถ free slots ให้เพียงพอต่ำกว่า limit major GC จะถูก invoke เฉพาะแล้ว หากยังมี free slots ไม่พอ heap จะถูกขยาย
Major GC แพงกว่า minor GC เพราะมันดู objects มากกว่า
ทฤษฎีเบื้องหลังทำไม generational GC ถึงมีประสิทธิภาพมากกว่าคือ objects มักจะอยู่ในสองหมวดหมู่:
- Objects ที่ allocated และเร็วๆ นี้ out of scope ในแอป Rails models ที่ fetched จาก DB เพื่อ render หน้าจะ out of scope เมื่อ request จบ
- Objects ที่ allocated และเก็บไว้นาน Classes และ caches น่าจะยังในใช้ตลอดอายุของแอป
Major GC จะรันหลัง minor GC ด้วย หากจำนวน old objects อยู่เหนือ threshold เหล่านี้ แม้จะมี free slots พอ จำกัดนี้เพิ่มขึ้นตามขนาด heap และสามารถควบคุมได้โดย environment variable RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR
เมื่อคุณมี leak คุณสร้าง objects ที่ไม่สามารถ clean up ได้ — 越来越多 old objects นี่หมายความว่า major (expensive) GC จะรันบ่อยกว่าที่ควรเป็นมาก เนื่องจากไม่มีอะไรทำงานเมื่อ GC กำลังรัน นี่คือเวลาที่คุณสูญเสีย
ฉันวางลิงก์บางอย่างไว้ท้ายบทความสำหรับการอ่านเพิ่มเติมเกี่ยวกับ memory layout และ garbage collector ใน Ruby
Memory Leak ใน Ruby 长什么样?
คุณสามารถเห็น memory leak ได้ด้วยเครื่องมือง่ายๆ ที่มีบน Unix system ใดๆ นำโค้ดต่อไปนี้เป็นตัวอย่าง
การพูดว่าโค้ดนี้ 'leak' มี点不公平 — มันแค่ leak! — แต่มัน serve our purposes
เราสามารถสังเกต leak ได้อย่างง่ายจาก command line โดยรันโปรแกรมนี้ในเทอร์มินัลหนึ่งและ watch memory increase ตามเวลาด้วย ps
pgrep -f "ruby ./leaky.rb" หา process ID ให้เรา เพื่อที่เราจะ restrict ps output ให้แค่ process ที่เราสนใจ ตามที่คุณเดาได้ มันเหมือน grep สำหรับ processes
Tool watch 允许我们轮询给定命令的输出并就地更新 给我们一个终端内的实时仪表板。
您将获得如下输出,每隔几秒更新一次。
您应该看到 %MEM 和 RSS 增加。它们是:
%MEM- 进程使用的内存量占主机内存的百分比。RSS(resident set size) - 进程使用的 RAM 量(以字节为单位)。
这个基本的仅操作系统信息足以发现您是否有泄漏 — 如果内存一直上升,意味着您有泄漏!
使用 Garbage Collector Module 找到 Ruby Leaks
我们还可以在 Ruby 代码本身中使用 GC 模块检测泄漏。
GC.stat 方法将返回一个包含许多有用信息的哈希。在这里,我们对 :heap_live_slots 感兴趣,这是正在使用的堆上槽位的数量。这与 :heap_free_slots 相反。在循环结束时,我们强制进行 major GC 并打印出使用的槽位数量,即 GC 后剩余的对象数量。
当我们运行我们的小程序时,我们看到这个无限增加。我们有泄漏!我们也可以用 GC.stat(:old_objects) 达到相同效果。
虽然 GC 模块可用于查看我们是否有泄漏,并且(如果你聪明地使用 puts 语句)泄漏可能发生在哪里,我们可以用 ObjectSpace 模块查看可能泄漏的对象类型。
ObjectSpace.count_objects 方法返回一个包含 live objects 计数的哈希。T_STRING 例如,是内存中 live strings 的数量。对于我们相当 leaky 的程序,这个值每次循环都会增加,即使在 GC 之后。我们可以看到我们正在泄漏 string objects。
Application Performance Monitoring 在 Production ด้วย AppSignal
虽然玩 ps 和 GC 可能是 toy projects 的合理路线 — 它们也有趣且有启发性! — 我 不 推荐它们作为您在 production apps 中的 memory leak detection solution。
这是您将使用 Application Performance Monitoring (APM) 工具的地方。如果您是一家非常大的公司,您可以自己构建这些。对于较小的组织,选择现成的 APM 是正解。您确实需要支付月费,但它们提供的信息更值得。
对于检测内存泄漏,您想找到 server 或 process memory use (有时称为 RSS) graphs 随时间变化。这是 AppSignal 的 'process memory usage' dashboard 的例子,显示一个健康的应用程序在部署后不久:

这是部署后不健康的应用程序:

AppSignal 甚至会显示 Ruby VM stats 像 GC 和 heap slots,这可以给您更清晰的信号表明内存泄漏。如果 live slots 的数量持续增长,您就有泄漏!

阅读更多关于 AppSignal for Ruby
总结和进一步阅读
在这篇文章中,我们快速浏览了 Ruby 的内存管理和垃圾收集器。然后我们诊断了如何使用 Unix 工具和 Ruby 的 GC 模块发现内存泄漏。
下次,我们将看到如何使用 memory_profiler 和 derailed_benchmarks 查找和修复泄漏。
同时,您可以阅读更多关于我们使用的工具:
watchpspgrep
额外的进一步阅读:
GC模块文档ObjectSpace模块文档- Garbage Collection Deep Dive
- Variable Width Allocation
快乐编码,下次见!
P.S. 如果您想在 Ruby Magic 文章一发表就阅读,请订阅我们的 Ruby Magic 时事通讯,永远不错过任何一篇文章!