ปัญหาหน่วยความจำล้นในแอปพลิเคชัน Ruby เป็นหัวข้อของการสนทนาบ่อยครั้ง ในโพสต์นี้ เราจะมาดูกันว่าการจัดการหน่วยความจำ Ruby สามารถผิดพลาดได้อย่างไร และสิ่งที่คุณสามารถทำได้เพื่อป้องกันไม่ให้แอปพลิเคชัน Ruby ของคุณทำงาน
อันดับแรก เราต้องเข้าใจว่าการบวมหมายถึงอะไรในบริบทของหน่วยความจำของแอปพลิเคชัน
มาดำน้ำกันเถอะ!
หน่วยความจำบวมในทับทิมคืออะไร
การขยายตัวของหน่วยความจำเกิดขึ้นเมื่อการใช้หน่วยความจำของแอปพลิเคชันของคุณเพิ่มขึ้นในทันทีโดยไม่มีคำอธิบายที่ชัดเจน การเพิ่มขึ้นนี้อาจหรือไม่เร็วก็ได้ แต่โดยส่วนใหญ่แล้ว จะเพิ่มขึ้นอย่างต่อเนื่อง ซึ่งแตกต่างจากหน่วยความจำรั่ว ซึ่งเป็นผลมาจากการดำเนินการหลายครั้งที่เกิดขึ้นในช่วงเวลาหนึ่ง
การขยายตัวของหน่วยความจำอาจเป็นหนึ่งในสิ่งเลวร้ายที่สุดที่จะเกิดขึ้นกับแอปพลิเคชัน Ruby ของคุณในการผลิต อย่างไรก็ตาม หลีกเลี่ยงได้หากคุณใช้มาตรการที่เหมาะสม ตัวอย่างเช่น หากคุณสังเกตเห็นการใช้หน่วยความจำของแอปพลิเคชันของคุณพุ่งสูงขึ้น ทางที่ดีควรตรวจสอบสัญญาณของหน่วยความจำที่ล้นก่อนที่จะแก้ไขปัญหาอื่นๆ
ก่อนที่เราจะสำรวจวิธีวินิจฉัยและแก้ไขหน่วยความจำบวม เรามาทำความรู้จักกับสถาปัตยกรรมหน่วยความจำของ Ruby กันก่อน
โครงสร้างหน่วยความจำของทับทิม
การใช้หน่วยความจำของ Ruby เกี่ยวข้องกับองค์ประกอบเฉพาะที่จัดการการใช้ทรัพยากรระบบที่มีอยู่อย่างรอบคอบ องค์ประกอบเหล่านี้รวมถึงภาษา Ruby ระบบปฏิบัติการโฮสต์ และเคอร์เนลของระบบ
นอกจากนี้ กระบวนการรวบรวมขยะยังมีบทบาทสำคัญในการกำหนดวิธีจัดการและนำหน่วยความจำ Ruby กลับมาใช้ใหม่
รูบี้ฮีปเพจและสล็อตหน่วยความจำ
ภาษา Ruby จัดระเบียบอ็อบเจ็กต์ออกเป็นส่วนๆ ที่เรียกว่าเพจฮีป พื้นที่ฮีปทั้งหมด (หน่วยความจำที่ใช้ได้) ถูกแบ่งออกเป็นส่วนที่ใช้และส่วนว่าง ฮีปเพจเหล่านี้ยังถูกแบ่งออกเป็นช่องที่มีขนาดเท่ากัน ซึ่งช่วยให้แต่ละออบเจ็กต์มีขนาดเท่ากัน
เมื่อจัดสรรหน่วยความจำให้กับอ็อบเจ็กต์ใหม่ อันดับแรก Ruby จะดูในพื้นที่ฮีปที่ใช้สำหรับช่องว่าง หากไม่พบ ระบบจะจัดสรรฮีพเพจใหม่จากส่วนที่ว่าง
สล็อตหน่วยความจำเป็นตำแหน่งหน่วยความจำขนาดเล็ก แต่ละช่องมีขนาดเกือบ 40 ไบต์ ข้อมูลที่ล้นออกจากช่องเหล่านี้จะถูกเก็บไว้ในพื้นที่อื่นนอกหน้าฮีป และแต่ละช่องจะเก็บตัวชี้ไปยังข้อมูลภายนอก
ตัวจัดสรรหน่วยความจำของระบบทำให้การจัดสรรทั้งหมดในสภาพแวดล้อมรันไทม์ของ Ruby รวมถึงฮีพเพจและพอยน์เตอร์ข้อมูลภายนอก
การจัดสรรหน่วยความจำระบบปฏิบัติการใน Ruby
การเรียกการจัดสรรหน่วยความจำที่ทำโดยภาษา Ruby จะได้รับการจัดการและตอบกลับโดยตัวจัดสรรหน่วยความจำของระบบปฏิบัติการโฮสต์
โดยปกติ ตัวจัดสรรหน่วยความจำประกอบด้วยกลุ่มของฟังก์ชัน C คือ malloc , โทร , realloc และ ฟรี . เรามาดูกันอย่างรวดเร็ว:
- มัลลอค :Malloc ย่อมาจากการจัดสรรหน่วยความจำ และใช้เพื่อจัดสรรหน่วยความจำว่างให้กับวัตถุ ต้องใช้ขนาดของหน่วยความจำในการจัดสรรและส่งคืนตัวชี้ไปยังดัชนีเริ่มต้นของบล็อกหน่วยความจำที่จัดสรรไว้
- โทรออก :Calloc ย่อมาจากการจัดสรรที่ต่อเนื่องกัน และอนุญาตให้ภาษา Ruby จัดสรรบล็อกหน่วยความจำที่ต่อเนื่องกัน เป็นประโยชน์เมื่อจัดสรรอาร์เรย์อ็อบเจ็กต์ที่มีความยาวที่ทราบ
- Realloc :Realloc ย่อมาจาก re-allocation และอนุญาตให้ภาษาจัดสรรหน่วยความจำใหม่ด้วยขนาดใหม่
- ฟรี :ฟรีใช้เพื่อล้างชุดตำแหน่งหน่วยความจำที่จัดสรรไว้ล่วงหน้า มันใช้ตัวชี้ไปยังดัชนีเริ่มต้นของบล็อกหน่วยความจำที่จะต้องว่าง
การเก็บขยะในทับทิม
กระบวนการรวบรวมขยะของรันไทม์ภาษาส่งผลกระทบอย่างมากต่อการใช้หน่วยความจำที่มีอยู่
Ruby มีการรวบรวมขยะขั้นสูงที่ใช้วิธี API ที่อธิบายข้างต้นทั้งหมดเพื่อเพิ่มประสิทธิภาพการใช้หน่วยความจำของแอปพลิเคชันตลอดเวลา
ข้อเท็จจริงที่น่าสนใจเกี่ยวกับกระบวนการรวบรวมขยะใน Ruby คือการหยุดแอปพลิเคชันทั้งหมด! เพื่อให้แน่ใจว่าไม่มีการจัดสรรอ็อบเจ็กต์ใหม่เกิดขึ้นระหว่างการรวบรวมขยะ ด้วยเหตุนี้ กิจวัตรการรวบรวมขยะของคุณจึงไม่ควรเกิดขึ้นไม่บ่อยและรวดเร็วที่สุด
สาเหตุทั่วไปสองประการที่ทำให้หน่วยความจำบวมในทับทิม
ส่วนนี้จะกล่าวถึงสาเหตุที่สำคัญที่สุดสองประการที่ทำให้หน่วยความจำบวมใน Ruby:การแตกแฟรกเมนต์และการรีลีสช้า
การแยกส่วนหน่วยความจำ
การกระจายตัวของหน่วยความจำคือเมื่อการจัดสรรอ็อบเจ็กต์ในหน่วยความจำกระจัดกระจายไปทั่ว ช่วยลดจำนวนหน่วยความจำว่างที่อยู่ติดกัน ไม่สามารถจัดสรรหน่วยความจำให้กับอ็อบเจ็กต์ที่ไม่มีบล็อกที่ต่อเนื่องกัน แม้ว่าจะมีหน่วยความจำว่างเพียงพอบนดิสก์ก็ตาม ปัญหานี้อาจเกิดขึ้นได้ในภาษาการเขียนโปรแกรมหรือสภาพแวดล้อม และแต่ละภาษามีวิธีการแก้ปัญหา
การแบ่งส่วนสามารถเกิดขึ้นได้สองระดับที่แตกต่างกัน:ระดับของภาษาและระดับของตัวจัดสรรหน่วยความจำ มาดูรายละเอียดทั้งสองนี้กันดีกว่า
การแยกส่วนในระดับทับทิม
การแยกส่วนในระดับภาษาเกิดขึ้นเนื่องจากการออกแบบกระบวนการรวบรวมขยะ กระบวนการรวบรวมขยะจะทำเครื่องหมายสล็อตเพจ Ruby heap ว่าว่าง อนุญาตให้นำสล็อตนั้นกลับมาใช้ใหม่เพื่อจัดสรรอ็อบเจ็กต์อื่นในหน่วยความจำ หากหน้า Ruby heap ที่สมบูรณ์ประกอบด้วยช่องว่างเท่านั้น หน้า heap นั้นจะถูกปล่อยไปยังตัวจัดสรรหน่วยความจำเพื่อนำกลับมาใช้ใหม่ได้
แต่ถ้าช่องจำนวนเล็กน้อยไม่ได้ทำเครื่องหมายว่างบนฮีปล่ะ จะไม่ถูกปล่อยกลับไปยังตัวจัดสรรหน่วยความจำ ตอนนี้ ให้นึกถึงช่องจำนวนมากในหน้าฮีพต่างๆ ที่จัดสรรและปลดปล่อยโดยการรวบรวมขยะพร้อมกัน เป็นไปไม่ได้ที่เพจฮีปทั้งหมดจะถูกปล่อยออกมาพร้อมกัน แม้ว่ากระบวนการรวบรวมขยะจะทำให้หน่วยความจำว่าง แต่ก็ไม่สามารถใช้ซ้ำโดยตัวจัดสรรหน่วยความจำได้ เนื่องจากหน่วยความจำบล็อกบางส่วนใช้พื้นที่นั้น
การกระจายตัวที่ระดับตัวจัดสรรหน่วยความจำ
ตัวจัดสรรหน่วยความจำเองก็ประสบปัญหาที่คล้ายกัน:ต้องปล่อย OS heaps เมื่อว่างทั้งหมด แต่ไม่น่าจะเป็นไปได้ที่ฮีปของ OS ทั้งหมดจะว่างในคราวเดียวโดยพิจารณาจากลักษณะสุ่มของกระบวนการรวบรวมขยะ
ตัวจัดสรรหน่วยความจำยังจัดเตรียม OS heaps จากหน่วยความจำระบบสำหรับการใช้งานของแอปพลิเคชัน มันจะย้ายไปยังการจัดเตรียม OS heap ใหม่ แม้ว่าฮีปที่มีอยู่จะมีหน่วยความจำว่างเพียงพอที่จะตอบสนองความต้องการด้านหน่วยความจำของแอปพลิเคชัน นี่เป็นสูตรที่สมบูรณ์แบบสำหรับการเพิ่มเมตริกหน่วยความจำของแอปพลิเคชัน
ปล่อยช้า
สาเหตุสำคัญอีกประการหนึ่งที่ทำให้หน่วยความจำเพิ่มขึ้นใน Ruby คือการปล่อยหน่วยความจำที่ว่างกลับคืนสู่ระบบอย่างช้าๆ ในสถานการณ์นี้ หน่วยความจำจะว่างช้ากว่าอัตราที่บล็อกหน่วยความจำใหม่ถูกจัดสรรให้กับอ็อบเจ็กต์ แม้ว่าสิ่งนี้จะไม่ใช่ปัญหาทั่วไปหรือปัญหามือใหม่ที่ต้องแก้ไข แต่ก็ส่งผลกระทบอย่างมากต่อหน่วยความจำที่ขยายตัว — มากกว่าการแตกแฟรกเมนต์!
เมื่อตรวจสอบแหล่งที่มาของตัวจัดสรรหน่วยความจำ ปรากฎว่าตัวจัดสรรได้รับการออกแบบให้เผยแพร่หน้า OS ที่ส่วนท้ายของฮีปของระบบปฏิบัติการ และถึงแม้จะเป็นบางครั้งเท่านั้น นี่อาจเป็นเพราะเหตุผลด้านประสิทธิภาพ แต่อาจย้อนกลับมาและส่งผลตรงกันข้าม
วิธีแก้ไข Ruby Memory Bloat
ตอนนี้เรารู้แล้วว่าอะไรเป็นสาเหตุให้หน่วยความจำของ Ruby บวม มาดูวิธีแก้ไขปัญหาเหล่านี้และปรับปรุงประสิทธิภาพของแอปผ่านการจัดเรียงข้อมูลและตัดแต่ง
แก้ไขหน่วยความจำทับทิมด้วยการจัดเรียงข้อมูล
การแยกส่วนเกิดขึ้นเนื่องจากการออกแบบการรวบรวมขยะ และคุณแก้ไขอะไรไม่ได้มาก อย่างไรก็ตาม มีไม่กี่ขั้นตอนที่คุณสามารถปฏิบัติตามเพื่อลดโอกาสที่จะจบลงด้วยดิสก์หน่วยความจำที่กระจัดกระจาย:
- หากคุณประกาศการอ้างอิงไปยังอ็อบเจ็กต์ที่ใช้หน่วยความจำจำนวนมาก ให้ตรวจสอบว่าคุณว่างด้วยตนเองเมื่องานเสร็จสิ้น
- พยายามประกาศการจัดสรรออบเจ็กต์แบบคงที่ทั้งหมดของคุณในบล็อกขนาดใหญ่ก้อนเดียว สิ่งนี้จะทำให้คลาสถาวร วัตถุ และข้อมูลอื่น ๆ ของคุณอยู่ในฮีปเพจเดียวกัน ต่อมา เมื่อคุณลองใช้การจัดสรรแบบไดนามิก คุณจะไม่ต้องกังวลกับหน้าสแตติกฮีป
- ถ้าเป็นไปได้ ให้ลองทำการจัดสรรแบบไดนามิกขนาดใหญ่ที่ จุดเริ่มต้น ของรหัสของคุณ สิ่งนี้จะทำให้พวกมันอยู่ใกล้กับบล็อกหน่วยความจำการจัดสรรแบบคงที่ที่ใหญ่กว่าของคุณ และจะทำให้หน่วยความจำที่เหลือของคุณสะอาดอยู่เสมอ
- หากคุณใช้แคชขนาดเล็กและไม่ค่อยถูกล้าง ควรจัดกลุ่มด้วยการจัดสรรคงที่แบบถาวรในตอนเริ่มต้น คุณสามารถลองลบออกทั้งหมดเพื่อปรับปรุงการจัดการหน่วยความจำของแอปได้
- ใช้ jemalloc แทนตัวจัดสรรหน่วยความจำ glibc มาตรฐาน การปรับแต่งเล็กน้อยนี้สามารถลดการใช้หน่วยความจำ Ruby ของคุณได้ถึงสี่เท่า ข้อแม้เดียวที่นี่คืออาจเข้ากันไม่ได้กับทุกสภาพแวดล้อม ดังนั้นอย่าลืมทดสอบแอปของคุณอย่างละเอียดก่อนที่จะเริ่มใช้งานจริง
การตัดแต่งเพื่อแก้ไข Ruby Memory Bloat
คุณต้องแทนที่กระบวนการรวบรวมขยะและปล่อยหน่วยความจำบ่อยขึ้นเพื่อแก้ไขการปล่อยหน่วยความจำที่ช้า มี API ที่สามารถทำได้ที่เรียกว่า malloc_trim . สิ่งที่คุณต้องทำคือแก้ไข Ruby เพื่อเรียกใช้ฟังก์ชันนี้ระหว่างกระบวนการรวบรวมขยะ
นี่คือรหัส Ruby 2.6 ที่แก้ไขแล้วซึ่งเรียก malloc_trim ในฟังก์ชัน gc.c 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);
หมายเหตุ: ไม่แนะนำในแอปพลิเคชันที่ใช้งานจริงเพราะอาจทำให้แอปของคุณไม่เสถียร อย่างไรก็ตาม จะมีประโยชน์อย่างยิ่งเมื่อหน่วยความจำที่ช้าทำให้ประสิทธิภาพการทำงานของคุณสะดุด และคุณพร้อมที่จะลองใช้วิธีแก้ปัญหาใดๆ
สรุปและขั้นตอนถัดไป
หน่วยความจำบวมนั้นยากที่จะระบุและยากกว่าในการแก้ไข
บทความนี้กล่าวถึงสาเหตุสำคัญ 2 ประการที่ทำให้หน่วยความจำเพิ่มขึ้นในแอป Ruby ได้แก่ การแตกแฟรกเมนต์และการปล่อยช้า และการแก้ไข 2 รายการ ได้แก่ การจัดเรียงข้อมูลและการตัดแต่ง
คุณต้องคอยจับตาดูเมตริกของแอปอยู่เสมอเพื่อระบุอุบัติการณ์ขยายตัวที่กำลังจะเกิดขึ้นและแก้ไขก่อนที่จะทำให้แอปของคุณล่ม
ฉันหวังว่าจะได้ช่วยคุณดำเนินการแก้ไขหน่วยความจำที่บวมในแอปพลิเคชัน Ruby ของคุณ
ป.ล. หากคุณต้องการอ่านโพสต์ Ruby Magic ทันทีที่ออกจากสื่อ สมัครรับจดหมายข่าว Ruby Magic ของเราและไม่พลาดแม้แต่โพสต์เดียว!
ผู้เขียนรับเชิญของเรา Kumar Harsh เป็นผู้พัฒนาซอฟต์แวร์ที่กำลังมาแรงโดยฝีมือ เขาเป็นนักเขียนที่กระตือรือร้นที่รวบรวมเนื้อหาเกี่ยวกับเทคโนโลยีเว็บยอดนิยมอย่าง Ruby และ JavaScript คุณสามารถหาข้อมูลเพิ่มเติมเกี่ยวกับเขาผ่านทางเว็บไซต์และติดตามเขาบน Twitter