2 คะแนน โดย GN⁺ 2025-03-01 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ในอดีต การใช้ CPU ของระบบของฉันพุ่งไปถึง 3,200% จนคอร์ทั้ง 32 คอร์ทำงานเต็มทั้งหมด
  • ขณะนั้นใช้ Java 17 runtime และเมื่อตรวจดูเวลา CPU จาก thread dump แล้วจัดเรียงตามเวลา CPU ก็พบเธรดลักษณะคล้ายกันจำนวนมาก
  • การวิเคราะห์โค้ดที่เป็นปัญหา
    • ตรวจสอบบรรทัดที่ 29 ของคลาส BusinessLogic ผ่าน stack trace
    • โค้ดดังกล่าวมีลักษณะเป็นการวนลูป unrelatedObjects แล้วแทรกค่าของ relatedObject ลงใน treeMap
    • นี่เป็นโค้ดที่ไม่มีประสิทธิภาพ เพราะไม่ได้ใช้ unrelatedObject ภายในลูป

การแก้ไขโค้ดและการทดสอบ

  • ลบลูปที่ไม่จำเป็นออก แล้วแก้เป็นบรรทัดเดียว treeMap.put(relatedObject.a(), relatedObject.b());
  • มีการรัน unit test ก่อนและหลังแก้ไข แต่ไม่สามารถทำให้ปัญหาเกิดขึ้นซ้ำได้
  • แม้ treeMap และ unrelatedObjects จะมีขนาดมากกว่า 1,000,000 รายการ ก็ยังไม่เกิดปัญหา

ค้นพบสาเหตุของปัญหา

  • treeMap ถูกเข้าถึงพร้อมกันจากหลายเธรด และไม่มีการซิงโครไนซ์
  • ปัญหานี้เกิดจากหลายเธรดแก้ไข TreeMap พร้อมกัน

การทำให้ปัญหาเกิดซ้ำผ่านการทดลอง

  • ทำการทดลองให้หลายเธรดอัปเดต TreeMap ที่แชร์ร่วมกันแบบสุ่ม
  • ตั้งค่าให้ใช้บล็อก try-catch เพื่อเพิกเฉยต่อ NullPointerException
  • ผลการทดลองพบว่าการใช้ CPU สามารถเพิ่มขึ้นได้ถึง 500%

