1 คะแนน โดย GN⁺ 2024-05-12 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • หน่วยความจำของ 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 ความคิดเห็น

 
GN⁺ 2024-05-12
ความคิดเห็นบน Hacker News
  • ไม่เข้าใจว่าทำไมการจัดการหน่วยความจำแบบแมนนวลถึงน่ากลัวขนาดนั้น แค่มี RAII กับกฎความเป็นเจ้าของที่ชัดเจน การจัดการหน่วยความจำก็เป็นงานวิศวกรรมที่ง่าย
    กลับรู้สึกว่าเฟรมเวิร์กที่บังคับให้ใช้ reference counting หรือ shared pointer ยากกว่าเสียอีก เพราะความเป็นเจ้าของมันพร่าเลือน
    ถ้าสร้างเองก็ปล่อยเอง ถ้าส่งต่อไปแล้วก็ไม่ต้องสนใจอีก ทรัพยากรของ OS อย่าง handle และ socket เรายังจัดการแบบแมนนวลโดยไม่มีตัวจัดการทรัพยากรอัตโนมัติได้เลย จึงไม่เห็นเหตุผลว่าทำไมต้องทำให้การออกแบบซับซ้อนขึ้นด้วยการจัดการหน่วยความจำอัตโนมัติ

    • การจัดการหน่วยความจำแบบแมนนวลเพิ่ม ภาระทางความคิด เวลาต้องให้เหตุผลเกี่ยวกับซอฟต์แวร์ ความจุของ working memory แตกต่างกันมากในแต่ละคน และกลายเป็นปัจจัยจำกัดประสิทธิภาพเมื่อออกแบบระบบที่ซับซ้อน
      หลังจากพัฒนาซอฟต์แวร์มาหลายปี ก็เห็นว่านักพัฒนาส่วนใหญ่ไม่มีพื้นที่ working memory เหลือพอที่จะให้เหตุผลเรื่องการจัดการหน่วยความจำไปพร้อมกันได้ แม้จะรู้วิธีแบบเป็นกลไก แต่ถ้าต้องโยนหลายอย่างไว้ในหัวมากเกินไปก็จะหลุดได้
      ในทางกลับกัน ก็มีคนส่วนน้อยที่จัดการหน่วยความจำแบบแมนนวลได้ถูกต้องแทบทุกครั้งโดยแทบไม่ลำบาก สำหรับพวกเขามันง่ายจริง ๆ จึงไม่ค่อยเข้าใจว่าทำไมคนอื่นถึงรู้สึกว่ายาก สำหรับคนแบบนี้ การจัดการหน่วยความจำอัตโนมัติอาจดูเหมือนมีข้อดีไม่ชัด แต่ข้อเสียกลับใหญ่กว่า
    • มองว่า บั๊กหน่วยความจำ เป็นประเภทบั๊กที่แทบจะแก้ได้แล้ว ถ้าใช้ภาษาที่มี garbage collector สมัยใหม่ซึ่งจัดการ circular reference ได้ ก็มีโอกาสสูงที่จะไม่เจอบั๊กหน่วยความจำเลยตลอดทั้งโปรเจกต์
      พูดคร่าว ๆ คือบั๊กพวกนี้ไม่ได้ถูกแทนที่ด้วยบั๊กแบบอื่น แต่มันหายไปเฉย ๆ และไม่ได้เรียกร้องงานเพิ่มจากโปรแกรมเมอร์ ตรงกันข้าม ยังลดงานเมื่อเทียบกับการจัดการหน่วยความจำแบบแมนนวลด้วย
      แน่นอนว่า garbage collection ไม่ได้ชนะเสมอไป และมีข้อเสียจริง ๆ แต่สำหรับโปรแกรมส่วนใหญ่ garbage collector สมัยใหม่ดีพอจนข้อเสียเหล่านั้นไม่ค่อยเป็นปัญหา
    • ไม่ใช่ว่าการจัดการหน่วยความจำเองยากเท่านั้น แต่เพราะนักพัฒนาไม่ได้สมบูรณ์แบบ จึงยากที่จะเขียนโปรแกรมที่ไม่มี undefined behavior และไม่มี memory leak เลย ความผิดพลาดเพียงครั้งเดียวอาจนำไปสู่ CVE, การใช้หน่วยความจำที่ค่อย ๆ เพิ่มขึ้นในโปรแกรมที่รันยาวนาน หรือบั๊กที่ระเบิดขึ้นมาหนึ่งครั้งในทุก 1,000 ครั้งได้
      บั๊กเชิงตรรกะก็มีปัญหาคล้ายกัน และแม้แต่ภาษาอย่าง 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 ดีขึ้น ก็มองว่าสิ่งนี้จะกลายเป็นกระแสหลักมากขึ้นเช่นกัน
    • เคยทำ การจัดการหน่วยความจำแบบแมนนวล ในระบบ 24/7 มา 10 ปี แต่ก็ไม่คิดถึงมัน ตัวมันเองไม่ได้ยากหรือน่ากลัว แต่ถ้าเป็นโครงสร้างที่อาจเกิด reference cycle หรือสถาปัตยกรรมแบบ event handler ที่ย้าย reference ไปมา ก็ต้องออกแบบการจัดการหน่วยความจำอย่างระมัดระวังมาก แทนที่จะได้โฟกัสแค่ problem domain
    • ส่วนหนึ่งของคำตอบคือ 35% ของช่องโหว่ในบริษัทเทคโนโลยีขนาดใหญ่เกิดจากบั๊ก use-after-free ช่องโหว่ร้ายแรงมากกว่า 90% มาจากบั๊กหน่วยความจำที่เป็นไปไม่ได้ในภาษา memory-safe
  • “ผมไม่ใช่โปรแกรมเมอร์ตัวจริง ผมแค่เอาโน่นนี่มาปะให้มันเหมือนทำงานได้แล้วก็ข้ามไป โปรแกรมเมอร์ตัวจริงคงจะพูดว่า ‘มันทำงานก็จริง แต่หน่วยความจำรั่วไปทั่วเลยนะ ไม่ควรแก้หน่อยหรือ’ ผมก็แค่รีสตาร์ต Apache ทุก ๆ 10 request” — Rasmus Lerdorf, PHP Non-Designer
    https://en.wikiquote.org/wiki/Rasmus_Lerdorf

    • ถ้ารู้ lifespan ของโปรเซสอย่างแน่ชัด การไม่เรียก free() เลย ก็เป็นกลยุทธ์การจัดการหน่วยความจำที่ใช้ได้
  • ที่ทำงานเก่าของผมน่าจะได้รางวัลวิธีที่โง่ที่สุดในการเสียเงิน 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
    การแก้ยากกว่า แต่ถ้าการออกแบบถูกต้อง โดยทั่วไปก็มักจะง่าย ปกติแล้วถ้าไม่ใช่ฟังก์ชันที่จัดสรรหน่วยความจำให้ผู้เรียก ก็มักจะจัดสรรและปล่อยภายในฟังก์ชันเดียวกัน ถ้าเป็นฟังก์ชันที่จัดสรรให้ผู้เรียก ก็ถือว่าการเรียกนั้นเองเป็นการจัดสรรฝั่งผู้เรียก

    • ส่วนที่ยากคือการ reproduce บั๊ก
      เมื่อทำ static analysis กับ codebase เส้นทางการจัดการข้อผิดพลาดเป็นสาเหตุที่พบบ่อยที่สุดของปัญหา
    • ใน C ก็ทำคล้ายกัน แต่คิดเรื่องนี้เป็น ระดับ scope ต่าง ๆ ภายใน abstraction
      เช่นเดียวกับที่มี 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

    • สำหรับ ad server ทั่วไป 8,000 requests ใช้เวลาประมาณ 500 มิลลิวินาที
      ถ้าวิธีนั้นจะใช้ได้ การ restart ต้องเร็วมาก ๆ
  • ตอนที่เป็นนักพัฒนา Rails การเพิ่ม hardware เข้าไปจัดการปัญหาแบบนี้ถือเป็นการประนีประนอมที่โอเคเพื่อ productivity บรรยากาศคือถ้าใส่ใจปัญหาแบบนี้ก็ไปใช้เครื่องมือที่เข้มงวดกว่านี้ได้
    ส่วนตัวด้วยนิสัย perfectionist ทำให้ยอมรับแนวทางนั้นได้ยาก แต่ก็ปฏิเสธได้ยากเช่นกันว่ามันใช้งานได้จริง

    • แทนที่จะยอมรับว่า reboot server ทุก 10 นาทีเพื่อล้าง memory leak ถ้าเรียกว่า กลยุทธ์ arena allocation แบบเป็นขั้น ๆ ก็โอเคขึ้น
  • เคยใช้ทั้งภาษาที่มี garbage collection และไม่มี ปกติแล้ว การจัดการเองแบบ manual เขียนยากกว่า ส่วนการจัดการอัตโนมัติแก้ปัญหายากกว่า
    อยากใช้ภาษาที่ทำได้ทั้งสองแบบ ตอนเขียนโค้ดเชิงสำรวจ automatic memory management สะดวกกว่า และสำหรับโค้ดบางประเภท manual memory management ก็ได้เปรียบกว่า
    น่าหงุดหงิดที่หาจุดกึ่งกลางระหว่างการห้ามกับการบังคับไม่ได้

    • V ใช้ garbage collector เป็นค่าเริ่มต้น แต่สามารถปิดได้ง่ายเป็นรายฟังก์ชันหรือรายโมดูลด้วย attribute @[manualfree] และปิดทั้งโปรเจกต์ได้ด้วย v -gc none
      https://vlang.io
    • ภาษานั้นคือ C++ แทบไม่ต้องจัดการหน่วยความจำเองแบบ manual แต่ถ้าต้องการก็ทำได้
  • “มีบทความมากมายเขียนถึงเครื่องมือต่าง ๆ สำหรับ profiling leak, การทำความเข้าใจ heap dump และสาเหตุ leak ที่พบบ่อย”
    อืม leak กับ heap dump นี่นะ ดูเหมือนมีบางคนต้องปรับอาหารให้ดีต่อสุขภาพกว่านี้แล้ว