ปัญหา memory bloat ใน Ruby applications เป็นหัวข้อกล่าวกันบ่อย ในโพสต์นี้ จะดูว่า Ruby memory management ผิดพลาดได้อย่างไร และป้องกันไม่ให้ app blow up ยังไง
ก่อนเข้าใจ bloat ใน context ของ application memory
เริ่มกันเลย!
Memory Bloat ใน Ruby คืออะไร?
Memory bloat = memory usage เพิ่มขึ้นทะลุท้องทะลุฟ้าโดยไม่มีเหตุผลชัดเจน อาจรวดเร็วหรือช้า แต่ส่วนใหญ่ต่อเนื่อง ต่างจาก memory leak ที่เกิดจาก multiple executions ตลอดเวลา
Memory bloat เป็นอันตรายต่อ Ruby app ใน production แต่กันได้ถ้า Maßnahmen ถูกต้อง เช่นเห็น spike memory usage ควรเช็ค memory bloat ก่อน troubleshoot อื่นๆ
ก่อน diagnose/fix memory bloat ลอง walkthrough Ruby memory architecture แบบเร็ว
Ruby Memory Structure
Ruby memory usage ขึ้นอยู่กับ elements จัดการ system resources: Ruby language, host OS, system kernel
นอกนี้ garbage collection process ก็สำคัญต่อการ manage และ reuse Ruby memory
Ruby Heap Pages และ Memory Slots
Ruby จัด objects เป็น segments เรียกว่า heap pages Heap space (available memory) แบ่ง used และ empty sections Heap pages แบ่งเป็น equal-sized slots แต่ละ slot = 1 unit-sized object
Allocate memory ให้ object ใหม่ Ruby หา free slots ใน used heap space ก่อน ไม่มี → allocate heap page ใหม่จาก empty section
Memory slots เล็ก ~40 bytes ข้อมูลล้นออกมาจาก slots เก็บในพื้นที่นอก heap page แต่ละ slot เก็บ pointer ไปข้อมูลภายนอก
System memory allocator ทำ allocations ทั้งหมดใน Ruby runtime (heap pages, external data pointers)
Operating System Memory Allocation ใน Ruby
Memory allocation calls จาก Ruby จัดการโดย host OS memory allocator
ปกติ memory allocator = group of C functions: malloc, calloc, realloc, free ดูแต่ละตัว:
- Malloc: memory allocation จัดสรร free memory ให้ objects รับ size คืน pointer ไป starting index ของ allocated block
- Calloc: contiguous allocation จัดสรร consecutive memory blocks เหมาะสำหรับ object arrays ความยาวรู้
- Realloc: re-allocation ปรับขนาด memory ใหม่
- Free: ลบ pre-allocated memory blocks รับ pointer starting index ของ block ที่จะ free
Garbage Collection ใน Ruby
GC process ของ language runtime ส่งผลมากต่อ memory utilization
Ruby มี GC advanced ใช้ API methods ทั้งหมด optimize memory consumption ตลอด
น่าสนใจ: GC ใน Ruby หยุด application ทั้งหมด! รับรองว่าไม่มี object allocation ใหม่ระหว่าง GC ดังนั้น GC routine ควร infrequent และเร็วที่สุด
Two Common Causes ของ Memory Bloat ใน Ruby
สองสาเหตุใหญ่: fragmentation และ slow release
Memory Fragmentation
Memory fragmentation = object allocations ใน memoryกระจายกระจาย ลด contiguous free memory chunks Memory ไม่ allocate ให้ objects ได้โดยไม่มี contiguous blocks แม้ free memory พอดีบน disk ปัญหาเกิดในภาษา/environment ใดๆ แต่ละภาษามีวิธีแก้
Fragmentation 2 ระดับ: language level และ memory allocator level มาดูทั้งคู่
Fragmentation ที่ Ruby Level
Fragmentation ที่ language level เกิดจาก GC process design GC marks heap page slot ว่าง ให้ reuse slot สำหรับ object ใหม่ ถ้า heap page ทั้งหน้าว่าง slots → release page ให้ memory allocator reuse
แต่ถ้าเล็กน้อย slots ไม่ marked free บน heap? จะไม่ release กลับ allocator คิดถึงหลาย slots บนหลาย heap pages allocated/freed พร้อมกันโดย GC ไม่น่าเป็นไปได้ที่ entire heap pages released พร้อมกัน แม้ GC free memory แต่มัน reuse โดย allocator ไม่ได้ เพราะ memory blocks ครอบครองบางส่วน
Fragmentation ที่ Memory Allocator Level
Memory allocator เองเจอปัญหาคล้ายกัน: ต้อง release OS heaps เมื่อ free ทั้งหมด แต่น่าเป็นไปได้น้อยที่ OS heap จะ free พร้อมกัน เพราะ GC random nature
Allocator provision OS heaps จาก system memory ให้ app ใช้ จะ provision OS heaps ใหม่ต่อไป แม้ heaps เดิมมี free memory พอ จะทำให้ memory metrics spike
Slow Release
สาเหตุสำคัญอีกอย่าง: slow release ของ freed memory กลับ system Memory free ช้ากว่าอัตรา allocate memory blocks ใหม่ ไม่ใช่ conventional/rookie issue แต่ส่งผลต่อ memory bloat รุนแรง — มากกว่า fragmentation!
สืบ source ของ memory allocator พบว่า allocator designed release OS pages แค่ท้าย OS heaps และแม้แต่นั้นก็นานครั้ง ไม่แน่ใจแต่เพื่อ performance แต่ backfire ได้
แก้ Ruby Memory Bloat ยังไง?
รู้สาเหตุแล้ว มาดู fixes: defragmentation และ trimming
แก้ Memory Bloat ด้วย Defragmentation
Fragmentation เกิดจาก GC design ไม่มีอะไรแก้มาก แต่มี steps ลดโอกาส fragmented memory:
- Declare reference ถ้า object ใช้ memory มาก free 手动 เมื่อเสร็จงาน
- Declare static object allocations ใน 1 big block จะวาง permanent classes, objects, data บน heap pages เดียวกัน ตอน dynamic allocations ไม่กังวล static heap pages
- Large dynamic allocations ทำ ต้นทาง ของโค้ด จะวางใกล้ static allocation blocks ทำให้ memory ส่วนอื่น clean
- Small rarely-cleared cache group กับ static allocation ต้นทาง หรือ consider remove เพื่อ improve memory management
- ใช้ jemalloc แทน glibc memory allocator standard tweak เล็กน้อย ลด Ruby memory consumption ลงถึง 4 เท่า Caveat: อาจไม่ compatible ทุก environment test ดีก่อน production
Trimming แก้ Ruby Memory Bloat
Override GC process release memory บ่อยขึ้น เพื่อแก้ slow release มี API ชื่อ malloc_trim Modify Ruby ให้ call function นี้ระหว่าง GC process
โค้ด Ruby 2.6 modified เรียก malloc_trim ใน gc.c function gc_start:
gc_prof_timer_start(objspace); { gc_marks(objspace, do_full_mark); // BEGIN MODIFICATION if (do_full_mark) { malloc_trim(0); } // END MODIFICATION } gc_prof_timer_stop(objspace); หมายเหตุ: ไม่แนะนำ production apps อาจทำให้ unstable แต่ช่วยได้ตอน slow release สร้าง major performance hits และพร้อมลองทุก solution
Wrap-up และ Next Steps
Memory bloats ยาก identify ยิ่งกว่า fix
บทความดู 2 สาเหตุใหญ่: fragmentation และ slow release และ 2 fixes: defragmentation และ trimming
ต้องติดตาม app metrics ตลอด เพื่อจับ impending bloat incident แก้ก่อนจะ down app
หวังว่าช่วยก้าวหน้าแก้ memory bloat ใน Ruby app
P.S. อยากอ่าน Ruby Magic posts ทันทีเผยแพร่? สมัครรับ Ruby Magic newsletter!
Guest author Kumar Harsh: software developer เขียน content เกี่ยว web technologies เช่น Ruby, JavaScript หาเขาได้ที่ website และ Twitter