ปัญหา Memory Leak ของ Copilot
(stevenharman.net)สรุปกระบวนการแก้ปัญหา Memory Leak ที่เกี่ยวข้องกับ ActiveSupport::Notifications
-
สถานการณ์ที่เกิด Memory Leak
- ตั้งแต่ช่วงเวลาหนึ่ง การใช้หน่วยความจำของ
webDyno เริ่มเพิ่มขึ้นอย่างผิดปกติ - Pager เริ่มดัง และเกิดสถานการณ์ที่ดูเหมือนเป็น memory leak
- ตั้งแต่ช่วงเวลาหนึ่ง การใช้หน่วยความจำของ
-
การรับมือทันที
- บน Heroku หากสงสัยว่าเป็น memory leak สามารถแก้ชั่วคราวได้ด้วยการรีสตาร์ต Dyno
- รีสตาร์ตตามรอบ deploy ปกติ หรือรีสตาร์ต Dyno ที่เข้าใกล้ memory limit ด้วยตนเอง
-
ตรวจโค้ดต้องสงสัยเพื่อหาสาเหตุ
- ตรวจการเปลี่ยนแปลงของโค้ดที่ถูก deploy ก่อนเกิด memory spike ทันที
- ทยอย deploy โค้ดที่น่าสงสัยทีละส่วนเพื่อตรวจว่าทำให้เกิด memory leak หรือไม่
- แม้จะ rollback การเปลี่ยนแปลงของ tooling ที่อาจเป็นสาเหตุ แต่ memory leak ก็ยังคงเกิดขึ้น
-
วิเคราะห์รูปแบบการเพิ่มขึ้นของ memory
- leak เกิดเฉพาะใน
webDyno เท่านั้น ส่วน Sidekiq และ Delayed::Job Dyno ปกติ - ไม่ใช่ทุก
webDyno ที่ leak ตลอดเวลา บางครั้งใช้งานปกติหลายชั่วโมงก่อนจะเริ่ม leak ในหนึ่งหรือสอง Dyno หรือทุก Dyno - คาดว่าเกิดจากทราฟฟิกบางประเภท มากกว่าจะขึ้นกับปริมาณทราฟฟิก
- ไม่ใช่ Puma worker ทุกตัวใน Dyno ที่เกิด leak แต่มี worker เพียงไม่กี่ตัวที่ใช้หน่วยความจำส่วนใหญ่ของทั้งหมด
- leak เกิดเฉพาะใน
-
การเก็บและวิเคราะห์ 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แล้ว
- ระยะสั้น: อัปเกรดเวอร์ชัน Bugsnag gem เพื่อไม่ให้ raise error แม้จะเกิด
ความเห็นของ 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 ความคิดเห็น
ความคิดเห็นจาก Hacker News
สามารถแก้ได้ด้วยการฝึกฝนเชิงวิศวกรรม โดยไม่ต้องกลัวการจัดการหน่วยความจำแบบแมนนวล
กรณีสูญเสีย 5 ล้านดอลลาร์จาก memory leak
เครื่องมือดีบัก memory leak และแนวทางแก้ไข
คำชมเกี่ยวกับสไตล์การเขียน