1 คะแนน โดย GN⁺ 2024-05-12 | 1 ความคิดเห็น | แชร์ทาง WhatsApp

สรุปกระบวนการแก้ปัญหา Memory Leak ที่เกี่ยวข้องกับ ActiveSupport::Notifications

  • สถานการณ์ที่เกิด Memory Leak

    • ตั้งแต่ช่วงเวลาหนึ่ง การใช้หน่วยความจำของ web Dyno เริ่มเพิ่มขึ้นอย่างผิดปกติ
    • Pager เริ่มดัง และเกิดสถานการณ์ที่ดูเหมือนเป็น memory leak
  • การรับมือทันที

    • บน Heroku หากสงสัยว่าเป็น memory leak สามารถแก้ชั่วคราวได้ด้วยการรีสตาร์ต Dyno
    • รีสตาร์ตตามรอบ deploy ปกติ หรือรีสตาร์ต Dyno ที่เข้าใกล้ memory limit ด้วยตนเอง
  • ตรวจโค้ดต้องสงสัยเพื่อหาสาเหตุ

    • ตรวจการเปลี่ยนแปลงของโค้ดที่ถูก deploy ก่อนเกิด memory spike ทันที
    • ทยอย deploy โค้ดที่น่าสงสัยทีละส่วนเพื่อตรวจว่าทำให้เกิด memory leak หรือไม่
    • แม้จะ rollback การเปลี่ยนแปลงของ tooling ที่อาจเป็นสาเหตุ แต่ memory leak ก็ยังคงเกิดขึ้น
  • วิเคราะห์รูปแบบการเพิ่มขึ้นของ memory

    • leak เกิดเฉพาะใน web Dyno เท่านั้น ส่วน Sidekiq และ Delayed::Job Dyno ปกติ
    • ไม่ใช่ทุก web Dyno ที่ leak ตลอดเวลา บางครั้งใช้งานปกติหลายชั่วโมงก่อนจะเริ่ม leak ในหนึ่งหรือสอง Dyno หรือทุก Dyno
    • คาดว่าเกิดจากทราฟฟิกบางประเภท มากกว่าจะขึ้นกับปริมาณทราฟฟิก
    • ไม่ใช่ Puma worker ทุกตัวใน Dyno ที่เกิด leak แต่มี worker เพียงไม่กี่ตัวที่ใช้หน่วยความจำส่วนใหญ่ของทั้งหมด
  • การเก็บและวิเคราะห์ heap dump

    • ใช้ rbtrace เพื่อเก็บ heap dump ของ Ruby process ที่กำลังเกิด leak
      • ใช้ heroku ps:exec เพื่อ ssh เข้า dyno ที่กำลัง leak
      • ใช้คำสั่ง ps เพื่อเลือก Ruby worker process ที่ใช้หน่วยความจำมากที่สุด
      • attach rbtrace เข้ากับ pid นั้นแล้วเริ่มติดตามการจัดสรรหน่วยความจำ (ObjectSpace.trace_object_allocations_start)
      • เก็บ heap dump ด้วย ObjectSpace.dump_all และบีบอัดด้วย gzip หากไฟล์มีขนาดใหญ่
      • ใช้ heroku ps:copy เพื่อนำไฟล์ dump กลับมาที่เครื่องโลคัล
    • ใช้ reap เพื่อแสดงผล heap dump เป็น flamegraph
      • พบ Thread ที่อ้างอิงหน่วยความจำ 1.9GB และมี Array ใต้ลงมาที่อ้างอิงอ็อบเจ็กต์ 32,067 ตัว
    • ใช้ sheap เพื่อสำรวจอ็อบเจ็กต์ที่น่าสงสัย
      • พบว่า Thread ดังกล่าวคือ Puma worker thread
      • อ็อบเจ็กต์ ActiveSupport::SubscriberQueueRegistry อ้างอิง Hash และภายในมีอ็อบเจ็กต์ String กับ Array
      • ใน Array ที่เป็นปัญหามีอ็อบเจ็กต์ ActiveSupport::Notifications::Event สะสมอยู่มากกว่า 32,000 ตัว
  • การคาดเดาสาเหตุ

    • คาดว่าอ็อบเจ็กต์ Event ของ ActiveSupport::Notifications ถูกสะสมผิดพลาดใน array #children
    • หากเกิดข้อผิดพลาดภายในบล็อก ActiveSupport::Notifications.instrument คาดว่า Event นั้นจะไม่ถูกนำออกจาก #children และทำให้เกิด memory leak
  • การทำซ้ำปัญหาในเครื่องโลคัล

    • ส่ง request ในเครื่องโลคัลด้วย request path และ parameter ที่น่าสงสัยซึ่งพบใน production
    • ยืนยันได้ว่าเกิด 500 Internal Server Error พร้อม URI::InvalidURIError
    • ยืนยันได้ว่าการใช้หน่วยความจำของ production dyno ที่รับ request นั้นเพิ่มขึ้นอย่างรวดเร็ว
  • วิเคราะห์สาเหตุเชิงลึก

    • มีบั๊กเกี่ยวกับ Event#children ของ ActiveSupport::Notifications ซึ่งถูกแก้แล้วใน Rails 7.1
    • เมื่อบั๊กนี้ซ้อนกับบั๊กใน Bugsnag gem ที่ทำความสะอาด request url แล้วเกิด URI::InvalidURIError ระหว่าง URI.parse จึงทำให้เกิด memory leak
    • ข้อผิดพลาดที่ถูก raise ภายในบล็อก ActiveSupport::Notifications.subscribe ไม่ถูกจับไว้ ส่งผลให้ Event นั้นไม่ถูกลบออกจาก array #children และสะสมต่อเนื่องจนเกิด memory leak
  • แนวทางแก้ไข

    • ระยะสั้น: อัปเกรดเวอร์ชัน Bugsnag gem เพื่อไม่ให้ raise error แม้จะเกิด URI::InvalidURIError
    • ระยะยาว: อัปเกรดเป็น Rails 7.x ที่แก้บั๊กของ ActiveSupport::Notifications แล้ว

