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

วิธีลดการบวมของหน่วยความจำใน Ruby

ปัญหาหน่วยความจำล้นในแอปพลิเคชัน 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