ตามจับ Memory Leak ใน Node.js ด้วย Chrome Memory Profiling Tools

Nantaphop Phuengphae
True e-Logistics
4 min readMay 26, 2018

--

มาทำความรู้จัก กับเทคนิคการวิเคราะห์หา Memory Leak ด้วยการทำ Heap Snapshot แล้วนำไปวิเคราะห์ด้วย Chrome Memory Profiling Tools

TL;DR

เวลาที่ ​โปรแกรมเรา ใช้ Memory เยอะขึ้นเรื่อยๆ เราสามารถทำ Heap Snapshot เพื่อเก็บข้อมูล Memory ของเราในเวลานั้นๆ ว่ามันมีอะไรบ้างใน Memory ซึ่งเราสามารถ ทำหลายๆ ครั้ง แล้วเอามาเทียบกันได้ว่า มันมีอะไรเพิ่มขึ้นมาใน Memory เพื่อจะได้ดูได้ว่า อะไรมันเพิ่มมาวะ? จะได้ไปหาต่อใน Code ว่ามันมาจากไหน

Video Version

เผื่อใครอยากดูเวอชั่นวีดีโอนะครับ เชิญได้เลย ลองสองเวอชั่นก็ดีนะครับ พูดไปเรื่อย เนื้อหาไม่ได้เหมือนในบทความเป๊ะๆ

Memory Leak คือ ?

เราคงเคยเรียนกันมาตั้งแต่สมัยเริ่มเขียนโปรแกรมนะครับว่า ตัวแปร ต่างๆ ที่เราสร้างเนี่ย มันจะถูกนำไปเก็บอยู่ใน Memory หรือก็คือ RAM ของเครื่องนั่นเอง ถ้าใครเริ่มต้นเขียนโปรแกรมด้วยภาษา C อาจจะคุ้นเคยกับการสั่งให้ตัวแปรเท่ากับ null เวลาเราเลิกใช้ (จำรายละเอียดไม่ได้นะครับ นานนนนนนน มากแล้ว)

แต่ในปัจจุบัน ส่วนใหญ่เรา ไม่ค่อยจะต้องกังวลกับการจัดการ Memory มากนัก เพราะเรามีผู้ช่วยที่ชื่อว่า Garbage Collection(GC) ซึ่งแปลเป็นไทยว่า การเก็บขยะ นั่นเอง

หลักการคร่าวๆ คือ GC จะทำการ ตามหา Object ต่างๆ ใน Memory ที่ไม่มีใครใช้แล้ว แล้วทำการคืน Memory ส่วนนั้นกลับคืนสู่ระบบ เช่นตัวอย่าง Code ด้านล่าง เมื่อจบ Function test ตัวแปลที่ a b ก็จะไม่มีใครเข้าถึงได้อีกต่อไป เมื่อ GC ทำงาน ก็จะสามารถคืน Memory กลับสู่ระบบได้

แต่ในกรณีที่ยังมี Reference ถึงตัวแปรใดๆ อยู่ GC จะถือว่า ค่านั้นๆ ยังมีการใช้งานอยู่ และจะไม่สามารถคืน Memory กลับสู่ระบบได้ เช่นตัวอย่างด้านล่างนะครับ เมื่อมีการเรียก Function test ตัวแปร a b จะถูก Reference ไปหา String “Hello” และ “World” ซึ่ง String ทั้งสองจะถูกเก็บไว้ใน Memory ตลอดไป จนว่าจะไม่มี Reference ใดๆ เรียกถึง

ซึ่งการที่ มี Reference ถึงข้อมูลใดๆ ในระบบ แล้ว GC ไม่สามารถคืน Memory ได้ แบบนี้แหละครับ คืออาการ Memory Leak แบบนึง ตัวอย่างอาจไม่ครอบคลุมเท่าไหร่นะครับ แต่นี่แหละ สาเหตุนึงที่ GC คืน Memory ไม่ได้

เริ่มสร้างตัวอย่างจำลอง Memory Leak ง่ายๆ

ลุยกันเลยนะครับ ผมได้สร้างตัวอย่าง Express App มาตัวนึง ตาม Gist

อธิบาย Code เพิ่มเติมนะครับ เราใช้เพียง express มารับ request ซึ่งทุกครั้งที่เรียกเข้ามา /doLeak จะทำการเพิ่ม Object 10,000 ตัวเข้าไปใน Array ที่ชื่อ mem ตามบรรทัดที่ 9 นะครับ

ส่วน package heapdump ที่เพิ่มเข้ามา จะทำให้เราสามารถใช้ Command kill ส่ง signal USR2 ไปที่ Process Node ของเรา เพื่อให้สร้างไฟล์ Heap Snapshot ในระหว่างที่โปรแกรม Run อยู่นะครับ ซึ่ง Heap Snapshot คืออะไร เดี๋ยวจะอธิบายต่อนะครับ

Start Node!!!

ลืมตั้งเงารูปน้อยๆ ขี้เกียจทำใหม่ -_-