ความเห็นของ GN⁺

  • กระบวนการค้นหาปัญหาและไล่หาสาเหตุอย่างเป็นระบบน่าประทับใจ บทความนี้สรุปขั้นตอนวิเคราะห์พื้นฐานที่ควรลองเมื่อสงสัยว่าเกิด memory leak ได้ดี
  • ดูเหมือนว่าจะมีการพัฒนาเครื่องมือโอเพนซอร์สสำหรับเก็บข้อมูล ทำ visualization และวิเคราะห์ Ruby heap dump อย่างคึกคัก (rbtrace, reap, sheap เป็นต้น) แม้ไม่ใช่ Ruby ก็ตาม การรู้จักเครื่องมือวิเคราะห์หน่วยความจำที่มีประโยชน์ในแต่ละภาษาและนำไปใช้กับปัญหาได้ถือว่าสำคัญ
  • ที่จริงแล้วสาเหตุของ memory leak มักมาจากบั๊กของไลบรารีหรือเฟรมเวิร์กที่ใช้งานอยู่ แต่ในหลายกรณีอาจไม่มีสภาพแวดล้อมให้วิเคราะห์และแก้บั๊กนั้นเองแล้ว deploy ได้ ดังนั้นการใช้วิธีหลีกเลี่ยงปัญหาที่ทำได้ให้เร็วที่สุดจึงสำคัญ การส่ง bug report พร้อมเสนอทางเลือกที่พอใช้แทนกันได้ก็เป็นวิธีที่ดี
  • จุดที่ดีอีกอย่างคือไม่ได้หยุดแค่แก้ memory leak ให้หาย แต่ลงลึกถึง root cause ของปัญหา ท่าทีการวิเคราะห์ที่พยายามตรวจโค้ดภายในเฟรมเวิร์กอย่างละเอียดเพื่อไล่ถึงสาเหตุแท้จริงเป็นสิ่งที่นักพัฒนาควรมี
  • ท้ายที่สุด สาเหตุของ memory leak กลับมาจากการอัปเดตเวอร์ชันไลบรารีเล็กน้อยที่ตอนแรกดูไม่น่าเกี่ยวข้องเลย เป็นตัวอย่างที่แสดงให้เห็นถึงความสำคัญของการจัดการ dependency และการติดตามการเปลี่ยนแปลง แม้เป็นการเปลี่ยนแปลงเล็กน้อยก็ควรวิเคราะห์ผลกระทบอย่างรอบคอบ และควรมีการมอนิเตอร์หลัง deploy ต่อไป

