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

กำจัด Memory Bloat ใน Ruby: กลยุทธ์ที่พิสูจน์แล้วสำหรับ Optimal Performance

ปัญหา 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