เมื่อเราทำการ Start ไฟล์ ของเรานะครับ ก็จะมี log บอกว่า จะสร้าง Heap Snapshot ได้โดยใช้ Command อะไรนะครับ (Kill ใช้ได้บน Linux/Mac นะครับ เสียใจกับชาว Window ด้วยนะครับ แต่สามารถสร้าง Heap จากในโปรแกรมได้ ตาม README นะครับ https://www.npmjs.com/package/heapdump)

kill -USR2 35761 ครั้งแรก เพื่อทำ Heap Snapshot ตอนเริ่มโปรแกรม

ก็เปิด Terminal แล้วพิมพ์ คำสั่ง kill -USR2 35761 ได้เลยครับ เลข 35761 คือ PID ของ Node Process เราครับ ซึ่งเราจะได้ไฟล์ Heap Snapshot ดังรูปด้านล่างครับ ซึ่งประเดี๋ยวเราต้องเอาไปเข้าโปรแกรมวิเคราะห์ต่อ

ทดลองเรียก /doLeak เพื่อสร้าง Object ขึ้นมาเยอะๆ ใน Heap Memory

ก็เปิด Browser แล้วเรียกเลยครับ http://localhost:3000/doLeak

เรียกครั้งแรก มี 10k Objects เพิ่มมาใน Array
จัดไปรัวๆ 5 ครั้ง ได้มา 50k Objects ใน Array

kill -USR2 35761 ครั้งที่สอง เพื่อทำ Heap Snapshot หลังเรายัด 50k Objects เข้าไปใน Array

แบบนี้นะครับ ได้มา 2 Heap Snapshot แล้ว ตอนเริ่มใหม่ ใสๆ กับตอนผ่านศึกโดนยัด 50k Objects เข้าไปใน Array

เอา Heap Snapshots เข้า Chrome Memory Profiling Tools

เข้า Chrome เลยครับ เปิด Developer Tools แล้วไปที่ Tab Memory แล้วกด Load เลือกไฟล์ Heap Snapshots ของเราเข้าไป ทีละไฟล์เลยครับ

ใครเข้าไม่เป็นก็ผ่านเลยนะครับ บทความนี้คงโหดร้ายเกินไปสำหรับคุณแล้ว 555

หลังเอาเข้าไปทั้ง 2 ไฟล์ เราก็จะเห็นว่าเรามี 2 ไฟล์แล้วในตอนนี้นะครับ

ลองทำการคลิกไปที่ไฟล์นะครับ เราจะเห็นข้อมูลของ Heap Memory Snapshot ที่เราเลือก ในโหมดการดูข้อมูลสรุป(Summary) คร่าวๆ คือเราจะเห็นตัวแปรประเภทต่างๆ จำนวน Object และขนาดใน Memory นะครับ รายละเอียดยังไง ก็ไป Google หาเอาต่อนะครับ ยาวไปๆ

เปลี่ยนเป็น Comparison Mode เพื่อดูว่า มีอะไรเปลี่ยนแปลงไปบ้างระหว่าง 2 Snapshots

จะเห็นว่ามี Column Delta เพิ่มมานะครับ เพื่อบอกว่ามีอะไรเปลี่ยนแปลง

ผมลองกดไปที่ Concatenated String นะครับ ซึ่งจะเห็นว่า Delta + มาเยอะที่สุด

เมื่อกดเข้าไปจะเห็นว่า มันคือ String ที่ถูก Concat มาจาก Code เรานี่เองส่วน หน้าต่าง Retainers หมายถึงข้อมูลว่า ตัวแปรนั้น ถูกเข้าถึง หรือถูก Reference จากไหนนะครับ

ซึ่งเราจะเห็นว่า มันถูก Reference จาก Field ชื่อ msg ใน Object หมายเลข 143011 ซึ่งตัว Object นั้นถูก Reference อีกทอดนึง โดย Index ที่ 3657 ใน Array หมายเลข 120145

นี่เลย Field msg ใน Object ที่อยู่ใน Array

ภาพต่อไป ผมลองกดไปที่ Object นะครับ จะเห็นว่า Delta + 50020 หลักๆ ก็คือ 50k Objects ที่เราเพิ่มไป ส่วนอีก 20 ก็คงเป็นอะไรอื่นๆ ที่ Express หรือ Node สร้างมาเอง ช่างมันครับ

นอกจากนี้ถ้าเราคลิกไปดู Retainers ของแต่ละ Object ก็จะเห็นว่า มันถูก Reference จาก Array หมายเลข 120145 ซึ่งคือ Object ในรูปก่อนหน้านี้เองครับ

สรุปส่งท้าย

Heap Snapshot คือข้อมูลของ Object ต่างๆ ที่ถูกเก็บใน Memory ณ เวลานั้นๆ ที่เราทำ Snapshot ซึ่งเราสามารถนำมาวิเคราะห์ด้วยเครื่องมือ Memory Profiling ต่างๆ เช่นใน Chrome ได้ เพื่อทำให้เราสามารถทราบได้ว่า โปรแกรมของเรา ใช้ ​Memory ไปกับอะไรบ้าง Code ของเราตรงไหน ที่ทำให้เกิดตัวแปรขึ้นมากมายใน Memory

ในการวิเคราะห์ Memory ในโปรแกรมจริงๆ นั้น คงไม่ง่ายเช่นนี้ เพราะเราไม่รู้ว่าไอ้ที่ Leak มันอยู่ตรงไหนแน่ ยังไงก็ต้องใช้ประสบการณ์ และการคลำๆ เอาแหละครับ 5555 แต่ยังไง ถ้าเรารู้จักเครื่องมือ Profiling พวกนี้ ก็จะทำให้เราหาสาเหตุได้ง่ายขึ้นครับ

--

--