1 ความคิดเห็น

 
GN⁺ 2024-05-12
ความคิดเห็นจาก Hacker News

สามารถแก้ได้ด้วยการฝึกฝนเชิงวิศวกรรม โดยไม่ต้องกลัวการจัดการหน่วยความจำแบบแมนนวล

  • หากมีเพียง RAII และกฎความเป็นเจ้าของที่ชัดเจน การจัดการหน่วยความจำก็เป็นงานวิศวกรรมที่ง่าย
  • กลับกัน เฟรมเวิร์กที่ยึดติดกับ reference counting และ shared pointer กลับทำให้ความเป็นเจ้าของคลุมเครือและยากกว่าเดิม
  • สร้างแล้วก็ต้องคืนหน่วยความจำ ย้ายความเป็นเจ้าของแล้วก็ไม่ต้องกังวลต่อ นี่คือส่วนหนึ่งของวินัยทางวิศวกรรม
  • บั๊กหน่วยความจำไม่ได้ต่างจากบั๊กทางลอจิก ดังนั้นการแก้ไขจึงเป็นเรื่องปกติ
  • ทรัพยากรของ OS (handle, socket เป็นต้น) ก็จัดการแบบแมนนวลโดยไม่มีตัวจัดการทรัพยากรอัตโนมัติอยู่แล้ว ดังนั้นหน่วยความจำก็เข้าถึงในแนวทางเดียวกันได้

กรณีสูญเสีย 5 ล้านดอลลาร์จาก memory leak

  • เล่าเกร็ดเหตุการณ์ที่เกิดจากบั๊ก memory leak ในไดรเวอร์เครื่องพิมพ์ Solaris ช่วงทศวรรษ 90
  • ตอนนั้นในธนาคารมีการยืนยันธุรกรรมทางกฎหมายด้วยวิธีส่งแฟกซ์ ตรวจสอบ พิมพ์ออกมา แล้วโทรอ่านให้อีกฝ่ายฟังพร้อมบันทึกเสียง
  • เพราะ memory leak ทำให้ไดรเวอร์เครื่องพิมพ์ล่ม เอกสารยืนยันจึงไม่ถูกพิมพ์ออกมา ส่งผลให้ธุรกรรมถูกยกเลิกและขาดทุน 5 ล้านดอลลาร์
  • สุดท้าย นักพัฒนาจึงได้แก้บั๊กหลังจาก CEO ของ Sun ออกมาบ่น

เครื่องมือดีบัก memory leak และแนวทางแก้ไข

  • ใช้ Valgrind แล้วสามารถหา memory leak ในภาษา C ได้ไม่ยาก
  • หากออกแบบมาดี โดยมากการจองและการคืนหน่วยความจำจะอยู่ในฟังก์ชันเดียวกัน จึงแก้ได้ง่าย
  • แนะนำกรณี memory leak ของเซิร์ฟเวอร์โฆษณา Yahoo และวิธีแก้แบบชั่วคราว
  • ยกคำพูดติดตลกของผู้ออกแบบ PHP เพื่อสะท้อนท่าทีที่เลือกความใช้งานได้จริงมากกว่าความสมบูรณ์แบบ
  • ใน Rails มักแก้ปัญหาด้วยฮาร์ดแวร์เพื่อให้ได้ประสิทธิภาพในการพัฒนา

คำชมเกี่ยวกับสไตล์การเขียน

  • มีคอมเมนต์ว่าการเขียนของผู้เขียนอ่านเพลิน ไม่แน่ใจว่าเป็นเพราะอีโมติคอนหรือการจัดรูปแบบ