- หน่วยความจำของ
web Dyno ใน แอป Rails บนโปรดักชันที่มีอายุ 10 ปี พุ่งสูงระหว่างการ deploy และเนื่องจากเป็นบริการที่รองรับโหลดต่อเนื่อง 400–500 req/s และช่วงพีกหลายพัน req/s จึงต้องบรรเทาปัญหาอย่างรวดเร็ว
- บน Heroku ได้รีสตาร์ต Dyno ที่ใกล้ถึงขีดจำกัดหน่วยความจำ และย้อนกลับการเปลี่ยนแปลงโค้ดกับเมตริกในช่วง 3 วันที่ผ่านมา แต่ memory leak ยังคงเกิดขึ้น
- Sidekiq และ Delayed::Job ปกติ แต่มีแพตเทิร์นที่ Puma worker บางตัวเท่านั้นที่โตขึ้น จึงสงสัยว่าเกี่ยวข้องกับ ประเภทของทราฟฟิก บางอย่าง
- เมื่อติดตาม heap ด้วย
rbtrace, ObjectSpace, heapy, sheap, reap พบว่า thread ประมวลผล request ของ Puma กำลังยึด object 32,067 รายการและหน่วยความจำ 1.9GiB ไว้ผ่าน array @children ของ ActiveSupport::Notifications::Event
- query parameter ที่ถูกดัดแปลงทำให้เกิด
URI::InvalidURIError ในกระบวนการทำความสะอาด URL ของ Bugsnag วิธีรับมือระยะสั้นคืออัปเกรด Bugsnag ส่วนระยะยาวคืออัปเกรด Rails
การรั่วเริ่มขึ้นในแอป Rails ที่กำลังให้บริการจริง
- เป้าหมายคือ แอป Rails อายุ 10 ปี ซึ่งเป็นบริการโปรดักชันที่สร้างรายได้จริง
- โหลดต่อเนื่องตามปกติอยู่ที่ 400–500 req/s และช่วงพีกสูงถึงหลายพัน request ต่อวินาที
- ระหว่างกระบวนการ deploy ตามปกติ memory spike เริ่มเกิดขึ้น และมี pager alert แจ้งเตือน
- เนื่องจากรันอยู่บน Heroku จึงดูสถานะจากตัวเลขหน่วยความจำราย Dyno
การบรรเทาเหตุขัดข้องเริ่มจากการรีสตาร์ต Dyno
- อาการดูไม่ใช่แค่หน่วยความจำบวม (bloat) ธรรมดา แต่เหมือน leak และวิธีแก้ชั่วคราวคือรีสตาร์ต process
- โดยปกติการ deploy รายวันหลายครั้งจะรีสตาร์ต
web instance อยู่แล้ว แต่ Dyno ที่เข้าใกล้ขีดจำกัดหน่วยความจำถูกรีสตาร์ตด้วยมือ
แม้ย้อนกลับการเปลี่ยนแปลงที่สงสัยแล้ว leak ก็ยังอยู่
- ไล่ audit การเปลี่ยนแปลงโค้ด 3 วัน ย้อนกลับจากก่อน spike ใหญ่ครั้งแรก
- มีการเปลี่ยนแปลง 3 รายการที่ดูอาจเกี่ยวข้อง
- การเปลี่ยนแปลงที่ทำให้เกิด memory leak ในโหมด
development เพราะ Rails code reloading
- การเปลี่ยนแปลงที่ทำให้มีการเรียก Redis มากกว่าที่ตั้งใจระหว่างการกรอง request บางประเภท
- การเปลี่ยนแปลงแบบ N+1 ที่ทำให้เกิดการเรียก database มากขึ้นและโหลด instance ของ
ActiveRecord มากขึ้น
- แก้สองรายการแรก และ rollback รายการที่สาม จากนั้น deploy ทีละรายการ แต่ leak ยังคงเกิดขึ้น
- การเปลี่ยนแปลงเครื่องมือเพื่อเก็บเมตริกภาษา Ruby และเมตริกการใช้งาน Puma pool ก็ถูกย้อนกลับเช่นกัน แต่ การเพิ่มขึ้นของหน่วยความจำ ไม่หยุดลง
แพตเทิร์นของ leak ชี้ไปที่ทราฟฟิกบางประเภท
- leak เกิดเฉพาะบน
web Dyno ส่วน Sidekiq และ Delayed::Job Dyno ดูปกติ
- ไม่ใช่
web Dyno ทุกตัวที่จะ leak เสมอไป
- บางตัวแสดงการใช้หน่วยความจำค่อนข้างราบเรียบเป็นเวลาหลายชั่วโมง เหมือน web process ที่รันยาวตามปกติ
- จากนั้น ณ จุดหนึ่ง Dyno หนึ่งตัว บางส่วน หรือทั้งหมดเริ่ม leak
- Puma รันในโหมด cluster และแต่ละ Dyno ใช้ 12 worker process สำหรับ 8 vCPU
- แม้ภายใน Dyno เดียวกัน บางครั้ง worker เพียงบางตัวจาก 12 ตัวก็ใช้หน่วยความจำเกือบทั้งหมด
- OpenTelemetry Traces มีการ sampling หนัก ทำให้เชื่อมโยง request บางประเภทกับ Dyno บางตัวได้ยาก และการวิเคราะห์ความสัมพันธ์กับ log ที่ไม่ถูก sampling ก็ไม่ง่ายด้วยเครื่องมือที่มี
ขั้นตอนการเก็บ heap dump
- ใช้
rbtrace เพื่อ attach เข้ากับ Ruby process ที่กำลังรันอยู่
rbtrace ต้องถูกโหลดอยู่ใน process จึงใส่ไว้ใน Gemfile และควบคุมว่าจะโหลดหรือไม่ด้วย environment variable
gem "rbtrace", require: String(ENV.fetch("FEATURE_ENABLE_MEMORY_DUMPS", false)) == "true"
- บน Heroku ใช้
heroku ps:exec เปิด SSH tunnel ไปยัง Dyno ที่กำลัง leak แล้วใช้ ps เรียง Ruby process ตาม RSS
ps -eo pid,ppid,comm,rss,vsz --sort -rss | grep ruby
- บน
web Dyno process ที่มี PPID เดียวกันคือ Puma worker และเลือก PID ของ worker ที่ใช้หน่วยความจำมากที่สุดเป็นเป้าหมาย
- เปิดการติดตาม memory allocation ด้วย
ObjectSpace.trace_object_allocations_start ซึ่งอาจกระทบต่อ performance, หน่วยความจำ และ CPU
DUMP_PID=<pid>
rbtrace --pid="${DUMP_PID}" --eval="Thread.new{require 'objspace';ObjectSpace.trace_object_allocations_start}.join"
- สร้าง heap dump ไปที่
/tmp ด้วย ObjectSpace.dump_all และใน process ที่ leak ซึ่งรันมาหลายชั่วโมง ไฟล์ JSON ใหญ่ได้ถึง 5–6GiB
rbtrace --pid="${DUMP_PID}" --eval="Thread.new{require 'objspace'; GC.start(); io=File.open('/tmp/heap-${DUMP_PID}.json', 'w'); ObjectSpace.dump_all(output: io); io.close}.join" --timeout=600
gzip "/tmp/heap-${DUMP_PID}.json"
- บน Heroku ใช้
heroku ps:copy นำ dump ลงมาเครื่อง local และหากต้องการดู retained memory ด้วย heapy จะเก็บ dump อย่างน้อยประมาณสามชุด
- หลังทำงานเสร็จ ให้ปิด allocation tracing และลบ dump หรือรีสตาร์ต Dyno
การวิเคราะห์ heap เผย Thread ที่ยึดไว้ 1.9GiB
- รายงาน retained memory ของ
heapy และ diff ของ sheap เพียงอย่างเดียวทำให้หาจุดเริ่มต้นได้ยาก
- สร้าง flame graph ด้วย
reap ซึ่งวิเคราะห์และแสดงภาพ reference graph ของ Ruby heap dump
- flame graph แสดง reference จาก root ในมุมมองของ Ruby GC ลงไปยัง object ด้านล่าง โดย object ที่ยึดหน่วยความจำไว้มากกว่าจะมี cell กว้างกว่า
- ใน heap dump ชุดที่สาม มี
Thread หนึ่งยึดหน่วยความจำไว้ 1.9GiB
- ในความเป็นจริง
Array ด้านล่างกำลังอ้างอิง object 32,067 รายการและคงหน่วยความจำ 1.9GiB ไว้
ไล่ตามเส้นทาง reference ด้วย sheap
- ใช้
sheap จาก branch main ล่าสุดเพื่อเปรียบเทียบ dump ชุดที่สองและสาม
- เนื่องจากขนาด dump ใกล้ 6GiB การ parse จึงใช้เวลา
- ผลลัพธ์
find_path ชี้ว่า Thread ที่เป็นปัญหาไม่ใช่ background thread ของเครื่องมือ telemetry หรือ metric แต่เป็น Puma thread ที่ประมวลผล request
ActiveSupport::SubscriberQueueRegistry ใน Rails 6.1 ทำงานเป็น Hash ต่อ thread สำหรับเก็บรายการ ActiveSupport::Subscriber แยกตามชื่อ event
- registry ดังกล่าวอ้างอิง
Hash และหนึ่งใน Array ภายในนั้นยึด ActiveSupport::Notifications::Event ไว้
Event นั้นอ้างอิง child Event มากกว่า 32,067 object ผ่าน array @children
- ชื่อของ child
Event ตัวแรกคือ redirect_to.action_controller และภายในมี object ActionDispatch::Request
request ผิดปกติกลายเป็นเบาะแสในการทำซ้ำ
ActionDispatch::Request ใน heap มี route จริงและ public resource ID ที่ถูกต้อง แต่ query parameter อยู่ในรูปแบบที่ถูกดัดแปลง
- request path มี
password=[FILTERED] อยู่ แสดงว่ากระบวนการทำความสะอาดข้อมูลอ่อนไหวเข้ามาเกี่ยวข้อง
- เมื่อเรียก production app ด้วย path และ parameter เดียวกันจากเบราว์เซอร์โหมดไม่ระบุตัวตน เกิด 500 server error
- log บันทึก
URI::InvalidURIError ไว้ และยังยืนยัน Dyno ที่ request ไปถึงได้
- Dyno นั้นในขณะนั้นมีการใช้หน่วยความจำปกติ แต่เมื่อหยุด deploy ชั่วคราวและสังเกตต่อ ก็เริ่มเห็นแนวโน้ม leak
- บน local ใส่
binding.pry และการ debug ด้วย puts ลงใน Gem activesupport เพื่อจำลองสถานการณ์และ backtrace เดียวกัน
สาเหตุจริงคือการผสมกันของการเปลี่ยนแปลงใน Rails และ Bugsnag
- error backtrace ชี้ไปที่ Gem
uri ใน Ruby standard library ซึ่งถูกใช้ใน Bugsnag.cleaner.clean_url ของ Bugsnag
- โค้ดนี้อยู่ในกระบวนการทำความสะอาด Rails breadcrumb URL ภายในบล็อก
ActiveSupport::Notifications.subscribe
- ปัญหาเกิดจากสองอย่างประกอบกัน
ActiveSupport::Subscriber ของ Rails 6.1 ติดตาม event ด้วย Event#children และ shared Array
- การเปลี่ยนแปลงของ Bugsnag ใช้
URI เพื่อทำความสะอาด Rails breadcrumb URL และอาจเกิด exception กับ URI ที่ไม่ถูกต้อง
- เมื่อ
URI โยน error จาก invalid URI บล็อก subscribe ของ Bugsnag ทำให้เกิด exception ระหว่างการประมวลผล ActiveSupport::Notifications::Event
- เพราะ exception นั้น parent
Event จึงไม่ถูก pop ออกจาก Subscriber#event_stack และ parent Event ที่ค้างอยู่ทำให้หน่วยความจำ leak
- parent
Event ยังอ้างอิง child Event ต่อไปผ่าน array #children ทำให้รักษาหน่วยความจำไว้มากขึ้น
- การแก้ไข Rails 7.1 ของ John Hawthorn ลบแนวคิด
Event#children และ shared Array สำหรับติดตาม event ออกไป จึงกำจัดสาเหตุ leak ทั้งสองอย่างพร้อมกัน
วิธีแก้คืออัปเกรด Bugsnag และอัปเกรด Rails
- ใน Rails เวอร์ชันล่าสุด ปัญหานี้จะไม่เกิดขึ้นอีกเพราะการแก้ไขของ John Hawthorn
- แอปในตอนนั้นยังเป็น Rails 6.1 จึงไม่สามารถรับผลจากการแก้ไข Rails ได้ทันที
- Bugsnag ได้
แก้ไขให้ Bugsnag.cleaner.clean_url ไม่โยน exception เมื่อเจอ invalid URI ไปแล้ว
- วิธีแก้ระยะสั้นคืออัปเกรดเป็น เวอร์ชันของ Bugsnag Gem ที่รวมการแก้ไขดังกล่าว
- วิธีแก้ระยะยาวคืออัปเกรดเวอร์ชัน Rails
- การเปลี่ยนแปลงที่เกิดตรงกับช่วง memory spike ครั้งแรกคือการอัปเกรด Bugsnag จาก
v6.26.0 เป็น v6.26.1 โดยมีเป้าหมายเพื่อแก้ deprecation warning ของ dependency อื่น
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
ไม่เข้าใจว่าทำไมการจัดการหน่วยความจำแบบแมนนวลถึงน่ากลัวขนาดนั้น แค่มี RAII กับกฎความเป็นเจ้าของที่ชัดเจน การจัดการหน่วยความจำก็เป็นงานวิศวกรรมที่ง่าย
กลับรู้สึกว่าเฟรมเวิร์กที่บังคับให้ใช้ reference counting หรือ shared pointer ยากกว่าเสียอีก เพราะความเป็นเจ้าของมันพร่าเลือน
ถ้าสร้างเองก็ปล่อยเอง ถ้าส่งต่อไปแล้วก็ไม่ต้องสนใจอีก ทรัพยากรของ OS อย่าง handle และ socket เรายังจัดการแบบแมนนวลโดยไม่มีตัวจัดการทรัพยากรอัตโนมัติได้เลย จึงไม่เห็นเหตุผลว่าทำไมต้องทำให้การออกแบบซับซ้อนขึ้นด้วยการจัดการหน่วยความจำอัตโนมัติ
หลังจากพัฒนาซอฟต์แวร์มาหลายปี ก็เห็นว่านักพัฒนาส่วนใหญ่ไม่มีพื้นที่ working memory เหลือพอที่จะให้เหตุผลเรื่องการจัดการหน่วยความจำไปพร้อมกันได้ แม้จะรู้วิธีแบบเป็นกลไก แต่ถ้าต้องโยนหลายอย่างไว้ในหัวมากเกินไปก็จะหลุดได้
ในทางกลับกัน ก็มีคนส่วนน้อยที่จัดการหน่วยความจำแบบแมนนวลได้ถูกต้องแทบทุกครั้งโดยแทบไม่ลำบาก สำหรับพวกเขามันง่ายจริง ๆ จึงไม่ค่อยเข้าใจว่าทำไมคนอื่นถึงรู้สึกว่ายาก สำหรับคนแบบนี้ การจัดการหน่วยความจำอัตโนมัติอาจดูเหมือนมีข้อดีไม่ชัด แต่ข้อเสียกลับใหญ่กว่า
พูดคร่าว ๆ คือบั๊กพวกนี้ไม่ได้ถูกแทนที่ด้วยบั๊กแบบอื่น แต่มันหายไปเฉย ๆ และไม่ได้เรียกร้องงานเพิ่มจากโปรแกรมเมอร์ ตรงกันข้าม ยังลดงานเมื่อเทียบกับการจัดการหน่วยความจำแบบแมนนวลด้วย
แน่นอนว่า garbage collection ไม่ได้ชนะเสมอไป และมีข้อเสียจริง ๆ แต่สำหรับโปรแกรมส่วนใหญ่ garbage collector สมัยใหม่ดีพอจนข้อเสียเหล่านั้นไม่ค่อยเป็นปัญหา
บั๊กเชิงตรรกะก็มีปัญหาคล้ายกัน และแม้แต่ภาษาอย่าง Java ก็ยังเกิด memory leak ได้เป็นครั้งคราว แต่ภาษา memory-safe ถือเป็นการปรับปรุง เหมือนกับที่ TypeScript ดีกว่า JavaScript หากมีระบบอัตโนมัติที่ลดข้อผิดพลาดหน่วยความจำจาก 1% เหลือ 0.01% ได้ ก็ไม่เข้าใจว่าทำไมการป้องกัน leak และ undefined behavior ยังต้องปล่อยให้เป็นเรื่องที่คนต้องคอยสนใจเองต่อไป
จะใช้ภาษา garbage-collected ที่ง่ายแต่มี overhead อย่าง Java ก็ได้ หรือใช้ภาษาที่บังคับ ownership แบบ Rust ซึ่งมี learning curve แต่ไม่มี overhead ก็ได้ บั๊กเชิงตรรกะก็น่าปวดหัวเช่นกัน แต่บั๊กหน่วยความจำขึ้นชื่อเป็นพิเศษ เพราะมักไม่ให้ข้อความผิดพลาดที่ชัดเจน หรือแม้เกิดขึ้นแล้วโปรแกรมก็อาจไม่หยุดทำงาน
นอกเรื่องเล็กน้อย formal verification ก็เป็นวิธีที่แทบจะกำจัดบั๊กบางประเภทได้เหมือนกัน ตอนนี้มักเห็นเฉพาะในระบบที่ความถูกต้องสำคัญที่สุด เพราะต่างจากการจัดการหน่วยความจำ ข้อเสียของมันใหญ่เกินไป โค้ดจะยืดยาวและยุ่งยากมาก อีกทั้งบังคับโครงสร้างบางอย่าง แต่ถ้า formal verification ดีขึ้น ก็มองว่าสิ่งนี้จะกลายเป็นกระแสหลักมากขึ้นเช่นกัน
“ผมไม่ใช่โปรแกรมเมอร์ตัวจริง ผมแค่เอาโน่นนี่มาปะให้มันเหมือนทำงานได้แล้วก็ข้ามไป โปรแกรมเมอร์ตัวจริงคงจะพูดว่า ‘มันทำงานก็จริง แต่หน่วยความจำรั่วไปทั่วเลยนะ ไม่ควรแก้หน่อยหรือ’ ผมก็แค่รีสตาร์ต Apache ทุก ๆ 10 request” — Rasmus Lerdorf, PHP Non-Designer
https://en.wikiquote.org/wiki/Rasmus_Lerdorf
ที่ทำงานเก่าของผมน่าจะได้รางวัลวิธีที่โง่ที่สุดในการเสียเงิน 5 ล้านดอลลาร์เพราะ memory leak
ในยุค 90 ไดรเวอร์พรินเตอร์ของ Solaris มี memory leak[1] ตอนนั้นผมทำงานเป็น contractor ให้ธนาคารใหญ่แห่งหนึ่ง ในยุคนั้นสถานะทางกฎหมายของแฟกซ์ในการยืนยันสัญญายังไม่ได้รับการทดสอบในศาลอย่างเพียงพอ ธนาคารจึงบันทึกธุรกรรมผ่านแฟกซ์ ระบบที่ส่งแฟกซ์จะส่งเอกสารไปยังพรินเตอร์เฉพาะด้วยเพื่อพิมพ์ใบยืนยันธุรกรรม แล้วจะมีคนหยิบใบยืนยันนั้นขึ้นมาอ่านให้คู่สัญญาฟังทางโทรศัพท์ เพื่อให้เข้าไปอยู่ในบันทึกการโทร[2] และยืนยันในทางกฎหมาย
วันหนึ่งเพราะ memory leak ทำให้ไดรเวอร์พรินเตอร์ล่ม ใบยืนยันฉบับหนึ่งจึงไม่ถูกพิมพ์ และพนักงานที่รับผิดชอบก็อ่านให้ฟังทางโทรศัพท์ไม่ได้ ตลาดเคลื่อนไหวแรง และอีกฝ่าย DK ธุรกรรมนั้น[3] ต่อให้ผู้บริหารธนาคารโวยวายแค่ไหนก็ไม่เป็นผล สุดท้ายต้องบันทึกขาดทุน 5 ล้านดอลลาร์ แล้วตั้งนโยบายว่าจะไม่ทำธุรกิจกับธนาคารนั้นอีก[4] งานพรินต์แฟกซ์ถูกย้ายไป Windows NT
[1] ตามหนังสือยอดเยี่ยม “Expert C Programming” ปัญหานี้เกิดขึ้นบ่อยกับ Scott McNealy ซีอีโอของ Sun Microsystems ในขณะนั้น เพราะแม้เป็นซีอีโอ เขากลับได้เวิร์กสเตชันประสิทธิภาพต่ำ และหลังจากบ่นมากพอ นักพัฒนาก็แก้ไขในที่สุด https://progforperf.github.io/Expert_C_Programming.pdf
[2] สายโทรศัพท์ของฝ่ายหลักทรัพย์ในธนาคารมักถูกบันทึกเกือบเสมอ ด้วยเหตุผลด้านกฎหมายและ compliance
[3] DK ย่อมาจาก “Don’t know” ถ้าอีกฝ่ายบอกว่า “ไม่รู้จัก” ธุรกรรมนั้น ก็เท่ากับโต้แย้งว่ามีการทำสัญญาเกิดขึ้นจริงหรือไม่
[4] อีกฝ่ายสามารถไปเทรดที่อื่นและจ่ายค่าธรรมเนียมให้ธนาคารอื่นได้ ดังนั้นฝ่ายเราน่าจะเสียหายมากกว่า
Citi ก็เคยถูกฟ้องเพราะชำระคืนเงินกู้เร็วเกินไป ในวงการการเงิน ถ้ามันเป็นประโยชน์กับตัวเอง ใคร ๆ ก็คงหยิบสัญญาเป็นลายลักษณ์อักษรมายืนกรานอย่างแข็งกร้าว
ในภาษา C การหา memory leak นั้นง่ายมากเพราะมี Valgrind
การแก้ยากกว่า แต่ถ้าการออกแบบถูกต้อง โดยทั่วไปก็มักจะง่าย ปกติแล้วถ้าไม่ใช่ฟังก์ชันที่จัดสรรหน่วยความจำให้ผู้เรียก ก็มักจะจัดสรรและปล่อยภายในฟังก์ชันเดียวกัน ถ้าเป็นฟังก์ชันที่จัดสรรให้ผู้เรียก ก็ถือว่าการเรียกนั้นเองเป็นการจัดสรรฝั่งผู้เรียก
เมื่อทำ static analysis กับ codebase เส้นทางการจัดการข้อผิดพลาดเป็นสาเหตุที่พบบ่อยที่สุดของปัญหา
เช่นเดียวกับที่มี block scope, function scope, file scope, global scope ในโมเดลซึ่งเป็น abstraction ของขอบเขตปัญหาหรือวิธีแก้ก็มี scope หลายระดับเช่นกัน เพียงแต่ไม่เคยเห็นว่ามีการสอนเรื่องนี้
กรณีที่ scope ใด scope หนึ่งได้ resource ใน
$SCOPE::foo()แล้วไม่ปล่อยใน$SCOPE::cleanup()ค่อนข้างมองหาได้ง่ายด้วยตา ความสามารถในการ model ขอบเขตปัญหาและวิธีแก้ที่เสนอ ก่อนกระโดดลงไปเขียนโค้ดนั้นมีประโยชน์นึกถึงเรื่องที่เคยได้ยินเกี่ยวกับ Yahoo มี memory leak ใน ad server ทำให้เกิด หน่วยความจำไม่พอ หลังจากประมาณ 10,000 requests
วิธีแก้คือ restart server หลังจาก 8,000 requests วิธีนี้ใช้ได้อยู่ 1–2 ปี แต่หลังจากนั้นเริ่มเกิดหน่วยความจำไม่พอแม้หลัง 8,000 requests
วิธีแก้ถัดมาคือ restart server หลังจาก 6,000 requests
ถ้าวิธีนั้นจะใช้ได้ การ restart ต้องเร็วมาก ๆ
ตอนที่เป็นนักพัฒนา Rails การเพิ่ม hardware เข้าไปจัดการปัญหาแบบนี้ถือเป็นการประนีประนอมที่โอเคเพื่อ productivity บรรยากาศคือถ้าใส่ใจปัญหาแบบนี้ก็ไปใช้เครื่องมือที่เข้มงวดกว่านี้ได้
ส่วนตัวด้วยนิสัย perfectionist ทำให้ยอมรับแนวทางนั้นได้ยาก แต่ก็ปฏิเสธได้ยากเช่นกันว่ามันใช้งานได้จริง
เคยใช้ทั้งภาษาที่มี garbage collection และไม่มี ปกติแล้ว การจัดการเองแบบ manual เขียนยากกว่า ส่วนการจัดการอัตโนมัติแก้ปัญหายากกว่า
อยากใช้ภาษาที่ทำได้ทั้งสองแบบ ตอนเขียนโค้ดเชิงสำรวจ automatic memory management สะดวกกว่า และสำหรับโค้ดบางประเภท manual memory management ก็ได้เปรียบกว่า
น่าหงุดหงิดที่หาจุดกึ่งกลางระหว่างการห้ามกับการบังคับไม่ได้
@[manualfree]และปิดทั้งโปรเจกต์ได้ด้วยv -gc nonehttps://vlang.io
“มีบทความมากมายเขียนถึงเครื่องมือต่าง ๆ สำหรับ profiling leak, การทำความเข้าใจ heap dump และสาเหตุ leak ที่พบบ่อย”
อืม leak กับ heap dump นี่นะ ดูเหมือนมีบางคนต้องปรับอาหารให้ดีต่อสุขภาพกว่านี้แล้ว