คู่มือเกมแลค › L10 หน่วยความจำ
GC ของเซิร์ฟเวอร์หยุดทั้งระบบ Stop-the-world GC pause
ID สาเหตุ mem-gc · ผู้รับผิดชอบหลัก พัฒนาเซิร์ฟเวอร์ (ทีมพัฒนาเกม) · ร่วมกับ อินฟราเซิร์ฟเวอร์ (ทีมอินฟรา)
เปิดการ์ดในฉบับหลักที่มีภาพและการทดลอง →
ระหว่างที่เซิร์ฟเวอร์ Java หรือ C# หยุดทุกเธรดเพื่อเก็บคืน garbage (stop-the-world) ทั้งเซิร์ฟเวอร์จะหยุดชะงักไปด้วย
ทำไม heap เต็ม GC จึงเริ่มทำงาน → ผลคือ หยุดเธรดเกมทั้งหมดแล้วเก็บคืน (ยิ่งมีข้อมูลที่ยังใช้อยู่มาก ยิ่งนาน) → บนหน้าจอ ทุกคนบนเซิร์ฟเวอร์ค้างพร้อมกันแล้วตามด้วยอาการกรอเร็ว
อาการ ค้าง , กรอเร็ว
ปัจจัย การหยุดชะงัก
ใครเจอ ทั้งเซิร์ฟเวอร์
เกิดเมื่อไร เป็นรอบสม่ำเสมอ, ยิ่งเปิดไว้นานยิ่งเป็น
ผู้รับผิดชอบ ผู้รับผิดชอบหลัก พัฒนาเซิร์ฟเวอร์ (ทีมพัฒนาเกม) · ร่วมกับ อินฟราเซิร์ฟเวอร์ (ทีมอินฟรา)
งานฝั่งทีมพัฒนาเกม ระบุ GC ที่หยุดสั้น (ZGC, Shenandoah หรือ G1 ที่ลดเวลาเป้าหมายลง) ในออปชันตอนรันโดยตรง, ลดการจัดสรรหน่วยความจำ, ปรับขนาด heap
งานฝั่งทีมอินฟรา ใช้อินสแตนซ์ที่มีหน่วยความจำพอจะให้ heap ได้อย่างเหลือเฟือ, ให้คอนเทนเนอร์มี CPU อย่างน้อย 2 ตัวและหน่วยความจำประมาณ 1.8 GB ขึ้นไป (ถ้าน้อยกว่านั้น JDK 26 ลงไปจะเลือก Serial GC เป็นค่าเริ่มต้น), มอนิเตอร์เวลา GC pause
ตัวเลขที่ควรรู้ Minor GC ที่เก็บคืนเฉพาะออบเจ็กต์ใหม่ (Young generation) ใช้เวลาหลักหน่วยถึงหลักสิบ ms ส่วน Full GC ที่เก็บคืนทั้ง heap ซึ่งมีข้อมูลที่ยังใช้อยู่ (live data) หลาย GB อาจนานเกิน 1 วินาที ZGC หยุดไม่ถึง 1 ms แทบไม่ขึ้นกับขนาด heap และ Shenandoah ก็หยุดสั้นเพราะเวลาหยุดไม่แปรตามขนาด heap
บนกราฟ พุ่งเป็นรอบ · เวลาต่อทิกของเซิร์ฟเวอร์, เวลา GC pause
จุดที่ต้องดู เปิด GC log แล้ววางช่วงที่หยุดและระยะเวลาที่หยุดซ้อนบนกราฟเวลาต่อทิกของเซิร์ฟเวอร์ Java ดูบรรทัด Pause จากออปชันตอนเริ่ม -Xlog:gc* (JDK 8 ลงไปใช้ -XX:+PrintGCDetails), .NET ดูเมตริก GC pause ของ dotnet-counters (ตั้งแต่ .NET 9 คือ dotnet.gc.pause.time, 8 ลงไปคือ % Time in GC since last GC), Go ดูบรรทัดที่ GODEBUG=gctrace=1 เขียนทุกครั้งที่ GC ทำงาน สัญญาณว่าใช่ ช่วงที่ทิกพุ่งตรงกับช่วง GC pause และระยะเวลาที่หยุดใกล้เคียงกับระยะเวลาที่ทิกพุ่ง ทุกโซนและแชนแนลบนเซิร์ฟเวอร์พุ่งพร้อมกัน สัญญาณว่าไม่ใช่ GC log ไม่มีการหยุดนานแต่ทิกยังพุ่ง: น่าจะเป็นสาเหตุอื่น เช่น ล็อก, synchronous call หรือการเขียนดิสก์ ถ้าพุ่งแค่โซนเดียว น่าจะเป็น GC ของสคริปต์เอนจิน (mem-script-gc) หรือโหลดของโซนนั้น วิธีตรวจ ใช้เครื่องมือฝั่งอินฟรา (ไม่ต้องใช้โค้ดเกม)
รายละเอียดเพิ่มเติม เป้าหมายเวลาหยุดต่อครั้งของ G1 ใน Java มีค่าเริ่มต้น 200 ms ซึ่งเท่ากับ 4 ทิกบนเซิร์ฟเวอร์ 20 ทิก ถ้าให้คอนเทนเนอร์มี CPU น้อยกว่า 2 ตัวหรือหน่วยความจำน้อยกว่าประมาณ 1.8 GB Java รุ่น JDK 26 ลงไปจะเลือก Serial GC ที่เก็บคืนด้วยเธรดเดียวเป็น GC เริ่มต้น ทำให้หยุดนานขึ้นมาก เซิร์ฟเวอร์ C# (.NET) มักเปิด server GC และ background GC ไว้ แต่การเก็บคืน generation 0 และ 1 (Gen0/1) ซึ่งเป็นที่อยู่ของออบเจ็กต์ใหม่ และ Full GC ที่มีการบีบอัด (compaction) ยังหยุดทุกเธรดอยู่ดี ส่วน Go ปกติหยุดไม่ถึง 1 ms แต่ถ้าจัดสรรหน่วยความจำมาก ฝั่งที่ขอหน่วยความจำต้องช่วยแบ่งงาน GC ทิกจึงช้าลง ไม่ว่าแบบไหน ถ้าจัดสรรเร็วกว่าเก็บคืน สุดท้ายเธรดเกมก็ต้องหยุด G1 จะเปลี่ยนไปทำ Full GC ส่วน ZGC จะหยุดเธรดที่ขอหน่วยความจำไว้จนกว่าการเก็บคืนจะเสร็จ
กรณีจริง Riot Games 2021: League of Legends EUW ขัดข้อง 5 ชั่วโมง: DB เสริมตัวเดียวทำให้ทั้งเซิร์ฟเวอร์หยุด
แหล่งอ้างอิง Garbage-First (G1) Garbage Collector Oracle ค่าเริ่มต้นของเป้าหมายเวลาหยุดของ G1 คือ 200 ms (MaxGCPauseMillis), ถ้าหน่วยความจำหมดระหว่างเก็บคืนจะเปลี่ยนไปทำ Full GC ที่หยุดทั้ง heap แล้วบีบอัด JEP 523: Make G1 the Default Garbage Collector in All Environments OpenJDK ที่ผ่านมาถ้ามี CPU 1 ตัวหรือหน่วยความจำน้อยกว่า 1792 MB จะเลือก Serial GC เป็นค่าเริ่มต้น และตั้งแต่ JDK 27 G1 เป็นค่าเริ่มต้นในทุกสภาพแวดล้อม JEP 439: Generational ZGC OpenJDK ZGC หยุดไม่เกิน 1 ms และไม่ขึ้นกับขนาด heap ส่วน G1 หยุดตั้งแต่หลาย ms ถึงหลายวินาที ถ้าจัดสรรเร็วกว่าเก็บคืนเสี่ยงเกิด allocation stall JEP 189: Shenandoah: A Low-Pause-Time Garbage Collector (Experimental) OpenJDK เวลาหยุดของ Shenandoah ใกล้เคียงกันไม่ว่า heap จะ 200 MB หรือ 200 GB Background garbage collection .NET background GC ใช้กับการเก็บคืน generation 2 เท่านั้น ส่วนการเก็บคืน generation 0 และ 1 (foreground GC) หยุด managed thread ทั้งหมด A Guide to the Go Garbage Collector Go GC ของ Go ส่วนใหญ่ทำงานพร้อมกับโปรแกรม (concurrent) และมีแค่ช่วง stop-the-world สั้น ๆ, ถ้าจัดสรรมาก goroutine ต้องรับงาน GC ไปช่วยทำ (assist) จนเกิดดีเลย์ JEP 271: Unified GC Logging OpenJDK ตั้งแต่ JDK 9 GC log ถูกสร้างใหม่บน unified logging (-Xlog), -Xlog:gc เขียนหนึ่งบรรทัดต่อ GC หนึ่งครั้งเหมือน -XX:+PrintGC แบบเดิม The java Command Oracle ตารางเทียบออปชัน GC log แบบเดิมกับ -Xlog: -XX:+PrintGCDetails คือ -Xlog:gc* dotnet-counters diagnostic tool .NET ตั้งแต่ .NET 9 แสดงเป็น meter ของ System.Runtime (dotnet.gc.pause.time ฯลฯ) ส่วน .NET 8 ลงไปแสดงเป็น EventCounter แบบเดิม (% Time in GC since last GC ฯลฯ) runtime package Go GODEBUG=gctrace=1: หนึ่งบรรทัดต่อ GC หนึ่งครั้ง, wall clock time ของแต่ละเฟส, ขนาด heap ตอนเริ่มและจบ GC และ heap เป้าหมาย
สาเหตุที่ควรดูประกอบ
ชั้นเดียวกัน: L10 หน่วยความจำ
สาเหตุจากชั้นอื่นที่ทำให้เกิดอาการเดียวกัน (ค้าง)
ดูการ์ดในฉบับหลักที่มีภาพและการทดลอง