บทสรุป

  • การแก้ไข TreeMap พร้อมกันโดยไม่มีการซิงโครไนซ์ อาจก่อให้เกิดปัญหาด้านประสิทธิภาพอย่างรุนแรง
  • เพื่อป้องกันปัญหานี้ แนะนำให้ซิงโครไนซ์ TreeMap หรือใช้คอลเลกชันที่ปลอดภัยต่อเธรด เช่น ConcurrentMap

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

 
GN⁺ 2025-03-01
ความเห็นจาก Hacker News
  • เคยคิดว่า race condition ทำให้ข้อมูลเสียหายหรือเกิด deadlock เท่านั้น แต่ไม่เคยนึกมาก่อนว่ามันจะก่อปัญหาด้านประสิทธิภาพได้ด้วย ข้อมูลอาจเสียหายในลักษณะที่สร้างลูปไม่สิ้นสุดได้

    • โดยหลักการแล้วคิดว่าควรแก้ไข error, พฤติกรรมผิดปกติ, และ warning ในโปรเจกต์ทั้งหมด เพราะสิ่งเหล่านี้อาจก่อให้เกิดปัญหาที่ดูไม่เกี่ยวข้องกันได้
    • เป็นที่รู้กันดีว่า core collection ของ Java ไม่ได้ thread-safe ตามการออกแบบ ผู้เขียนต้นฉบับควรตรวจสอบด้วยว่ามีหลาย thread เข้ามาจัดการ collection ในส่วนอื่นของโค้ดหรือไม่
    • วิธีแก้ที่ง่ายที่สุดคือห่อ TreeMap ด้วย Collections.synchronizedMap หรือเปลี่ยนไปใช้ ConcurrentHashMap แล้วค่อย sort เมื่อต้องการ
    • แม้จะทำให้การทำงานกับ map แต่ละรายการเป็นแบบ thread-safe ได้ แต่ก็ไม่อาจมั่นใจได้ว่าชุดการทำงานต่อเนื่องจะ thread-safe ด้วย และยังไม่แน่ใจได้ด้วยว่าอ็อบเจ็กต์ที่เป็นเจ้าของ TreeMap นั้น thread-safe หรือไม่
    • วิธีแก้ที่ยังเป็นข้อถกเถียงอย่างการติดตามโหนดที่เคยเยี่ยมชม ไม่ใช่วิธีที่ดี เพราะ collection ก็ยังไม่ thread-safe อยู่ดี และอาจล้มเหลวด้วยวิธีที่ละเอียดอ่อนอื่น ๆ ได้
    • นักพัฒนาที่ใส่ใจรายละเอียดอาจสังเกตเห็นการจับคู่กันของ thread กับ TreeMap หรืออาจเสนอว่าไม่ควรใช้ TreeMap หากไม่จำเป็นต้องมีองค์ประกอบที่เรียงลำดับ แต่ในกรณีนี้ไม่ได้เกิดขึ้น
    • ปัญหาคือมีการละเมิดสัญญาของ collection และต่อให้เปลี่ยน TreeMap เป็น HashMap ก็ยังผิดอยู่ดี
  • ในโค้ดที่มีหลาย thread ทำงานอยู่ กลยุทธ์เดียวที่แน่นอนจริง ๆ คือทำให้อ็อบเจ็กต์ทั้งหมด immutable และสำหรับอ็อบเจ็กต์ที่ทำให้ immutable ไม่ได้ ก็ต้องจำกัดให้อยู่ในส่วนที่เล็ก พึ่งพาตัวเองได้ และควบคุมอย่างเข้มงวด

    • ได้เขียนโมดูลหลักใหม่โดยยึดหลักการเหล่านี้ และมันเปลี่ยนจากแหล่งที่มาของปัญหาอย่างต่อเนื่อง กลายเป็นหนึ่งในส่วนของ codebase ที่ทนทานที่สุด
    • เมื่อมีแนวทางเหล่านี้แล้ว การรีวิวโค้ดก็ง่ายขึ้นมาก
  • ประโยคที่ว่า "แทบจะ ssh เข้าไปไม่ได้" ทำให้นึกถึงตอนเรียนบัณฑิตศึกษาที่ใช้ Sun UltraSparc 170

    • มีผู้ใช้ใหม่หรือนักศึกษาพยายามรันงานแบบขนาน โดยแบ่งไฟล์ข้อความขนาดใหญ่ออกเป็นหลายส่วนตามหมายเลขบรรทัด แล้วประมวลผลแต่ละส่วนแบบขนาน
    • RAM ถูกใช้ไปจำนวนมาก และความพยายามสลับหน่วยความจำก็เกิดการ seek อย่างหนักเพื่ออ่านส่วนต่าง ๆ ของไฟล์เดียวกัน
    • แม้จะไม่สามารถได้ login prompt จากคอนโซล แต่ก็ยังมีเซสชันที่ล็อกอินอยู่ก่อนแล้ว และสามารถเข้าถึง root session เพื่อแก้ปัญหาได้
    • ปัญหาคือไม่เข้าใจขีดจำกัดของระบบ
  • โค้ดสามารถย่อให้เหลือเพียงประมาณนี้ได้

    • โค้ดเดิมเรียก <i>treeMap.put</i> เฉพาะเมื่อ <i>unrelatedObjects</i> ไม่ว่าง ซึ่งนี่อาจเป็นบั๊กก็ได้
    • ควรตรวจสอบว่า <i>a</i> และ <i>b</i> คืนค่าเดียวกันทุกครั้งหรือไม่ และตรวจสอบว่า <i>treeMap</i> ทำงานเหมือน map จริงหรือไม่
  • อีกวิธีหนึ่งที่จะเกิดลูปไม่สิ้นสุดได้ คือใช้ implementation ของ <i>Comparator</i> หรือ <i>Comparable</i> ที่ไม่ได้สร้าง total order ที่สอดคล้องกัน

    • เรื่องนี้ไม่เกี่ยวกับ concurrency และอาจเกิดขึ้นได้ตามข้อมูลเฉพาะชุดและลำดับการประมวลผล
  • อาจพิจารณาใช้ตัวนับที่เพิ่มขึ้นเพื่อตรวจจับ cycle และโยน exception หากเกินความลึกของต้นไม้หรือขนาดของ collection

    • วิธีนี้แทบไม่เพิ่มภาระด้านหน่วยความจำหรือ CPU และน่าจะได้รับการยอมรับมากกว่า
  • ใน Java การทำงานพร้อมกันกับอ็อบเจ็กต์ที่ไม่ thread-safe มักสร้างบั๊กที่น่าสนใจที่สุด

  • มีคำถามว่า TreeMap ที่ไม่ได้ป้องกันไว้สามารถทำให้เกิดการใช้งาน 3,200% ได้จริงหรือไม่

    • เคยเห็นปัญหาคล้ายกันราวปี 2009 และมันก็ยังอาจเกิดขึ้นได้อยู่
    • เป็นเรื่องน่าผิดหวังสำหรับคนที่คิดว่า data race แค่แย่นิดหน่อย
  • ผู้เขียนได้ค้นพบ Poison Pill รูปแบบหนึ่ง ซึ่งพบได้บ่อยกว่าในระบบ event sourcing และเป็นข้อความที่ฆ่าทุกสิ่งที่มันไปเจอ

    • เมื่อโครงสร้างข้อมูลเข้าสู่สถานะที่ไม่ถูกต้อง thread ทุกตัวที่ตามมาหลังจากนั้นก็จะติดอยู่กับ logic bomb เดียวกัน
  • exception ใน thread เป็นปัญหาร้ายแรงอย่างแท้จริง

    • มีเรื่องเล่าการล่าบั๊กสุดสยองที่เกี่ยวกับ C++, select(), และ thread ที่ปา exception ไปมา